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:
parent
44c40efecf
commit
da0fd286b4
59 changed files with 2700 additions and 54 deletions
12
BUILD.gn
12
BUILD.gn
|
@ -202,6 +202,15 @@ webpack_build("electron_isolated_renderer_bundle") {
|
|||
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") {
|
||||
deps = [
|
||||
":electron_asar_bundle",
|
||||
|
@ -209,6 +218,7 @@ action("electron_js2c") {
|
|||
":electron_isolated_renderer_bundle",
|
||||
":electron_renderer_bundle",
|
||||
":electron_sandboxed_renderer_bundle",
|
||||
":electron_utility_bundle",
|
||||
":electron_worker_bundle",
|
||||
]
|
||||
|
||||
|
@ -218,6 +228,7 @@ action("electron_js2c") {
|
|||
"$target_gen_dir/js2c/isolated_bundle.js",
|
||||
"$target_gen_dir/js2c/renderer_init.js",
|
||||
"$target_gen_dir/js2c/sandbox_bundle.js",
|
||||
"$target_gen_dir/js2c/utility_init.js",
|
||||
"$target_gen_dir/js2c/worker_init.js",
|
||||
]
|
||||
|
||||
|
@ -368,6 +379,7 @@ source_set("electron_lib") {
|
|||
"chromium_src:chrome",
|
||||
"chromium_src:chrome_spellchecker",
|
||||
"shell/common/api:mojo",
|
||||
"shell/services/node/public/mojom",
|
||||
"//base:base_static",
|
||||
"//base/allocator:buildflags",
|
||||
"//chrome:strings",
|
||||
|
|
4
build/webpack/webpack.config.utility.js
Normal file
4
build/webpack/webpack.config.utility.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
module.exports = require('./webpack.config.base')({
|
||||
target: 'utility',
|
||||
alwaysHasNode: true
|
||||
});
|
46
docs/api/parent-port.md
Normal file
46
docs/api/parent-port.md
Normal 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.
|
|
@ -113,6 +113,7 @@ A `string` representing the current process's type, can be:
|
|||
* `browser` - The main process
|
||||
* `renderer` - A renderer process
|
||||
* `worker` - In a web worker
|
||||
* `utility` - In a node process launched as a service
|
||||
|
||||
### `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.
|
||||
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
|
||||
|
||||
The `process` object has the following methods:
|
||||
|
|
136
docs/api/utility-process.md
Normal file
136
docs/api/utility-process.md
Normal 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
|
|
@ -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
|
||||
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 is Google's open source JavaScript engine. It is written in C++ and is
|
||||
|
@ -231,4 +240,5 @@ embedded content.
|
|||
[renderer]: #renderer-process
|
||||
[userland]: #userland
|
||||
[using native node modules]: tutorial/using-native-node-modules.md
|
||||
[UtilityProcess]: api/utility-process.md
|
||||
[v8]: #v8
|
||||
|
|
|
@ -36,6 +36,7 @@ auto_filenames = {
|
|||
"docs/api/net-log.md",
|
||||
"docs/api/net.md",
|
||||
"docs/api/notification.md",
|
||||
"docs/api/parent-port.md",
|
||||
"docs/api/power-monitor.md",
|
||||
"docs/api/power-save-blocker.md",
|
||||
"docs/api/process.md",
|
||||
|
@ -62,6 +63,7 @@ auto_filenames = {
|
|||
"docs/api/touch-bar-spacer.md",
|
||||
"docs/api/touch-bar.md",
|
||||
"docs/api/tray.md",
|
||||
"docs/api/utility-process.md",
|
||||
"docs/api/web-contents.md",
|
||||
"docs/api/web-frame-main.md",
|
||||
"docs/api/web-frame.md",
|
||||
|
@ -220,6 +222,7 @@ auto_filenames = {
|
|||
"lib/browser/api/system-preferences.ts",
|
||||
"lib/browser/api/touch-bar.ts",
|
||||
"lib/browser/api/tray.ts",
|
||||
"lib/browser/api/utility-process.ts",
|
||||
"lib/browser/api/view.ts",
|
||||
"lib/browser/api/views/image-view.ts",
|
||||
"lib/browser/api/web-contents-view.ts",
|
||||
|
@ -331,4 +334,20 @@ auto_filenames = {
|
|||
"typings/internal-ambient.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",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -311,6 +311,8 @@ filenames = {
|
|||
"shell/browser/api/electron_api_tray.h",
|
||||
"shell/browser/api/electron_api_url_loader.cc",
|
||||
"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.h",
|
||||
"shell/browser/api/electron_api_web_contents.cc",
|
||||
|
@ -679,6 +681,10 @@ filenames = {
|
|||
"shell/renderer/renderer_client_base.h",
|
||||
"shell/renderer/web_worker_observer.cc",
|
||||
"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.h",
|
||||
]
|
||||
|
|
|
@ -31,6 +31,7 @@ export const browserModuleList: ElectronInternal.ModuleEntry[] = [
|
|||
{ name: 'systemPreferences', loader: () => require('./system-preferences') },
|
||||
{ name: 'TouchBar', loader: () => require('./touch-bar') },
|
||||
{ name: 'Tray', loader: () => require('./tray') },
|
||||
{ name: 'utilityProcess', loader: () => require('./utility-process') },
|
||||
{ name: 'View', loader: () => require('./view') },
|
||||
{ name: 'webContents', loader: () => require('./web-contents') },
|
||||
{ name: 'WebContentsView', loader: () => require('./web-contents-view') },
|
||||
|
|
150
lib/browser/api/utility-process.ts
Normal file
150
lib/browser/api/utility-process.ts
Normal 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);
|
||||
}
|
|
@ -33,20 +33,29 @@ function wrap <T extends AnyFn> (func: T, wrapper: (fn: AnyFn) => T) {
|
|||
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);
|
||||
|
||||
global.setImmediate = timers.setImmediate = wrapWithActivateUvLoop(timers.setImmediate);
|
||||
global.clearImmediate = timers.clearImmediate;
|
||||
|
||||
// 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
|
||||
// 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.setInterval = wrapWithActivateUvLoop(timers.setInterval);
|
||||
|
||||
// Only override the global setTimeout/setInterval impls in the browser process
|
||||
if (process.type === 'browser') {
|
||||
// Update the global version of the timer apis to use the above wrapper
|
||||
// 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.setInterval = timers.setInterval;
|
||||
}
|
||||
|
|
21
lib/utility/.eslintrc.json
Normal file
21
lib/utility/.eslintrc.json
Normal 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/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
6
lib/utility/api/exports/electron.ts
Normal file
6
lib/utility/api/exports/electron.ts
Normal 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);
|
2
lib/utility/api/module-list.ts
Normal file
2
lib/utility/api/module-list.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
// Utility side modules, please sort alphabetically.
|
||||
export const utilityNodeModuleList: ElectronInternal.ModuleEntry[] = [];
|
38
lib/utility/init.ts
Normal file
38
lib/utility/init.ts
Normal 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);
|
30
lib/utility/parent-port.ts
Normal file
30
lib/utility/parent-port.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -115,6 +115,7 @@ add_electron_deps_to_license_credits_file.patch
|
|||
fix_crash_loading_non-standard_schemes_in_iframes.patch
|
||||
fix_return_v8_value_from_localframe_requestexecutescript.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
|
||||
fix_on-screen-keyboard_hides_on_input_blur_in_webview.patch
|
||||
preconnect_manager.patch
|
||||
|
|
|
@ -3,28 +3,28 @@ From: Jeremy Apthorp <nornagon@nornagon.net>
|
|||
Date: Mon, 26 Aug 2019 12:02:51 -0700
|
||||
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
|
||||
index dd5ccfc0bdc2e071999d1bf864dc065dd1311407..7464e84f6e610749dce5c3a46afce262f29020cc 100644
|
||||
index dd5ccfc0bdc2e071999d1bf864dc065dd1311407..cfadd28fca9f80bf57578db78d5472c4f75414e1 100644
|
||||
--- a/content/browser/child_process_launcher_helper_linux.cc
|
||||
+++ b/content/browser/child_process_launcher_helper_linux.cc
|
||||
@@ -54,6 +54,18 @@ bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread(
|
||||
if (GetProcessType() == switches::kRendererProcess) {
|
||||
const int sandbox_fd = SandboxHostLinux::GetInstance()->GetChildSocket();
|
||||
@@ -56,6 +56,18 @@ bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread(
|
||||
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) {
|
||||
options->fds_to_remap.emplace_back(remapped_fd.second.get(),
|
||||
remapped_fd.first);
|
||||
|
|
|
@ -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);
|
||||
|
|
@ -40,6 +40,10 @@ const main = async () => {
|
|||
{
|
||||
name: 'asar_bundle_deps',
|
||||
config: 'webpack.config.asar.js'
|
||||
},
|
||||
{
|
||||
name: 'utility_bundle_deps',
|
||||
config: 'webpack.config.utility.js'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
#include "shell/app/command_line_args.h"
|
||||
#include "shell/browser/api/electron_api_menu.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/gpuinfo_manager.h"
|
||||
#include "shell/browser/browser_process_impl.h"
|
||||
|
@ -922,6 +923,12 @@ void App::BrowserChildProcessCrashedOrKilled(
|
|||
if (!data.name.empty()) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
420
shell/browser/api/electron_api_utility_process.cc
Normal file
420
shell/browser/api/electron_api_utility_process.cc
Normal 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", ¶ms->script);
|
||||
if (dict.Has("args") && !dict.Get("args", ¶ms->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", ¶ms->exec_args)) {
|
||||
args->ThrowTypeError("Invalid value for execArgv");
|
||||
return gin::Handle<UtilityProcessWrapper>();
|
||||
}
|
||||
|
||||
opts.Get("serviceName", &display_name);
|
||||
opts.Get("cwd", ¤t_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)
|
100
shell/browser/api/electron_api_utility_process.h
Normal file
100
shell/browser/api/electron_api_utility_process.h
Normal 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_
|
|
@ -24,13 +24,17 @@
|
|||
#include "components/os_crypt/key_storage_config_linux.h"
|
||||
#include "components/os_crypt/os_crypt.h"
|
||||
#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/child_process_data.h"
|
||||
#include "content/public/browser/child_process_security_policy.h"
|
||||
#include "content/public/browser/device_service.h"
|
||||
#include "content/public/browser/first_party_sets_handler.h"
|
||||
#include "content/public/browser/web_ui_controller_factory.h"
|
||||
#include "content/public/common/content_features.h"
|
||||
#include "content/public/common/content_switches.h"
|
||||
#include "content/public/common/process_type.h"
|
||||
#include "content/public/common/result_codes.h"
|
||||
#include "electron/buildflags/buildflags.h"
|
||||
#include "electron/fuses.h"
|
||||
|
@ -39,6 +43,7 @@
|
|||
#include "services/tracing/public/cpp/stack_sampling/tracing_sampler_profiler.h"
|
||||
#include "shell/app/electron_main_delegate.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_process_impl.h"
|
||||
#include "shell/browser/electron_browser_client.h"
|
||||
|
@ -273,12 +278,15 @@ void ElectronBrowserMainParts::PostEarlyInitialization() {
|
|||
// Add Electron extended APIs.
|
||||
electron_bindings_->BindTo(js_env_->isolate(), env->process_object());
|
||||
|
||||
// Load everything.
|
||||
node_bindings_->LoadEnvironment(env);
|
||||
// Create explicit microtasks runner.
|
||||
js_env_->CreateMicrotasksRunner();
|
||||
|
||||
// Wrap the uv loop with global env.
|
||||
node_bindings_->set_uv_env(env);
|
||||
|
||||
// Load everything.
|
||||
node_bindings_->LoadEnvironment(env);
|
||||
|
||||
// We already initialized the feature list in PreEarlyInitialization(), but
|
||||
// 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
|
||||
|
@ -503,7 +511,6 @@ int ElectronBrowserMainParts::PreMainMessageLoopRun() {
|
|||
|
||||
void ElectronBrowserMainParts::WillRunMainMessageLoop(
|
||||
std::unique_ptr<base::RunLoop>& run_loop) {
|
||||
js_env_->OnMessageLoopCreated();
|
||||
exit_code_ = content::RESULT_CODE_NORMAL_EXIT;
|
||||
Browser::Get()->SetMainMessageLoopQuitClosure(
|
||||
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
|
||||
// invoke Node/V8 APIs inside them.
|
||||
node_env_->env()->set_trace_sync_io(false);
|
||||
js_env_->OnMessageLoopDestroying();
|
||||
js_env_->DestroyMicrotasksRunner();
|
||||
node::Stop(node_env_->env());
|
||||
node_env_.reset();
|
||||
|
||||
|
|
|
@ -38,6 +38,17 @@ class EventEmitterMixin {
|
|||
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...);
|
||||
template <typename... Args>
|
||||
bool EmitCustomEvent(base::StringPiece name,
|
||||
|
|
|
@ -287,13 +287,13 @@ v8::Isolate* JavascriptEnvironment::GetIsolate() {
|
|||
return g_isolate;
|
||||
}
|
||||
|
||||
void JavascriptEnvironment::OnMessageLoopCreated() {
|
||||
void JavascriptEnvironment::CreateMicrotasksRunner() {
|
||||
DCHECK(!microtasks_runner_);
|
||||
microtasks_runner_ = std::make_unique<MicrotasksRunner>(isolate());
|
||||
base::CurrentThread::Get()->AddTaskObserver(microtasks_runner_.get());
|
||||
}
|
||||
|
||||
void JavascriptEnvironment::OnMessageLoopDestroying() {
|
||||
void JavascriptEnvironment::DestroyMicrotasksRunner() {
|
||||
DCHECK(microtasks_runner_);
|
||||
{
|
||||
v8::HandleScope scope(isolate_);
|
||||
|
|
|
@ -29,8 +29,8 @@ class JavascriptEnvironment {
|
|||
JavascriptEnvironment(const JavascriptEnvironment&) = delete;
|
||||
JavascriptEnvironment& operator=(const JavascriptEnvironment&) = delete;
|
||||
|
||||
void OnMessageLoopCreated();
|
||||
void OnMessageLoopDestroying();
|
||||
void CreateMicrotasksRunner();
|
||||
void DestroyMicrotasksRunner();
|
||||
|
||||
node::MultiIsolatePlatform* platform() const { return platform_; }
|
||||
v8::Isolate* isolate() const { return isolate_; }
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
V(electron_browser_system_preferences) \
|
||||
V(electron_browser_base_window) \
|
||||
V(electron_browser_tray) \
|
||||
V(electron_browser_utility_process) \
|
||||
V(electron_browser_view) \
|
||||
V(electron_browser_web_contents) \
|
||||
V(electron_browser_web_contents_view) \
|
||||
|
@ -87,7 +88,8 @@
|
|||
V(electron_renderer_context_bridge) \
|
||||
V(electron_renderer_crash_reporter) \
|
||||
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)
|
||||
|
||||
|
@ -390,7 +392,11 @@ void NodeBindings::Initialize() {
|
|||
std::vector<std::string> argv = {"electron"};
|
||||
std::vector<std::string> exec_argv;
|
||||
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())
|
||||
process_flags |= node::ProcessFlags::kDisableNodeOptionsEnv;
|
||||
|
||||
|
@ -417,16 +423,9 @@ void NodeBindings::Initialize() {
|
|||
|
||||
node::Environment* NodeBindings::CreateEnvironment(
|
||||
v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform) {
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
auto& atom_args = ElectronCommandLine::argv();
|
||||
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
|
||||
|
||||
node::MultiIsolatePlatform* platform,
|
||||
std::vector<std::string> args,
|
||||
std::vector<std::string> exec_args) {
|
||||
// Feed node the path to initialization script.
|
||||
std::string process_type;
|
||||
switch (browser_env_) {
|
||||
|
@ -439,14 +438,20 @@ node::Environment* NodeBindings::CreateEnvironment(
|
|||
case BrowserEnvironment::kWorker:
|
||||
process_type = "worker";
|
||||
break;
|
||||
case BrowserEnvironment::kUtility:
|
||||
process_type = "utility";
|
||||
break;
|
||||
}
|
||||
|
||||
v8::Isolate* isolate = context->GetIsolate();
|
||||
gin_helper::Dictionary global(isolate, context->Global());
|
||||
// Do not set DOM globals for renderer process.
|
||||
// We must set this before the node bootstrapper which is run inside
|
||||
// CreateEnvironment
|
||||
if (browser_env_ != BrowserEnvironment::kBrowser)
|
||||
// Avoids overriding globals like setImmediate, clearImmediate
|
||||
// queueMicrotask etc during the bootstrap phase of Node.js
|
||||
// for processes that already have these defined by DOM.
|
||||
// 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);
|
||||
|
||||
if (browser_env_ == BrowserEnvironment::kBrowser) {
|
||||
|
@ -464,7 +469,6 @@ node::Environment* NodeBindings::CreateEnvironment(
|
|||
: search_paths));
|
||||
}
|
||||
|
||||
std::vector<std::string> exec_args;
|
||||
base::FilePath resources_path = GetResourcesPath();
|
||||
std::string init_script = "electron/js2c/" + process_type + "_init";
|
||||
|
||||
|
@ -478,7 +482,8 @@ node::Environment* NodeBindings::CreateEnvironment(
|
|||
node::EnvironmentFlags::kHideConsoleWindows |
|
||||
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 -
|
||||
// in renderer processes this should be blink. We need to tell Node.js
|
||||
// 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
|
||||
// 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
|
||||
// _noBrowserGlobals is read correctly before we remove it
|
||||
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 already listen for uncaught exceptions and handle them there.
|
||||
is.should_abort_on_uncaught_exception_callback = [](v8::Isolate*) {
|
||||
return false;
|
||||
};
|
||||
// For utility process we expect the process to behave as standard
|
||||
// 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
|
||||
// renderer process.
|
||||
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.
|
||||
is.policy = v8::MicrotasksPolicy::kExplicit;
|
||||
} else {
|
||||
|
@ -585,6 +597,20 @@ node::Environment* NodeBindings::CreateEnvironment(
|
|||
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) {
|
||||
node::LoadEnvironment(env, node::StartExecutionCallback{});
|
||||
gin_helper::EmitEvent(env->isolate(), env->process_object(), "loaded");
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
#ifndef ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_
|
||||
#define ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_
|
||||
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include "base/files/file_path.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
|
@ -74,7 +76,7 @@ class UvHandle {
|
|||
|
||||
class NodeBindings {
|
||||
public:
|
||||
enum class BrowserEnvironment { kBrowser, kRenderer, kWorker };
|
||||
enum class BrowserEnvironment { kBrowser, kRenderer, kUtility, kWorker };
|
||||
|
||||
static NodeBindings* Create(BrowserEnvironment browser_env);
|
||||
static void RegisterBuiltinModules();
|
||||
|
@ -86,6 +88,10 @@ class NodeBindings {
|
|||
void Initialize();
|
||||
|
||||
// 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::MultiIsolatePlatform* platform);
|
||||
|
||||
|
|
104
shell/services/node/node_service.cc
Normal file
104
shell/services/node/node_service.cc
Normal 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
|
44
shell/services/node/node_service.h
Normal file
44
shell/services/node/node_service.h
Normal 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_
|
133
shell/services/node/parent_port.cc
Normal file
133
shell/services/node/parent_port.cc
Normal 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)
|
68
shell/services/node/parent_port.h
Normal file
68
shell/services/node/parent_port.h
Normal 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_
|
14
shell/services/node/public/mojom/BUILD.gn
Normal file
14
shell/services/node/public/mojom/BUILD.gn
Normal 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",
|
||||
]
|
||||
}
|
21
shell/services/node/public/mojom/node_service.mojom
Normal file
21
shell/services/node/public/mojom/node_service.mojom
Normal 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);
|
||||
};
|
|
@ -16,6 +16,8 @@
|
|||
#include "services/proxy_resolver/proxy_resolver_factory_impl.h"
|
||||
#include "services/proxy_resolver/public/mojom/proxy_resolver.mojom.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)
|
||||
#include "chrome/services/util_win/public/mojom/util_read_icon.mojom.h"
|
||||
|
@ -72,6 +74,10 @@ auto RunProxyResolver(
|
|||
std::move(receiver));
|
||||
}
|
||||
|
||||
auto RunNodeService(mojo::PendingReceiver<node::mojom::NodeService> receiver) {
|
||||
return std::make_unique<electron::NodeService>(std::move(receiver));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ElectronContentUtilityClient::ElectronContentUtilityClient() = default;
|
||||
|
@ -115,6 +121,8 @@ void ElectronContentUtilityClient::RegisterMainThreadServices(
|
|||
(BUILDFLAG(ENABLE_PRINTING) && BUILDFLAG(IS_WIN))
|
||||
services.Add(RunPrintingService);
|
||||
#endif
|
||||
|
||||
services.Add(RunNodeService);
|
||||
}
|
||||
|
||||
void ElectronContentUtilityClient::RegisterIOThreadServices(
|
||||
|
|
364
spec/api-utility-process-spec.ts
Normal file
364
spec/api-utility-process-spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
1
spec/fixtures/api/utility-process/crash.js
vendored
Normal file
1
spec/fixtures/api/utility-process/crash.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
process.crash();
|
3
spec/fixtures/api/utility-process/custom-exit.js
vendored
Normal file
3
spec/fixtures/api/utility-process/custom-exit.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
const arg = process.argv[2];
|
||||
const code = arg.split('=')[1];
|
||||
process.exit(code);
|
1
spec/fixtures/api/utility-process/empty.js
vendored
Normal file
1
spec/fixtures/api/utility-process/empty.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
process.exit(0);
|
1
spec/fixtures/api/utility-process/endless.js
vendored
Normal file
1
spec/fixtures/api/utility-process/endless.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
setInterval(() => {}, 2000);
|
22
spec/fixtures/api/utility-process/env-app/main.js
vendored
Normal file
22
spec/fixtures/api/utility-process/env-app/main.js
vendored
Normal 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();
|
||||
});
|
||||
});
|
4
spec/fixtures/api/utility-process/env-app/package.json
vendored
Normal file
4
spec/fixtures/api/utility-process/env-app/package.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "electron-test-utility-process-env-app",
|
||||
"main": "main.js"
|
||||
}
|
2
spec/fixtures/api/utility-process/env-app/test.js
vendored
Normal file
2
spec/fixtures/api/utility-process/env-app/test.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
process.parentPort.postMessage(process.env.FROM);
|
||||
process.exit(0);
|
1
spec/fixtures/api/utility-process/exception.js
vendored
Normal file
1
spec/fixtures/api/utility-process/exception.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
nonExistingFunc(); // eslint-disable-line
|
10
spec/fixtures/api/utility-process/inherit-stderr/main.js
vendored
Normal file
10
spec/fixtures/api/utility-process/inherit-stderr/main.js
vendored
Normal 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();
|
||||
});
|
||||
});
|
4
spec/fixtures/api/utility-process/inherit-stderr/package.json
vendored
Normal file
4
spec/fixtures/api/utility-process/inherit-stderr/package.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "electron-test-utility-process-inherit-stderr",
|
||||
"main": "main.js"
|
||||
}
|
3
spec/fixtures/api/utility-process/inherit-stderr/test.js
vendored
Normal file
3
spec/fixtures/api/utility-process/inherit-stderr/test.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
process.stderr.write(process.argv[2].split('--payload=')[1]);
|
||||
process.stderr.end();
|
||||
process.exit(0);
|
10
spec/fixtures/api/utility-process/inherit-stdout/main.js
vendored
Normal file
10
spec/fixtures/api/utility-process/inherit-stdout/main.js
vendored
Normal 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();
|
||||
});
|
||||
});
|
4
spec/fixtures/api/utility-process/inherit-stdout/package.json
vendored
Normal file
4
spec/fixtures/api/utility-process/inherit-stdout/package.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "electron-test-utility-process-inherit-stdout",
|
||||
"main": "main.js"
|
||||
}
|
3
spec/fixtures/api/utility-process/inherit-stdout/test.js
vendored
Normal file
3
spec/fixtures/api/utility-process/inherit-stdout/test.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
process.stdout.write(process.argv[2].split('--payload=')[1]);
|
||||
process.stdout.end();
|
||||
process.exit(0);
|
3
spec/fixtures/api/utility-process/log.js
vendored
Normal file
3
spec/fixtures/api/utility-process/log.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
console.log('hello');
|
||||
process.stderr.write('world');
|
||||
process.exit(0);
|
10
spec/fixtures/api/utility-process/post-message-queue.js
vendored
Normal file
10
spec/fixtures/api/utility-process/post-message-queue.js
vendored
Normal 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);
|
3
spec/fixtures/api/utility-process/post-message.js
vendored
Normal file
3
spec/fixtures/api/utility-process/post-message.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
process.parentPort.on('message', (e) => {
|
||||
process.parentPort.postMessage(e.data);
|
||||
});
|
5
spec/fixtures/api/utility-process/preload.js
vendored
Normal file
5
spec/fixtures/api/utility-process/preload.js
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
const { ipcRenderer } = require('electron');
|
||||
|
||||
ipcRenderer.on('port', (e, msg) => {
|
||||
e.ports[0].postMessage(msg);
|
||||
});
|
6
spec/fixtures/api/utility-process/receive-message.js
vendored
Normal file
6
spec/fixtures/api/utility-process/receive-message.js
vendored
Normal 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();
|
||||
});
|
2
spec/fixtures/api/utility-process/suid.js
vendored
Normal file
2
spec/fixtures/api/utility-process/suid.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
const result = require('child_process').execSync('sudo --help');
|
||||
process.parentPort.postMessage(result);
|
1
typings/internal-ambient.d.ts
vendored
1
typings/internal-ambient.d.ts
vendored
|
@ -251,6 +251,7 @@ declare namespace NodeJS {
|
|||
|
||||
// Additional properties
|
||||
_firstFileName?: string;
|
||||
_serviceStartupScript: string;
|
||||
|
||||
helperExecPath: string;
|
||||
mainModule?: NodeJS.Module | undefined;
|
||||
|
|
15
typings/internal-electron.d.ts
vendored
15
typings/internal-electron.d.ts
vendored
|
@ -9,7 +9,8 @@ declare namespace Electron {
|
|||
enum ProcessType {
|
||||
browser = 'browser',
|
||||
renderer = 'renderer',
|
||||
worker = 'worker'
|
||||
worker = 'worker',
|
||||
utility = 'utility'
|
||||
}
|
||||
|
||||
interface App {
|
||||
|
@ -254,6 +255,18 @@ declare namespace ElectronInternal {
|
|||
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 {
|
||||
static observedAttributes: Array<string>;
|
||||
|
||||
|
|
Loading…
Reference in a new issue