feat: UtilityProcess API (#34980)

* chore: initial scaffolding

* chore: implement interface and docs

* chore: address code style review

* fix: cleanup of utility process on shutdown

* chore: simplify NodeBindings::CreateEnvironment

* chore: rename disableLibraryValidation => allowLoadingUnsignedLibraries

* chore: implement process.parentPort

* chore(posix): implement stdio pipe interface

* chore(win): implement stdio interface

* chore: reenable SetNodeOptions for utility process

* chore: add specs

* chore: fix lint

* fix: update kill API

* fix: update process.parentPort API

* fix: exit event

* docs: update exit event

* fix: tests on linux

* chore: expand on some comments

* fix: shutdown of pipe reader

Avoid logging since it is always the case that reader end of
pipe will terminate after the child process.

* fix: remove exit code check for crash spec

* fix: rm PR_SET_NO_NEW_PRIVS for unsandbox utility process

* chore: fix incorrect rebase

* fix: address review feedback

* chore: rename utility_process -> utility

* chore: update docs

* chore: cleanup c++ implemantation

* fix: leak in NodeServiceHost impl

* chore: minor cleanup

* chore: cleanup JS implementation

* chore: flip default stdio to inherit

* fix: some api improvements

* Support cwd option
* Remove path restriction for modulePath
* Rewire impl for env support

* fix: add tests for cwd and env option

* chore: alt impl for reading stdio handles

* chore: support message queuing

* chore: fix lint

* chore: new UtilityProcess => utilityProcess.fork

* fix: support for uncaught exception exits

* chore: remove process.execArgv as default

* fix: windows build

* fix: style changes

* fix: docs and style changes

* chore: update patches

* spec: disable flaky test on win32 arm CI

Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
This commit is contained in:
Robo 2022-10-20 14:49:49 +09:00 committed by GitHub
parent 44c40efecf
commit da0fd286b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 2700 additions and 54 deletions

View file

@ -202,6 +202,15 @@ webpack_build("electron_isolated_renderer_bundle") {
out_file = "$target_gen_dir/js2c/isolated_bundle.js" out_file = "$target_gen_dir/js2c/isolated_bundle.js"
} }
webpack_build("electron_utility_bundle") {
deps = [ ":build_electron_definitions" ]
inputs = auto_filenames.utility_bundle_deps
config_file = "//electron/build/webpack/webpack.config.utility.js"
out_file = "$target_gen_dir/js2c/utility_init.js"
}
action("electron_js2c") { action("electron_js2c") {
deps = [ deps = [
":electron_asar_bundle", ":electron_asar_bundle",
@ -209,6 +218,7 @@ action("electron_js2c") {
":electron_isolated_renderer_bundle", ":electron_isolated_renderer_bundle",
":electron_renderer_bundle", ":electron_renderer_bundle",
":electron_sandboxed_renderer_bundle", ":electron_sandboxed_renderer_bundle",
":electron_utility_bundle",
":electron_worker_bundle", ":electron_worker_bundle",
] ]
@ -218,6 +228,7 @@ action("electron_js2c") {
"$target_gen_dir/js2c/isolated_bundle.js", "$target_gen_dir/js2c/isolated_bundle.js",
"$target_gen_dir/js2c/renderer_init.js", "$target_gen_dir/js2c/renderer_init.js",
"$target_gen_dir/js2c/sandbox_bundle.js", "$target_gen_dir/js2c/sandbox_bundle.js",
"$target_gen_dir/js2c/utility_init.js",
"$target_gen_dir/js2c/worker_init.js", "$target_gen_dir/js2c/worker_init.js",
] ]
@ -368,6 +379,7 @@ source_set("electron_lib") {
"chromium_src:chrome", "chromium_src:chrome",
"chromium_src:chrome_spellchecker", "chromium_src:chrome_spellchecker",
"shell/common/api:mojo", "shell/common/api:mojo",
"shell/services/node/public/mojom",
"//base:base_static", "//base:base_static",
"//base/allocator:buildflags", "//base/allocator:buildflags",
"//chrome:strings", "//chrome:strings",

View file

@ -0,0 +1,4 @@
module.exports = require('./webpack.config.base')({
target: 'utility',
alwaysHasNode: true
});

46
docs/api/parent-port.md Normal file
View file

@ -0,0 +1,46 @@
# parentPort
> Interface for communication with parent process.
Process: [Utility](../glossary.md#utility-process)
`parentPort` is an [EventEmitter][event-emitter].
_This object is not exported from the `'electron'` module. It is only available as a property of the process object in the Electron API._
```js
// Main process
const child = utilityProcess.fork(path.join(__dirname, 'test.js'))
child.postMessage({ message: 'hello' })
child.on('message', (data) => {
console.log(data) // hello world!
})
// Child process
process.parentPort.on('message', (e) => {
process.parentPort.postMessage(`${e.data} world!`)
})
```
## Events
The `parentPort` object emits the following events:
### Event: 'message'
Returns:
* `messageEvent` Object
* `data` any
* `ports` MessagePortMain[]
Emitted when the process receives a message. Messages received on
this port will be queued up until a handler is registered for this
event.
## Methods
### `parentPort.postMessage(message)`
* `message` any
Sends a message from the process to its parent.

View file

@ -113,6 +113,7 @@ A `string` representing the current process's type, can be:
* `browser` - The main process * `browser` - The main process
* `renderer` - A renderer process * `renderer` - A renderer process
* `worker` - In a web worker * `worker` - In a web worker
* `utility` - In a node process launched as a service
### `process.versions.chrome` _Readonly_ ### `process.versions.chrome` _Readonly_
@ -134,6 +135,11 @@ Each frame has its own JavaScript context. When contextIsolation is enabled, the
world also has a separate JavaScript context. world also has a separate JavaScript context.
This property is only available in the renderer process. This property is only available in the renderer process.
### `process.parentPort`
A [`Electron.ParentPort`](parent-port.md) property if this is a [`UtilityProcess`](utility-process.md)
(or `null` otherwise) allowing communication with the parent process.
## Methods ## Methods
The `process` object has the following methods: The `process` object has the following methods:

136
docs/api/utility-process.md Normal file
View file

@ -0,0 +1,136 @@
# utilityProcess
`utilityProcess` creates a child process with
Node.js and Message ports enabled. It provides the equivalent of [`child_process.fork`][] API from Node.js
but instead uses [Services API][] from Chromium to launch the child process.
Process: [Main](../glossary.md#main-process)<br />
## Methods
### `utilityProcess.fork(modulePath[, args][, options])`
* `modulePath` string - Path to the script that should run as entrypoint in the child process.
* `args` string[] (optional) - List of string arguments that will be available as `process.argv`
in the child process.
* `options` Object (optional)
* `env` Object (optional) - Environment key-value pairs. Default is `process.env`.
* `execArgv` string[] (optional) - List of string arguments passed to the executable.
* `cwd` string (optional) - Current working directory of the child process.
* `stdio` (string[] | string) (optional) - Allows configuring the mode for `stdout` and `stderr`
of the child process. Default is `inherit`.
String value can be one of `pipe`, `ignore`, `inherit`, for more details on these values you can refer to
[stdio][] documentation from Node.js. Currently this option only supports configuring `stdout` and
`stderr` to either `pipe`, `inherit` or `ignore`. Configuring `stdin` is not supported; `stdin` will
always be ignored.
For example, the supported values will be processed as following:
* `pipe`: equivalent to ['ignore', 'pipe', 'pipe'] (the default)
* `ignore`: equivalent to 'ignore', 'ignore', 'ignore']
* `inherit`: equivalent to ['ignore', 'inherit', 'inherit']
* `serviceName` string (optional) - Name of the process that will appear in `name` property of
[`child-process-gone` event of `app`](app.md#event-child-process-gone).
Default is `node.mojom.NodeService`.
* `allowLoadingUnsignedLibraries` boolean (optional) _macOS_ - With this flag, the utility process will be
launched via the `Electron Helper (Plugin).app` helper executable on macOS, which can be
codesigned with `com.apple.security.cs.disable-library-validation` and
`com.apple.security.cs.allow-unsigned-executable-memory` entitlements. This will allow the utility process
to load unsigned libraries. Unless you specifically need this capability, it is best to leave this disabled.
Default is `false`.
Returns [`UtilityProcess`](utility-process.md#class-utilityprocess)
## Class: UtilityProcess
> Instances of the `UtilityProcess` represent the Chromium spawned child process
> with Node.js integration.
`UtilityProcess` is an [EventEmitter][event-emitter].
### Instance Methods
#### `child.postMessage(message, [transfer])`
* `message` any
* `transfer` MessagePortMain[] (optional)
Send a message to the child process, optionally transferring ownership of
zero or more [`MessagePortMain`][] objects.
For example:
```js
// Main process
const { port1, port2 } = new MessageChannelMain()
const child = utilityProcess.fork(path.join(__dirname, 'test.js'))
child.postMessage({ message: 'hello' }, [port1])
// Child process
process.parentPort.once('message', (e) => {
const [port] = e.ports
// ...
})
```
#### `child.kill()`
Returns `boolean`
Terminates the process gracefully. On POSIX, it uses SIGTERM
but will ensure the process is reaped on exit. This function returns
true if the kill is successful, and false otherwise.
### Instance Properties
#### `child.pid`
A `Integer | undefined` representing the process identifier (PID) of the child process.
If the child process fails to spawn due to errors, then the value is `undefined`. When
the child process exits, then the value is `undefined` after the `exit` event is emitted.
#### `child.stdout`
A `NodeJS.ReadableStream | null` that represents the child process's stdout.
If the child was spawned with options.stdio[1] set to anything other than 'pipe', then this will be `null`.
When the child process exits, then the value is `null` after the `exit` event is emitted.
```js
// Main process
const { port1, port2 } = new MessageChannelMain()
const child = utilityProcess.fork(path.join(__dirname, 'test.js'))
child.stdout.on('data', (data) => {
console.log(`Received chunk ${data}`)
})
```
#### `child.stderr`
A `NodeJS.ReadableStream | null` that represents the child process's stderr.
If the child was spawned with options.stdio[2] set to anything other than 'pipe', then this will be `null`.
When the child process exits, then the value is `null` after the `exit` event is emitted.
### Instance Events
#### Event: 'spawn'
Emitted once the child process has spawned successfully.
#### Event: 'exit'
Returns:
* `code` number - Contains the exit code for
the process obtained from waitpid on posix, or GetExitCodeProcess on windows.
Emitted after the child process ends.
#### Event: 'message'
Returns:
* `message` any
Emitted when the child process sends a message using [`process.parentPort.postMessage()`](process.md#processparentport).
[`child_process.fork`]: https://nodejs.org/dist/latest-v16.x/docs/api/child_process.html#child_processforkmodulepath-args-options
[Services API]: https://chromium.googlesource.com/chromium/src/+/master/docs/mojo_and_services.md
[stdio]: https://nodejs.org/dist/latest/docs/api/child_process.html#optionsstdio

View file

@ -194,6 +194,15 @@ overly prescriptive about how it should be used. Userland enables users to
create and share tools that provide additional functionality on top of what is create and share tools that provide additional functionality on top of what is
available in "core". available in "core".
### utility process
The utility process is a child of the main process that allows running any
untrusted services that cannot be run in the main process. Chromium uses this
process to perform network I/O, audio/video processing, device inputs etc.
In Electron, you can create this process using [UtilityProcess][] API.
See also: [process](#process), [main process](#main-process)
### V8 ### V8
V8 is Google's open source JavaScript engine. It is written in C++ and is V8 is Google's open source JavaScript engine. It is written in C++ and is
@ -231,4 +240,5 @@ embedded content.
[renderer]: #renderer-process [renderer]: #renderer-process
[userland]: #userland [userland]: #userland
[using native node modules]: tutorial/using-native-node-modules.md [using native node modules]: tutorial/using-native-node-modules.md
[UtilityProcess]: api/utility-process.md
[v8]: #v8 [v8]: #v8

View file

@ -36,6 +36,7 @@ auto_filenames = {
"docs/api/net-log.md", "docs/api/net-log.md",
"docs/api/net.md", "docs/api/net.md",
"docs/api/notification.md", "docs/api/notification.md",
"docs/api/parent-port.md",
"docs/api/power-monitor.md", "docs/api/power-monitor.md",
"docs/api/power-save-blocker.md", "docs/api/power-save-blocker.md",
"docs/api/process.md", "docs/api/process.md",
@ -62,6 +63,7 @@ auto_filenames = {
"docs/api/touch-bar-spacer.md", "docs/api/touch-bar-spacer.md",
"docs/api/touch-bar.md", "docs/api/touch-bar.md",
"docs/api/tray.md", "docs/api/tray.md",
"docs/api/utility-process.md",
"docs/api/web-contents.md", "docs/api/web-contents.md",
"docs/api/web-frame-main.md", "docs/api/web-frame-main.md",
"docs/api/web-frame.md", "docs/api/web-frame.md",
@ -220,6 +222,7 @@ auto_filenames = {
"lib/browser/api/system-preferences.ts", "lib/browser/api/system-preferences.ts",
"lib/browser/api/touch-bar.ts", "lib/browser/api/touch-bar.ts",
"lib/browser/api/tray.ts", "lib/browser/api/tray.ts",
"lib/browser/api/utility-process.ts",
"lib/browser/api/view.ts", "lib/browser/api/view.ts",
"lib/browser/api/views/image-view.ts", "lib/browser/api/views/image-view.ts",
"lib/browser/api/web-contents-view.ts", "lib/browser/api/web-contents-view.ts",
@ -331,4 +334,20 @@ auto_filenames = {
"typings/internal-ambient.d.ts", "typings/internal-ambient.d.ts",
"typings/internal-electron.d.ts", "typings/internal-electron.d.ts",
] ]
utility_bundle_deps = [
"lib/browser/message-port-main.ts",
"lib/common/define-properties.ts",
"lib/common/init.ts",
"lib/common/reset-search-paths.ts",
"lib/utility/api/exports/electron.ts",
"lib/utility/api/module-list.ts",
"lib/utility/init.ts",
"lib/utility/parent-port.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",
"typings/internal-ambient.d.ts",
"typings/internal-electron.d.ts",
]
} }

View file

@ -311,6 +311,8 @@ filenames = {
"shell/browser/api/electron_api_tray.h", "shell/browser/api/electron_api_tray.h",
"shell/browser/api/electron_api_url_loader.cc", "shell/browser/api/electron_api_url_loader.cc",
"shell/browser/api/electron_api_url_loader.h", "shell/browser/api/electron_api_url_loader.h",
"shell/browser/api/electron_api_utility_process.cc",
"shell/browser/api/electron_api_utility_process.h",
"shell/browser/api/electron_api_view.cc", "shell/browser/api/electron_api_view.cc",
"shell/browser/api/electron_api_view.h", "shell/browser/api/electron_api_view.h",
"shell/browser/api/electron_api_web_contents.cc", "shell/browser/api/electron_api_web_contents.cc",
@ -679,6 +681,10 @@ filenames = {
"shell/renderer/renderer_client_base.h", "shell/renderer/renderer_client_base.h",
"shell/renderer/web_worker_observer.cc", "shell/renderer/web_worker_observer.cc",
"shell/renderer/web_worker_observer.h", "shell/renderer/web_worker_observer.h",
"shell/services/node/node_service.cc",
"shell/services/node/node_service.h",
"shell/services/node/parent_port.cc",
"shell/services/node/parent_port.h",
"shell/utility/electron_content_utility_client.cc", "shell/utility/electron_content_utility_client.cc",
"shell/utility/electron_content_utility_client.h", "shell/utility/electron_content_utility_client.h",
] ]

View file

@ -31,6 +31,7 @@ export const browserModuleList: ElectronInternal.ModuleEntry[] = [
{ name: 'systemPreferences', loader: () => require('./system-preferences') }, { name: 'systemPreferences', loader: () => require('./system-preferences') },
{ name: 'TouchBar', loader: () => require('./touch-bar') }, { name: 'TouchBar', loader: () => require('./touch-bar') },
{ name: 'Tray', loader: () => require('./tray') }, { name: 'Tray', loader: () => require('./tray') },
{ name: 'utilityProcess', loader: () => require('./utility-process') },
{ name: 'View', loader: () => require('./view') }, { name: 'View', loader: () => require('./view') },
{ name: 'webContents', loader: () => require('./web-contents') }, { name: 'webContents', loader: () => require('./web-contents') },
{ name: 'WebContentsView', loader: () => require('./web-contents-view') }, { name: 'WebContentsView', loader: () => require('./web-contents-view') },

View file

@ -0,0 +1,150 @@
import { EventEmitter } from 'events';
import { Duplex, PassThrough } from 'stream';
import { Socket } from 'net';
import { MessagePortMain } from '@electron/internal/browser/message-port-main';
const { _fork } = process._linkedBinding('electron_browser_utility_process');
class ForkUtilityProcess extends EventEmitter {
#handle: ElectronInternal.UtilityProcessWrapper | null;
#stdout: Duplex | null = null;
#stderr: Duplex | null = null;
constructor (modulePath: string, args?: string[], options?: Electron.ForkOptions) {
super();
if (!modulePath) {
throw new Error('Missing UtilityProcess entry script.');
}
if (args == null) {
args = [];
} else if (typeof args === 'object' && !Array.isArray(args)) {
options = args;
args = [];
}
if (options == null) {
options = {};
} else {
options = { ...options };
}
if (!options) {
throw new Error('Options cannot be undefined.');
}
if (options.execArgv != null) {
if (!Array.isArray(options.execArgv)) {
throw new Error('execArgv must be an array of strings.');
}
}
if (options.serviceName != null) {
if (typeof options.serviceName !== 'string') {
throw new Error('serviceName must be a string.');
}
}
if (options.cwd != null) {
if (typeof options.cwd !== 'string') {
throw new Error('cwd path must be a string.');
}
}
if (typeof options.stdio === 'string') {
const stdio : Array<'pipe' | 'ignore' | 'inherit'> = [];
switch (options.stdio) {
case 'inherit':
case 'ignore':
stdio.push('ignore', options.stdio, options.stdio);
break;
case 'pipe':
this.#stderr = new PassThrough();
this.#stdout = new PassThrough();
stdio.push('ignore', options.stdio, options.stdio);
break;
default:
throw new Error('stdio must be of the following values: inherit, pipe, ignore');
}
options.stdio = stdio;
} else if (Array.isArray(options.stdio)) {
if (options.stdio.length >= 3) {
if (options.stdio[0] !== 'ignore') {
throw new Error('stdin value other than ignore is not supported.');
}
if (options.stdio[1] === 'pipe') {
this.#stdout = new PassThrough();
} else if (options.stdio[1] !== 'ignore' && options.stdio[1] !== 'inherit') {
throw new Error('stdout configuration must be of the following values: inherit, pipe, ignore');
}
if (options.stdio[2] === 'pipe') {
this.#stderr = new PassThrough();
} else if (options.stdio[2] !== 'ignore' && options.stdio[2] !== 'inherit') {
throw new Error('stderr configuration must be of the following values: inherit, pipe, ignore');
}
} else {
throw new Error('configuration missing for stdin, stdout or stderr.');
}
}
this.#handle = _fork({ options, modulePath, args });
this.#handle!.emit = (channel: string | symbol, ...args: any[]) => {
if (channel === 'exit') {
try {
this.emit('exit', ...args);
} finally {
this.#handle = null;
if (this.#stdout) {
this.#stdout.removeAllListeners();
this.#stdout = null;
}
if (this.#stderr) {
this.#stderr.removeAllListeners();
this.#stderr = null;
}
}
return false;
} else if (channel === 'stdout' && this.#stdout) {
new Socket({ fd: args[0], readable: true }).pipe(this.#stdout);
return true;
} else if (channel === 'stderr' && this.#stderr) {
new Socket({ fd: args[0], readable: true }).pipe(this.#stderr);
return true;
} else {
return this.emit(channel, ...args);
}
};
}
get pid () {
return this.#handle?.pid;
}
get stdout () {
return this.#stdout;
}
get stderr () {
return this.#stderr;
}
postMessage (message: any, transfer?: MessagePortMain[]) {
if (Array.isArray(transfer)) {
transfer = transfer.map((o: any) => o instanceof MessagePortMain ? o._internalPort : o);
return this.#handle?.postMessage(message, transfer);
}
return this.#handle?.postMessage(message);
}
kill () : boolean {
if (this.#handle === null) {
return false;
}
return this.#handle.kill();
}
}
export function fork (modulePath: string, args?: string[], options?: Electron.ForkOptions) {
return new ForkUtilityProcess(modulePath, args, options);
}

View file

@ -33,20 +33,29 @@ function wrap <T extends AnyFn> (func: T, wrapper: (fn: AnyFn) => T) {
return wrapped; return wrapped;
} }
// process.nextTick and setImmediate make use of uv_check and uv_prepare to
// run the callbacks, however since we only run uv loop on requests, the
// callbacks wouldn't be called until something else activated the uv loop,
// which would delay the callbacks for arbitrary long time. So we should
// initiatively activate the uv loop once process.nextTick and setImmediate is
// called.
process.nextTick = wrapWithActivateUvLoop(process.nextTick); process.nextTick = wrapWithActivateUvLoop(process.nextTick);
global.setImmediate = timers.setImmediate = wrapWithActivateUvLoop(timers.setImmediate); global.setImmediate = timers.setImmediate = wrapWithActivateUvLoop(timers.setImmediate);
global.clearImmediate = timers.clearImmediate; global.clearImmediate = timers.clearImmediate;
// setTimeout needs to update the polling timeout of the event loop, when // setTimeout needs to update the polling timeout of the event loop, when
// called under Chromium's event loop the node's event loop won't get a chance // called under Chromium's event loop the node's event loop won't get a chance
// to update the timeout, so we have to force the node's event loop to // to update the timeout, so we have to force the node's event loop to
// recalculate the timeout in browser process. // recalculate the timeout in the process.
timers.setTimeout = wrapWithActivateUvLoop(timers.setTimeout); timers.setTimeout = wrapWithActivateUvLoop(timers.setTimeout);
timers.setInterval = wrapWithActivateUvLoop(timers.setInterval); timers.setInterval = wrapWithActivateUvLoop(timers.setInterval);
// Only override the global setTimeout/setInterval impls in the browser process // Update the global version of the timer apis to use the above wrapper
if (process.type === 'browser') { // only in the process that runs node event loop alongside chromium
// event loop. We skip renderer with nodeIntegration here because node globals
// are deleted in these processes, see renderer/init.js for reference.
if (process.type === 'browser' ||
process.type === 'utility') {
global.setTimeout = timers.setTimeout; global.setTimeout = timers.setTimeout;
global.setInterval = timers.setInterval; global.setInterval = timers.setInterval;
} }

View file

@ -0,0 +1,21 @@
{
"rules": {
"no-restricted-imports": [
"error",
{
"paths": [
"electron",
"electron/renderer"
],
"patterns": [
"./*",
"../*",
"@electron/internal/isolated_renderer/*",
"@electron/internal/renderer/*",
"@electron/internal/sandboxed_worker/*",
"@electron/internal/worker/*"
]
}
]
}
}

View file

@ -0,0 +1,6 @@
import { defineProperties } from '@electron/internal/common/define-properties';
import { utilityNodeModuleList } from '@electron/internal/utility/api/module-list';
module.exports = {};
defineProperties(module.exports, utilityNodeModuleList);

View file

@ -0,0 +1,2 @@
// Utility side modules, please sort alphabetically.
export const utilityNodeModuleList: ElectronInternal.ModuleEntry[] = [];

38
lib/utility/init.ts Normal file
View file

@ -0,0 +1,38 @@
import { ParentPort } from '@electron/internal/utility/parent-port';
const Module = require('module');
const v8Util = process._linkedBinding('electron_common_v8_util');
const entryScript: string = v8Util.getHiddenValue(process, '_serviceStartupScript');
// We modified the original process.argv to let node.js load the init.js,
// we need to restore it here.
process.argv.splice(1, 1, entryScript);
// Clear search paths.
require('../common/reset-search-paths');
// Import common settings.
require('@electron/internal/common/init');
const parentPort: ParentPort = new ParentPort();
Object.defineProperty(process, 'parentPort', {
enumerable: true,
writable: false,
value: parentPort
});
// Based on third_party/electron_node/lib/internal/worker/io.js
parentPort.on('newListener', (name: string) => {
if (name === 'message' && parentPort.listenerCount('message') === 0) {
parentPort.start();
}
});
parentPort.on('removeListener', (name: string) => {
if (name === 'message' && parentPort.listenerCount('message') === 0) {
parentPort.pause();
}
});
// Finally load entry script.
process._firstFileName = Module._resolveFilename(entryScript, null, false);
Module._load(entryScript, Module, true);

View file

@ -0,0 +1,30 @@
import { EventEmitter } from 'events';
import { MessagePortMain } from '@electron/internal/browser/message-port-main';
const { createParentPort } = process._linkedBinding('electron_utility_parent_port');
export class ParentPort extends EventEmitter {
#port: ParentPort
constructor () {
super();
this.#port = createParentPort();
this.#port.emit = (channel: string | symbol, event: { ports: any[] }) => {
if (channel === 'message') {
event = { ...event, ports: event.ports.map(p => new MessagePortMain(p)) };
}
this.emit(channel, event);
return false;
};
}
start () : void {
this.#port.start();
}
pause () : void {
this.#port.pause();
}
postMessage (message: any) : void {
this.#port.postMessage(message);
}
}

View file

@ -115,6 +115,7 @@ add_electron_deps_to_license_credits_file.patch
fix_crash_loading_non-standard_schemes_in_iframes.patch fix_crash_loading_non-standard_schemes_in_iframes.patch
fix_return_v8_value_from_localframe_requestexecutescript.patch fix_return_v8_value_from_localframe_requestexecutescript.patch
create_browser_v8_snapshot_file_name_fuse.patch create_browser_v8_snapshot_file_name_fuse.patch
feat_configure_launch_options_for_service_process.patch
feat_ensure_mas_builds_of_the_same_application_can_use_safestorage.patch feat_ensure_mas_builds_of_the_same_application_can_use_safestorage.patch
fix_on-screen-keyboard_hides_on_input_blur_in_webview.patch fix_on-screen-keyboard_hides_on_input_blur_in_webview.patch
preconnect_manager.patch preconnect_manager.patch

View file

@ -3,28 +3,28 @@ From: Jeremy Apthorp <nornagon@nornagon.net>
Date: Mon, 26 Aug 2019 12:02:51 -0700 Date: Mon, 26 Aug 2019 12:02:51 -0700
Subject: allow new privileges in unsandboxed child processes Subject: allow new privileges in unsandboxed child processes
This allows unsandboxed renderers to launch setuid processes on Linux. This allows unsandboxed child process to launch setuid processes on Linux.
diff --git a/content/browser/child_process_launcher_helper_linux.cc b/content/browser/child_process_launcher_helper_linux.cc diff --git a/content/browser/child_process_launcher_helper_linux.cc b/content/browser/child_process_launcher_helper_linux.cc
index dd5ccfc0bdc2e071999d1bf864dc065dd1311407..7464e84f6e610749dce5c3a46afce262f29020cc 100644 index dd5ccfc0bdc2e071999d1bf864dc065dd1311407..cfadd28fca9f80bf57578db78d5472c4f75414e1 100644
--- a/content/browser/child_process_launcher_helper_linux.cc --- a/content/browser/child_process_launcher_helper_linux.cc
+++ b/content/browser/child_process_launcher_helper_linux.cc +++ b/content/browser/child_process_launcher_helper_linux.cc
@@ -54,6 +54,18 @@ bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread( @@ -56,6 +56,18 @@ bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread(
if (GetProcessType() == switches::kRendererProcess) {
const int sandbox_fd = SandboxHostLinux::GetInstance()->GetChildSocket();
options->fds_to_remap.push_back(std::make_pair(sandbox_fd, GetSandboxFD())); options->fds_to_remap.push_back(std::make_pair(sandbox_fd, GetSandboxFD()));
+
+ // (For Electron), if we're launching without zygote, that means we're
+ // launching an unsandboxed process (since all sandboxed processes are
+ // forked from the zygote). Relax the allow_new_privs option to permit
+ // launching suid processes from unsandboxed renderers.
+ ZygoteHandle zygote_handle =
+ base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kNoZygote)
+ ? nullptr
+ : delegate_->GetZygote();
+ if (!zygote_handle) {
+ options->allow_new_privs = true;
+ }
} }
+ // (For Electron), if we're launching without zygote, that means we're
+ // launching an unsandboxed process (since all sandboxed processes are
+ // forked from the zygote). Relax the allow_new_privs option to permit
+ // launching suid processes from unsandboxed child processes.
+ ZygoteHandle zygote_handle =
+ base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kNoZygote)
+ ? nullptr
+ : delegate_->GetZygote();
+ if (!zygote_handle) {
+ options->allow_new_privs = true;
+ }
+
for (const auto& remapped_fd : file_data_->additional_remapped_fds) { for (const auto& remapped_fd : file_data_->additional_remapped_fds) {
options->fds_to_remap.emplace_back(remapped_fd.second.get(),
remapped_fd.first);

View file

@ -0,0 +1,671 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: deepak1556 <hop2deep@gmail.com>
Date: Wed, 17 Aug 2022 22:04:47 +0900
Subject: feat: configure launch options for service process
- POSIX:
Allows configuring base::LaunchOptions::fds_to_remap when launching the child process.
- Win:
Allows configuring base::LaunchOptions::handles_to_inherit, base::LaunchOptions::stdout_handle
and base::LaunchOptions::stderr_handle when launching the child process.
- All:
Allows configuring base::LauncOptions::current_directory, base::LaunchOptions::enviroment
and base::LaunchOptions::clear_environment.
An example use of this option, UtilityProcess API allows reading the output From
stdout and stderr of child process by creating a pipe, whose write end is remapped
to STDOUT_FILENO/STD_OUTPUT_HANDLE and STDERR_FILENO/STD_ERROR_HANDLE allowing the
parent process to read from the pipe.
diff --git a/content/browser/child_process_launcher.h b/content/browser/child_process_launcher.h
index ba1f0d6e958cdb534b8af7717a0d6d8f2ee296bf..626f771ffbd88f1cf2e9475b745456f98575cda1 100644
--- a/content/browser/child_process_launcher.h
+++ b/content/browser/child_process_launcher.h
@@ -31,6 +31,7 @@
#if BUILDFLAG(IS_WIN)
#include "base/win/windows_types.h"
+#include "base/win/scoped_handle.h"
#endif
#if BUILDFLAG(IS_POSIX)
@@ -163,7 +164,10 @@ struct ChildProcessLauncherFileData {
delete;
~ChildProcessLauncherFileData();
-#if BUILDFLAG(IS_POSIX)
+#if BUILDFLAG(IS_WIN)
+ base::win::ScopedHandle stdout_handle;
+ base::win::ScopedHandle stderr_handle;
+#elif BUILDFLAG(IS_POSIX)
// Files opened by the browser and passed as corresponding file descriptors
// in the child process.
// Currently only supported on Linux, ChromeOS and Android platforms.
diff --git a/content/browser/child_process_launcher_helper_linux.cc b/content/browser/child_process_launcher_helper_linux.cc
index cfadd28fca9f80bf57578db78d5472c4f75414e1..4925dc5cafbf312c3c9640d5873d62193e87f636 100644
--- a/content/browser/child_process_launcher_helper_linux.cc
+++ b/content/browser/child_process_launcher_helper_linux.cc
@@ -73,7 +73,9 @@ bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread(
remapped_fd.first);
}
+ options->current_directory = delegate_->GetCurrentDirectory();
options->environment = delegate_->GetEnvironment();
+ options->clear_environment = !delegate_->ShouldInheritEnvironment();
return true;
}
diff --git a/content/browser/child_process_launcher_helper_mac.cc b/content/browser/child_process_launcher_helper_mac.cc
index d74a40c0e5731281b132cc1c3dc2416f9dc2b083..dd8a9d35af617441c6643ed643b459a35b612969 100644
--- a/content/browser/child_process_launcher_helper_mac.cc
+++ b/content/browser/child_process_launcher_helper_mac.cc
@@ -73,7 +73,8 @@ bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread(
'mojo', base::MachRendezvousPort(endpoint.TakeMachReceiveRight())));
options->environment = delegate_->GetEnvironment();
-
+ options->clear_environment = !delegate_->ShouldInheritEnvironment();
+ options->current_directory = delegate_->GetCurrentDirectory();
options->disclaim_responsibility = delegate_->DisclaimResponsibility();
options->enable_cpu_security_mitigations =
delegate_->EnableCpuSecurityMitigations();
diff --git a/content/browser/child_process_launcher_helper_win.cc b/content/browser/child_process_launcher_helper_win.cc
index 799ad0a6e0b5c629d10f481d10dd4d6959d40b42..13c610ae1bb24fb6d274a082562dcd103df50513 100644
--- a/content/browser/child_process_launcher_helper_win.cc
+++ b/content/browser/child_process_launcher_helper_win.cc
@@ -19,6 +19,8 @@
#include "sandbox/policy/win/sandbox_win.h"
#include "sandbox/win/src/sandbox_types.h"
+#include <windows.h>
+
namespace content {
namespace internal {
@@ -54,6 +56,30 @@ bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread(
mojo_channel_->PrepareToPassRemoteEndpoint(&options->handles_to_inherit,
command_line());
}
+
+ if (file_data_->stdout_handle.IsValid() || file_data_->stderr_handle.IsValid()) {
+ // base::LaunchProcess requires that if any of the stdio handle is customized then
+ // the other two handles should also be set.
+ // https://source.chromium.org/chromium/chromium/src/+/main:base/process/launch_win.cc;l=341-350
+ options->stdin_handle = INVALID_HANDLE_VALUE;
+ if (file_data_->stdout_handle.IsValid()) {
+ options->stdout_handle = file_data_->stdout_handle.get();
+ } else {
+ options->stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
+ }
+
+ if (file_data_->stderr_handle.IsValid()) {
+ options->stderr_handle = file_data_->stderr_handle.get();
+ } else {
+ options->stderr_handle = GetStdHandle(STD_ERROR_HANDLE);
+ }
+ options->handles_to_inherit.push_back(options->stdout_handle);
+ options->handles_to_inherit.push_back(options->stderr_handle);
+ }
+
+ options->current_directory = delegate_->GetCurrentDirectory();
+ options->environment = delegate_->GetEnvironment();
+ options->clear_environment = !delegate_->ShouldInheritEnvironment();
return true;
}
@@ -81,7 +107,7 @@ ChildProcessLauncherHelper::LaunchProcessOnLauncherThread(
ChildProcessLauncherHelper::Process process;
*launch_result =
StartSandboxedProcess(delegate_.get(), *command_line(),
- options.handles_to_inherit, &process.process);
+ options, &process.process);
return process;
}
diff --git a/content/browser/service_process_host_impl.cc b/content/browser/service_process_host_impl.cc
index e547f42bc0d06b485797ccc1605969259631831f..0f3041f4a5b636440d9579303721f2ae7e1855c6 100644
--- a/content/browser/service_process_host_impl.cc
+++ b/content/browser/service_process_host_impl.cc
@@ -190,6 +190,15 @@ void LaunchServiceProcess(mojo::GenericPendingReceiver receiver,
host->SetExtraCommandLineSwitches(std::move(options.extra_switches));
if (options.child_flags)
host->set_child_flags(*options.child_flags);
+#if BUILDFLAG(IS_WIN)
+ host->SetStdioHandles(std::move(options.stdout_handle), std::move(options.stderr_handle));
+#elif BUILDFLAG(IS_POSIX)
+ host->SetAdditionalFds(std::move(options.fds_to_remap));
+#endif
+ host->SetCurrentDirectory(options.current_directory);
+ host->SetEnv(options.environment);
+ if (options.clear_environment)
+ host->ClearEnvironment();
host->Start();
host->GetChildProcess()->BindServiceInterface(std::move(receiver));
}
diff --git a/content/browser/utility_process_host.cc b/content/browser/utility_process_host.cc
index 171b8440c25580d717f87c4f68bd8f4734b5fcf1..35826081dc3fc2f17fd7ceaf25c2c014ae623304 100644
--- a/content/browser/utility_process_host.cc
+++ b/content/browser/utility_process_host.cc
@@ -108,11 +108,13 @@ const ChildProcessData& UtilityProcessHost::GetData() {
return process_->GetData();
}
-#if BUILDFLAG(IS_POSIX)
void UtilityProcessHost::SetEnv(const base::EnvironmentMap& env) {
env_ = env;
}
-#endif
+
+void UtilityProcessHost::ClearEnvironment() {
+ inherit_environment_ = false;
+}
bool UtilityProcessHost::Start() {
return StartProcess();
@@ -153,6 +155,24 @@ void UtilityProcessHost::SetExtraCommandLineSwitches(
extra_switches_ = std::move(switches);
}
+#if BUILDFLAG(IS_WIN)
+void UtilityProcessHost::SetStdioHandles(
+ base::win::ScopedHandle stdout_handle,
+ base::win::ScopedHandle stderr_handle) {
+ stdout_handle_ = std::move(stdout_handle);
+ stderr_handle_ = std::move(stderr_handle);
+}
+#elif BUILDFLAG(IS_POSIX)
+void UtilityProcessHost::SetAdditionalFds(base::FileHandleMappingVector mapping) {
+ fds_to_remap_ = std::move(mapping);
+}
+#endif
+
+void UtilityProcessHost::SetCurrentDirectory(
+ const base::FilePath& cwd) {
+ current_directory_ = cwd;
+}
+
mojom::ChildProcess* UtilityProcessHost::GetChildProcess() {
return static_cast<ChildProcessHostImpl*>(process_->GetHost())
->child_process();
@@ -356,9 +376,22 @@ bool UtilityProcessHost::StartProcess() {
}
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+#if BUILDFLAG(IS_WIN)
+ file_data->stdout_handle = std::move(stdout_handle_);
+ file_data->stderr_handle = std::move(stderr_handle_);
+#elif BUILDFLAG(IS_POSIX)
+ if (!fds_to_remap_.empty()) {
+ for (const auto& remapped_fd : fds_to_remap_) {
+ file_data->additional_remapped_fds.emplace(
+ remapped_fd.second, remapped_fd.first);
+ }
+ }
+#endif
+
std::unique_ptr<UtilitySandboxedProcessLauncherDelegate> delegate =
std::make_unique<UtilitySandboxedProcessLauncherDelegate>(
- sandbox_type_, env_, *cmd_line);
+ sandbox_type_, env_, current_directory_, *cmd_line,
+ inherit_environment_);
process_->LaunchWithFileData(std::move(delegate), std::move(cmd_line),
std::move(file_data), true);
diff --git a/content/browser/utility_process_host.h b/content/browser/utility_process_host.h
index 13de4795df7731f27760901aff17c143008a72c1..3b8af456d86e7aaf3b57e6b039c7f444e1c9e5fe 100644
--- a/content/browser/utility_process_host.h
+++ b/content/browser/utility_process_host.h
@@ -29,6 +29,10 @@
#include "mojo/public/cpp/system/message_pipe.h"
#endif
+#if BUILDFLAG(IS_WIN)
+#include "base/win/scoped_handle.h"
+#endif
+
namespace base {
class Thread;
} // namespace base
@@ -87,9 +91,13 @@ class CONTENT_EXPORT UtilityProcessHost
// Returns information about the utility child process.
const ChildProcessData& GetData();
-#if BUILDFLAG(IS_POSIX)
+
+ // Set/Unset environment variables.
void SetEnv(const base::EnvironmentMap& env);
-#endif
+
+ // Clear the environment for the new process before processing
+ // changes from SetEnv.
+ void ClearEnvironment();
// Starts the utility process.
bool Start();
@@ -118,6 +126,16 @@ class CONTENT_EXPORT UtilityProcessHost
// Provides extra switches to append to the process's command line.
void SetExtraCommandLineSwitches(std::vector<std::string> switches);
+#if BUILDFLAG(IS_WIN)
+ void SetStdioHandles(base::win::ScopedHandle stdout_handle,
+ base::win::ScopedHandle stderr_handle);
+#elif BUILDFLAG(IS_POSIX)
+ void SetAdditionalFds(base::FileHandleMappingVector mapping);
+#endif
+
+ // Sets the working directory of the process.
+ void SetCurrentDirectory(const base::FilePath& cwd);
+
// Returns a control interface for the running child process.
mojom::ChildProcess* GetChildProcess();
@@ -159,6 +177,22 @@ class CONTENT_EXPORT UtilityProcessHost
// Extra command line switches to append.
std::vector<std::string> extra_switches_;
+#if BUILDFLAG(IS_WIN)
+ // Specifies the handles for redirection of stdout and stderr.
+ base::win::ScopedHandle stdout_handle_;
+ base::win::ScopedHandle stderr_handle_;
+#elif BUILDFLAG(IS_POSIX)
+ // Specifies file descriptors to propagate into the child process
+ // based on the mapping.
+ base::FileHandleMappingVector fds_to_remap_;
+#endif
+
+ // If not empty, change to this directory before executing the new process.
+ base::FilePath current_directory_;
+
+ // Inherit enviroment from parent process.
+ bool inherit_environment_ = true;
+
// Indicates whether the process has been successfully launched yet, or if
// launch failed.
enum class LaunchState {
diff --git a/content/browser/utility_sandbox_delegate.cc b/content/browser/utility_sandbox_delegate.cc
index 070ee151ee96baa771cec6fe4de9f8762eff91bc..d7621b234e45f94a2ca8bc79f25345025b3bc48a 100644
--- a/content/browser/utility_sandbox_delegate.cc
+++ b/content/browser/utility_sandbox_delegate.cc
@@ -29,13 +29,15 @@ UtilitySandboxedProcessLauncherDelegate::
UtilitySandboxedProcessLauncherDelegate(
sandbox::mojom::Sandbox sandbox_type,
const base::EnvironmentMap& env,
- const base::CommandLine& cmd_line)
+ const base::FilePath& cwd,
+ const base::CommandLine& cmd_line,
+ bool inherit_environment)
:
-#if BUILDFLAG(IS_POSIX)
env_(env),
-#endif
+ current_directory_(cwd),
sandbox_type_(sandbox_type),
- cmd_line_(cmd_line) {
+ cmd_line_(cmd_line),
+ inherit_environment_(inherit_environment) {
#if DCHECK_IS_ON()
bool supported_sandbox_type =
sandbox_type_ == sandbox::mojom::Sandbox::kNoSandbox ||
@@ -93,11 +95,17 @@ UtilitySandboxedProcessLauncherDelegate::GetSandboxType() {
return sandbox_type_;
}
-#if BUILDFLAG(IS_POSIX)
base::EnvironmentMap UtilitySandboxedProcessLauncherDelegate::GetEnvironment() {
return env_;
}
-#endif // BUILDFLAG(IS_POSIX)
+
+bool UtilitySandboxedProcessLauncherDelegate::ShouldInheritEnvironment() {
+ return inherit_environment_;
+}
+
+base::FilePath UtilitySandboxedProcessLauncherDelegate::GetCurrentDirectory() {
+ return current_directory_;
+}
#if BUILDFLAG(USE_ZYGOTE_HANDLE)
ZygoteHandle UtilitySandboxedProcessLauncherDelegate::GetZygote() {
diff --git a/content/browser/utility_sandbox_delegate.h b/content/browser/utility_sandbox_delegate.h
index 41d93b41e7fff8ba4a7138d05035e4bc24b7a85b..20cb410fc71994e26cff6ac9801d42ebd11d9fee 100644
--- a/content/browser/utility_sandbox_delegate.h
+++ b/content/browser/utility_sandbox_delegate.h
@@ -26,7 +26,9 @@ class UtilitySandboxedProcessLauncherDelegate
public:
UtilitySandboxedProcessLauncherDelegate(sandbox::mojom::Sandbox sandbox_type,
const base::EnvironmentMap& env,
- const base::CommandLine& cmd_line);
+ const base::FilePath& cwd,
+ const base::CommandLine& cmd_line,
+ bool inherit_environment);
~UtilitySandboxedProcessLauncherDelegate() override;
sandbox::mojom::Sandbox GetSandboxType() override;
@@ -45,16 +47,16 @@ class UtilitySandboxedProcessLauncherDelegate
ZygoteHandle GetZygote() override;
#endif // BUILDFLAG(USE_ZYGOTE_HANDLE)
-#if BUILDFLAG(IS_POSIX)
base::EnvironmentMap GetEnvironment() override;
-#endif // BUILDFLAG(IS_POSIX)
+ bool ShouldInheritEnvironment() override;
+ base::FilePath GetCurrentDirectory() override;
private:
-#if BUILDFLAG(IS_POSIX)
base::EnvironmentMap env_;
-#endif // BUILDFLAG(IS_POSIX)
+ base::FilePath current_directory_;
sandbox::mojom::Sandbox sandbox_type_;
base::CommandLine cmd_line_;
+ bool inherit_environment_;
};
} // namespace content
diff --git a/content/common/sandbox_init_win.cc b/content/common/sandbox_init_win.cc
index 498f60227d13eb2e476413f88eaa58cc0babf461..b2d7a009477293bf73f3ae4a0c8452d1b1bf1dd8 100644
--- a/content/common/sandbox_init_win.cc
+++ b/content/common/sandbox_init_win.cc
@@ -23,7 +23,7 @@ namespace content {
sandbox::ResultCode StartSandboxedProcess(
SandboxedProcessLauncherDelegate* delegate,
const base::CommandLine& target_command_line,
- const base::HandlesToInheritVector& handles_to_inherit,
+ const base::LaunchOptions& options,
base::Process* process) {
std::string type_str =
target_command_line.GetSwitchValueASCII(switches::kProcessType);
@@ -45,7 +45,7 @@ sandbox::ResultCode StartSandboxedProcess(
}
return sandbox::policy::SandboxWin::StartSandboxedProcess(
- full_command_line, type_str, handles_to_inherit, delegate, process);
+ full_command_line, type_str, options, delegate, process);
}
} // namespace content
diff --git a/content/public/browser/service_process_host.cc b/content/public/browser/service_process_host.cc
index 6d25170e3badb65745c7dbea9c9664bdf8c91b0e..df79ba6137c8a9264ba32e4f9e1c1d7893e8f38a 100644
--- a/content/public/browser/service_process_host.cc
+++ b/content/public/browser/service_process_host.cc
@@ -46,12 +46,45 @@ ServiceProcessHost::Options::WithExtraCommandLineSwitches(
return *this;
}
+#if BUILDFLAG(IS_WIN)
+ServiceProcessHost::Options& ServiceProcessHost::Options::WithStdoutHandle(
+ base::win::ScopedHandle handle) {
+ stdout_handle = std::move(handle);
+ return *this;
+}
+
+ServiceProcessHost::Options& ServiceProcessHost::Options::WithStderrHandle(
+ base::win::ScopedHandle handle) {
+ stderr_handle = std::move(handle);
+ return *this;
+}
+#elif BUILDFLAG(IS_POSIX)
+ServiceProcessHost::Options& ServiceProcessHost::Options::WithAdditionalFds(
+ base::FileHandleMappingVector mapping) {
+ fds_to_remap = std::move(mapping);
+ return *this;
+}
+#endif
+
ServiceProcessHost::Options& ServiceProcessHost::Options::WithProcessCallback(
base::OnceCallback<void(const base::Process&)> callback) {
process_callback = std::move(callback);
return *this;
}
+ServiceProcessHost::Options& ServiceProcessHost::Options::WithCurrentDirectory(
+ const base::FilePath& cwd) {
+ current_directory = cwd;
+ return *this;
+}
+
+ServiceProcessHost::Options& ServiceProcessHost::Options::WithEnvironment(
+ const base::EnvironmentMap& env, bool new_environment) {
+ environment = env;
+ clear_environment = new_environment;
+ return *this;
+}
+
ServiceProcessHost::Options ServiceProcessHost::Options::Pass() {
return std::move(*this);
}
diff --git a/content/public/browser/service_process_host.h b/content/public/browser/service_process_host.h
index a308d46612c1b30163cf9988117d2224a43ab5ad..5a41c3c907c0f0cf42759c52e7493cbf675f6fa6 100644
--- a/content/public/browser/service_process_host.h
+++ b/content/public/browser/service_process_host.h
@@ -13,6 +13,7 @@
#include "base/callback.h"
#include "base/command_line.h"
#include "base/observer_list_types.h"
+#include "base/process/launch.h"
#include "base/process/process_handle.h"
#include "base/strings/string_piece.h"
#include "build/chromecast_buildflags.h"
@@ -29,6 +30,10 @@
#include "mojo/public/cpp/system/message_pipe.h"
#endif
+#if BUILDFLAG(IS_WIN)
+#include "base/win/scoped_handle.h"
+#endif
+
namespace base {
class Process;
} // namespace base
@@ -88,11 +93,30 @@ class CONTENT_EXPORT ServiceProcessHost {
// Specifies extra command line switches to append before launch.
Options& WithExtraCommandLineSwitches(std::vector<std::string> switches);
+#if BUILDFLAG(IS_WIN)
+ // Specifies the handles for redirection of stdout and stderr.
+ Options& WithStdoutHandle(base::win::ScopedHandle stdout_handle);
+ Options& WithStderrHandle(base::win::ScopedHandle stderr_handle);
+#elif BUILDFLAG(IS_POSIX)
+ // Specifies file descriptors to propagate into the child process
+ // based on the mapping.
+ Options& WithAdditionalFds(base::FileHandleMappingVector mapping);
+#endif
+
// Specifies a callback to be invoked with service process once it's
// launched. Will be on UI thread.
Options& WithProcessCallback(
base::OnceCallback<void(const base::Process&)>);
+ // Specifies the working directory for the launched process.
+ Options& WithCurrentDirectory(const base::FilePath& cwd);
+
+ // Specifies the environment that should be applied to the process.
+ // |new_environment| controls whether the process should inherit
+ // environment from the parent process.
+ Options& WithEnvironment(const base::EnvironmentMap& environment,
+ bool new_environment);
+
// Passes the contents of this Options object to a newly returned Options
// value. This must be called when moving a built Options object into a call
// to |Launch()|.
@@ -101,7 +125,16 @@ class CONTENT_EXPORT ServiceProcessHost {
std::u16string display_name;
absl::optional<int> child_flags;
std::vector<std::string> extra_switches;
+#if BUILDFLAG(IS_WIN)
+ base::win::ScopedHandle stdout_handle;
+ base::win::ScopedHandle stderr_handle;
+#elif BUILDFLAG(IS_POSIX)
+ base::FileHandleMappingVector fds_to_remap;
+#endif
base::OnceCallback<void(const base::Process&)> process_callback;
+ base::FilePath current_directory;
+ base::EnvironmentMap environment;
+ bool clear_environment = false;
};
// An interface which can be implemented and registered/unregistered with
diff --git a/content/public/common/sandbox_init_win.h b/content/public/common/sandbox_init_win.h
index 9bb4b30ba0f5d37ec2b28f0848d94f34c24f9423..c19cceae4215d74ae74f6e6005125f326453f955 100644
--- a/content/public/common/sandbox_init_win.h
+++ b/content/public/common/sandbox_init_win.h
@@ -29,7 +29,7 @@ class SandboxedProcessLauncherDelegate;
CONTENT_EXPORT sandbox::ResultCode StartSandboxedProcess(
SandboxedProcessLauncherDelegate* delegate,
const base::CommandLine& target_command_line,
- const base::HandlesToInheritVector& handles_to_inherit,
+ const base::LaunchOptions& options,
base::Process* process);
} // namespace content
diff --git a/content/public/common/sandboxed_process_launcher_delegate.cc b/content/public/common/sandboxed_process_launcher_delegate.cc
index ee7cdddba192f151346b74b68ef1eabe5f46e84a..4378d5ac7f455eb54f9f39364184649d7a63666f 100644
--- a/content/public/common/sandboxed_process_launcher_delegate.cc
+++ b/content/public/common/sandboxed_process_launcher_delegate.cc
@@ -53,11 +53,17 @@ ZygoteHandle SandboxedProcessLauncherDelegate::GetZygote() {
}
#endif // BUILDFLAG(USE_ZYGOTE_HANDLE)
-#if BUILDFLAG(IS_POSIX)
base::EnvironmentMap SandboxedProcessLauncherDelegate::GetEnvironment() {
return base::EnvironmentMap();
}
-#endif // BUILDFLAG(IS_POSIX)
+
+bool SandboxedProcessLauncherDelegate::ShouldInheritEnvironment() {
+ return true;
+}
+
+base::FilePath SandboxedProcessLauncherDelegate::GetCurrentDirectory() {
+ return base::FilePath();
+}
#if BUILDFLAG(IS_MAC)
diff --git a/content/public/common/sandboxed_process_launcher_delegate.h b/content/public/common/sandboxed_process_launcher_delegate.h
index 1e8f3994764a2b4e4efb87a08c522cc0e0103e18..83cc16ffbf484aa78b1c350d20a5a15ffd0dd0e8 100644
--- a/content/public/common/sandboxed_process_launcher_delegate.h
+++ b/content/public/common/sandboxed_process_launcher_delegate.h
@@ -6,6 +6,7 @@
#define CONTENT_PUBLIC_COMMON_SANDBOXED_PROCESS_LAUNCHER_DELEGATE_H_
#include "base/environment.h"
+#include "base/files/file_path.h"
#include "base/files/scoped_file.h"
#include "base/process/process.h"
#include "build/build_config.h"
@@ -48,10 +49,14 @@ class CONTENT_EXPORT SandboxedProcessLauncherDelegate
virtual ZygoteHandle GetZygote();
#endif // BUILDFLAG(USE_ZYGOTE_HANDLE)
-#if BUILDFLAG(IS_POSIX)
// Override this if the process needs a non-empty environment map.
virtual base::EnvironmentMap GetEnvironment();
-#endif // BUILDFLAG(IS_POSIX)
+
+ // Override this if the process should not inherit parent environment.
+ virtual bool ShouldInheritEnvironment();
+
+ // Specifies the directory to change to before executing the process.
+ virtual base::FilePath GetCurrentDirectory();
#if BUILDFLAG(IS_MAC)
// Whether or not to disclaim TCC responsibility for the process, defaults to
diff --git a/sandbox/policy/win/sandbox_win.cc b/sandbox/policy/win/sandbox_win.cc
index 2191f51de17cfde5bb39f8231c8210dea6aa4fdd..6239f68771832d245d7270fd83e04f4fdce44032 100644
--- a/sandbox/policy/win/sandbox_win.cc
+++ b/sandbox/policy/win/sandbox_win.cc
@@ -851,11 +851,9 @@ ResultCode GenerateConfigForSandboxedProcess(const base::CommandLine& cmd_line,
// command line flag.
ResultCode LaunchWithoutSandbox(
const base::CommandLine& cmd_line,
- const base::HandlesToInheritVector& handles_to_inherit,
+ base::LaunchOptions options,
SandboxDelegate* delegate,
base::Process* process) {
- base::LaunchOptions options;
- options.handles_to_inherit = handles_to_inherit;
// Network process runs in a job even when unsandboxed. This is to ensure it
// does not outlive the browser, which could happen if there is a lot of I/O
// on process shutdown, in which case TerminateProcess can fail. See
@@ -1091,7 +1089,7 @@ bool SandboxWin::InitTargetServices(TargetServices* target_services) {
ResultCode SandboxWin::GeneratePolicyForSandboxedProcess(
const base::CommandLine& cmd_line,
const std::string& process_type,
- const base::HandlesToInheritVector& handles_to_inherit,
+ const base::LaunchOptions& options,
SandboxDelegate* delegate,
TargetPolicy* policy) {
const base::CommandLine& launcher_process_command_line =
@@ -1105,7 +1103,7 @@ ResultCode SandboxWin::GeneratePolicyForSandboxedProcess(
}
// Add any handles to be inherited to the policy.
- for (HANDLE handle : handles_to_inherit)
+ for (HANDLE handle : options.handles_to_inherit)
policy->AddHandleToShare(handle);
if (!policy->GetConfig()->IsConfigured()) {
@@ -1120,6 +1118,13 @@ ResultCode SandboxWin::GeneratePolicyForSandboxedProcess(
// have no effect. These calls can fail with SBOX_ERROR_BAD_PARAMS.
policy->SetStdoutHandle(GetStdHandle(STD_OUTPUT_HANDLE));
policy->SetStderrHandle(GetStdHandle(STD_ERROR_HANDLE));
+#else
+ if (options.stdout_handle != nullptr && options.stdout_handle != INVALID_HANDLE_VALUE) {
+ policy->SetStdoutHandle(options.stdout_handle);
+ }
+ if (options.stderr_handle != nullptr && options.stderr_handle != INVALID_HANDLE_VALUE) {
+ policy->SetStderrHandle(options.stderr_handle);
+ }
#endif
if (!delegate->PreSpawnTarget(policy))
@@ -1132,7 +1137,7 @@ ResultCode SandboxWin::GeneratePolicyForSandboxedProcess(
ResultCode SandboxWin::StartSandboxedProcess(
const base::CommandLine& cmd_line,
const std::string& process_type,
- const base::HandlesToInheritVector& handles_to_inherit,
+ const base::LaunchOptions& options,
SandboxDelegate* delegate,
base::Process* process) {
const base::ElapsedTimer timer;
@@ -1140,7 +1145,7 @@ ResultCode SandboxWin::StartSandboxedProcess(
// Avoid making a policy if we won't use it.
if (IsUnsandboxedProcess(delegate->GetSandboxType(), cmd_line,
*base::CommandLine::ForCurrentProcess())) {
- return LaunchWithoutSandbox(cmd_line, handles_to_inherit, delegate,
+ return LaunchWithoutSandbox(cmd_line, options, delegate,
process);
}
@@ -1151,7 +1156,7 @@ ResultCode SandboxWin::StartSandboxedProcess(
auto policy = g_broker_services->CreatePolicy(tag);
auto time_policy_created = timer.Elapsed();
ResultCode result = GeneratePolicyForSandboxedProcess(
- cmd_line, process_type, handles_to_inherit, delegate, policy.get());
+ cmd_line, process_type, options, delegate, policy.get());
if (SBOX_ALL_OK != result)
return result;
auto time_policy_generated = timer.Elapsed();
diff --git a/sandbox/policy/win/sandbox_win.h b/sandbox/policy/win/sandbox_win.h
index d1adadc10de3053f69fde39387d196054a96beda..0111a9c4becca009f17a3839d4d4bef3d9d880b8 100644
--- a/sandbox/policy/win/sandbox_win.h
+++ b/sandbox/policy/win/sandbox_win.h
@@ -50,7 +50,7 @@ class SANDBOX_POLICY_EXPORT SandboxWin {
static ResultCode StartSandboxedProcess(
const base::CommandLine& cmd_line,
const std::string& process_type,
- const base::HandlesToInheritVector& handles_to_inherit,
+ const base::LaunchOptions& options,
SandboxDelegate* delegate,
base::Process* process);
@@ -64,7 +64,7 @@ class SANDBOX_POLICY_EXPORT SandboxWin {
static ResultCode GeneratePolicyForSandboxedProcess(
const base::CommandLine& cmd_line,
const std::string& process_type,
- const base::HandlesToInheritVector& handles_to_inherit,
+ const base::LaunchOptions& options,
SandboxDelegate* delegate,
TargetPolicy* policy);

View file

@ -40,6 +40,10 @@ const main = async () => {
{ {
name: 'asar_bundle_deps', name: 'asar_bundle_deps',
config: 'webpack.config.asar.js' config: 'webpack.config.asar.js'
},
{
name: 'utility_bundle_deps',
config: 'webpack.config.utility.js'
} }
]; ];

View file

@ -45,6 +45,7 @@
#include "shell/app/command_line_args.h" #include "shell/app/command_line_args.h"
#include "shell/browser/api/electron_api_menu.h" #include "shell/browser/api/electron_api_menu.h"
#include "shell/browser/api/electron_api_session.h" #include "shell/browser/api/electron_api_session.h"
#include "shell/browser/api/electron_api_utility_process.h"
#include "shell/browser/api/electron_api_web_contents.h" #include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/api/gpuinfo_manager.h" #include "shell/browser/api/gpuinfo_manager.h"
#include "shell/browser/browser_process_impl.h" #include "shell/browser/browser_process_impl.h"
@ -922,6 +923,12 @@ void App::BrowserChildProcessCrashedOrKilled(
if (!data.name.empty()) { if (!data.name.empty()) {
details.Set("name", data.name); details.Set("name", data.name);
} }
if (data.process_type == content::PROCESS_TYPE_UTILITY) {
base::ProcessId pid = data.GetProcess().Pid();
auto utility_process_wrapper = UtilityProcessWrapper::FromProcessId(pid);
if (utility_process_wrapper)
utility_process_wrapper->Shutdown(info.exit_code);
}
Emit("child-process-gone", details); Emit("child-process-gone", details);
} }

View file

@ -0,0 +1,420 @@
// Copyright (c) 2022 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/api/electron_api_utility_process.h"
#include <map>
#include <utility>
#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/no_destructor.h"
#include "base/process/kill.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "content/public/browser/service_process_host.h"
#include "content/public/common/child_process_host.h"
#include "content/public/common/result_codes.h"
#include "gin/handle.h"
#include "gin/object_template_builder.h"
#include "gin/wrappable.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "shell/browser/api/message_port.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/node_includes.h"
#include "shell/common/v8_value_serializer.h"
#include "third_party/blink/public/common/messaging/message_port_descriptor.h"
#include "third_party/blink/public/common/messaging/transferable_message_mojom_traits.h"
#if BUILDFLAG(IS_POSIX)
#include "base/posix/eintr_wrapper.h"
#endif
#if BUILDFLAG(IS_WIN)
#include <fcntl.h>
#include <io.h>
#include "base/win/windows_types.h"
#endif
namespace electron {
base::IDMap<api::UtilityProcessWrapper*, base::ProcessId>&
GetAllUtilityProcessWrappers() {
static base::NoDestructor<
base::IDMap<api::UtilityProcessWrapper*, base::ProcessId>>
s_all_utility_process_wrappers;
return *s_all_utility_process_wrappers;
}
namespace api {
gin::WrapperInfo UtilityProcessWrapper::kWrapperInfo = {
gin::kEmbedderNativeGin};
UtilityProcessWrapper::UtilityProcessWrapper(
node::mojom::NodeServiceParamsPtr params,
std::u16string display_name,
std::map<IOHandle, IOType> stdio,
base::EnvironmentMap env_map,
base::FilePath current_working_directory,
bool use_plugin_helper) {
#if BUILDFLAG(IS_WIN)
base::win::ScopedHandle stdout_write(nullptr);
base::win::ScopedHandle stderr_write(nullptr);
#elif BUILDFLAG(IS_POSIX)
base::FileHandleMappingVector fds_to_remap;
#endif
for (const auto& [io_handle, io_type] : stdio) {
if (io_type == IOType::IO_PIPE) {
#if BUILDFLAG(IS_WIN)
HANDLE read = nullptr;
HANDLE write = nullptr;
// Ideally we would create with SECURITY_ATTRIBUTES.bInheritHandles
// set to TRUE so that the write handle can be duplicated into the
// child process for use,
// See
// https://learn.microsoft.com/en-us/windows/win32/procthread/inheritance#inheriting-handles
// for inheritance behavior of child process. But we don't do it here
// since base::Launch already takes of setting the
// inherit attribute when configuring
// `base::LaunchOptions::handles_to_inherit` Refs
// https://source.chromium.org/chromium/chromium/src/+/main:base/process/launch_win.cc;l=303-332
if (!::CreatePipe(&read, &write, nullptr, 0)) {
PLOG(ERROR) << "pipe creation failed";
return;
}
if (io_handle == IOHandle::STDOUT) {
stdout_write.Set(write);
stdout_read_handle_ = read;
stdout_read_fd_ =
_open_osfhandle(reinterpret_cast<intptr_t>(read), _O_RDONLY);
} else if (io_handle == IOHandle::STDERR) {
stderr_write.Set(write);
stderr_read_handle_ = read;
stderr_read_fd_ =
_open_osfhandle(reinterpret_cast<intptr_t>(read), _O_RDONLY);
}
#elif BUILDFLAG(IS_POSIX)
int pipe_fd[2];
if (HANDLE_EINTR(pipe(pipe_fd)) < 0) {
PLOG(ERROR) << "pipe creation failed";
return;
}
if (io_handle == IOHandle::STDOUT) {
fds_to_remap.push_back(std::make_pair(pipe_fd[1], STDOUT_FILENO));
stdout_read_fd_ = pipe_fd[0];
} else if (io_handle == IOHandle::STDERR) {
fds_to_remap.push_back(std::make_pair(pipe_fd[1], STDERR_FILENO));
stderr_read_fd_ = pipe_fd[0];
}
#endif
} else if (io_type == IOType::IO_IGNORE) {
#if BUILDFLAG(IS_WIN)
HANDLE handle =
CreateFileW(L"NUL", FILE_GENERIC_WRITE | FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, 0, nullptr);
if (handle == INVALID_HANDLE_VALUE) {
PLOG(ERROR) << "Failed to create null handle";
return;
}
if (io_handle == IOHandle::STDOUT) {
stdout_write.Set(handle);
} else if (io_handle == IOHandle::STDERR) {
stderr_write.Set(handle);
}
#elif BUILDFLAG(IS_POSIX)
int devnull = open("/dev/null", O_WRONLY);
if (devnull < 0) {
PLOG(ERROR) << "failed to open /dev/null";
return;
}
if (io_handle == IOHandle::STDOUT) {
fds_to_remap.push_back(std::make_pair(devnull, STDOUT_FILENO));
} else if (io_handle == IOHandle::STDERR) {
fds_to_remap.push_back(std::make_pair(devnull, STDERR_FILENO));
}
#endif
}
}
mojo::PendingReceiver<node::mojom::NodeService> receiver =
node_service_remote_.BindNewPipeAndPassReceiver();
content::ServiceProcessHost::Launch(
std::move(receiver),
content::ServiceProcessHost::Options()
.WithDisplayName(display_name.empty()
? std::u16string(u"Node Utility Process")
: display_name)
.WithExtraCommandLineSwitches(params->exec_args)
.WithCurrentDirectory(current_working_directory)
// Inherit parent process environment when there is no custom
// environment provided by the user.
.WithEnvironment(env_map,
env_map.empty() ? false : true /*clear_environment*/)
#if BUILDFLAG(IS_WIN)
.WithStdoutHandle(std::move(stdout_write))
.WithStderrHandle(std::move(stderr_write))
#elif BUILDFLAG(IS_POSIX)
.WithAdditionalFds(std::move(fds_to_remap))
#endif
#if BUILDFLAG(IS_MAC)
.WithChildFlags(use_plugin_helper
? content::ChildProcessHost::CHILD_PLUGIN
: content::ChildProcessHost::CHILD_NORMAL)
#endif
.WithProcessCallback(
base::BindOnce(&UtilityProcessWrapper::OnServiceProcessLaunched,
weak_factory_.GetWeakPtr()))
.Pass());
node_service_remote_.set_disconnect_with_reason_handler(
base::BindOnce(&UtilityProcessWrapper::OnServiceProcessDisconnected,
weak_factory_.GetWeakPtr()));
// We use a separate message pipe to support postMessage API
// instead of the existing receiver interface so that we can
// support queuing of messages without having to block other
// interfaces.
blink::MessagePortDescriptorPair pipe;
host_port_ = pipe.TakePort0();
params->port = pipe.TakePort1();
connector_ = std::make_unique<mojo::Connector>(
host_port_.TakeHandleToEntangleWithEmbedder(),
mojo::Connector::SINGLE_THREADED_SEND,
base::ThreadTaskRunnerHandle::Get());
connector_->set_incoming_receiver(this);
connector_->set_connection_error_handler(base::BindOnce(
&UtilityProcessWrapper::CloseConnectorPort, weak_factory_.GetWeakPtr()));
node_service_remote_->Initialize(std::move(params));
}
UtilityProcessWrapper::~UtilityProcessWrapper() = default;
void UtilityProcessWrapper::OnServiceProcessLaunched(
const base::Process& process) {
DCHECK(node_service_remote_.is_connected());
pid_ = process.Pid();
GetAllUtilityProcessWrappers().AddWithID(this, pid_);
if (stdout_read_fd_ != -1) {
EmitWithoutCustomEvent("stdout", stdout_read_fd_);
}
if (stderr_read_fd_ != -1) {
EmitWithoutCustomEvent("stderr", stderr_read_fd_);
}
// Emit 'spawn' event
EmitWithoutCustomEvent("spawn");
}
void UtilityProcessWrapper::OnServiceProcessDisconnected(
uint32_t error_code,
const std::string& description) {
if (pid_ != base::kNullProcessId)
GetAllUtilityProcessWrappers().Remove(pid_);
CloseConnectorPort();
// Emit 'exit' event
EmitWithoutCustomEvent("exit", error_code);
Unpin();
}
void UtilityProcessWrapper::CloseConnectorPort() {
if (!connector_closed_ && connector_->is_valid()) {
host_port_.GiveDisentangledHandle(connector_->PassMessagePipe());
connector_ = nullptr;
host_port_.Reset();
connector_closed_ = true;
}
}
void UtilityProcessWrapper::Shutdown(int exit_code) {
if (pid_ != base::kNullProcessId)
GetAllUtilityProcessWrappers().Remove(pid_);
node_service_remote_.reset();
CloseConnectorPort();
// Emit 'exit' event
EmitWithoutCustomEvent("exit", exit_code);
Unpin();
}
void UtilityProcessWrapper::PostMessage(gin::Arguments* args) {
if (!node_service_remote_.is_connected())
return;
blink::TransferableMessage transferable_message;
v8::Local<v8::Value> message_value;
if (args->GetNext(&message_value)) {
if (!electron::SerializeV8Value(args->isolate(), message_value,
&transferable_message)) {
// SerializeV8Value sets an exception.
return;
}
}
v8::Local<v8::Value> transferables;
std::vector<gin::Handle<MessagePort>> wrapped_ports;
if (args->GetNext(&transferables)) {
if (!gin::ConvertFromV8(args->isolate(), transferables, &wrapped_ports)) {
gin_helper::ErrorThrower(args->isolate())
.ThrowTypeError("Invalid value for transfer");
return;
}
}
bool threw_exception = false;
transferable_message.ports = MessagePort::DisentanglePorts(
args->isolate(), wrapped_ports, &threw_exception);
if (threw_exception)
return;
mojo::Message mojo_message = blink::mojom::TransferableMessage::WrapAsMessage(
std::move(transferable_message));
connector_->Accept(&mojo_message);
}
bool UtilityProcessWrapper::Kill() const {
if (pid_ == base::kNullProcessId)
return 0;
base::Process process = base::Process::Open(pid_);
bool result = process.Terminate(content::RESULT_CODE_NORMAL_EXIT, false);
// Refs https://bugs.chromium.org/p/chromium/issues/detail?id=818244
// Currently utility process is not sandboxed which
// means Zygote is not used on linux, refs
// content::UtilitySandboxedProcessLauncherDelegate::GetZygote.
// If sandbox feature is enabled for the utility process, then the
// process reap should be signaled through the zygote via
// content::ZygoteCommunication::EnsureProcessTerminated.
base::EnsureProcessTerminated(std::move(process));
return result;
}
v8::Local<v8::Value> UtilityProcessWrapper::GetOSProcessId(
v8::Isolate* isolate) const {
if (pid_ == base::kNullProcessId)
return v8::Undefined(isolate);
return gin::ConvertToV8(isolate, pid_);
}
bool UtilityProcessWrapper::Accept(mojo::Message* mojo_message) {
blink::TransferableMessage message;
if (!blink::mojom::TransferableMessage::DeserializeFromMessage(
std::move(*mojo_message), &message)) {
return false;
}
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Value> message_value =
electron::DeserializeV8Value(isolate, message);
EmitWithoutCustomEvent("message", message_value);
return true;
}
// static
raw_ptr<UtilityProcessWrapper> UtilityProcessWrapper::FromProcessId(
base::ProcessId pid) {
auto* utility_process_wrapper = GetAllUtilityProcessWrappers().Lookup(pid);
return !!utility_process_wrapper ? utility_process_wrapper : nullptr;
}
// static
gin::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
gin::Arguments* args) {
gin_helper::Dictionary dict;
if (!args->GetNext(&dict)) {
args->ThrowTypeError("Options must be an object.");
return gin::Handle<UtilityProcessWrapper>();
}
std::u16string display_name;
bool use_plugin_helper = false;
std::map<IOHandle, IOType> stdio;
base::FilePath current_working_directory;
base::EnvironmentMap env_map;
node::mojom::NodeServiceParamsPtr params =
node::mojom::NodeServiceParams::New();
dict.Get("modulePath", &params->script);
if (dict.Has("args") && !dict.Get("args", &params->args)) {
args->ThrowTypeError("Invalid value for args");
return gin::Handle<UtilityProcessWrapper>();
}
gin_helper::Dictionary opts;
if (dict.Get("options", &opts)) {
if (opts.Has("env") && !opts.Get("env", &env_map)) {
args->ThrowTypeError("Invalid value for env");
return gin::Handle<UtilityProcessWrapper>();
}
if (opts.Has("execArgv") && !opts.Get("execArgv", &params->exec_args)) {
args->ThrowTypeError("Invalid value for execArgv");
return gin::Handle<UtilityProcessWrapper>();
}
opts.Get("serviceName", &display_name);
opts.Get("cwd", &current_working_directory);
std::vector<std::string> stdio_arr{"ignore", "inherit", "inherit"};
opts.Get("stdio", &stdio_arr);
for (size_t i = 0; i < 3; i++) {
IOType type;
if (stdio_arr[i] == "ignore")
type = IOType::IO_IGNORE;
else if (stdio_arr[i] == "inherit")
type = IOType::IO_INHERIT;
else if (stdio_arr[i] == "pipe")
type = IOType::IO_PIPE;
stdio.emplace(static_cast<IOHandle>(i), type);
}
#if BUILDFLAG(IS_MAC)
opts.Get("allowLoadingUnsignedLibraries", &use_plugin_helper);
#endif
}
auto handle = gin::CreateHandle(
args->isolate(),
new UtilityProcessWrapper(std::move(params), display_name,
std::move(stdio), env_map,
current_working_directory, use_plugin_helper));
handle->Pin(args->isolate());
return handle;
}
// static
gin::ObjectTemplateBuilder UtilityProcessWrapper::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin_helper::EventEmitterMixin<
UtilityProcessWrapper>::GetObjectTemplateBuilder(isolate)
.SetMethod("postMessage", &UtilityProcessWrapper::PostMessage)
.SetMethod("kill", &UtilityProcessWrapper::Kill)
.SetProperty("pid", &UtilityProcessWrapper::GetOSProcessId);
}
const char* UtilityProcessWrapper::GetTypeName() {
return "UtilityProcessWrapper";
}
} // namespace api
} // namespace electron
namespace {
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
void* priv) {
v8::Isolate* isolate = context->GetIsolate();
gin_helper::Dictionary dict(isolate, exports);
dict.SetMethod("_fork", &electron::api::UtilityProcessWrapper::Create);
}
} // namespace
NODE_LINKED_MODULE_CONTEXT_AWARE(electron_browser_utility_process, Initialize)

View file

@ -0,0 +1,100 @@
// Copyright (c) 2022 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_UTILITY_PROCESS_H_
#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_UTILITY_PROCESS_H_
#include <map>
#include <memory>
#include <string>
#include <vector>
#include "base/containers/id_map.h"
#include "base/environment.h"
#include "base/memory/weak_ptr.h"
#include "base/process/process_handle.h"
#include "gin/wrappable.h"
#include "mojo/public/cpp/bindings/connector.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/common/gin_helper/pinnable.h"
#include "shell/services/node/public/mojom/node_service.mojom.h"
#include "v8/include/v8.h"
namespace gin {
class Arguments;
template <typename T>
class Handle;
} // namespace gin
namespace base {
class Process;
} // namespace base
namespace electron {
namespace api {
class UtilityProcessWrapper
: public gin::Wrappable<UtilityProcessWrapper>,
public gin_helper::Pinnable<UtilityProcessWrapper>,
public gin_helper::EventEmitterMixin<UtilityProcessWrapper>,
public mojo::MessageReceiver {
public:
enum class IOHandle : size_t { STDIN = 0, STDOUT = 1, STDERR = 2 };
enum class IOType { IO_PIPE, IO_INHERIT, IO_IGNORE };
~UtilityProcessWrapper() override;
static gin::Handle<UtilityProcessWrapper> Create(gin::Arguments* args);
static raw_ptr<UtilityProcessWrapper> FromProcessId(base::ProcessId pid);
void Shutdown(int exit_code);
// gin::Wrappable
static gin::WrapperInfo kWrapperInfo;
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override;
const char* GetTypeName() override;
private:
UtilityProcessWrapper(node::mojom::NodeServiceParamsPtr params,
std::u16string display_name,
std::map<IOHandle, IOType> stdio,
base::EnvironmentMap env_map,
base::FilePath current_working_directory,
bool use_plugin_helper);
void OnServiceProcessDisconnected(uint32_t error_code,
const std::string& description);
void OnServiceProcessLaunched(const base::Process& process);
void CloseConnectorPort();
void PostMessage(gin::Arguments* args);
bool Kill() const;
v8::Local<v8::Value> GetOSProcessId(v8::Isolate* isolate) const;
// mojo::MessageReceiver
bool Accept(mojo::Message* mojo_message) override;
base::ProcessId pid_ = base::kNullProcessId;
#if BUILDFLAG(IS_WIN)
// Non-owning handles, these will be closed when the
// corresponding FD are closed via _close.
HANDLE stdout_read_handle_;
HANDLE stderr_read_handle_;
#endif
int stdout_read_fd_ = -1;
int stderr_read_fd_ = -1;
bool connector_closed_ = false;
std::unique_ptr<mojo::Connector> connector_;
blink::MessagePortDescriptor host_port_;
mojo::Remote<node::mojom::NodeService> node_service_remote_;
base::WeakPtrFactory<UtilityProcessWrapper> weak_factory_{this};
};
} // namespace api
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_UTILITY_PROCESS_H_

View file

@ -24,13 +24,17 @@
#include "components/os_crypt/key_storage_config_linux.h" #include "components/os_crypt/key_storage_config_linux.h"
#include "components/os_crypt/os_crypt.h" #include "components/os_crypt/os_crypt.h"
#include "content/browser/browser_main_loop.h" // nogncheck #include "content/browser/browser_main_loop.h" // nogncheck
#include "content/public/browser/browser_child_process_host_delegate.h"
#include "content/public/browser/browser_child_process_host_iterator.h"
#include "content/public/browser/browser_thread.h" #include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_data.h"
#include "content/public/browser/child_process_security_policy.h" #include "content/public/browser/child_process_security_policy.h"
#include "content/public/browser/device_service.h" #include "content/public/browser/device_service.h"
#include "content/public/browser/first_party_sets_handler.h" #include "content/public/browser/first_party_sets_handler.h"
#include "content/public/browser/web_ui_controller_factory.h" #include "content/public/browser/web_ui_controller_factory.h"
#include "content/public/common/content_features.h" #include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h" #include "content/public/common/content_switches.h"
#include "content/public/common/process_type.h"
#include "content/public/common/result_codes.h" #include "content/public/common/result_codes.h"
#include "electron/buildflags/buildflags.h" #include "electron/buildflags/buildflags.h"
#include "electron/fuses.h" #include "electron/fuses.h"
@ -39,6 +43,7 @@
#include "services/tracing/public/cpp/stack_sampling/tracing_sampler_profiler.h" #include "services/tracing/public/cpp/stack_sampling/tracing_sampler_profiler.h"
#include "shell/app/electron_main_delegate.h" #include "shell/app/electron_main_delegate.h"
#include "shell/browser/api/electron_api_app.h" #include "shell/browser/api/electron_api_app.h"
#include "shell/browser/api/electron_api_utility_process.h"
#include "shell/browser/browser.h" #include "shell/browser/browser.h"
#include "shell/browser/browser_process_impl.h" #include "shell/browser/browser_process_impl.h"
#include "shell/browser/electron_browser_client.h" #include "shell/browser/electron_browser_client.h"
@ -273,12 +278,15 @@ void ElectronBrowserMainParts::PostEarlyInitialization() {
// Add Electron extended APIs. // Add Electron extended APIs.
electron_bindings_->BindTo(js_env_->isolate(), env->process_object()); electron_bindings_->BindTo(js_env_->isolate(), env->process_object());
// Load everything. // Create explicit microtasks runner.
node_bindings_->LoadEnvironment(env); js_env_->CreateMicrotasksRunner();
// Wrap the uv loop with global env. // Wrap the uv loop with global env.
node_bindings_->set_uv_env(env); node_bindings_->set_uv_env(env);
// Load everything.
node_bindings_->LoadEnvironment(env);
// We already initialized the feature list in PreEarlyInitialization(), but // We already initialized the feature list in PreEarlyInitialization(), but
// the user JS script would not have had a chance to alter the command-line // the user JS script would not have had a chance to alter the command-line
// switches at that point. Lets reinitialize it here to pick up the // switches at that point. Lets reinitialize it here to pick up the
@ -503,7 +511,6 @@ int ElectronBrowserMainParts::PreMainMessageLoopRun() {
void ElectronBrowserMainParts::WillRunMainMessageLoop( void ElectronBrowserMainParts::WillRunMainMessageLoop(
std::unique_ptr<base::RunLoop>& run_loop) { std::unique_ptr<base::RunLoop>& run_loop) {
js_env_->OnMessageLoopCreated();
exit_code_ = content::RESULT_CODE_NORMAL_EXIT; exit_code_ = content::RESULT_CODE_NORMAL_EXIT;
Browser::Get()->SetMainMessageLoopQuitClosure( Browser::Get()->SetMainMessageLoopQuitClosure(
run_loop->QuitWhenIdleClosure()); run_loop->QuitWhenIdleClosure());
@ -565,10 +572,39 @@ void ElectronBrowserMainParts::PostMainMessageLoopRun() {
} }
} }
// Shutdown utility process created with Electron API before
// stopping Node.js so that exit events can be emitted. We don't let
// content layer perform this action since it destroys
// child process only after this step (PostMainMessageLoopRun) via
// BrowserProcessIOThread::ProcessHostCleanUp() which is too late for our
// use case.
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/browser_main_loop.cc;l=1086-1108
//
// The following logic is based on
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/browser_process_io_thread.cc;l=127-159
//
// Although content::BrowserChildProcessHostIterator is only to be called from
// IO thread, it is safe to call from PostMainMessageLoopRun because thread
// restrictions have been lifted.
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/browser_main_loop.cc;l=1062-1078
for (content::BrowserChildProcessHostIterator it(
content::PROCESS_TYPE_UTILITY);
!it.Done(); ++it) {
if (it.GetDelegate()->GetServiceName() == node::mojom::NodeService::Name_) {
auto& process = it.GetData().GetProcess();
if (!process.IsValid())
continue;
auto utility_process_wrapper =
api::UtilityProcessWrapper::FromProcessId(process.Pid());
if (utility_process_wrapper)
utility_process_wrapper->Shutdown(0 /* exit_code */);
}
}
// Destroy node platform after all destructors_ are executed, as they may // Destroy node platform after all destructors_ are executed, as they may
// invoke Node/V8 APIs inside them. // invoke Node/V8 APIs inside them.
node_env_->env()->set_trace_sync_io(false); node_env_->env()->set_trace_sync_io(false);
js_env_->OnMessageLoopDestroying(); js_env_->DestroyMicrotasksRunner();
node::Stop(node_env_->env()); node::Stop(node_env_->env());
node_env_.reset(); node_env_.reset();

View file

@ -38,6 +38,17 @@ class EventEmitterMixin {
std::forward<Args>(args)...); std::forward<Args>(args)...);
} }
// this.emit(name, args...);
template <typename... Args>
void EmitWithoutCustomEvent(base::StringPiece name, Args&&... args) {
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Object> wrapper;
if (!static_cast<T*>(this)->GetWrapper(isolate).ToLocal(&wrapper))
return;
gin_helper::EmitEvent(isolate, wrapper, name, std::forward<Args>(args)...);
}
// this.emit(name, event, args...); // this.emit(name, event, args...);
template <typename... Args> template <typename... Args>
bool EmitCustomEvent(base::StringPiece name, bool EmitCustomEvent(base::StringPiece name,

View file

@ -287,13 +287,13 @@ v8::Isolate* JavascriptEnvironment::GetIsolate() {
return g_isolate; return g_isolate;
} }
void JavascriptEnvironment::OnMessageLoopCreated() { void JavascriptEnvironment::CreateMicrotasksRunner() {
DCHECK(!microtasks_runner_); DCHECK(!microtasks_runner_);
microtasks_runner_ = std::make_unique<MicrotasksRunner>(isolate()); microtasks_runner_ = std::make_unique<MicrotasksRunner>(isolate());
base::CurrentThread::Get()->AddTaskObserver(microtasks_runner_.get()); base::CurrentThread::Get()->AddTaskObserver(microtasks_runner_.get());
} }
void JavascriptEnvironment::OnMessageLoopDestroying() { void JavascriptEnvironment::DestroyMicrotasksRunner() {
DCHECK(microtasks_runner_); DCHECK(microtasks_runner_);
{ {
v8::HandleScope scope(isolate_); v8::HandleScope scope(isolate_);

View file

@ -29,8 +29,8 @@ class JavascriptEnvironment {
JavascriptEnvironment(const JavascriptEnvironment&) = delete; JavascriptEnvironment(const JavascriptEnvironment&) = delete;
JavascriptEnvironment& operator=(const JavascriptEnvironment&) = delete; JavascriptEnvironment& operator=(const JavascriptEnvironment&) = delete;
void OnMessageLoopCreated(); void CreateMicrotasksRunner();
void OnMessageLoopDestroying(); void DestroyMicrotasksRunner();
node::MultiIsolatePlatform* platform() const { return platform_; } node::MultiIsolatePlatform* platform() const { return platform_; }
v8::Isolate* isolate() const { return isolate_; } v8::Isolate* isolate() const { return isolate_; }

View file

@ -70,6 +70,7 @@
V(electron_browser_system_preferences) \ V(electron_browser_system_preferences) \
V(electron_browser_base_window) \ V(electron_browser_base_window) \
V(electron_browser_tray) \ V(electron_browser_tray) \
V(electron_browser_utility_process) \
V(electron_browser_view) \ V(electron_browser_view) \
V(electron_browser_web_contents) \ V(electron_browser_web_contents) \
V(electron_browser_web_contents_view) \ V(electron_browser_web_contents_view) \
@ -87,7 +88,8 @@
V(electron_renderer_context_bridge) \ V(electron_renderer_context_bridge) \
V(electron_renderer_crash_reporter) \ V(electron_renderer_crash_reporter) \
V(electron_renderer_ipc) \ V(electron_renderer_ipc) \
V(electron_renderer_web_frame) V(electron_renderer_web_frame) \
V(electron_utility_parent_port)
#define ELECTRON_VIEWS_MODULES(V) V(electron_browser_image_view) #define ELECTRON_VIEWS_MODULES(V) V(electron_browser_image_view)
@ -390,7 +392,11 @@ void NodeBindings::Initialize() {
std::vector<std::string> argv = {"electron"}; std::vector<std::string> argv = {"electron"};
std::vector<std::string> exec_argv; std::vector<std::string> exec_argv;
std::vector<std::string> errors; std::vector<std::string> errors;
uint64_t process_flags = node::ProcessFlags::kEnableStdioInheritance; uint64_t process_flags = node::ProcessFlags::kNoFlags;
// We do not want the child processes spawned from the utility process
// to inherit the custom stdio handles created for the parent.
if (browser_env_ != BrowserEnvironment::kUtility)
process_flags |= node::ProcessFlags::kEnableStdioInheritance;
if (!fuses::IsNodeOptionsEnabled()) if (!fuses::IsNodeOptionsEnabled())
process_flags |= node::ProcessFlags::kDisableNodeOptionsEnv; process_flags |= node::ProcessFlags::kDisableNodeOptionsEnv;
@ -417,16 +423,9 @@ void NodeBindings::Initialize() {
node::Environment* NodeBindings::CreateEnvironment( node::Environment* NodeBindings::CreateEnvironment(
v8::Handle<v8::Context> context, v8::Handle<v8::Context> context,
node::MultiIsolatePlatform* platform) { node::MultiIsolatePlatform* platform,
#if BUILDFLAG(IS_WIN) std::vector<std::string> args,
auto& atom_args = ElectronCommandLine::argv(); std::vector<std::string> exec_args) {
std::vector<std::string> args(atom_args.size());
std::transform(atom_args.cbegin(), atom_args.cend(), args.begin(),
[](auto& a) { return base::WideToUTF8(a); });
#else
auto args = ElectronCommandLine::argv();
#endif
// Feed node the path to initialization script. // Feed node the path to initialization script.
std::string process_type; std::string process_type;
switch (browser_env_) { switch (browser_env_) {
@ -439,14 +438,20 @@ node::Environment* NodeBindings::CreateEnvironment(
case BrowserEnvironment::kWorker: case BrowserEnvironment::kWorker:
process_type = "worker"; process_type = "worker";
break; break;
case BrowserEnvironment::kUtility:
process_type = "utility";
break;
} }
v8::Isolate* isolate = context->GetIsolate(); v8::Isolate* isolate = context->GetIsolate();
gin_helper::Dictionary global(isolate, context->Global()); gin_helper::Dictionary global(isolate, context->Global());
// Do not set DOM globals for renderer process. // Avoids overriding globals like setImmediate, clearImmediate
// We must set this before the node bootstrapper which is run inside // queueMicrotask etc during the bootstrap phase of Node.js
// CreateEnvironment // for processes that already have these defined by DOM.
if (browser_env_ != BrowserEnvironment::kBrowser) // Check //third_party/electron_node/lib/internal/bootstrap/node.js
// for the list of overrides on globalThis.
if (browser_env_ == BrowserEnvironment::kRenderer ||
browser_env_ == BrowserEnvironment::kWorker)
global.Set("_noBrowserGlobals", true); global.Set("_noBrowserGlobals", true);
if (browser_env_ == BrowserEnvironment::kBrowser) { if (browser_env_ == BrowserEnvironment::kBrowser) {
@ -464,7 +469,6 @@ node::Environment* NodeBindings::CreateEnvironment(
: search_paths)); : search_paths));
} }
std::vector<std::string> exec_args;
base::FilePath resources_path = GetResourcesPath(); base::FilePath resources_path = GetResourcesPath();
std::string init_script = "electron/js2c/" + process_type + "_init"; std::string init_script = "electron/js2c/" + process_type + "_init";
@ -478,7 +482,8 @@ node::Environment* NodeBindings::CreateEnvironment(
node::EnvironmentFlags::kHideConsoleWindows | node::EnvironmentFlags::kHideConsoleWindows |
node::EnvironmentFlags::kNoGlobalSearchPaths; node::EnvironmentFlags::kNoGlobalSearchPaths;
if (browser_env_ != BrowserEnvironment::kBrowser) { if (browser_env_ == BrowserEnvironment::kRenderer ||
browser_env_ == BrowserEnvironment::kWorker) {
// Only one ESM loader can be registered per isolate - // Only one ESM loader can be registered per isolate -
// in renderer processes this should be blink. We need to tell Node.js // in renderer processes this should be blink. We need to tell Node.js
// not to register its handler (overriding blinks) in non-browser processes. // not to register its handler (overriding blinks) in non-browser processes.
@ -514,7 +519,8 @@ node::Environment* NodeBindings::CreateEnvironment(
// Clean up the global _noBrowserGlobals that we unironically injected into // Clean up the global _noBrowserGlobals that we unironically injected into
// the global scope // the global scope
if (browser_env_ != BrowserEnvironment::kBrowser) { if (browser_env_ == BrowserEnvironment::kRenderer ||
browser_env_ == BrowserEnvironment::kWorker) {
// We need to bootstrap the env in non-browser processes so that // We need to bootstrap the env in non-browser processes so that
// _noBrowserGlobals is read correctly before we remove it // _noBrowserGlobals is read correctly before we remove it
global.Delete("_noBrowserGlobals"); global.Delete("_noBrowserGlobals");
@ -528,15 +534,21 @@ node::Environment* NodeBindings::CreateEnvironment(
// We don't want to abort either in the renderer or browser processes. // We don't want to abort either in the renderer or browser processes.
// We already listen for uncaught exceptions and handle them there. // We already listen for uncaught exceptions and handle them there.
is.should_abort_on_uncaught_exception_callback = [](v8::Isolate*) { // For utility process we expect the process to behave as standard
return false; // Node.js runtime and abort the process with appropriate exit
}; // code depending on a handler being set for `uncaughtException` event.
if (browser_env_ != BrowserEnvironment::kUtility) {
is.should_abort_on_uncaught_exception_callback = [](v8::Isolate*) {
return false;
};
}
// Use a custom callback here to allow us to leverage Blink's logic in the // Use a custom callback here to allow us to leverage Blink's logic in the
// renderer process. // renderer process.
is.allow_wasm_code_generation_callback = AllowWasmCodeGenerationCallback; is.allow_wasm_code_generation_callback = AllowWasmCodeGenerationCallback;
if (browser_env_ == BrowserEnvironment::kBrowser) { if (browser_env_ == BrowserEnvironment::kBrowser ||
browser_env_ == BrowserEnvironment::kUtility) {
// Node.js requires that microtask checkpoints be explicitly invoked. // Node.js requires that microtask checkpoints be explicitly invoked.
is.policy = v8::MicrotasksPolicy::kExplicit; is.policy = v8::MicrotasksPolicy::kExplicit;
} else { } else {
@ -585,6 +597,20 @@ node::Environment* NodeBindings::CreateEnvironment(
return env; return env;
} }
node::Environment* NodeBindings::CreateEnvironment(
v8::Handle<v8::Context> context,
node::MultiIsolatePlatform* platform) {
#if BUILDFLAG(IS_WIN)
auto& electron_args = ElectronCommandLine::argv();
std::vector<std::string> args(electron_args.size());
std::transform(electron_args.cbegin(), electron_args.cend(), args.begin(),
[](auto& a) { return base::WideToUTF8(a); });
#else
auto args = ElectronCommandLine::argv();
#endif
return CreateEnvironment(context, platform, args, {});
}
void NodeBindings::LoadEnvironment(node::Environment* env) { void NodeBindings::LoadEnvironment(node::Environment* env) {
node::LoadEnvironment(env, node::StartExecutionCallback{}); node::LoadEnvironment(env, node::StartExecutionCallback{});
gin_helper::EmitEvent(env->isolate(), env->process_object(), "loaded"); gin_helper::EmitEvent(env->isolate(), env->process_object(), "loaded");

View file

@ -5,7 +5,9 @@
#ifndef ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_ #ifndef ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_
#define ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_ #define ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_
#include <string>
#include <type_traits> #include <type_traits>
#include <vector>
#include "base/files/file_path.h" #include "base/files/file_path.h"
#include "base/memory/weak_ptr.h" #include "base/memory/weak_ptr.h"
@ -74,7 +76,7 @@ class UvHandle {
class NodeBindings { class NodeBindings {
public: public:
enum class BrowserEnvironment { kBrowser, kRenderer, kWorker }; enum class BrowserEnvironment { kBrowser, kRenderer, kUtility, kWorker };
static NodeBindings* Create(BrowserEnvironment browser_env); static NodeBindings* Create(BrowserEnvironment browser_env);
static void RegisterBuiltinModules(); static void RegisterBuiltinModules();
@ -86,6 +88,10 @@ class NodeBindings {
void Initialize(); void Initialize();
// Create the environment and load node.js. // Create the environment and load node.js.
node::Environment* CreateEnvironment(v8::Handle<v8::Context> context,
node::MultiIsolatePlatform* platform,
std::vector<std::string> args,
std::vector<std::string> exec_args);
node::Environment* CreateEnvironment(v8::Handle<v8::Context> context, node::Environment* CreateEnvironment(v8::Handle<v8::Context> context,
node::MultiIsolatePlatform* platform); node::MultiIsolatePlatform* platform);

View file

@ -0,0 +1,104 @@
// Copyright (c) 2022 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/services/node/node_service.h"
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/strings/utf_string_conversions.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/api/electron_bindings.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/node_bindings.h"
#include "shell/common/node_includes.h"
#include "shell/services/node/parent_port.h"
namespace electron {
NodeService::NodeService(
mojo::PendingReceiver<node::mojom::NodeService> receiver)
: node_bindings_(
NodeBindings::Create(NodeBindings::BrowserEnvironment::kUtility)),
electron_bindings_(
std::make_unique<ElectronBindings>(node_bindings_->uv_loop())) {
if (receiver.is_valid())
receiver_.Bind(std::move(receiver));
}
NodeService::~NodeService() {
if (!node_env_stopped_) {
node_env_->env()->set_trace_sync_io(false);
js_env_->DestroyMicrotasksRunner();
node::Stop(node_env_->env());
}
}
void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
if (NodeBindings::IsInitialized())
return;
ParentPort::GetInstance()->Initialize(std::move(params->port));
js_env_ = std::make_unique<JavascriptEnvironment>(node_bindings_->uv_loop());
v8::HandleScope scope(js_env_->isolate());
node_bindings_->Initialize();
// Append program path for process.argv0
auto program = base::CommandLine::ForCurrentProcess()->GetProgram();
#if defined(OS_WIN)
params->args.insert(params->args.begin(), base::WideToUTF8(program.value()));
#else
params->args.insert(params->args.begin(), program.value());
#endif
// Create the global environment.
node::Environment* env = node_bindings_->CreateEnvironment(
js_env_->context(), js_env_->platform(), params->args, params->exec_args);
node_env_ = std::make_unique<NodeEnvironment>(env);
node::SetProcessExitHandler(env,
[this](node::Environment* env, int exit_code) {
// Destroy node platform.
env->set_trace_sync_io(false);
js_env_->DestroyMicrotasksRunner();
node::Stop(env);
node_env_stopped_ = true;
receiver_.ResetWithReason(exit_code, "");
});
env->set_trace_sync_io(env->options()->trace_sync_io);
// Add Electron extended APIs.
electron_bindings_->BindTo(env->isolate(), env->process_object());
// Add entry script to process object.
gin_helper::Dictionary process(env->isolate(), env->process_object());
process.SetHidden("_serviceStartupScript", params->script);
// Setup microtask runner.
js_env_->CreateMicrotasksRunner();
// Wrap the uv loop with global env.
node_bindings_->set_uv_env(env);
// LoadEnvironment should be called after setting up
// JavaScriptEnvironment including the microtask runner
// since this call will start compilation and execution
// of the entry script. If there is an uncaught exception
// the exit handler set above will be triggered and it expects
// both Node Env and JavaScriptEnviroment are setup to perform
// a clean shutdown of this process.
node_bindings_->LoadEnvironment(env);
// Run entry script.
node_bindings_->PrepareEmbedThread();
node_bindings_->StartPolling();
}
} // namespace electron

View file

@ -0,0 +1,44 @@
// Copyright (c) 2022 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_SERVICES_NODE_NODE_SERVICE_H_
#define ELECTRON_SHELL_SERVICES_NODE_NODE_SERVICE_H_
#include <memory>
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "shell/services/node/public/mojom/node_service.mojom.h"
namespace electron {
class ElectronBindings;
class JavascriptEnvironment;
class NodeBindings;
class NodeEnvironment;
class NodeService : public node::mojom::NodeService {
public:
explicit NodeService(
mojo::PendingReceiver<node::mojom::NodeService> receiver);
~NodeService() override;
NodeService(const NodeService&) = delete;
NodeService& operator=(const NodeService&) = delete;
// mojom::NodeService implementation:
void Initialize(node::mojom::NodeServiceParamsPtr params) override;
private:
bool node_env_stopped_ = false;
std::unique_ptr<JavascriptEnvironment> js_env_;
std::unique_ptr<NodeBindings> node_bindings_;
std::unique_ptr<ElectronBindings> electron_bindings_;
std::unique_ptr<NodeEnvironment> node_env_;
mojo::Receiver<node::mojom::NodeService> receiver_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_SERVICES_NODE_NODE_SERVICE_H_

View file

@ -0,0 +1,133 @@
// Copyright (c) 2022 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/services/node/parent_port.h"
#include <utility>
#include "base/no_destructor.h"
#include "gin/data_object_builder.h"
#include "gin/handle.h"
#include "shell/browser/api/message_port.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/event_emitter_caller.h"
#include "shell/common/node_includes.h"
#include "shell/common/v8_value_serializer.h"
#include "third_party/blink/public/common/messaging/transferable_message_mojom_traits.h"
namespace electron {
gin::WrapperInfo ParentPort::kWrapperInfo = {gin::kEmbedderNativeGin};
ParentPort* ParentPort::GetInstance() {
static base::NoDestructor<ParentPort> instance;
return instance.get();
}
ParentPort::ParentPort() = default;
ParentPort::~ParentPort() = default;
void ParentPort::Initialize(blink::MessagePortDescriptor port) {
port_ = std::move(port);
connector_ = std::make_unique<mojo::Connector>(
port_.TakeHandleToEntangleWithEmbedder(),
mojo::Connector::SINGLE_THREADED_SEND,
base::ThreadTaskRunnerHandle::Get());
connector_->PauseIncomingMethodCallProcessing();
connector_->set_incoming_receiver(this);
connector_->set_connection_error_handler(
base::BindOnce(&ParentPort::Close, base::Unretained(this)));
}
void ParentPort::PostMessage(v8::Local<v8::Value> message_value) {
if (!connector_closed_ && connector_ && connector_->is_valid()) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
blink::TransferableMessage transferable_message;
electron::SerializeV8Value(isolate, message_value, &transferable_message);
mojo::Message mojo_message =
blink::mojom::TransferableMessage::WrapAsMessage(
std::move(transferable_message));
connector_->Accept(&mojo_message);
}
}
void ParentPort::Close() {
if (!connector_closed_ && connector_->is_valid()) {
port_.GiveDisentangledHandle(connector_->PassMessagePipe());
connector_ = nullptr;
port_.Reset();
connector_closed_ = true;
}
}
void ParentPort::Start() {
if (!connector_closed_ && connector_ && connector_->is_valid()) {
connector_->ResumeIncomingMethodCallProcessing();
}
}
void ParentPort::Pause() {
if (!connector_closed_ && connector_ && connector_->is_valid()) {
connector_->PauseIncomingMethodCallProcessing();
}
}
bool ParentPort::Accept(mojo::Message* mojo_message) {
blink::TransferableMessage message;
if (!blink::mojom::TransferableMessage::DeserializeFromMessage(
std::move(*mojo_message), &message)) {
return false;
}
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
auto wrapped_ports =
MessagePort::EntanglePorts(isolate, std::move(message.ports));
v8::Local<v8::Value> message_value =
electron::DeserializeV8Value(isolate, message);
v8::Local<v8::Object> self;
if (!GetWrapper(isolate).ToLocal(&self))
return false;
auto event = gin::DataObjectBuilder(isolate)
.Set("data", message_value)
.Set("ports", wrapped_ports)
.Build();
gin_helper::EmitEvent(isolate, self, "message", event);
return true;
}
// static
gin::Handle<ParentPort> ParentPort::Create(v8::Isolate* isolate) {
return gin::CreateHandle(isolate, ParentPort::GetInstance());
}
// static
gin::ObjectTemplateBuilder ParentPort::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin::Wrappable<ParentPort>::GetObjectTemplateBuilder(isolate)
.SetMethod("postMessage", &ParentPort::PostMessage)
.SetMethod("start", &ParentPort::Start)
.SetMethod("pause", &ParentPort::Pause);
}
const char* ParentPort::GetTypeName() {
return "ParentPort";
}
} // namespace electron
namespace {
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
void* priv) {
v8::Isolate* isolate = context->GetIsolate();
gin_helper::Dictionary dict(isolate, exports);
dict.SetMethod("createParentPort", &electron::ParentPort::Create);
}
} // namespace
NODE_LINKED_MODULE_CONTEXT_AWARE(electron_utility_parent_port, Initialize)

View file

@ -0,0 +1,68 @@
// Copyright (c) 2022 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_SERVICES_NODE_PARENT_PORT_H_
#define ELECTRON_SHELL_SERVICES_NODE_PARENT_PORT_H_
#include <memory>
#include "gin/wrappable.h"
#include "mojo/public/cpp/bindings/connector.h"
#include "mojo/public/cpp/bindings/message.h"
#include "shell/browser/event_emitter_mixin.h"
namespace v8 {
template <class T>
class Local;
class Value;
class Isolate;
} // namespace v8
namespace gin {
class Arguments;
template <typename T>
class Handle;
} // namespace gin
namespace electron {
// There is only a single instance of this class
// for the lifetime of a Utility Process which
// also means that GC lifecycle is ignored by this class.
class ParentPort : public gin::Wrappable<ParentPort>,
public mojo::MessageReceiver {
public:
static ParentPort* GetInstance();
static gin::Handle<ParentPort> Create(v8::Isolate* isolate);
ParentPort(const ParentPort&) = delete;
ParentPort& operator=(const ParentPort&) = delete;
ParentPort();
~ParentPort() override;
void Initialize(blink::MessagePortDescriptor port);
// gin::Wrappable
static gin::WrapperInfo kWrapperInfo;
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override;
const char* GetTypeName() override;
private:
void PostMessage(v8::Local<v8::Value> message_value);
void Close();
void Start();
void Pause();
// mojo::MessageReceiver
bool Accept(mojo::Message* mojo_message) override;
bool connector_closed_ = false;
std::unique_ptr<mojo::Connector> connector_;
blink::MessagePortDescriptor port_;
};
} // namespace electron
#endif // ELECTRON_SHELL_SERVICES_NODE_PARENT_PORT_H_

View file

@ -0,0 +1,14 @@
# Copyright (c) 2022 Microsoft, Inc.
# Use of this source code is governed by the MIT license that can be
# found in the LICENSE file.
import("//mojo/public/tools/bindings/mojom.gni")
mojom("mojom") {
sources = [ "node_service.mojom" ]
public_deps = [
"//mojo/public/mojom/base",
"//sandbox/policy/mojom",
"//third_party/blink/public/mojom:mojom_core",
]
}

View file

@ -0,0 +1,21 @@
// Copyright (c) 2022 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
module node.mojom;
import "mojo/public/mojom/base/file_path.mojom";
import "sandbox/policy/mojom/sandbox.mojom";
import "third_party/blink/public/mojom/messaging/message_port_descriptor.mojom";
struct NodeServiceParams {
mojo_base.mojom.FilePath script;
array<string> args;
array<string> exec_args;
blink.mojom.MessagePortDescriptor port;
};
[ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox]
interface NodeService {
Initialize(NodeServiceParams params);
};

View file

@ -16,6 +16,8 @@
#include "services/proxy_resolver/proxy_resolver_factory_impl.h" #include "services/proxy_resolver/proxy_resolver_factory_impl.h"
#include "services/proxy_resolver/public/mojom/proxy_resolver.mojom.h" #include "services/proxy_resolver/public/mojom/proxy_resolver.mojom.h"
#include "services/service_manager/public/cpp/service.h" #include "services/service_manager/public/cpp/service.h"
#include "shell/services/node/node_service.h"
#include "shell/services/node/public/mojom/node_service.mojom.h"
#if BUILDFLAG(IS_WIN) #if BUILDFLAG(IS_WIN)
#include "chrome/services/util_win/public/mojom/util_read_icon.mojom.h" #include "chrome/services/util_win/public/mojom/util_read_icon.mojom.h"
@ -72,6 +74,10 @@ auto RunProxyResolver(
std::move(receiver)); std::move(receiver));
} }
auto RunNodeService(mojo::PendingReceiver<node::mojom::NodeService> receiver) {
return std::make_unique<electron::NodeService>(std::move(receiver));
}
} // namespace } // namespace
ElectronContentUtilityClient::ElectronContentUtilityClient() = default; ElectronContentUtilityClient::ElectronContentUtilityClient() = default;
@ -115,6 +121,8 @@ void ElectronContentUtilityClient::RegisterMainThreadServices(
(BUILDFLAG(ENABLE_PRINTING) && BUILDFLAG(IS_WIN)) (BUILDFLAG(ENABLE_PRINTING) && BUILDFLAG(IS_WIN))
services.Add(RunPrintingService); services.Add(RunPrintingService);
#endif #endif
services.Add(RunNodeService);
} }
void ElectronContentUtilityClient::RegisterIOThreadServices( void ElectronContentUtilityClient::RegisterIOThreadServices(

View file

@ -0,0 +1,364 @@
import { expect } from 'chai';
import * as childProcess from 'child_process';
import * as path from 'path';
import { BrowserWindow, MessageChannelMain, utilityProcess } from 'electron/main';
import { emittedOnce } from './events-helpers';
import { ifit } from './spec-helpers';
import { closeWindow } from './window-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process');
const isWindowsOnArm = process.platform === 'win32' && process.arch === 'arm64';
describe('utilityProcess module', () => {
describe('UtilityProcess constructor', () => {
it('throws when empty script path is provided', async () => {
expect(() => {
/* eslint-disable no-new */
utilityProcess.fork('');
/* eslint-disable no-new */
}).to.throw();
});
it('throws when options.stdio is not valid', async () => {
expect(() => {
/* eslint-disable no-new */
utilityProcess.fork(path.join(fixturesPath, 'empty.js'), [], {
execArgv: ['--test', '--test2'],
serviceName: 'test',
stdio: 'ipc'
});
/* eslint-disable no-new */
}).to.throw(/stdio must be of the following values: inherit, pipe, ignore/);
expect(() => {
/* eslint-disable no-new */
utilityProcess.fork(path.join(fixturesPath, 'empty.js'), [], {
execArgv: ['--test', '--test2'],
serviceName: 'test',
stdio: ['ignore', 'ignore']
});
/* eslint-disable no-new */
}).to.throw(/configuration missing for stdin, stdout or stderr/);
expect(() => {
/* eslint-disable no-new */
utilityProcess.fork(path.join(fixturesPath, 'empty.js'), [], {
execArgv: ['--test', '--test2'],
serviceName: 'test',
stdio: ['pipe', 'inherit', 'inherit']
});
/* eslint-disable no-new */
}).to.throw(/stdin value other than ignore is not supported/);
});
});
describe('lifecycle events', () => {
it('emits \'spawn\' when child process successfully launches', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
await emittedOnce(child, 'spawn');
});
it('emits \'exit\' when child process exits gracefully', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
const [code] = await emittedOnce(child, 'exit');
expect(code).to.equal(0);
});
it('emits \'exit\' when child process crashes', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'crash.js'));
// Do not check for exit code in this case,
// SIGSEGV code can be 139 or 11 across our different CI pipeline.
await emittedOnce(child, 'exit');
});
it('emits \'exit\' corresponding to the child process', async () => {
const child1 = utilityProcess.fork(path.join(fixturesPath, 'endless.js'));
await emittedOnce(child1, 'spawn');
const child2 = utilityProcess.fork(path.join(fixturesPath, 'crash.js'));
await emittedOnce(child2, 'exit');
expect(child1.kill()).to.be.true();
await emittedOnce(child1, 'exit');
});
it('emits \'exit\' when there is uncaught exception', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'exception.js'));
const [code] = await emittedOnce(child, 'exit');
expect(code).to.equal(1);
});
it('emits \'exit\' when process.exit is called', async () => {
const exitCode = 2;
const child = utilityProcess.fork(path.join(fixturesPath, 'custom-exit.js'), [`--exitCode=${exitCode}`]);
const [code] = await emittedOnce(child, 'exit');
expect(code).to.equal(exitCode);
});
});
describe('kill() API', () => {
it('terminates the child process gracefully', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'endless.js'), [], {
serviceName: 'endless'
});
await emittedOnce(child, 'spawn');
expect(child.kill()).to.be.true();
await emittedOnce(child, 'exit');
});
});
describe('pid property', () => {
it('is valid when child process launches successfully', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
await emittedOnce(child, 'spawn');
expect(child.pid).to.not.be.null();
});
it('is undefined when child process fails to launch', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'does-not-exist.js'));
expect(child.pid).to.be.undefined();
});
});
describe('stdout property', () => {
it('is null when child process launches with default stdio', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'));
await emittedOnce(child, 'spawn');
expect(child.stdout).to.be.null();
expect(child.stderr).to.be.null();
await emittedOnce(child, 'exit');
});
it('is null when child process launches with ignore stdio configuration', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
stdio: 'ignore'
});
await emittedOnce(child, 'spawn');
expect(child.stdout).to.be.null();
expect(child.stderr).to.be.null();
await emittedOnce(child, 'exit');
});
it('is valid when child process launches with pipe stdio configuration', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
stdio: 'pipe'
});
await emittedOnce(child, 'spawn');
expect(child.stdout).to.not.be.null();
let log = '';
child.stdout!.on('data', (chunk) => {
log += chunk.toString('utf8');
});
await emittedOnce(child, 'exit');
expect(log).to.equal('hello\n');
});
});
describe('stderr property', () => {
it('is null when child process launches with default stdio', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'));
await emittedOnce(child, 'spawn');
expect(child.stdout).to.be.null();
expect(child.stderr).to.be.null();
await emittedOnce(child, 'exit');
});
it('is null when child process launches with ignore stdio configuration', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
stdio: 'ignore'
});
await emittedOnce(child, 'spawn');
expect(child.stderr).to.be.null();
await emittedOnce(child, 'exit');
});
ifit(!isWindowsOnArm)('is valid when child process launches with pipe stdio configuration', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
stdio: ['ignore', 'pipe', 'pipe']
});
await emittedOnce(child, 'spawn');
expect(child.stderr).to.not.be.null();
let log = '';
child.stderr!.on('data', (chunk) => {
log += chunk.toString('utf8');
});
await emittedOnce(child, 'exit');
expect(log).to.equal('world');
});
});
describe('postMessage() API', () => {
it('establishes a default ipc channel with the child process', async () => {
const result = 'I will be echoed.';
const child = utilityProcess.fork(path.join(fixturesPath, 'post-message.js'));
await emittedOnce(child, 'spawn');
child.postMessage(result);
const [data] = await emittedOnce(child, 'message');
expect(data).to.equal(result);
const exit = emittedOnce(child, 'exit');
expect(child.kill()).to.be.true();
await exit;
});
it('supports queuing messages on the receiving end', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'post-message-queue.js'));
const p = emittedOnce(child, 'spawn');
child.postMessage('This message');
child.postMessage(' is');
child.postMessage(' queued');
await p;
const [data] = await emittedOnce(child, 'message');
expect(data).to.equal('This message is queued');
const exit = emittedOnce(child, 'exit');
expect(child.kill()).to.be.true();
await exit;
});
});
describe('behavior', () => {
it('supports starting the v8 inspector with --inspect-brk', (done) => {
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
stdio: 'pipe',
execArgv: ['--inspect-brk']
});
let output = '';
const cleanup = () => {
child.stderr!.removeListener('data', listener);
child.stdout!.removeListener('data', listener);
child.once('exit', () => { done(); });
child.kill();
};
const listener = (data: Buffer) => {
output += data;
if (/Debugger listening on ws:/m.test(output)) {
cleanup();
}
};
child.stderr!.on('data', listener);
child.stdout!.on('data', listener);
});
it('supports starting the v8 inspector with --inspect and a provided port', (done) => {
const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
stdio: 'pipe',
execArgv: ['--inspect=17364']
});
let output = '';
const cleanup = () => {
child.stderr!.removeListener('data', listener);
child.stdout!.removeListener('data', listener);
child.once('exit', () => { done(); });
child.kill();
};
const listener = (data: Buffer) => {
output += data;
if (/Debugger listening on ws:/m.test(output)) {
expect(output.trim()).to.contain(':17364', 'should be listening on port 17364');
cleanup();
}
};
child.stderr!.on('data', listener);
child.stdout!.on('data', listener);
});
ifit(process.platform !== 'win32')('supports redirecting stdout to parent process', async () => {
const result = 'Output from utility process';
const appProcess = childProcess.spawn(process.execPath, [path.join(fixturesPath, 'inherit-stdout'), `--payload=${result}`]);
let output = '';
appProcess.stdout.on('data', (data: Buffer) => { output += data; });
await emittedOnce(appProcess, 'exit');
expect(output).to.equal(result);
});
ifit(process.platform !== 'win32')('supports redirecting stderr to parent process', async () => {
const result = 'Error from utility process';
const appProcess = childProcess.spawn(process.execPath, [path.join(fixturesPath, 'inherit-stderr'), `--payload=${result}`]);
let output = '';
appProcess.stderr.on('data', (data: Buffer) => { output += data; });
await emittedOnce(appProcess, 'exit');
expect(output).to.include(result);
});
it('can establish communication channel with sandboxed renderer', async () => {
const result = 'Message from sandboxed renderer';
const w = new BrowserWindow({
show: false,
webPreferences: {
preload: path.join(fixturesPath, 'preload.js')
}
});
await w.loadFile(path.join(__dirname, 'fixtures', 'blank.html'));
// Create Message port pair for Renderer <-> Utility Process.
const { port1: rendererPort, port2: childPort1 } = new MessageChannelMain();
w.webContents.postMessage('port', result, [rendererPort]);
// Send renderer and main channel port to utility process.
const child = utilityProcess.fork(path.join(fixturesPath, 'receive-message.js'));
await emittedOnce(child, 'spawn');
child.postMessage('', [childPort1]);
const [data] = await emittedOnce(child, 'message');
expect(data).to.equal(result);
// Cleanup.
const exit = emittedOnce(child, 'exit');
expect(child.kill()).to.be.true();
await exit;
await closeWindow(w);
});
ifit(process.platform === 'linux')('allows executing a setuid binary with child_process', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'suid.js'));
await emittedOnce(child, 'spawn');
const [data] = await emittedOnce(child, 'message');
expect(data).to.not.be.empty();
const exit = emittedOnce(child, 'exit');
expect(child.kill()).to.be.true();
await exit;
});
it('inherits parent env as default', async () => {
const appProcess = childProcess.spawn(process.execPath, [path.join(fixturesPath, 'env-app')], {
env: {
FROM: 'parent',
...process.env
}
});
let output = '';
appProcess.stdout.on('data', (data: Buffer) => { output += data; });
await emittedOnce(appProcess.stdout, 'end');
const result = process.platform === 'win32' ? '\r\nparent' : 'parent';
expect(output).to.equal(result);
});
it('does not inherit parent env when custom env is provided', async () => {
const appProcess = childProcess.spawn(process.execPath, [path.join(fixturesPath, 'env-app'), '--create-custom-env'], {
env: {
FROM: 'parent',
...process.env
}
});
let output = '';
appProcess.stdout.on('data', (data: Buffer) => { output += data; });
await emittedOnce(appProcess.stdout, 'end');
const result = process.platform === 'win32' ? '\r\nchild' : 'child';
expect(output).to.equal(result);
});
it('changes working directory with cwd', async () => {
const child = utilityProcess.fork('./log.js', [], {
cwd: fixturesPath,
stdio: ['ignore', 'pipe', 'ignore']
});
await emittedOnce(child, 'spawn');
expect(child.stdout).to.not.be.null();
let log = '';
child.stdout!.on('data', (chunk) => {
log += chunk.toString('utf8');
});
await emittedOnce(child, 'exit');
expect(log).to.equal('hello\n');
});
});
});

View file

@ -0,0 +1 @@
process.crash();

View file

@ -0,0 +1,3 @@
const arg = process.argv[2];
const code = arg.split('=')[1];
process.exit(code);

View file

@ -0,0 +1 @@
process.exit(0);

View file

@ -0,0 +1 @@
setInterval(() => {}, 2000);

View file

@ -0,0 +1,22 @@
const { app, utilityProcess } = require('electron');
const path = require('path');
app.whenReady().then(() => {
let child = null;
if (app.commandLine.hasSwitch('create-custom-env')) {
child = utilityProcess.fork(path.join(__dirname, 'test.js'), {
env: {
FROM: 'child'
}
});
} else {
child = utilityProcess.fork(path.join(__dirname, 'test.js'));
}
child.on('message', (data) => {
process.stdout.write(data);
process.stdout.end();
});
child.on('exit', () => {
app.quit();
});
});

View file

@ -0,0 +1,4 @@
{
"name": "electron-test-utility-process-env-app",
"main": "main.js"
}

View file

@ -0,0 +1,2 @@
process.parentPort.postMessage(process.env.FROM);
process.exit(0);

View file

@ -0,0 +1 @@
nonExistingFunc(); // eslint-disable-line

View file

@ -0,0 +1,10 @@
const { app, utilityProcess } = require('electron');
const path = require('path');
app.whenReady().then(() => {
const payload = app.commandLine.getSwitchValue('payload');
const child = utilityProcess.fork(path.join(__dirname, 'test.js'), [`--payload=${payload}`]);
child.on('exit', () => {
app.quit();
});
});

View file

@ -0,0 +1,4 @@
{
"name": "electron-test-utility-process-inherit-stderr",
"main": "main.js"
}

View file

@ -0,0 +1,3 @@
process.stderr.write(process.argv[2].split('--payload=')[1]);
process.stderr.end();
process.exit(0);

View file

@ -0,0 +1,10 @@
const { app, utilityProcess } = require('electron');
const path = require('path');
app.whenReady().then(() => {
const payload = app.commandLine.getSwitchValue('payload');
const child = utilityProcess.fork(path.join(__dirname, 'test.js'), [`--payload=${payload}`]);
child.on('exit', () => {
app.quit();
});
});

View file

@ -0,0 +1,4 @@
{
"name": "electron-test-utility-process-inherit-stdout",
"main": "main.js"
}

View file

@ -0,0 +1,3 @@
process.stdout.write(process.argv[2].split('--payload=')[1]);
process.stdout.end();
process.exit(0);

View file

@ -0,0 +1,3 @@
console.log('hello');
process.stderr.write('world');
process.exit(0);

View file

@ -0,0 +1,10 @@
setTimeout(() => {
let called = 0;
let result = '';
process.parentPort.on('message', (e) => {
result += e.data;
if (++called === 3) {
process.parentPort.postMessage(result);
}
});
}, 3000);

View file

@ -0,0 +1,3 @@
process.parentPort.on('message', (e) => {
process.parentPort.postMessage(e.data);
});

View file

@ -0,0 +1,5 @@
const { ipcRenderer } = require('electron');
ipcRenderer.on('port', (e, msg) => {
e.ports[0].postMessage(msg);
});

View file

@ -0,0 +1,6 @@
process.parentPort.on('message', (e) => {
e.ports[0].on('message', (ev) => {
process.parentPort.postMessage(ev.data);
});
e.ports[0].start();
});

View file

@ -0,0 +1,2 @@
const result = require('child_process').execSync('sudo --help');
process.parentPort.postMessage(result);

View file

@ -251,6 +251,7 @@ declare namespace NodeJS {
// Additional properties // Additional properties
_firstFileName?: string; _firstFileName?: string;
_serviceStartupScript: string;
helperExecPath: string; helperExecPath: string;
mainModule?: NodeJS.Module | undefined; mainModule?: NodeJS.Module | undefined;

View file

@ -9,7 +9,8 @@ declare namespace Electron {
enum ProcessType { enum ProcessType {
browser = 'browser', browser = 'browser',
renderer = 'renderer', renderer = 'renderer',
worker = 'worker' worker = 'worker',
utility = 'utility'
} }
interface App { interface App {
@ -254,6 +255,18 @@ declare namespace ElectronInternal {
loader: ModuleLoader; loader: ModuleLoader;
} }
interface UtilityProcessWrapper extends NodeJS.EventEmitter {
readonly pid: (number) | (undefined);
kill(): boolean;
postMessage(message: any, transfer?: any[]): void;
}
interface ParentPort extends NodeJS.EventEmitter {
start(): void;
pause(): void;
postMessage(message: any): void;
}
class WebViewElement extends HTMLElement { class WebViewElement extends HTMLElement {
static observedAttributes: Array<string>; static observedAttributes: Array<string>;