// 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/compiler_specific.h"
#include "base/memory/shared_memory_mapping.h"
#include "base/memory/unsafe_shared_memory_region.h"
#include "base/memory/weak_ptr.h"
#include "base/process/process.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "build/blink_buildflags.h"
#include "build/build_config.h"
#include "content/public/browser/service_process_observer_hub.h"
#include "content/public/common/content_switches.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

#if (BUILDFLAG(IS_MAC) || (BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_IOS_TVOS)))
#include "content/public/browser/browser_child_process_host.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(); }
  // Returns true if the crash was a 'startup crash'.
  bool WaitForCrash() {
    crash_loop_.Run();
    EXPECT_TRUE(crashed_pre_ipc_.has_value());
    return *crashed_pre_ipc_;
  }

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

 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 {
    CHECK(info.crashed_pre_ipc().has_value());
    crashed_pre_ipc_ = info.crashed_pre_ipc().value();

    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_;
  std::optional<bool> crashed_pre_ipc_;
};

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();
  UNSAFE_TODO(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];
  UNSAFE_TODO(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();
  bool crashed_pre_ipc = observer.WaitForCrash();
  EXPECT_FALSE(crashed_pre_ipc);
}

// Verifies that a per-instance observer (WithObserver) receives notifications
// for its specific service process.
IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, PerInstanceObserver) {
  base::RunLoop launch_loop;
  base::RunLoop death_loop;

  class InstanceObserver : public ServiceProcessHost::Observer {
   public:
    void OnServiceProcessLaunched(const ServiceProcessInfo& info) override {
      launched = true;
      if (launch_quit) {
        std::move(launch_quit).Run();
      }
    }
    void OnServiceProcessTerminatedNormally(
        const ServiceProcessInfo& info) override {
      terminated = true;
      if (death_quit) {
        std::move(death_quit).Run();
      }
    }
    void OnServiceProcessCrashed(const ServiceProcessInfo& info) override {}

    bool launched = false;
    bool terminated = false;
    base::OnceClosure launch_quit;
    base::OnceClosure death_quit;
    base::WeakPtrFactory<InstanceObserver> weak_factory{this};
  };

  InstanceObserver instance_observer;
  instance_observer.launch_quit = launch_loop.QuitClosure();
  instance_observer.death_quit = death_loop.QuitClosure();

  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>(
      ServiceProcessHost::Options()
          .WithObserver(instance_observer.weak_factory.GetWeakPtr())
          .Pass());
  launch_loop.Run();
  EXPECT_TRUE(instance_observer.launched);

  echo_service.reset();
  death_loop.Run();
  EXPECT_TRUE(instance_observer.terminated);
}

// Verifies that a per-instance observer does NOT fire for a different service.
IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest,
                       PerInstanceObserverIsolation) {
  class TrackingObserver : public ServiceProcessHost::Observer {
   public:
    void OnServiceProcessLaunched(const ServiceProcessInfo& info) override {
      launch_count++;
      if (launch_quit) {
        std::move(launch_quit).Run();
      }
    }
    void OnServiceProcessTerminatedNormally(
        const ServiceProcessInfo& info) override {
      terminate_count++;
      if (terminate_quit) {
        std::move(terminate_quit).Run();
      }
    }
    void OnServiceProcessCrashed(const ServiceProcessInfo& info) override {}

    int launch_count = 0;
    int terminate_count = 0;
    base::OnceClosure launch_quit;
    base::OnceClosure terminate_quit;
    base::WeakPtrFactory<TrackingObserver> weak_factory{this};
  };

  TrackingObserver observer1;
  TrackingObserver observer2;

  base::RunLoop launch1_loop;
  base::RunLoop launch2_loop;
  base::RunLoop death1_loop;
  base::RunLoop death2_loop;
  observer1.launch_quit = launch1_loop.QuitClosure();
  observer2.launch_quit = launch2_loop.QuitClosure();
  observer1.terminate_quit = death1_loop.QuitClosure();
  observer2.terminate_quit = death2_loop.QuitClosure();

  // Launch two services, each with its own per-instance observer.
  auto echo1 = ServiceProcessHost::Launch<echo::mojom::EchoService>(
      ServiceProcessHost::Options()
          .WithObserver(observer1.weak_factory.GetWeakPtr())
          .Pass());
  auto echo2 = ServiceProcessHost::Launch<echo::mojom::EchoService>(
      ServiceProcessHost::Options()
          .WithObserver(observer2.weak_factory.GetWeakPtr())
          .Pass());

  // Wait for both launches.
  launch1_loop.Run();
  launch2_loop.Run();

  // Each observer should have seen exactly one launch — its own.
  EXPECT_EQ(1, observer1.launch_count);
  EXPECT_EQ(1, observer2.launch_count);

  // Terminate echo2 — only observer2 should fire.
  echo2.reset();
  death2_loop.Run();

  EXPECT_EQ(0, observer1.terminate_count);
  EXPECT_EQ(1, observer2.terminate_count);

  // Terminate echo1 — only observer1 should fire.
  echo1.reset();
  death1_loop.Run();

  EXPECT_EQ(1, observer1.terminate_count);
  EXPECT_EQ(1, observer2.terminate_count);
}

// Pre-IPC crash detection is only available on Windows.
#if BUILDFLAG(IS_WIN)
IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, ObservePreIpcCrash) {
  EchoServiceProcessObserver observer;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>(
      ServiceProcessHost::Options()
          .WithSite(GURL(kTestUrl))
          .WithExtraCommandLineSwitches(
              {switches::kUtilityImmediateCrashForTesting})
          .Pass());
  observer.WaitForLaunch();
  bool crashed_pre_ipc = observer.WaitForCrash();
  EXPECT_TRUE(crashed_pre_ipc);
}
#endif  // #if BUILDFLAG(IS_WIN)

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_winmm_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_winmm_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_winmm_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_winmm_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)

IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, UtilityCheckIsTest) {
  mojo::Remote<echo::mojom::EchoService> echo_service;
  content::ServiceProcessHost::Launch(
      echo_service.BindNewPipeAndPassReceiver());
  base::test::TestFuture<bool> future;
  echo_service->VerifyCheckIsTest(future.GetCallback());
  EXPECT_TRUE(future.Get());
}

IN_PROC_BROWSER_TEST_F(ServiceProcessHostBrowserTest, Priority) {
#if BUILDFLAG(IS_ANDROID)
  // Process priority elevation is not supported on Android utility processes.
  GTEST_SKIP();
#else
  if (!base::Process::CanSetPriority()) {
    GTEST_SKIP()
        << "Setting process priority is not supported on this platform.";
  }

  EchoServiceProcessObserver observer;
  auto echo_service = ServiceProcessHost::Launch<echo::mojom::EchoService>(
      ServiceProcessHost::Options()
          .WithPriority(base::Process::Priority::kUserBlocking)
          .Pass());
  observer.WaitForLaunch();
  base::Process::Priority priority = observer.process().GetPriority(
#if (BUILDFLAG(IS_MAC) || (BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_IOS_TVOS)))
      content::BrowserChildProcessHost::GetPortProvider()
#endif
  );
  EXPECT_EQ(base::Process::Priority::kUserBlocking, priority);
#endif
}

}  // namespace content
