Skip to content

Commit

Permalink
feat: Add data parameter to app.requestSingleInstanceLock() (electr…
Browse files Browse the repository at this point in the history
…on#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 <hop2deep@gmail.com>
  • Loading branch information
2 people authored and t57ser committed Oct 27, 2021
1 parent dd706bd commit 20d0d52
Show file tree
Hide file tree
Showing 9 changed files with 442 additions and 22 deletions.
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.

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)

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,
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

0 comments on commit 20d0d52

Please sign in to comment.