Skip to content

Commit

Permalink
feat: UtilityProcess API (#34980)
Browse files Browse the repository at this point in the history
* 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>

Co-authored-by: Robo <hop2deep@gmail.com>
  • Loading branch information
trop[bot] and deepak1556 committed Oct 20, 2022
1 parent 6ee24c9 commit 7405ff6
Show file tree
Hide file tree
Showing 59 changed files with 2,701 additions and 54 deletions.
12 changes: 12 additions & 0 deletions BUILD.gn
Expand Up @@ -202,13 +202,23 @@ 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",
":electron_browser_bundle",
":electron_isolated_renderer_bundle",
":electron_renderer_bundle",
":electron_sandboxed_renderer_bundle",
":electron_utility_bundle",
":electron_worker_bundle",
]

Expand All @@ -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",
]

Expand Down Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions build/webpack/webpack.config.utility.js
@@ -0,0 +1,4 @@
module.exports = require('./webpack.config.base')({
target: 'utility',
alwaysHasNode: true
});
46 changes: 46 additions & 0 deletions docs/api/parent-port.md
@@ -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.
6 changes: 6 additions & 0 deletions docs/api/process.md
Expand Up @@ -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_

Expand All @@ -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:
Expand Down
136 changes: 136 additions & 0 deletions docs/api/utility-process.md
@@ -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
10 changes: 10 additions & 0 deletions docs/glossary.md
Expand Up @@ -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
Expand Down Expand Up @@ -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
19 changes: 19 additions & 0 deletions filenames.auto.gni
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
]
}
6 changes: 6 additions & 0 deletions filenames.gni
Expand Up @@ -313,6 +313,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",
Expand Down Expand Up @@ -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",
]
Expand Down
1 change: 1 addition & 0 deletions lib/browser/api/module-list.ts
Expand Up @@ -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') },
Expand Down

0 comments on commit 7405ff6

Please sign in to comment.