// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/public/browser/service_process_host.h"

#include <string.h>

#include <array>

#include "base/memory/shared_memory_mapping.h"
#include "base/memory/unsafe_shared_memory_region.h"
#include "base/process/process.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "services/test/echo/public/mojom/echo.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_WIN)
#include <windows.h>

#include "base/base_paths.h"
#include "base/files/file_path.h"
#include "base/path_service.h"
#include "content/public/browser/service_process_host_passkeys.h"
#endif

namespace content {

#if BUILDFLAG(IS_WIN)
using LoadStatus = echo::mojom::EchoService::LoadStatus;
namespace {
// This is used as the module name to load, and to make the DLL filename.
constexpr const wchar_t* kEchoPreloadLibrary = L"echo_preload_library";

// DLL path relative to current executable.
base::FilePath GetDllPath(std::wstring mod_name) {
  mod_name.append(L".dll");
  auto exe_dir = base::PathService::CheckedGet(base::BasePathKey::DIR_EXE);
  return exe_dir.Append(mod_name);
}
}  // namespace
#endif  // BUILDFLAG(IS_WIN)

constexpr char kTestUrl[] = "https://foo.bar";

using ServiceProcessHostBrowserTest = ContentBrowserTest;

class EchoServiceProcessObserver : public ServiceProcessHost::Observer {
 public:
  EchoServiceProcessObserver() { ServiceProcessHost::AddObserver(this); }

  EchoServiceProcessObserver(const EchoServiceProcessObserver&) = delete;
  EchoServiceProcessObserver& operator=(const EchoServiceProcessObserver&) =
      delete;

  ~EchoServiceProcessObserver() override {
    ServiceProcessHost::RemoveObserver(this);
  }

  void WaitForLaunch() { launch_loop_.Run(); }
  void WaitForDeath() { death_loop_.Run(); }
  void WaitForCrash() { crash_loop_.Run(); }

  // Valid after WaitForLaunch.
  base::ProcessId pid() const { return process_.Pid(); }

 private:
  // ServiceProcessHost::Observer:
  void OnServiceProcessLaunched(const ServiceProcessInfo& info) override {
    if (info.IsService<echo::mojom::EchoService>()) {
      process_ = info.GetProcess().Duplicate();
      launch_loop_.Quit();
    }
  }

  void OnServiceProcessTerminatedNormally(
      const ServiceProcessInfo& info) override {
    if (info.IsService<echo::mojom::EchoService>())
      death_loop_.Quit();
  }

  void OnServiceProcessCrashed(const ServiceProcessInfo& info) override {
    if (info.IsService<echo::mojom::EchoService>()) {
      ASSERT_EQ(info.site(), GURL(kTestUrl));
      crash_loop_.Quit();
    }
  }

  base::RunLoop launch_loop_;
  base::RunLoop death_loop_;
  base::RunLoop crash_loop_;
  base::Process process_;
};

IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, Launch) {
  EchoServiceProcessObserver observer;
  base::ProcessId pid_from_callback = base::kNullProcessId;
  base::RunLoop pid_loop;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>(
      ServiceProcessHost::Options()
          .WithProcessCallback(
              base::BindLambdaForTesting([&](const base::Process& process) {
                pid_from_callback = process.Pid();
                pid_loop.Quit();
              }))
          .Pass());
  observer.WaitForLaunch();
  pid_loop.Run();
  EXPECT_EQ(pid_from_callback, observer.pid());
  EXPECT_NE(base::kNullProcessId, pid_from_callback);

  const std::string kTestString =
      "Aurora borealis! At this time of year? At this time of day? "
      "In this part of the country? Localized entirely within your kitchen?";
  base::RunLoop loop;
  echo_service->EchoString(
      kTestString,
      base::BindLambdaForTesting([&](const std::string& echoed_input) {
        EXPECT_EQ(kTestString, echoed_input);
        loop.Quit();
      }));
  loop.Run();
}

IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, LocalDisconnectQuits) {
  EchoServiceProcessObserver observer;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>();
  observer.WaitForLaunch();
  echo_service.reset();
  observer.WaitForDeath();
}

IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, RemoteDisconnectQuits) {
  EchoServiceProcessObserver observer;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>();
  observer.WaitForLaunch();
  echo_service->Quit();
  observer.WaitForDeath();
}

IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, AllMessagesReceived) {
  // Verifies that messages sent right before disconnection are always received
  // and dispatched by the service before it self-terminates.
  EchoServiceProcessObserver observer;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>();

  const size_t kBufferSize = 256;
  const auto kMessages = std::to_array<std::string>({
      "I thought we were having steamed clams.",
      "D'oh, no! I said steamed hams. That's what I call hamburgers.",
      "You call hamburgers, \"steamed hams?\"",
      "Yes. It's a regional dialect.",
  });
  auto region = base::UnsafeSharedMemoryRegion::Create(kBufferSize);
  base::WritableSharedMemoryMapping mapping = region.Map();
  memset(mapping.memory(), 0, kBufferSize);

  // Send several messages, since it helps to verify a lack of raciness between
  // service-side message dispatch and service termination.
  for (const auto& message : kMessages) {
    ASSERT_LE(message.size(), kBufferSize);
    echo_service->EchoStringToSharedMemory(message, region.Duplicate());
  }
  echo_service.reset();
  observer.WaitForDeath();

  const std::string& kLastMessage = kMessages[std::size(kMessages) - 1];
  EXPECT_EQ(0,
            memcmp(mapping.memory(), kLastMessage.data(), kLastMessage.size()));
}

IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, ObserveCrash) {
  EchoServiceProcessObserver observer;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>(
      ServiceProcessHost::Options().WithSite(GURL(kTestUrl)).Pass());
  observer.WaitForLaunch();
  echo_service->Crash();
  observer.WaitForCrash();
}

IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, IdleTimeout) {
  EchoServiceProcessObserver observer;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>();

  base::RunLoop wait_for_idle_loop;
  constexpr auto kTimeout = base::Seconds(1);
  echo_service.set_idle_handler(kTimeout, base::BindLambdaForTesting([&] {
                                  wait_for_idle_loop.Quit();
                                  echo_service.reset();
                                }));

  // Send a message and wait for the reply. Once the message is sent we should
  // observe at least |kTimeout| time elapsing before the RunLoop quits, because
  // the service process must wait at least that long to report itself as idle.
  base::ElapsedTimer timer;
  const std::string kTestString =
      "Yes, and you call them steamed hams despite the fact that they are "
      "obviously grilled.";
  echo_service->EchoString(
      kTestString,
      base::BindLambdaForTesting([&](const std::string& echoed_input) {
        EXPECT_EQ(kTestString, echoed_input);
      }));
  wait_for_idle_loop.Run();
  EXPECT_GE(timer.Elapsed(), kTimeout);

  // And since the idle handler resets |echo_service|, we should imminently see
  // normal service process termination.
  observer.WaitForDeath();
}

#if BUILDFLAG(IS_WIN)
IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, PreloadLibraryNotSet) {
  EchoServiceProcessObserver observer;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>(
      ServiceProcessHost::Options().Pass());
  observer.WaitForLaunch();

  base::RunLoop loop;
  echo_service->LoadNativeLibrary(
      GetDllPath(kEchoPreloadLibrary), /*call_sec32_delayload=*/false,
      base::BindLambdaForTesting([&](LoadStatus status, uint32_t result) {
        EXPECT_EQ(LoadStatus::kFailedLoadLibrary, status);
        EXPECT_EQ(DWORD{ERROR_ACCESS_DENIED}, result);
        loop.Quit();
      }));
  loop.Run();
}

IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, PreloadLibraryPreloaded) {
  std::vector<base::FilePath> preloads;
  preloads.push_back(GetDllPath(kEchoPreloadLibrary));

  EchoServiceProcessObserver observer;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>(
      ServiceProcessHost::Options()
          .WithPreloadedLibraries(
              preloads, ServiceProcessHostPreloadLibraries::GetPassKey())
          .Pass());
  observer.WaitForLaunch();

  base::RunLoop loop;
  echo_service->LoadNativeLibrary(
      GetDllPath(kEchoPreloadLibrary),
      /*call_sec32_delayload=*/true,
      base::BindLambdaForTesting([&](LoadStatus status, uint32_t result) {
        EXPECT_EQ(LoadStatus::kSuccess, status);
        EXPECT_EQ(0u, result);
        loop.Quit();
      }));
  loop.Run();
}

IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, PreloadLibraryMultiple) {
  std::vector<base::FilePath> preloads;
  // dbghelp is a placeholder - it will likely be loaded already - this test is
  // validating that multiple libraries can be sent into the child.
  preloads.push_back(GetDllPath(L"dbghelp"));
  preloads.push_back(GetDllPath(kEchoPreloadLibrary));

  EchoServiceProcessObserver observer;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>(
      ServiceProcessHost::Options()
          .WithPreloadedLibraries(
              preloads, ServiceProcessHostPreloadLibraries::GetPassKey())
          .Pass());
  observer.WaitForLaunch();

  base::RunLoop loop;
  echo_service->LoadNativeLibrary(
      GetDllPath(kEchoPreloadLibrary), /*call_sec32_delayload=*/false,
      base::BindLambdaForTesting([&](LoadStatus status, uint32_t result) {
        EXPECT_EQ(LoadStatus::kSuccess, status);
        EXPECT_EQ(0u, result);
        loop.Quit();
      }));
  loop.Run();
}

IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, PreloadLibraryModName) {
  std::vector<base::FilePath> preloads;
  preloads.push_back(GetDllPath(kEchoPreloadLibrary));

  EchoServiceProcessObserver observer;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>(
      ServiceProcessHost::Options()
          .WithPreloadedLibraries(
              preloads, ServiceProcessHostPreloadLibraries::GetPassKey())
          .Pass());
  observer.WaitForLaunch();

  base::RunLoop loop;
  // Once preloaded can people simply provide the module name?
  echo_service->LoadNativeLibrary(
      base::FilePath(kEchoPreloadLibrary), /*call_sec32_delayload=*/false,
      base::BindLambdaForTesting([&](LoadStatus status, uint32_t result) {
        EXPECT_EQ(LoadStatus::kSuccess, status);
        EXPECT_EQ(0u, result);
        loop.Quit();
      }));
  loop.Run();
}

// This test causes a CHECK in the child at startup.
IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, PreloadLibraryBadPath) {
  std::vector<base::FilePath> preloads;
  preloads.push_back(GetDllPath(L"this-is-not-a-library"));

  EchoServiceProcessObserver observer;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>(
      ServiceProcessHost::Options()
          .WithSite(GURL(kTestUrl))  // For WaitForCrash().
          .WithPreloadedLibraries(
              preloads, ServiceProcessHostPreloadLibraries::GetPassKey())
          .Pass());
  observer.WaitForLaunch();
  observer.WaitForCrash();
}
#endif  // BUILDFLAG(IS_WIN)

}  // namespace content