[headless] Prevent page handoff to an existing process.

Handing off pages to an existing Chrome process using the same profile
is the expected behavior for the Headfull Chrome, however, it is
unexpected and undesirable for the Headless Chrome.

This change modifies Native Headless Chrome behavior so that if there is
an existing Chrome process using a profile and a new Native Headless
Chrome process is started using the same profile, the new process is
terminated with the Chrome result chrome::RESULT_CODE_PROFILE_IN_USE
and no page hand off.

Bug: 1278077
Change-Id: Id0e0278203f561d35bc4b8e36a5e381ef7686d91
Cq-Include-Trybots: luci.chromium.try:linux-headless-shell-rel
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3324603
Reviewed-by: Andrey Kosyakov <[email protected]>
Reviewed-by: Lei Zhang <[email protected]>
Commit-Queue: Peter Kvitek <[email protected]>
Cr-Commit-Position: refs/heads/main@{#950351}
diff --git a/chrome/browser/chrome_process_singleton.cc b/chrome/browser/chrome_process_singleton.cc
index c0abd23..d412a4e0 100644
--- a/chrome/browser/chrome_process_singleton.cc
+++ b/chrome/browser/chrome_process_singleton.cc
@@ -6,6 +6,8 @@
 
 #include <utility>
 
+#include "chrome/browser/headless/headless_mode_util.h"
+
 ChromeProcessSingleton::ChromeProcessSingleton(
     const base::FilePath& user_data_dir,
     const ProcessSingleton::NotificationCallback& notification_callback)
@@ -20,6 +22,13 @@
 
 ProcessSingleton::NotifyResult
     ChromeProcessSingleton::NotifyOtherProcessOrCreate() {
+  // In headless mode we don't want to hand off pages to an existing processes,
+  // so short circuit process singleton creation and bail out if we're not
+  // the only process using this user data dir.
+  if (headless::IsChromeNativeHeadless()) {
+    return process_singleton_.Create() ? ProcessSingleton::PROCESS_NONE
+                                       : ProcessSingleton::PROFILE_IN_USE;
+  }
   return process_singleton_.NotifyOtherProcessOrCreate();
 }
 
diff --git a/chrome/browser/chrome_process_singleton.h b/chrome/browser/chrome_process_singleton.h
index 3a2e52c1..7f33028 100644
--- a/chrome/browser/chrome_process_singleton.h
+++ b/chrome/browser/chrome_process_singleton.h
@@ -39,6 +39,9 @@
   // instance. Callers are guaranteed to either have notified an existing
   // process or have grabbed the singleton (unless the profile is locked by an
   // unreachable process).
+  // The guarantee is a bit different if we're running in Native Headless mode,
+  // in which case an existing process is not notified and this method returns
+  // PROFILE_IN_USE if it happens to use the same profile directory.
   ProcessSingleton::NotifyResult NotifyOtherProcessOrCreate();
 
   // Clear any lock state during shutdown.
diff --git a/chrome/browser/headless/headless_mode_browsertest.cc b/chrome/browser/headless/headless_mode_browsertest.cc
index b9b5aec..c8e3265 100644
--- a/chrome/browser/headless/headless_mode_browsertest.cc
+++ b/chrome/browser/headless/headless_mode_browsertest.cc
@@ -8,13 +8,25 @@
 #include <string>
 
 #include "base/command_line.h"
+#include "base/files/file.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/test/multiprocess_test.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_timeouts.h"
 #include "build/build_config.h"
+#include "chrome/browser/chrome_process_singleton.h"
+#include "chrome/browser/process_singleton.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_window.h"
+#include "chrome/common/chrome_switches.h"
 #include "chrome/test/base/in_process_browser_test.h"
+#include "content/public/test/browser_task_environment.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "testing/gmock/include/gmock/gmock.h"
+#include "testing/multiprocess_func_list.h"
 #include "ui/gfx/switches.h"
 
 #if defined(OS_LINUX)
@@ -23,6 +35,7 @@
 
 namespace {
 const char kChrome[] = "chrome";
+const int kErrorResultCode = -1;
 }  // namespace
 
 class HeadlessModeBrowserTest : public InProcessBrowserTest {
@@ -64,3 +77,91 @@
   EXPECT_FALSE(browser()->window()->IsVisible());
 }
 #endif  // defined(OS_WIN)
+
+class HeadlessModeBrowserTestWithUserDataDir : public HeadlessModeBrowserTest {
+ public:
+  HeadlessModeBrowserTestWithUserDataDir() = default;
+
+  HeadlessModeBrowserTestWithUserDataDir(
+      const HeadlessModeBrowserTestWithUserDataDir&) = delete;
+  HeadlessModeBrowserTestWithUserDataDir& operator=(
+      const HeadlessModeBrowserTestWithUserDataDir&) = delete;
+
+  ~HeadlessModeBrowserTestWithUserDataDir() override = default;
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    HeadlessModeBrowserTest::SetUpCommandLine(command_line);
+
+    ASSERT_TRUE(user_data_dir_.CreateUniqueTempDir());
+    ASSERT_TRUE(base::IsDirectoryEmpty(user_data_dir()));
+
+    command_line->AppendSwitchPath(switches::kUserDataDir, user_data_dir());
+  }
+
+  const base::FilePath& user_data_dir() const {
+    return user_data_dir_.GetPath();
+  }
+
+ private:
+  base::ScopedTempDir user_data_dir_;
+};
+
+class MockChromeProcessSingleton : public ChromeProcessSingleton {
+ public:
+  explicit MockChromeProcessSingleton(const base::FilePath& user_data_dir)
+      : ChromeProcessSingleton(
+            user_data_dir,
+            base::BindRepeating(
+                &MockChromeProcessSingleton::NotificationCallback,
+                base::Unretained(this))) {}
+
+ private:
+  bool NotificationCallback(const base::CommandLine& command_line,
+                            const base::FilePath& current_directory) {
+    NOTREACHED();
+    return true;
+  }
+};
+
+// This test currently fails on ChromeOS, see https://crbug.com/1278540
+#if defined(OS_CHROMEOS)
+#define MAYBE_ChromeProcessSingletonExists DISABLED_ChromeProcessSingletonExists
+#else
+#define MAYBE_ChromeProcessSingletonExists ChromeProcessSingletonExists
+#endif
+IN_PROC_BROWSER_TEST_F(HeadlessModeBrowserTestWithUserDataDir,
+                       MAYBE_ChromeProcessSingletonExists) {
+  // Pass the user data dir to the child process which will try
+  // to create a mock ChromeProcessSingleton in it that is
+  // expected to fail.
+  base::CommandLine command_line(
+      base::GetMultiProcessTestChildBaseCommandLine());
+  command_line.AppendSwitchPath(switches::kUserDataDir, user_data_dir());
+
+  base::Process child_process =
+      base::SpawnMultiProcessTestChild("ChromeProcessSingletonChildProcessMain",
+                                       command_line, base::LaunchOptions());
+
+  int result = kErrorResultCode;
+  ASSERT_TRUE(base::WaitForMultiprocessTestChildExit(
+      child_process, TestTimeouts::action_timeout(), &result));
+
+  EXPECT_EQ(static_cast<ProcessSingleton::NotifyResult>(result),
+            ProcessSingleton::PROFILE_IN_USE);
+}
+
+MULTIPROCESS_TEST_MAIN(ChromeProcessSingletonChildProcessMain) {
+  content::BrowserTaskEnvironment task_environment;
+
+  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
+  const base::FilePath user_data_dir =
+      command_line->GetSwitchValuePath(switches::kUserDataDir);
+  if (user_data_dir.empty())
+    return kErrorResultCode;
+
+  MockChromeProcessSingleton chrome_process_singleton(user_data_dir);
+  ProcessSingleton::NotifyResult notify_result =
+      chrome_process_singleton.NotifyOtherProcessOrCreate();
+
+  return static_cast<int>(notify_result);
+}