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

#include "chrome/browser/process_singleton.h"

#include <windows.h>

#include <memory>
#include <string>

#include "base/check.h"
#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/notreached.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/process/process_handle.h"
#include "base/strings/string_number_conversions_win.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/multiprocess_test.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/win/scoped_handle.h"
#include "base/win/wrapped_window_proc.h"
#include "chrome/browser/win/chrome_process_finder.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_switches.h"
#include "content/public/common/result_codes.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"

namespace {

const char kReadyEventNameFlag[] = "ready_event_name";
const char kContinueEventNameFlag[] = "continue_event_name";
const char kCreateWindowFlag[] = "create_window";
const int kErrorResultCode = 0x345;

const char kLockfile[] = "lockfile";

bool NotificationCallback(base::CommandLine command_line,
                          const base::FilePath& current_directory) {
  // This is never called in this test, but would signal that the singleton
  // notification was successfully handled.
  NOTREACHED();
}

// The ProcessSingleton kills hung browsers with no visible windows without user
// interaction. If a hung browser has visible UI, however, it asks the user
// first.
// This class is the very minimal implementation to create a visible window
// in the hung test process to allow testing the latter path.
class ScopedVisibleWindow {
 public:
  ScopedVisibleWindow() : class_(0), window_(NULL) {}

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

  ~ScopedVisibleWindow() {
    if (window_)
      ::DestroyWindow(window_);
    if (class_)
      ::UnregisterClass(reinterpret_cast<LPCWSTR>(class_), NULL);
  }

  bool Create() {
    WNDCLASSEX wnd_cls = {0};
    base::win::InitializeWindowClass(
        L"ProcessSingletonTest", base::win::WrappedWindowProc<::DefWindowProc>,
        0,     // style
        0,     // class_extra
        0,     // window_extra
        NULL,  // cursor
        NULL,  // background
        NULL,  // menu_name
        NULL,  // large_icon
        NULL,  // small_icon
        &wnd_cls);

    class_ = ::RegisterClassEx(&wnd_cls);
    if (!class_)
      return false;
    window_ = ::CreateWindow(reinterpret_cast<LPCWSTR>(class_), 0, WS_POPUP, 0,
                             0, 0, 0, 0, 0, NULL, 0);
    if (!window_)
      return false;
    ::ShowWindow(window_, SW_SHOW);

    DCHECK(window_);
    return true;
  }

 private:
  ATOM class_;
  HWND window_;
};

MULTIPROCESS_TEST_MAIN(ProcessSingletonTestProcessMain) {
  base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
  base::FilePath user_data_dir =
      cmd_line->GetSwitchValuePath(switches::kUserDataDir);
  if (user_data_dir.empty())
    return kErrorResultCode;

  std::wstring ready_event_name =
      cmd_line->GetSwitchValueNative(kReadyEventNameFlag);

  base::win::ScopedHandle ready_event(
      ::OpenEvent(EVENT_MODIFY_STATE, FALSE, ready_event_name.c_str()));
  if (!ready_event.is_valid()) {
    return kErrorResultCode;
  }

  std::wstring continue_event_name =
      cmd_line->GetSwitchValueNative(kContinueEventNameFlag);

  base::win::ScopedHandle continue_event(
      ::OpenEvent(SYNCHRONIZE, FALSE, continue_event_name.c_str()));
  if (!continue_event.is_valid()) {
    return kErrorResultCode;
  }

  ScopedVisibleWindow visible_window;
  if (cmd_line->HasSwitch(kCreateWindowFlag)) {
    if (!visible_window.Create())
      return kErrorResultCode;
  }

  // Instantiate the process singleton.
  ProcessSingleton process_singleton(
      user_data_dir, base::BindRepeating(&NotificationCallback));

  if (!process_singleton.Create())
    return kErrorResultCode;

  // Signal ready and block for the continue event.
  if (!::SetEvent(ready_event.get())) {
    return kErrorResultCode;
  }

  if (::WaitForSingleObject(continue_event.get(), INFINITE) != WAIT_OBJECT_0) {
    return kErrorResultCode;
  }

  return 0;
}

// This fixture is for testing the Windows platform-specific failure modes
// of rendezvous, specifically the ones where the singleton-owning process
// is hung.
class ProcessSingletonTest : public base::MultiProcessTest {
 public:
  ProcessSingletonTest(const ProcessSingletonTest&) = delete;
  ProcessSingletonTest& operator=(const ProcessSingletonTest&) = delete;

 protected:
  enum WindowOption { kWithWindow, kNoWindow };

  ProcessSingletonTest()
      : window_option_(kNoWindow), should_kill_called_(false) {}

  void SetUp() override {
    ASSERT_NO_FATAL_FAILURE(base::MultiProcessTest::SetUp());

    // Drop the process finder notification timeout to one second for testing.
    old_notification_timeout_ =
        SetNotificationTimeoutForTesting(base::Seconds(1));
  }

  void TearDown() override {
    SetNotificationTimeoutForTesting(old_notification_timeout_);

    if (browser_victim_.IsValid()) {
      EXPECT_TRUE(::SetEvent(continue_event_.get()));
      EXPECT_TRUE(browser_victim_.WaitForExit(nullptr));
    }

    base::MultiProcessTest::TearDown();
  }

  void LaunchHungBrowserProcess(WindowOption window_option) {
    // Create a unique user data dir to rendezvous on.
    ASSERT_TRUE(user_data_dir_.CreateUniqueTempDir());

    // Create the named "ready" event, this is unique to our process.
    ready_event_name_ =
        L"ready-event-" + base::NumberToWString(base::GetCurrentProcId());
    base::win::ScopedHandle ready_event(
        ::CreateEvent(NULL, TRUE, FALSE, ready_event_name_.c_str()));
    ASSERT_TRUE(ready_event.is_valid());

    // Create the named "continue" event, this is unique to our process.
    continue_event_name_ =
        L"continue-event-" + base::NumberToWString(base::GetCurrentProcId());
    continue_event_.Set(
        ::CreateEvent(NULL, TRUE, FALSE, continue_event_name_.c_str()));
    ASSERT_TRUE(continue_event_.is_valid());

    window_option_ = window_option;

    base::LaunchOptions options;
    options.start_hidden = true;
    browser_victim_ =
        SpawnChildWithOptions("ProcessSingletonTestProcessMain", options);

    // Wait for the ready event (or process exit).
    HANDLE handles[] = {ready_event.get(), browser_victim_.Handle()};
    // The wait should always return because either |ready_event| is signaled or
    // |browser_victim_| died unexpectedly or exited on error.
    DWORD result =
        ::WaitForMultipleObjects(std::size(handles), handles, FALSE, INFINITE);
    ASSERT_EQ(WAIT_OBJECT_0, result);
  }

  base::CommandLine MakeCmdLine(const std::string& procname) override {
    base::CommandLine cmd_line = base::MultiProcessTest::MakeCmdLine(procname);

    cmd_line.AppendSwitchPath(switches::kUserDataDir, user_data_dir_.GetPath());
    cmd_line.AppendSwitchNative(kReadyEventNameFlag, ready_event_name_);
    cmd_line.AppendSwitchNative(kContinueEventNameFlag, continue_event_name_);
    if (window_option_ == kWithWindow) {
      cmd_line.AppendSwitch(kCreateWindowFlag);
    }

    return cmd_line;
  }

  void PrepareTest(WindowOption window_option, bool allow_kill) {
    ASSERT_NO_FATAL_FAILURE(LaunchHungBrowserProcess(window_option));

    // The ready event has been signalled - the process singleton is held by
    // the hung sub process.
    test_singleton_ = std::make_unique<ProcessSingleton>(
        user_data_dir(), base::BindRepeating(&NotificationCallback));

    test_singleton_->OverrideShouldKillRemoteProcessCallbackForTesting(
        base::BindRepeating(&ProcessSingletonTest::MockShouldKillRemoteProcess,
                            base::Unretained(this), allow_kill));
  }

  base::Process* browser_victim() { return &browser_victim_; }
  const base::FilePath& user_data_dir() const {
    return user_data_dir_.GetPath();
  }
  ProcessSingleton* test_singleton() const { return test_singleton_.get(); }
  bool should_kill_called() const { return should_kill_called_; }

  const base::HistogramTester& histogram_tester() const {
    return histogram_tester_;
  }

 private:
  bool MockShouldKillRemoteProcess(bool allow_kill) {
    should_kill_called_ = true;
    return allow_kill;
  }

  std::wstring ready_event_name_;
  std::wstring continue_event_name_;

  WindowOption window_option_;
  base::ScopedTempDir user_data_dir_;
  base::Process browser_victim_;
  base::win::ScopedHandle continue_event_;

  std::unique_ptr<ProcessSingleton> test_singleton_;

  base::TimeDelta old_notification_timeout_;
  bool should_kill_called_;
  base::HistogramTester histogram_tester_;
};

}  // namespace

TEST_F(ProcessSingletonTest, KillsHungBrowserWithNoWindows) {
  ASSERT_NO_FATAL_FAILURE(PrepareTest(kNoWindow, false));

  // As the hung browser has no visible window, it'll be killed without
  // user interaction.
  ProcessSingleton::NotifyResult notify_result =
      test_singleton()->NotifyOtherProcessOrCreate();

  // The hung process was killed and the notification is equivalent to
  // a non existent process.
  ASSERT_EQ(ProcessSingleton::PROCESS_NONE, notify_result);

  // The should-kill callback should not have been called, as the "browser" does
  // not have visible window.
  EXPECT_FALSE(should_kill_called());

  histogram_tester().ExpectUniqueSample(
      "Chrome.ProcessSingleton.RemoteProcessInteractionResult",
      ProcessSingleton::TERMINATE_SUCCEEDED, 1u);
  histogram_tester().ExpectTotalCount(
      "Chrome.ProcessSingleton.TerminateProcessTime", 1u);
  histogram_tester().ExpectUniqueSample(
      "Chrome.ProcessSingleton.TerminationWaitErrorCode.Windows", 0, 1u);
  histogram_tester().ExpectUniqueSample(
      "Chrome.ProcessSingleton.RemoteHungProcessTerminateReason",
      ProcessSingleton::NO_VISIBLE_WINDOW_FOUND, 1u);

  // Verify that the hung browser has been terminated with the
  // RESULT_CODE_HUNG exit code.
  int exit_code = 0;
  EXPECT_TRUE(
      browser_victim()->WaitForExitWithTimeout(base::TimeDelta(), &exit_code));
  EXPECT_EQ(content::RESULT_CODE_HUNG, exit_code);
}

TEST_F(ProcessSingletonTest, DoesntKillWithoutUserPermission) {
  ASSERT_NO_FATAL_FAILURE(PrepareTest(kWithWindow, false));

  // As the hung browser has a visible window, this should query the user
  // before killing the hung process.
  ProcessSingleton::NotifyResult notify_result =
      test_singleton()->NotifyOtherProcessOrCreate();
  ASSERT_EQ(ProcessSingleton::PROCESS_NOTIFIED, notify_result);

  // The should-kill callback should have been called, as the "browser" has a
  // visible window.
  EXPECT_TRUE(should_kill_called());

  histogram_tester().ExpectUniqueSample(
      "Chrome.ProcessSingleton.RemoteProcessInteractionResult",
      ProcessSingleton::USER_REFUSED_TERMINATION, 1u);

  // Make sure the process hasn't been killed.
  int exit_code = 0;
  EXPECT_FALSE(
      browser_victim()->WaitForExitWithTimeout(base::TimeDelta(), &exit_code));
}

TEST_F(ProcessSingletonTest, KillWithUserPermission) {
  ASSERT_NO_FATAL_FAILURE(PrepareTest(kWithWindow, true));

  // As the hung browser has a visible window, this should query the user
  // before killing the hung process.
  ProcessSingleton::NotifyResult notify_result =
      test_singleton()->NotifyOtherProcessOrCreate();

  // The hung process was killed and the notification is equivalent to
  // a non existent process.
  ASSERT_EQ(ProcessSingleton::PROCESS_NONE, notify_result);

  // The should-kill callback should have been called, as the "browser" has a
  // visible window.
  EXPECT_TRUE(should_kill_called());

  histogram_tester().ExpectUniqueSample(
      "Chrome.ProcessSingleton.RemoteProcessInteractionResult",
      ProcessSingleton::TERMINATE_SUCCEEDED, 1u);
  histogram_tester().ExpectTotalCount(
      "Chrome.ProcessSingleton.TerminateProcessTime", 1u);
  histogram_tester().ExpectUniqueSample(
      "Chrome.ProcessSingleton.TerminationWaitErrorCode.Windows", 0, 1u);
  histogram_tester().ExpectUniqueSample(
      "Chrome.ProcessSingleton.RemoteHungProcessTerminateReason",
      ProcessSingleton::USER_ACCEPTED_TERMINATION, 1u);

  // Verify that the hung browser has been terminated with the
  // RESULT_CODE_HUNG exit code.
  int exit_code = 0;
  EXPECT_TRUE(
      browser_victim()->WaitForExitWithTimeout(base::TimeDelta(), &exit_code));
  EXPECT_EQ(content::RESULT_CODE_HUNG, exit_code);
}

// Verifies that during ProcessSingleton destruction, the message window
// is destroyed while the lockfile is STILL actively held by the exiting
// process.
TEST_F(ProcessSingletonTest, DeterministicDestructionOrder) {
  base::ScopedTempDir profile_dir;
  ASSERT_TRUE(profile_dir.CreateUniqueTempDir());

  // Initialize ProcessSingleton directly with base::NullCallback()
  auto ps = std::make_unique<ProcessSingleton>(profile_dir.GetPath(),
                                               base::NullCallback());

  // Acquire the lock (we become the master)
  ASSERT_TRUE(ps->Create());
  bool observer_ran = false;

  // Set the observer callback to intercept the exact middle of the destructor
  ps->SetOnWindowDestroyedCallbackForTesting(base::BindLambdaForTesting([&]() {
    observer_ran = true;
    // CHECKPOINT A: The window must be GONE.
    HWND hwnd = FindRunningChromeWindow(profile_dir.GetPath());
    EXPECT_EQ(hwnd, nullptr);
    // CHECKPOINT B: The exiting process MUST still hold the lockfile!
    base::FilePath lock_file_path =
        profile_dir.GetPath().AppendASCII(kLockfile);
    HANDLE lock = ::CreateFile(
        lock_file_path.value().c_str(), GENERIC_WRITE, 0,  // 0 = No sharing
        NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

    EXPECT_EQ(lock, INVALID_HANDLE_VALUE);
    EXPECT_EQ(::GetLastError(), static_cast<DWORD>(ERROR_SHARING_VIOLATION));
  }));

  // Destroy the singleton. This triggers the destructor and our observer
  // callback.
  ps.reset();

  // Verify the observer callback actually executed.
  EXPECT_TRUE(observer_ran);

  // CHECKPOINT C: Lock is released and the lockfile is deleted.
  base::FilePath lock_file_path = profile_dir.GetPath().AppendASCII(kLockfile);
  EXPECT_FALSE(base::PathExists(lock_file_path));
}

// Verifies that if the lock file is temporarily busy during startup,
// ProcessSingleton will successfully wait, retry, and acquire it once
// it is released by the terminating process.
// (Uses the Sleep Hook to run 100% synchronously and flake-free in 0ms).
TEST_F(ProcessSingletonTest, LockFileRetrySuccess) {
  base::ScopedTempDir profile_dir;
  ASSERT_TRUE(profile_dir.CreateUniqueTempDir());
  base::FilePath lock_file_path = profile_dir.GetPath().AppendASCII(kLockfile);

  // 1. Lock the lockfile exclusively ourselves to simulate the race.
  base::File external_lock(::CreateFile(
      lock_file_path.value().c_str(), GENERIC_WRITE, 0,  // 0 = No sharing
      NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL));
  ASSERT_TRUE(external_lock.IsValid());
  bool sleep_callback_ran = false;

  // 2. Set the sleep callback to release our lock SYNCHRONOUSLY
  // instead of physically sleeping the thread.
  ProcessSingleton::SetSleepCallbackForTesting(
      base::BindLambdaForTesting([&](base::TimeDelta delay) {
        sleep_callback_ran = true;
        external_lock.Close();
      }));
  absl::Cleanup cleanup = [&] {
    ProcessSingleton::SetSleepCallbackForTesting(base::NullCallback());
  };

  // 3. Create ProcessSingleton directly with base::NullCallback() and trigger
  // startup.
  ProcessSingleton ps(profile_dir.GetPath(), base::NullCallback());
  ProcessSingleton::NotifyResult result = ps.NotifyOtherProcessOrCreate();

  // 4. Verify it successfully started as the master.
  EXPECT_EQ(result, ProcessSingleton::PROCESS_NONE);

  // Verify that the retry loop was actually entered and the sleep hook ran.
  EXPECT_TRUE(sleep_callback_ran);

  histogram_tester().ExpectUniqueSample(
      "Chrome.ProcessSingleton.CreateLockFileWithTimeout.Result", true, 1);
}

// Verifies that if the lock file remains busy indefinitely, ProcessSingleton
// will eventually fail with LOCK_ERROR after the 5-second timeout.
// (Uses Mock Time + the Sleep Hook to run the 5-second timeout instantly in
// 0ms).
TEST_F(ProcessSingletonTest, LockFileTimeoutFailure) {
  // 1. Initialize TaskEnvironment with MOCK_TIME.
  base::test::TaskEnvironment task_environment(
      base::test::TaskEnvironment::TimeSource::MOCK_TIME);

  base::ScopedTempDir profile_dir;
  ASSERT_TRUE(profile_dir.CreateUniqueTempDir());
  base::FilePath lock_file_path = profile_dir.GetPath().AppendASCII(kLockfile);

  // 2. Lock the lockfile exclusively and keep it locked.
  base::File external_lock(::CreateFile(
      lock_file_path.value().c_str(), GENERIC_WRITE, 0,  // 0 = No sharing
      NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL));
  ASSERT_TRUE(external_lock.IsValid());

  // 3. Set the sleep callback to fast-forward the VIRTUAL clock
  // instead of physically sleeping the thread.
  ProcessSingleton::SetSleepCallbackForTesting(base::BindLambdaForTesting(
      [&](base::TimeDelta delay) { task_environment.FastForwardBy(delay); }));
  absl::Cleanup cleanup = [&] {
    ProcessSingleton::SetSleepCallbackForTesting(base::NullCallback());
  };

  base::TimeTicks start = base::TimeTicks::Now();

  // 4. Create ProcessSingleton and trigger startup.
  // This will loop 50 times (50 * 100ms = 5s of virtual time) and fail
  // instantly.
  ProcessSingleton ps(profile_dir.GetPath(), base::NullCallback());
  EXPECT_EQ(ps.NotifyOtherProcessOrCreate(), ProcessSingleton::LOCK_ERROR);

  // 5. Verify that 5 seconds of VIRTUAL time elapsed.
  EXPECT_GE(base::TimeTicks::Now() - start, base::Seconds(5));

  histogram_tester().ExpectUniqueSample(
      "Chrome.ProcessSingleton.CreateLockFileWithTimeout.Result", false, 1);
}
