Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 14 commits into from Oct 15, 2021
11 changes: 9 additions & 2 deletions docs/api/app.md
Expand Up @@ -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()`.
Expand Down Expand Up @@ -931,6 +932,8 @@ app.setJumpList([

### `app.requestSingleInstanceLock()`

* `additionalData` unknown (optional) - A JSON object containing additional data to send to the first instance.
rzhao271 marked this conversation as resolved.
Show resolved Hide resolved

Returns `Boolean`

The return value of this method indicates whether or not this instance of your
Expand All @@ -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)
rzhao271 marked this conversation as resolved.
Show resolved Hide resolved

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()
Expand Down
1 change: 1 addition & 0 deletions patches/chromium/.patches
Expand Up @@ -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 changes: 343 additions & 0 deletions patches/chromium/feat_add_data_parameter_to_processsingleton.patch

Large diffs are not rendered by default.

39 changes: 28 additions & 11 deletions shell/browser/api/electron_api_app.cc
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"

Expand Down Expand Up @@ -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,
rzhao271 marked this conversation as resolved.
Show resolved Hide resolved
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();
Expand Down Expand Up @@ -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 {
Expand All @@ -1079,7 +1093,7 @@ bool App::HasSingleInstanceLock() const {
return false;
}

bool App::RequestSingleInstanceLock() {
bool App::RequestSingleInstanceLock(gin::Arguments* args) {
if (HasSingleInstanceLock())
return true;

Expand All @@ -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()) {
Expand Down
5 changes: 3 additions & 2 deletions shell/browser/api/electron_api_app.h
Expand Up @@ -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);
Expand Down
30 changes: 25 additions & 5 deletions spec-main/api-app-spec.ts
Expand Up @@ -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.
Expand All @@ -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');

Expand All @@ -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);
});
});

Expand Down
26 changes: 26 additions & 0 deletions 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 changes: 5 additions & 0 deletions spec/fixtures/api/singleton-data/package.json
@@ -0,0 +1,5 @@
{
"name": "electron-app-singleton-data",
"main": "main.js"
}

4 changes: 2 additions & 2 deletions spec/fixtures/api/singleton/main.js
Expand Up @@ -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);
});
});
Expand Down