// Copyright 2022 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/browser_child_process_observer.h"

#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "build/build_config.h"
#include "content/browser/browser_child_process_host_impl.h"
#include "content/browser/child_process_host_impl.h"
#include "content/browser/utility_process_host.h"
#include "content/public/browser/browser_child_process_host.h"
#include "content/public/browser/browser_child_process_host_delegate.h"
#include "content/public/browser/child_process_data.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/process_type.h"
#include "content/public/common/sandboxed_process_launcher_delegate.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/test_service.mojom.h"
#include "sandbox/policy/sandbox_type.h"
#include "testing/gmock/include/gmock/gmock.h"

namespace content {

namespace {

// An enum that represent the different type of notitifcations that exist in
// BrowserChildProcessObserver.
enum class Notification {
  kLaunchedAndConnected,
  kDisconnected,
  kCrashed,
  kKilled,
  kLaunchFailed,
  kExitedNormally,
};

// Nicer test output.
std::ostream& operator<<(std::ostream& os, Notification notification) {
  switch (notification) {
    case Notification::kLaunchedAndConnected:
      os << "LaunchedAndConnected";
      break;
    case Notification::kDisconnected:
      os << "Disconnected";
      break;
    case Notification::kCrashed:
      os << "Crashed";
      break;
    case Notification::kKilled:
      os << "Killed";
      break;
    case Notification::kLaunchFailed:
      os << "LaunchFailed";
      break;
    case Notification::kExitedNormally:
      os << "ExitedNormally";
      break;
  }
  return os;
}

// Returns true if a child process whose ID is |child_id| is still alive.
bool IsHostAlive(int child_id) {
  return BrowserChildProcessHost::FromID(child_id) != nullptr;
}

}  // namespace

// A test BrowserChildProcessObserver that transforms every call to one of the
// observer's method to a call to the notification callback.
class BrowserChildProcessNotificationObserver
    : public BrowserChildProcessObserver {
 public:
  using OnNotificationCallback =
      base::RepeatingCallback<void(Notification notification)>;

  BrowserChildProcessNotificationObserver(
      int child_id,
      OnNotificationCallback on_notification_callback)
      : child_id_(child_id),
        on_notification_callback_(std::move(on_notification_callback)) {
    BrowserChildProcessObserver::Add(this);
  }

  ~BrowserChildProcessNotificationObserver() override {
    BrowserChildProcessObserver::Remove(this);
  }

 protected:
  // BrowserChildProcessObserver:
  void BrowserChildProcessLaunchedAndConnected(
      const ChildProcessData& data) override {
    OnNotification(data, Notification::kLaunchedAndConnected);
  }
  void BrowserChildProcessHostDisconnected(
      const ChildProcessData& data) override {
    OnNotification(data, Notification::kDisconnected);
  }
  void BrowserChildProcessCrashed(
      const ChildProcessData& data,
      const ChildProcessTerminationInfo& info) override {
    OnNotification(data, Notification::kCrashed);
  }
  void BrowserChildProcessKilled(
      const ChildProcessData& data,
      const ChildProcessTerminationInfo& info) override {
    OnNotification(data, Notification::kKilled);
  }
  void BrowserChildProcessLaunchFailed(
      const ChildProcessData& data,
      const ChildProcessTerminationInfo& info) override {
    OnNotification(data, Notification::kLaunchFailed);
  }
  void BrowserChildProcessExitedNormally(
      const ChildProcessData& data,
      const ChildProcessTerminationInfo& info) override {
    OnNotification(data, Notification::kExitedNormally);
  }

  void OnNotification(const ChildProcessData& data, Notification notification) {
    if (data.id == child_id_)
      on_notification_callback_.Run(notification);
  }

 private:
  // Every notification coming for a child with a different ID will be ignored.
  int child_id_;

  // The callback to invoke every time a method of the observer is called.
  OnNotificationCallback on_notification_callback_;
};

// A helper class that allows the user to wait until a specific |notification|
// is sent for a child process whose ID matches |child_id|.
class WaitForNotificationObserver {
 public:
  WaitForNotificationObserver(int child_id, Notification notification)
      : inner_observer_(
            child_id,
            base::BindRepeating(&WaitForNotificationObserver::OnNotification,
                                base::Unretained(this))),
        notification_(notification) {}

  ~WaitForNotificationObserver() = default;

  // Waits until the notification is received. Returns immediately if it was
  // already received.
  void Wait() {
    if (notification_received_)
      return;

    DCHECK(!run_loop_.running());
    run_loop_.Run();
  }

 private:
  void OnNotification(Notification notification) {
    if (notification != notification_)
      return;

    notification_received_ = true;
    if (run_loop_.running())
      run_loop_.Quit();
  }

  BrowserChildProcessNotificationObserver inner_observer_;
  Notification notification_;
  base::RunLoop run_loop_;
  bool notification_received_ = false;
};

class TestSandboxedProcessLauncherDelegate
    : public SandboxedProcessLauncherDelegate {
 public:
  explicit TestSandboxedProcessLauncherDelegate(
      sandbox::mojom::Sandbox sandbox_type)
      : sandbox_type_(sandbox_type) {}
  ~TestSandboxedProcessLauncherDelegate() override = default;

  // SandboxedProcessLauncherDelegate:
  sandbox::mojom::Sandbox GetSandboxType() override { return sandbox_type_; }

 private:
  sandbox::mojom::Sandbox sandbox_type_;
};

// A test-specific type of process host. Self-owned.
class TestProcessHost : public BrowserChildProcessHostDelegate {
 public:
  static base::WeakPtr<TestProcessHost> Create() {
    auto* instance = new TestProcessHost();
    return instance->GetWeakPtr();
  }

  TestProcessHost()
      : process_(BrowserChildProcessHost::Create(
            PROCESS_TYPE_UTILITY,
            this,
            ChildProcessHost::IpcMode::kNormal)) {}
  ~TestProcessHost() override = default;

  // Returns the ID of the child process.
  int GetId() { return process_->GetData().id; }

  // Binds to the test service on the child process and returns the bound
  // remote.
  mojo::Remote<mojom::TestService> BindTestService() {
    mojo::Remote<mojom::TestService> test_service;

    static_cast<ChildProcessHostImpl*>(process_->GetHost())
        ->child_process()
        ->BindServiceInterface(test_service.BindNewPipeAndPassReceiver());

    return test_service;
  }

  // Returns the command line used to launch the child process.
  std::unique_ptr<base::CommandLine> GetChildCommandLine() {
    base::FilePath child_path =
        ChildProcessHost::GetChildPath(ChildProcessHost::CHILD_NORMAL);
    auto command_line = std::make_unique<base::CommandLine>(child_path);

    command_line->AppendSwitchASCII(switches::kProcessType,
                                    switches::kUtilityProcess);
    command_line->AppendSwitchASCII(switches::kUtilitySubType,
                                    "Test Utility Process");
    sandbox::policy::SetCommandLineFlagsForSandboxType(command_line.get(),
                                                       sandbox_type_);

    return command_line;
  }

  // Launches the child process using the default test launcher delegate.
  void LaunchProcess() {
    LaunchProcessWithDelegate(
        std::make_unique<TestSandboxedProcessLauncherDelegate>(sandbox_type_));
  }

  // Launches the child process using a supplied sandbox delegate.
  void LaunchProcessWithDelegate(
      std::unique_ptr<SandboxedProcessLauncherDelegate>
          sandboxed_process_launcher_delegate) {
    process_->SetName(u"Test utility process");

    auto command_line = GetChildCommandLine();
    bool terminate_on_shutdown = true;

    process_->Launch(std::move(sandboxed_process_launcher_delegate),
                     std::move(command_line), terminate_on_shutdown);

    test_service_ = BindTestService();
  }

  // Requests the child process to shutdown.
  void ForceShutdown() { process_->GetHost()->ForceShutdown(); }

  // Disconnects the bound remote from the test service.
  void Disconnect() { test_service_.reset(); }

  // Sets the sandbox type to use for the child process.
  void SetSandboxType(sandbox::mojom::Sandbox sandbox_type) {
    sandbox_type_ = sandbox_type;
  }

  mojom::TestService* service() const { return test_service_.get(); }

  base::WeakPtr<TestProcessHost> GetWeakPtr() {
    return weak_ptr_factory_.GetWeakPtr();
  }

 private:
  sandbox::mojom::Sandbox sandbox_type_ = sandbox::mojom::Sandbox::kUtility;

  std::unique_ptr<BrowserChildProcessHost> process_;

  mojo::Remote<mojom::TestService> test_service_;

  base::WeakPtrFactory<TestProcessHost> weak_ptr_factory_{this};
};

// A helper class that exposes which notifications were sent for a specific
// child process.
class TestBrowserChildProcessObserver {
 public:
  explicit TestBrowserChildProcessObserver(int child_id)
      : inner_observer_(child_id,
                        base::BindRepeating(
                            &TestBrowserChildProcessObserver::OnNotification,
                            base::Unretained(this))) {}

  ~TestBrowserChildProcessObserver() = default;

  // Returns the notifications received for |child_id|.
  const std::vector<Notification>& notifications() const {
    return notifications_;
  }

 private:
  void OnNotification(Notification notification) {
    notifications_.push_back(notification);
  }

  BrowserChildProcessNotificationObserver inner_observer_;

  std::vector<Notification> notifications_;
};

class BrowserChildProcessObserverBrowserTest : public ContentBrowserTest {};

// Tests that launching and then using ForceShutdown() results in a normal
// termination.
#if defined(ADDRESS_SANITIZER)
// TODO(crbug.com/40238612): Fix ASAN failures on trybot.
#define MAYBE_LaunchAndForceShutdown DISABLED_LaunchAndForceShutdown
#else
#define MAYBE_LaunchAndForceShutdown LaunchAndForceShutdown
#endif
IN_PROC_BROWSER_TEST_F(BrowserChildProcessObserverBrowserTest,
                       MAYBE_LaunchAndForceShutdown) {
  base::WeakPtr<TestProcessHost> host = TestProcessHost::Create();
  int child_id = host->GetId();

  TestBrowserChildProcessObserver observer(child_id);

  {
    WaitForNotificationObserver waiter(child_id,
                                       Notification::kLaunchedAndConnected);
    host->LaunchProcess();
    waiter.Wait();
  }

  {
    WaitForNotificationObserver waiter(child_id, Notification::kDisconnected);
    host->ForceShutdown();
    waiter.Wait();
  }

  Notification kExitNotification =
#if BUILDFLAG(IS_ANDROID)
      // TODO(pmonette): On Android, this currently causes a killed
      // notification. Consider fixing.
      Notification::kKilled;
#else
      Notification::kExitedNormally;
#endif  // BUILDFLAG(IS_ANDROID)

  // The host should be deleted now.
  EXPECT_FALSE(host);
  EXPECT_FALSE(IsHostAlive(child_id));
  EXPECT_THAT(observer.notifications(),
              testing::ElementsAreArray({Notification::kLaunchedAndConnected,
                                         kExitNotification,
                                         Notification::kDisconnected}));
}

// Tests that launching and then deleting the host results in a normal
// termination.
IN_PROC_BROWSER_TEST_F(BrowserChildProcessObserverBrowserTest,
                       LaunchAndDelete) {
  base::WeakPtr<TestProcessHost> host = TestProcessHost::Create();
  int child_id = host->GetId();

  TestBrowserChildProcessObserver observer(child_id);

  {
    WaitForNotificationObserver waiter(child_id,
                                       Notification::kLaunchedAndConnected);
    host->LaunchProcess();
    waiter.Wait();
  }

  {
    WaitForNotificationObserver waiter(child_id, Notification::kDisconnected);
    delete host.get();
    waiter.Wait();
  }

  // The host should be deleted now.
  EXPECT_FALSE(host);
  EXPECT_FALSE(IsHostAlive(child_id));
  EXPECT_THAT(observer.notifications(),
              testing::ElementsAreArray({Notification::kLaunchedAndConnected,
                                         Notification::kExitedNormally,
                                         Notification::kDisconnected}));
}

// Tests that launching and then disconnecting the service channel results in a
// normal termination.
// Note: This only works for services bound using BindServiceInterface(), not
// BindReceiver().
#if defined(ADDRESS_SANITIZER)
// TODO(crbug.com/40238612): Fix ASAN failures on trybot.
#define MAYBE_LaunchAndDisconnect DISABLED_LaunchAndDisconnect
#else
#define MAYBE_LaunchAndDisconnect LaunchAndDisconnect
#endif
IN_PROC_BROWSER_TEST_F(BrowserChildProcessObserverBrowserTest,
                       MAYBE_LaunchAndDisconnect) {
  base::WeakPtr<TestProcessHost> host = TestProcessHost::Create();
  int child_id = host->GetId();

  TestBrowserChildProcessObserver observer(child_id);

  {
    WaitForNotificationObserver waiter(child_id,
                                       Notification::kLaunchedAndConnected);
    host->LaunchProcess();
    waiter.Wait();
  }

  {
    WaitForNotificationObserver waiter(child_id, Notification::kDisconnected);
    host->Disconnect();
    waiter.Wait();
  }

  Notification kExitNotification =
#if BUILDFLAG(IS_ANDROID)
      // On Android, kKilled is always sent in the case of a crash.
      Notification::kKilled;
#else
      Notification::kExitedNormally;
#endif  // BUILDFLAG(IS_ANDROID)

  // The host should be deleted now.
  EXPECT_FALSE(host);
  EXPECT_FALSE(IsHostAlive(child_id));
  EXPECT_THAT(observer.notifications(), testing::ElementsAreArray({
                                            Notification::kLaunchedAndConnected,
                                            kExitNotification,
                                            Notification::kDisconnected,
                                        }));
}

// Tests that launching and then causing a crash the host results in a crashed
// notification.
// TODO(crbug.com/40868150): Times out on Android tests.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_LaunchAndCrash DISABLED_LaunchAndCrash
#else
#define MAYBE_LaunchAndCrash LaunchAndCrash
#endif
IN_PROC_BROWSER_TEST_F(BrowserChildProcessObserverBrowserTest,
                       MAYBE_LaunchAndCrash) {
  base::WeakPtr<TestProcessHost> host = TestProcessHost::Create();
  int child_id = host->GetId();

  TestBrowserChildProcessObserver observer(child_id);

  {
    WaitForNotificationObserver waiter(child_id,
                                       Notification::kLaunchedAndConnected);
    host->LaunchProcess();
    waiter.Wait();
  }

  {
    WaitForNotificationObserver waiter(child_id, Notification::kDisconnected);
    host->service()->DoCrashImmediately(base::DoNothing());
    waiter.Wait();
  }

  Notification kCrashedNotification =
#if BUILDFLAG(IS_ANDROID)
      // On Android, kKilled is always sent in the case of a crash.
      Notification::kKilled;
#else
      Notification::kCrashed;
#endif  // BUILDFLAG(IS_ANDROID)

  // The host should be deleted now.
  EXPECT_FALSE(host);
  EXPECT_FALSE(IsHostAlive(child_id));
  EXPECT_THAT(observer.notifications(),
              testing::ElementsAreArray({Notification::kLaunchedAndConnected,
                                         kCrashedNotification,
                                         Notification::kDisconnected}));
}

// Tests that kLaunchFailed is correctly sent when the child process fails to
// launch.
//
// This test won't work as-is on POSIX platforms, where fork()+exec() is used to
// launch child processes, failure does not happen until exec(), therefore the
// test will see a valid child process followed by a
// TERMINATION_STATUS_ABNORMAL_TERMINATION of the forked process. However,
// posix_spawn() is used on macOS.
// See also ServiceProcessLauncherTest.FailToLaunchProcess.
#if !BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_MAC)
IN_PROC_BROWSER_TEST_F(BrowserChildProcessObserverBrowserTest, LaunchFailed) {
  base::WeakPtr<TestProcessHost> host = TestProcessHost::Create();
  int child_id = host->GetId();

#if BUILDFLAG(IS_WIN)
  // The Windows sandbox does not like the child process being a different
  // process, so launch unsandboxed for the purpose of this test.
  host->SetSandboxType(sandbox::mojom::Sandbox::kNoSandbox);
#endif

  // Simulate a catastrophic launch failure for all child processes by
  // making the path to the process non-existent.
  base::CommandLine::ForCurrentProcess()->AppendSwitchPath(
      switches::kBrowserSubprocessPath,
      base::FilePath(FILE_PATH_LITERAL("non_existent_path")));

  TestBrowserChildProcessObserver observer(child_id);

  {
    WaitForNotificationObserver waiter(child_id, Notification::kLaunchFailed);
    host->LaunchProcess();
    waiter.Wait();
  }

  // The host should be deleted now.
  EXPECT_FALSE(host);
  EXPECT_FALSE(IsHostAlive(child_id));
  EXPECT_THAT(observer.notifications(),
              testing::ElementsAreArray({Notification::kLaunchFailed}));
}
#endif  // !BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_MAC)

#if BUILDFLAG(IS_WIN)
class TestPreSpawnTargetFailureSandboxedProcessLauncherDelegate
    : public TestSandboxedProcessLauncherDelegate {
 public:
  using TestSandboxedProcessLauncherDelegate::
      TestSandboxedProcessLauncherDelegate;

  // SandboxedProcessLauncherDelegate:
  bool PreSpawnTarget(sandbox::TargetPolicy* policy) override {
    // Force a failure in PreSpawnTarget().
    return false;
  }
};

// Override the observer to verify the error occurred in PreSpawnTarget().
class TestPreSpawnTargetFailureBrowserChildProcessNotificationObserver
    : public BrowserChildProcessNotificationObserver {
 public:
  using BrowserChildProcessNotificationObserver::
      BrowserChildProcessNotificationObserver;

  // BrowserChildProcessObserver:
  void BrowserChildProcessLaunchFailed(
      const ChildProcessData& data,
      const ChildProcessTerminationInfo& info) override {
    EXPECT_EQ(info.exit_code, sandbox::SBOX_ERROR_DELEGATE_PRE_SPAWN);
    BrowserChildProcessNotificationObserver::OnNotification(
        data, Notification::kLaunchFailed);
  }
};

// Tests that a pre spawn failure results in a failed launch.
IN_PROC_BROWSER_TEST_F(BrowserChildProcessObserverBrowserTest,
                       LaunchPreSpawnFailed) {
  base::WeakPtr<TestProcessHost> host = TestProcessHost::Create();
  int child_id = host->GetId();

  TestBrowserChildProcessObserver observer(child_id);

  {
    WaitForNotificationObserver waiter(child_id, Notification::kLaunchFailed);
    host->LaunchProcessWithDelegate(
        std::make_unique<
            TestPreSpawnTargetFailureSandboxedProcessLauncherDelegate>(
            sandbox::mojom::Sandbox::kUtility));
    waiter.Wait();
  }

  // The host should be deleted now.
  EXPECT_FALSE(host);
  EXPECT_FALSE(IsHostAlive(child_id));
  EXPECT_THAT(observer.notifications(),
              testing::ElementsAreArray({Notification::kLaunchFailed}));
}
#endif  // BUILDFLAG(IS_WIN)

}  // namespace content