Browse Source

feat: Add data parameter to `app.requestSingleInstanceLock()` (#30891)

* WIP

* Use serialization

* Rebase windows impl of new app requestSingleInstanceLock parameter

* Fix test

* Implement posix side

* Add backwards compatibility test

* Apply PR feedback Windows

* Fix posix impl

* Switch mac impl back to vector

* Refactor Windows impl

* Use vectors, inline make_span

* Use blink converter

* fix: ownership across sequences

* Fix upstream merge from Chromium

Co-authored-by: deepak1556 <[email protected]>
Raymond Zhao 3 years ago
parent
commit
db0a152bc1

+ 9 - 2
docs/api/app.md

@@ -483,6 +483,7 @@ Returns:
 * `event` Event
 * `argv` String[] - An array of the second instance's command line arguments
 * `workingDirectory` String - The second instance's working directory
+* `additionalData` unknown - A JSON object of additional data passed from the second instance
 
 This event will be emitted inside the primary instance of your application
 when a second instance has been executed and calls `app.requestSingleInstanceLock()`.
@@ -931,6 +932,8 @@ app.setJumpList([
 
 ### `app.requestSingleInstanceLock()`
 
+* `additionalData` unknown (optional) - A JSON object containing additional data to send to the first instance.
+
 Returns `Boolean`
 
 The return value of this method indicates whether or not this instance of your
@@ -956,12 +959,16 @@ starts:
 const { app } = require('electron')
 let myWindow = null
 
-const gotTheLock = app.requestSingleInstanceLock()
+const additionalData = { myKey: 'myValue' }
+const gotTheLock = app.requestSingleInstanceLock(additionalData)
 
 if (!gotTheLock) {
   app.quit()
 } else {
-  app.on('second-instance', (event, commandLine, workingDirectory) => {
+  app.on('second-instance', (event, commandLine, workingDirectory, additionalData) => {
+    // Print out data received from the second instance.
+    console.log(additionalData)
+
     // Someone tried to run a second instance, we should focus our window.
     if (myWindow) {
       if (myWindow.isMinimized()) myWindow.restore()

+ 1 - 0
patches/chromium/.patches

@@ -106,3 +106,4 @@ feat_expose_raw_response_headers_from_urlloader.patch
 chore_do_not_use_chrome_windows_in_cryptotoken_webrequestsender.patch
 process_singleton.patch
 fix_expose_decrementcapturercount_in_web_contents_impl.patch
+feat_add_data_parameter_to_processsingleton.patch

+ 343 - 0
patches/chromium/feat_add_data_parameter_to_processsingleton.patch

@@ -0,0 +1,343 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Raymond Zhao <[email protected]>
+Date: Tue, 7 Sep 2021 14:54:25 -0700
+Subject: feat: Add data parameter to ProcessSingleton
+
+This patch adds an additional_data parameter to the constructor of
+ProcessSingleton, so that the second instance can send additional
+data over to the first instance while requesting the ProcessSingleton
+lock.
+
+On the Electron side, we then expose an extra parameter to the
+app.requestSingleInstanceLock API so that users can pass in a JSON
+object for the second instance to send to the first instance.
+
+diff --git a/chrome/browser/process_singleton.h b/chrome/browser/process_singleton.h
+index eec994c4252f17d9c9c41e66d5dae6509ed98a18..e538c9b76da4d4435e10cd3848438446c2cc2cc8 100644
+--- a/chrome/browser/process_singleton.h
++++ b/chrome/browser/process_singleton.h
+@@ -19,6 +19,7 @@
+ #include "base/macros.h"
+ #include "base/memory/ref_counted.h"
+ #include "base/process/process.h"
++#include "base/containers/span.h"
+ #include "ui/gfx/native_widget_types.h"
+ 
+ #if defined(OS_POSIX) && !defined(OS_ANDROID)
+@@ -101,21 +102,24 @@ class ProcessSingleton {
+   // should handle it (i.e., because the current process is shutting down).
+   using NotificationCallback =
+       base::RepeatingCallback<bool(const base::CommandLine& command_line,
+-                                   const base::FilePath& current_directory)>;
++                                   const base::FilePath& current_directory,
++                                   const std::vector<const uint8_t> additional_data)>;
+ 
+ #if defined(OS_WIN)
+   ProcessSingleton(const std::string& program_name,
+                    const base::FilePath& user_data_dir,
++                   const base::span<const uint8_t> additional_data,
+                    bool is_sandboxed,
+                    const NotificationCallback& notification_callback);
+ #else
+   ProcessSingleton(const base::FilePath& user_data_dir,
++                   const base::span<const uint8_t> additional_data,
+                    const NotificationCallback& notification_callback);
++#endif
+ 
+   ProcessSingleton(const ProcessSingleton&) = delete;
+   ProcessSingleton& operator=(const ProcessSingleton&) = delete;
+ 
+-#endif
+   ~ProcessSingleton();
+ 
+   // Notify another process, if available. Otherwise sets ourselves as the
+@@ -179,6 +183,8 @@ class ProcessSingleton {
+ 
+  private:
+   NotificationCallback notification_callback_;  // Handler for notifications.
++  // Custom data to pass to the other instance during notify.
++  base::span<const uint8_t> additional_data_;
+ 
+ #if defined(OS_WIN)
+   bool EscapeVirtualization(const base::FilePath& user_data_dir);
+diff --git a/chrome/browser/process_singleton_posix.cc b/chrome/browser/process_singleton_posix.cc
+index 05c86df6c871ca7d0926836edc2f6137fcf229cb..01627f6b46c64a24870fa05b9efeaf949203c2ac 100644
+--- a/chrome/browser/process_singleton_posix.cc
++++ b/chrome/browser/process_singleton_posix.cc
+@@ -564,6 +564,7 @@ class ProcessSingleton::LinuxWatcher
+   // |reader| is for sending back ACK message.
+   void HandleMessage(const std::string& current_dir,
+                      const std::vector<std::string>& argv,
++                     const std::vector<const uint8_t> additional_data,
+                      SocketReader* reader);
+ 
+  private:
+@@ -620,13 +621,16 @@ void ProcessSingleton::LinuxWatcher::StartListening(int socket) {
+ }
+ 
+ void ProcessSingleton::LinuxWatcher::HandleMessage(
+-    const std::string& current_dir, const std::vector<std::string>& argv,
++    const std::string& current_dir,
++    const std::vector<std::string>& argv,
++    const std::vector<const uint8_t> additional_data,
+     SocketReader* reader) {
+   DCHECK(ui_task_runner_->BelongsToCurrentThread());
+   DCHECK(reader);
+ 
+   if (parent_->notification_callback_.Run(base::CommandLine(argv),
+-                                          base::FilePath(current_dir))) {
++                                          base::FilePath(current_dir),
++                                          std::move(additional_data))) {
+     // Send back "ACK" message to prevent the client process from starting up.
+     reader->FinishWithACK(kACKToken, base::size(kACKToken) - 1);
+   } else {
+@@ -674,7 +678,8 @@ void ProcessSingleton::LinuxWatcher::SocketReader::
+     }
+   }
+ 
+-  // Validate the message.  The shortest message is kStartToken\0x\0x
++  // Validate the message.  The shortest message kStartToken\0\00
++  // The shortest message with additional data is kStartToken\0\00\00\0.
+   const size_t kMinMessageLength = base::size(kStartToken) + 4;
+   if (bytes_read_ < kMinMessageLength) {
+     buf_[bytes_read_] = 0;
+@@ -704,10 +709,25 @@ void ProcessSingleton::LinuxWatcher::SocketReader::
+   tokens.erase(tokens.begin());
+   tokens.erase(tokens.begin());
+ 
++  size_t num_args;
++  base::StringToSizeT(tokens[0], &num_args);
++  std::vector<std::string> command_line(tokens.begin() + 1, tokens.begin() + 1 + num_args);
++
++  std::vector<const uint8_t> additional_data;
++  if (tokens.size() == 3 + num_args) {
++    size_t additional_data_size;
++    base::StringToSizeT(tokens[1 + num_args], &additional_data_size);
++    const uint8_t* additional_data_bits =
++        reinterpret_cast<const uint8_t*>(tokens[2 + num_args].c_str());
++    additional_data = std::vector<const uint8_t>(additional_data_bits,
++        additional_data_bits + additional_data_size);
++  }
++
+   // Return to the UI thread to handle opening a new browser tab.
+   ui_task_runner_->PostTask(
+       FROM_HERE, base::BindOnce(&ProcessSingleton::LinuxWatcher::HandleMessage,
+-                                parent_, current_dir, tokens, this));
++                                parent_, current_dir, command_line,
++                                std::move(additional_data), this));
+   fd_watch_controller_.reset();
+ 
+   // LinuxWatcher::HandleMessage() is in charge of destroying this SocketReader
+@@ -736,8 +756,10 @@ void ProcessSingleton::LinuxWatcher::SocketReader::FinishWithACK(
+ //
+ ProcessSingleton::ProcessSingleton(
+     const base::FilePath& user_data_dir,
++    const base::span<const uint8_t> additional_data,
+     const NotificationCallback& notification_callback)
+     : notification_callback_(notification_callback),
++      additional_data_(additional_data),
+       current_pid_(base::GetCurrentProcId()),
+       watcher_(new LinuxWatcher(this)) {
+   socket_path_ = user_data_dir.Append(chrome::kSingletonSocketFilename);
+@@ -854,7 +876,8 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessWithTimeout(
+              sizeof(socket_timeout));
+ 
+   // Found another process, prepare our command line
+-  // format is "START\0<current dir>\0<argv[0]>\0...\0<argv[n]>".
++  // format is "START\0<current-dir>\0<n-args>\0<argv[0]>\0...\0<argv[n]>
++  // \0<additional-data-length>\0<additional-data>".
+   std::string to_send(kStartToken);
+   to_send.push_back(kTokenDelimiter);
+ 
+@@ -864,11 +887,21 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessWithTimeout(
+   to_send.append(current_dir.value());
+ 
+   const std::vector<std::string>& argv = cmd_line.argv();
++  to_send.push_back(kTokenDelimiter);
++  to_send.append(base::NumberToString(argv.size()));
+   for (auto it = argv.begin(); it != argv.end(); ++it) {
+     to_send.push_back(kTokenDelimiter);
+     to_send.append(*it);
+   }
+ 
++  size_t data_to_send_size = additional_data_.size_bytes();
++  if (data_to_send_size) {
++    to_send.push_back(kTokenDelimiter);
++    to_send.append(base::NumberToString(data_to_send_size));
++    to_send.push_back(kTokenDelimiter);
++    to_send.append(reinterpret_cast<const char*>(additional_data_.data()), data_to_send_size);
++  }
++
+   // Send the message
+   if (!WriteToSocket(socket.fd(), to_send.data(), to_send.length())) {
+     // Try to kill the other process, because it might have been dead.
+diff --git a/chrome/browser/process_singleton_win.cc b/chrome/browser/process_singleton_win.cc
+index 19d5659d665321da54e05cee01be7da02e0c283b..600ff701b025ba190d05bc30994e3d3e8847df55 100644
+--- a/chrome/browser/process_singleton_win.cc
++++ b/chrome/browser/process_singleton_win.cc
+@@ -99,10 +99,12 @@ BOOL CALLBACK BrowserWindowEnumeration(HWND window, LPARAM param) {
+ 
+ bool ParseCommandLine(const COPYDATASTRUCT* cds,
+                       base::CommandLine* parsed_command_line,
+-                      base::FilePath* current_directory) {
++                      base::FilePath* current_directory,
++                      std::vector<const uint8_t>* parsed_additional_data) {
+   // We should have enough room for the shortest command (min_message_size)
+   // and also be a multiple of wchar_t bytes. The shortest command
+-  // possible is L"START\0\0" (empty current directory and command line).
++  // possible is L"START\0\0" (empty command line, current directory,
++  // and additional data).
+   static const int min_message_size = 7;
+   if (cds->cbData < min_message_size * sizeof(wchar_t) ||
+       cds->cbData % sizeof(wchar_t) != 0) {
+@@ -152,6 +154,37 @@ bool ParseCommandLine(const COPYDATASTRUCT* cds,
+     const std::wstring cmd_line =
+         msg.substr(second_null + 1, third_null - second_null);
+     *parsed_command_line = base::CommandLine::FromString(cmd_line);
++
++    const std::wstring::size_type fourth_null =
++        msg.find_first_of(L'\0', third_null + 1);
++    if (fourth_null == std::wstring::npos ||
++        fourth_null == msg.length()) {
++      // No additional data was provided.
++      return true;
++    }
++
++    // Get length of the additional data.
++    const std::wstring additional_data_length_string =
++        msg.substr(third_null + 1, fourth_null - third_null);
++    size_t additional_data_length;
++    base::StringToSizeT(additional_data_length_string, &additional_data_length);
++
++    const std::wstring::size_type fifth_null =
++        msg.find_first_of(L'\0', fourth_null + 1);
++    if (fifth_null == std::wstring::npos ||
++        fifth_null == msg.length()) {
++      LOG(WARNING) << "Invalid format for start command, we need a string in 6 "
++        "parts separated by NULLs";
++    }
++
++    // Get the actual additional data.
++    const std::wstring additional_data =
++        msg.substr(fourth_null + 1, fifth_null - fourth_null);
++    const uint8_t* additional_data_bytes =
++        reinterpret_cast<const uint8_t*>(additional_data.c_str());
++    *parsed_additional_data = std::vector<const uint8_t>(additional_data_bytes,
++        additional_data_bytes + additional_data_length);
++
+     return true;
+   }
+   return false;
+@@ -168,16 +201,16 @@ bool ProcessLaunchNotification(
+ 
+   // Handle the WM_COPYDATA message from another process.
+   const COPYDATASTRUCT* cds = reinterpret_cast<COPYDATASTRUCT*>(lparam);
+-
+   base::CommandLine parsed_command_line(base::CommandLine::NO_PROGRAM);
+   base::FilePath current_directory;
+-  if (!ParseCommandLine(cds, &parsed_command_line, &current_directory)) {
++  std::vector<const uint8_t> additional_data;
++  if (!ParseCommandLine(cds, &parsed_command_line, &current_directory, &additional_data)) {
+     *result = TRUE;
+     return true;
+   }
+ 
+-  *result = notification_callback.Run(parsed_command_line, current_directory) ?
+-      TRUE : FALSE;
++  *result = notification_callback.Run(parsed_command_line,
++      current_directory, std::move(additional_data)) ? TRUE : FALSE;
+   return true;
+ }
+ 
+@@ -274,9 +307,11 @@ bool ProcessSingleton::EscapeVirtualization(
+ ProcessSingleton::ProcessSingleton(
+     const std::string& program_name,
+     const base::FilePath& user_data_dir,
++    const base::span<const uint8_t> additional_data,
+     bool is_app_sandboxed,
+     const NotificationCallback& notification_callback)
+     : notification_callback_(notification_callback),
++      additional_data_(additional_data),
+       program_name_(program_name),
+       is_app_sandboxed_(is_app_sandboxed),
+       is_virtualized_(false),
+@@ -301,7 +336,7 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcess() {
+     return PROCESS_NONE;
+   }
+ 
+-  switch (chrome::AttemptToNotifyRunningChrome(remote_window_)) {
++  switch (chrome::AttemptToNotifyRunningChrome(remote_window_, additional_data_)) {
+     case chrome::NOTIFY_SUCCESS:
+       return PROCESS_NOTIFIED;
+     case chrome::NOTIFY_FAILED:
+diff --git a/chrome/browser/win/chrome_process_finder.cc b/chrome/browser/win/chrome_process_finder.cc
+index 788abf9a04f2a3725d67f7f8d84615016b241c8e..6ae6d97708e18c25c59a0b1e3d2d58f27d980ffb 100644
+--- a/chrome/browser/win/chrome_process_finder.cc
++++ b/chrome/browser/win/chrome_process_finder.cc
+@@ -34,7 +34,9 @@ HWND FindRunningChromeWindow(const base::FilePath& user_data_dir) {
+   return base::win::MessageWindow::FindWindow(user_data_dir.value());
+ }
+ 
+-NotifyChromeResult AttemptToNotifyRunningChrome(HWND remote_window) {
++NotifyChromeResult AttemptToNotifyRunningChrome(
++    HWND remote_window,
++    const base::span<const uint8_t> additional_data) {
+   DCHECK(remote_window);
+   DWORD process_id = 0;
+   DWORD thread_id = GetWindowThreadProcessId(remote_window, &process_id);
+@@ -42,7 +44,8 @@ NotifyChromeResult AttemptToNotifyRunningChrome(HWND remote_window) {
+     return NOTIFY_FAILED;
+ 
+   // Send the command line to the remote chrome window.
+-  // Format is "START\0<<<current directory>>>\0<<<commandline>>>".
++  // Format is
++  // "START\0<current-directory>\0<command-line>\0<additional-data-length>\0<additional-data>".
+   std::wstring to_send(L"START\0", 6);  // want the NULL in the string.
+   base::FilePath cur_dir;
+   if (!base::GetCurrentDirectory(&cur_dir))
+@@ -53,6 +56,22 @@ NotifyChromeResult AttemptToNotifyRunningChrome(HWND remote_window) {
+       base::CommandLine::ForCurrentProcess()->GetCommandLineString());
+   to_send.append(L"\0", 1);  // Null separator.
+ 
++  size_t additional_data_size = additional_data.size_bytes();
++  if (additional_data_size) {
++    // Send over the size, because the reinterpret cast to wchar_t could
++    // add padding.
++    to_send.append(base::UTF8ToWide(base::NumberToString(additional_data_size)));
++    to_send.append(L"\0", 1);  // Null separator.
++
++    size_t padded_size = additional_data_size / sizeof(wchar_t);
++    if (additional_data_size % sizeof(wchar_t) != 0) {
++      padded_size++;
++    }
++    to_send.append(reinterpret_cast<const wchar_t*>(additional_data.data()),
++                   padded_size);
++    to_send.append(L"\0", 1);  // Null separator.
++  }
++
+   // Allow the current running browser window to make itself the foreground
+   // window (otherwise it will just flash in the taskbar).
+   ::AllowSetForegroundWindow(process_id);
+diff --git a/chrome/browser/win/chrome_process_finder.h b/chrome/browser/win/chrome_process_finder.h
+index 5516673cee019f6060077091e59498bf9038cd6e..8edea5079b46c2cba67833114eb9c21d85cfc22d 100644
+--- a/chrome/browser/win/chrome_process_finder.h
++++ b/chrome/browser/win/chrome_process_finder.h
+@@ -7,6 +7,7 @@
+ 
+ #include <windows.h>
+ 
++#include "base/containers/span.h"
+ #include "base/time/time.h"
+ 
+ namespace base {
+@@ -27,7 +28,9 @@ HWND FindRunningChromeWindow(const base::FilePath& user_data_dir);
+ // Attempts to send the current command line to an already running instance of
+ // Chrome via a WM_COPYDATA message.
+ // Returns true if a running Chrome is found and successfully notified.
+-NotifyChromeResult AttemptToNotifyRunningChrome(HWND remote_window);
++NotifyChromeResult AttemptToNotifyRunningChrome(
++    HWND remote_window,
++    const base::span<const uint8_t> additional_data);
+ 
+ // Changes the notification timeout to |new_timeout|, returns the old timeout.
+ base::TimeDelta SetNotificationTimeoutForTesting(base::TimeDelta new_timeout);

+ 28 - 11
shell/browser/api/electron_api_app.cc

@@ -17,6 +17,7 @@
 #include "base/files/file_util.h"
 #include "base/path_service.h"
 #include "base/system/sys_info.h"
+#include "base/values.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/icon_manager.h"
 #include "chrome/common/chrome_features.h"
@@ -52,6 +53,7 @@
 #include "shell/common/electron_command_line.h"
 #include "shell/common/electron_paths.h"
 #include "shell/common/gin_converters/base_converter.h"
+#include "shell/common/gin_converters/blink_converter.h"
 #include "shell/common/gin_converters/callback_converter.h"
 #include "shell/common/gin_converters/file_path_converter.h"
 #include "shell/common/gin_converters/gurl_converter.h"
@@ -63,6 +65,7 @@
 #include "shell/common/node_includes.h"
 #include "shell/common/options_switches.h"
 #include "shell/common/platform_util.h"
+#include "shell/common/v8_value_serializer.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "ui/gfx/image/image.h"
 
@@ -513,17 +516,22 @@ int GetPathConstant(const std::string& name) {
 bool NotificationCallbackWrapper(
     const base::RepeatingCallback<
         void(const base::CommandLine& command_line,
-             const base::FilePath& current_directory)>& callback,
+             const base::FilePath& current_directory,
+             const std::vector<const uint8_t> additional_data)>& callback,
     const base::CommandLine& cmd,
-    const base::FilePath& cwd) {
+    const base::FilePath& cwd,
+    const std::vector<const uint8_t> additional_data) {
   // Make sure the callback is called after app gets ready.
   if (Browser::Get()->is_ready()) {
-    callback.Run(cmd, cwd);
+    callback.Run(cmd, cwd, std::move(additional_data));
   } else {
     scoped_refptr<base::SingleThreadTaskRunner> task_runner(
         base::ThreadTaskRunnerHandle::Get());
-    task_runner->PostTask(
-        FROM_HERE, base::BindOnce(base::IgnoreResult(callback), cmd, cwd));
+
+    // Make a copy of the span so that the data isn't lost.
+    task_runner->PostTask(FROM_HERE,
+                          base::BindOnce(base::IgnoreResult(callback), cmd, cwd,
+                                         std::move(additional_data)));
   }
   // ProcessSingleton needs to know whether current process is quiting.
   return !Browser::Get()->is_shutting_down();
@@ -1069,8 +1077,14 @@ std::string App::GetLocaleCountryCode() {
 }
 
 void App::OnSecondInstance(const base::CommandLine& cmd,
-                           const base::FilePath& cwd) {
-  Emit("second-instance", cmd.argv(), cwd);
+                           const base::FilePath& cwd,
+                           const std::vector<const uint8_t> additional_data) {
+  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+  v8::Locker locker(isolate);
+  v8::HandleScope handle_scope(isolate);
+  v8::Local<v8::Value> data_value =
+      DeserializeV8Value(isolate, std::move(additional_data));
+  Emit("second-instance", cmd.argv(), cwd, data_value);
 }
 
 bool App::HasSingleInstanceLock() const {
@@ -1079,7 +1093,7 @@ bool App::HasSingleInstanceLock() const {
   return false;
 }
 
-bool App::RequestSingleInstanceLock() {
+bool App::RequestSingleInstanceLock(gin::Arguments* args) {
   if (HasSingleInstanceLock())
     return true;
 
@@ -1090,15 +1104,18 @@ bool App::RequestSingleInstanceLock() {
 
   auto cb = base::BindRepeating(&App::OnSecondInstance, base::Unretained(this));
 
+  blink::CloneableMessage additional_data_message;
+  args->GetNext(&additional_data_message);
 #if defined(OS_WIN)
   bool app_is_sandboxed =
       IsSandboxEnabled(base::CommandLine::ForCurrentProcess());
   process_singleton_ = std::make_unique<ProcessSingleton>(
-      program_name, user_dir, app_is_sandboxed,
-      base::BindRepeating(NotificationCallbackWrapper, cb));
+      program_name, user_dir, additional_data_message.encoded_message,
+      app_is_sandboxed, base::BindRepeating(NotificationCallbackWrapper, cb));
 #else
   process_singleton_ = std::make_unique<ProcessSingleton>(
-      user_dir, base::BindRepeating(NotificationCallbackWrapper, cb));
+      user_dir, additional_data_message.encoded_message,
+      base::BindRepeating(NotificationCallbackWrapper, cb));
 #endif
 
   switch (process_singleton_->NotifyOtherProcessOrCreate()) {

+ 3 - 2
shell/browser/api/electron_api_app.h

@@ -189,9 +189,10 @@ class App : public ElectronBrowserClient::Delegate,
   std::string GetLocale();
   std::string GetLocaleCountryCode();
   void OnSecondInstance(const base::CommandLine& cmd,
-                        const base::FilePath& cwd);
+                        const base::FilePath& cwd,
+                        const std::vector<const uint8_t> additional_data);
   bool HasSingleInstanceLock() const;
-  bool RequestSingleInstanceLock();
+  bool RequestSingleInstanceLock(gin::Arguments* args);
   void ReleaseSingleInstanceLock();
   bool Relaunch(gin::Arguments* args);
   void DisableHardwareAcceleration(gin_helper::ErrorThrower thrower);

+ 25 - 5
spec-main/api-app-spec.ts

@@ -207,7 +207,7 @@ describe('app module', () => {
   describe('app.requestSingleInstanceLock', () => {
     it('prevents the second launch of app', async function () {
       this.timeout(120000);
-      const appPath = path.join(fixturesPath, 'api', 'singleton');
+      const appPath = path.join(fixturesPath, 'api', 'singleton-data');
       const first = cp.spawn(process.execPath, [appPath]);
       await emittedOnce(first.stdout, 'data');
       // Start second app when received output.
@@ -218,8 +218,8 @@ describe('app module', () => {
       expect(code1).to.equal(0);
     });
 
-    it('passes arguments to the second-instance event', async () => {
-      const appPath = path.join(fixturesPath, 'api', 'singleton');
+    async function testArgumentPassing (fixtureName: string, expectedSecondInstanceData: unknown) {
+      const appPath = path.join(fixturesPath, 'api', fixtureName);
       const first = cp.spawn(process.execPath, [appPath]);
       const firstExited = emittedOnce(first, 'exit');
 
@@ -236,14 +236,34 @@ describe('app module', () => {
       expect(code2).to.equal(1);
       const [code1] = await firstExited;
       expect(code1).to.equal(0);
-      const data2 = (await data2Promise)[0].toString('ascii');
-      const secondInstanceArgsReceived: string[] = JSON.parse(data2.toString('ascii'));
+      const received = await data2Promise;
+      const [args, additionalData] = received[0].toString('ascii').split('||');
+      const secondInstanceArgsReceived: string[] = JSON.parse(args.toString('ascii'));
+      const secondInstanceDataReceived = JSON.parse(additionalData.toString('ascii'));
 
       // Ensure secondInstanceArgs is a subset of secondInstanceArgsReceived
       for (const arg of secondInstanceArgs) {
         expect(secondInstanceArgsReceived).to.include(arg,
           `argument ${arg} is missing from received second args`);
       }
+      expect(secondInstanceDataReceived).to.be.deep.equal(expectedSecondInstanceData,
+        `received data ${JSON.stringify(secondInstanceDataReceived)} is not equal to expected data ${JSON.stringify(expectedSecondInstanceData)}.`);
+    }
+
+    it('passes arguments to the second-instance event', async () => {
+      const expectedSecondInstanceData = {
+        level: 1,
+        testkey: 'testvalue1',
+        inner: {
+          level: 2,
+          testkey: 'testvalue2'
+        }
+      };
+      await testArgumentPassing('singleton-data', expectedSecondInstanceData);
+    });
+
+    it('passes arguments to the second-instance event no additional data', async () => {
+      await testArgumentPassing('singleton', null);
     });
   });
 

+ 26 - 0
spec/fixtures/api/singleton-data/main.js

@@ -0,0 +1,26 @@
+const { app } = require('electron');
+
+app.whenReady().then(() => {
+  console.log('started'); // ping parent
+});
+
+const obj = {
+  level: 1,
+  testkey: 'testvalue1',
+  inner: {
+    level: 2,
+    testkey: 'testvalue2'
+  }
+};
+const gotTheLock = app.requestSingleInstanceLock(obj);
+
+app.on('second-instance', (event, args, workingDirectory, data) => {
+  setImmediate(() => {
+    console.log([JSON.stringify(args), JSON.stringify(data)].join('||'));
+    app.exit(0);
+  });
+});
+
+if (!gotTheLock) {
+  app.exit(1);
+}

+ 5 - 0
spec/fixtures/api/singleton-data/package.json

@@ -0,0 +1,5 @@
+{
+  "name": "electron-app-singleton-data",
+  "main": "main.js"
+}
+

+ 2 - 2
spec/fixtures/api/singleton/main.js

@@ -6,9 +6,9 @@ app.whenReady().then(() => {
 
 const gotTheLock = app.requestSingleInstanceLock();
 
-app.on('second-instance', (event, args) => {
+app.on('second-instance', (event, args, workingDirectory, data) => {
   setImmediate(() => {
-    console.log(JSON.stringify(args));
+    console.log([JSON.stringify(args), JSON.stringify(data)].join('||'));
     app.exit(0);
   });
 });