From db0a152bc1318feb912bdc9638e0c040a2626290 Mon Sep 17 00:00:00 2001 From: Raymond Zhao Date: Thu, 14 Oct 2021 18:32:32 -0700 Subject: [PATCH] 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 --- docs/api/app.md | 11 +- patches/chromium/.patches | 1 + ...d_data_parameter_to_processsingleton.patch | 343 ++++++++++++++++++ shell/browser/api/electron_api_app.cc | 39 +- shell/browser/api/electron_api_app.h | 5 +- spec-main/api-app-spec.ts | 30 +- spec/fixtures/api/singleton-data/main.js | 26 ++ spec/fixtures/api/singleton-data/package.json | 5 + spec/fixtures/api/singleton/main.js | 4 +- 9 files changed, 442 insertions(+), 22 deletions(-) create mode 100644 patches/chromium/feat_add_data_parameter_to_processsingleton.patch create mode 100644 spec/fixtures/api/singleton-data/main.js create mode 100644 spec/fixtures/api/singleton-data/package.json diff --git a/docs/api/app.md b/docs/api/app.md index 485e693d2159..005ab2fe9475 100755 --- a/docs/api/app.md +++ b/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() diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 817f56dc9b0d..45a062887ccd 100644 --- a/patches/chromium/.patches +++ b/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 diff --git a/patches/chromium/feat_add_data_parameter_to_processsingleton.patch b/patches/chromium/feat_add_data_parameter_to_processsingleton.patch new file mode 100644 index 000000000000..d7b9084c8b73 --- /dev/null +++ b/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 +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; ++ const base::FilePath& current_directory, ++ const std::vector additional_data)>; + + #if defined(OS_WIN) + ProcessSingleton(const std::string& program_name, + const base::FilePath& user_data_dir, ++ const base::span additional_data, + bool is_sandboxed, + const NotificationCallback& notification_callback); + #else + ProcessSingleton(const base::FilePath& user_data_dir, ++ const base::span 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 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& argv, ++ const std::vector 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& argv, ++ const std::string& current_dir, ++ const std::vector& argv, ++ const std::vector 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 command_line(tokens.begin() + 1, tokens.begin() + 1 + num_args); ++ ++ std::vector 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(tokens[2 + num_args].c_str()); ++ additional_data = std::vector(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 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\0\0...\0". ++ // format is "START\0\0\0\0...\0 ++ // \0\0". + 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& 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(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* 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(additional_data.c_str()); ++ *parsed_additional_data = std::vector(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(lparam); +- + base::CommandLine parsed_command_line(base::CommandLine::NO_PROGRAM); + base::FilePath current_directory; +- if (!ParseCommandLine(cds, &parsed_command_line, ¤t_directory)) { ++ std::vector additional_data; ++ if (!ParseCommandLine(cds, &parsed_command_line, ¤t_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 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 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<<>>\0<<>>". ++ // Format is ++ // "START\0\0\0\0". + 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(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 + ++#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 additional_data); + + // Changes the notification timeout to |new_timeout|, returns the old timeout. + base::TimeDelta SetNotificationTimeoutForTesting(base::TimeDelta new_timeout); diff --git a/shell/browser/api/electron_api_app.cc b/shell/browser/api/electron_api_app.cc index 515888d71b63..7328cedf934b 100644 --- a/shell/browser/api/electron_api_app.cc +++ b/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 additional_data)>& callback, const base::CommandLine& cmd, - const base::FilePath& cwd) { + const base::FilePath& cwd, + const std::vector 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 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 additional_data) { + v8::Isolate* isolate = JavascriptEnvironment::GetIsolate(); + v8::Locker locker(isolate); + v8::HandleScope handle_scope(isolate); + v8::Local 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( - 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( - user_dir, base::BindRepeating(NotificationCallbackWrapper, cb)); + user_dir, additional_data_message.encoded_message, + base::BindRepeating(NotificationCallbackWrapper, cb)); #endif switch (process_singleton_->NotifyOtherProcessOrCreate()) { diff --git a/shell/browser/api/electron_api_app.h b/shell/browser/api/electron_api_app.h index 4b0a1131e57c..e65c2f86d109 100644 --- a/shell/browser/api/electron_api_app.h +++ b/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 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); diff --git a/spec-main/api-app-spec.ts b/spec-main/api-app-spec.ts index ce770c4dc6c1..bb79595cad86 100644 --- a/spec-main/api-app-spec.ts +++ b/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); }); }); diff --git a/spec/fixtures/api/singleton-data/main.js b/spec/fixtures/api/singleton-data/main.js new file mode 100644 index 000000000000..02257199e358 --- /dev/null +++ b/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); +} diff --git a/spec/fixtures/api/singleton-data/package.json b/spec/fixtures/api/singleton-data/package.json new file mode 100644 index 000000000000..3c20945331c1 --- /dev/null +++ b/spec/fixtures/api/singleton-data/package.json @@ -0,0 +1,5 @@ +{ + "name": "electron-app-singleton-data", + "main": "main.js" +} + diff --git a/spec/fixtures/api/singleton/main.js b/spec/fixtures/api/singleton/main.js index 81660003b4ac..67b822bcef9b 100644 --- a/spec/fixtures/api/singleton/main.js +++ b/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); }); });