From da0fd286b44846da0e8207691a3df081051d7b5c Mon Sep 17 00:00:00 2001 From: Robo Date: Thu, 20 Oct 2022 14:49:49 +0900 Subject: [PATCH] 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> --- BUILD.gn | 12 + build/webpack/webpack.config.utility.js | 4 + docs/api/parent-port.md | 46 ++ docs/api/process.md | 6 + docs/api/utility-process.md | 136 ++++ docs/glossary.md | 10 + filenames.auto.gni | 19 + filenames.gni | 6 + lib/browser/api/module-list.ts | 1 + lib/browser/api/utility-process.ts | 150 ++++ lib/common/init.ts | 17 +- lib/utility/.eslintrc.json | 21 + lib/utility/api/exports/electron.ts | 6 + lib/utility/api/module-list.ts | 2 + lib/utility/init.ts | 38 + lib/utility/parent-port.ts | 30 + patches/chromium/.patches | 1 + ...leges_in_unsandboxed_child_processes.patch | 34 +- ...e_launch_options_for_service_process.patch | 671 ++++++++++++++++++ script/gen-filenames.ts | 4 + shell/browser/api/electron_api_app.cc | 7 + .../api/electron_api_utility_process.cc | 420 +++++++++++ .../api/electron_api_utility_process.h | 100 +++ shell/browser/electron_browser_main_parts.cc | 44 +- shell/browser/event_emitter_mixin.h | 11 + shell/browser/javascript_environment.cc | 4 +- shell/browser/javascript_environment.h | 4 +- shell/common/node_bindings.cc | 72 +- shell/common/node_bindings.h | 8 +- shell/services/node/node_service.cc | 104 +++ shell/services/node/node_service.h | 44 ++ shell/services/node/parent_port.cc | 133 ++++ shell/services/node/parent_port.h | 68 ++ shell/services/node/public/mojom/BUILD.gn | 14 + .../node/public/mojom/node_service.mojom | 21 + .../electron_content_utility_client.cc | 8 + spec/api-utility-process-spec.ts | 364 ++++++++++ spec/fixtures/api/utility-process/crash.js | 1 + .../api/utility-process/custom-exit.js | 3 + spec/fixtures/api/utility-process/empty.js | 1 + spec/fixtures/api/utility-process/endless.js | 1 + .../api/utility-process/env-app/main.js | 22 + .../api/utility-process/env-app/package.json | 4 + .../api/utility-process/env-app/test.js | 2 + .../fixtures/api/utility-process/exception.js | 1 + .../utility-process/inherit-stderr/main.js | 10 + .../inherit-stderr/package.json | 4 + .../utility-process/inherit-stderr/test.js | 3 + .../utility-process/inherit-stdout/main.js | 10 + .../inherit-stdout/package.json | 4 + .../utility-process/inherit-stdout/test.js | 3 + spec/fixtures/api/utility-process/log.js | 3 + .../api/utility-process/post-message-queue.js | 10 + .../api/utility-process/post-message.js | 3 + spec/fixtures/api/utility-process/preload.js | 5 + .../api/utility-process/receive-message.js | 6 + spec/fixtures/api/utility-process/suid.js | 2 + typings/internal-ambient.d.ts | 1 + typings/internal-electron.d.ts | 15 +- 59 files changed, 2700 insertions(+), 54 deletions(-) create mode 100644 build/webpack/webpack.config.utility.js create mode 100644 docs/api/parent-port.md create mode 100644 docs/api/utility-process.md create mode 100644 lib/browser/api/utility-process.ts create mode 100644 lib/utility/.eslintrc.json create mode 100644 lib/utility/api/exports/electron.ts create mode 100644 lib/utility/api/module-list.ts create mode 100644 lib/utility/init.ts create mode 100644 lib/utility/parent-port.ts create mode 100644 patches/chromium/feat_configure_launch_options_for_service_process.patch create mode 100644 shell/browser/api/electron_api_utility_process.cc create mode 100644 shell/browser/api/electron_api_utility_process.h create mode 100644 shell/services/node/node_service.cc create mode 100644 shell/services/node/node_service.h create mode 100644 shell/services/node/parent_port.cc create mode 100644 shell/services/node/parent_port.h create mode 100644 shell/services/node/public/mojom/BUILD.gn create mode 100644 shell/services/node/public/mojom/node_service.mojom create mode 100644 spec/api-utility-process-spec.ts create mode 100644 spec/fixtures/api/utility-process/crash.js create mode 100644 spec/fixtures/api/utility-process/custom-exit.js create mode 100644 spec/fixtures/api/utility-process/empty.js create mode 100644 spec/fixtures/api/utility-process/endless.js create mode 100644 spec/fixtures/api/utility-process/env-app/main.js create mode 100644 spec/fixtures/api/utility-process/env-app/package.json create mode 100644 spec/fixtures/api/utility-process/env-app/test.js create mode 100644 spec/fixtures/api/utility-process/exception.js create mode 100644 spec/fixtures/api/utility-process/inherit-stderr/main.js create mode 100644 spec/fixtures/api/utility-process/inherit-stderr/package.json create mode 100644 spec/fixtures/api/utility-process/inherit-stderr/test.js create mode 100644 spec/fixtures/api/utility-process/inherit-stdout/main.js create mode 100644 spec/fixtures/api/utility-process/inherit-stdout/package.json create mode 100644 spec/fixtures/api/utility-process/inherit-stdout/test.js create mode 100644 spec/fixtures/api/utility-process/log.js create mode 100644 spec/fixtures/api/utility-process/post-message-queue.js create mode 100644 spec/fixtures/api/utility-process/post-message.js create mode 100644 spec/fixtures/api/utility-process/preload.js create mode 100644 spec/fixtures/api/utility-process/receive-message.js create mode 100644 spec/fixtures/api/utility-process/suid.js diff --git a/BUILD.gn b/BUILD.gn index c3d7f685bc..59df0e1734 100644 --- a/BUILD.gn +++ b/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", diff --git a/build/webpack/webpack.config.utility.js b/build/webpack/webpack.config.utility.js new file mode 100644 index 0000000000..a80775d18e --- /dev/null +++ b/build/webpack/webpack.config.utility.js @@ -0,0 +1,4 @@ +module.exports = require('./webpack.config.base')({ + target: 'utility', + alwaysHasNode: true +}); diff --git a/docs/api/parent-port.md b/docs/api/parent-port.md new file mode 100644 index 0000000000..9c6ad3a08d --- /dev/null +++ b/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. diff --git a/docs/api/process.md b/docs/api/process.md index 4573899912..9cc34c691e 100644 --- a/docs/api/process.md +++ b/docs/api/process.md @@ -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: diff --git a/docs/api/utility-process.md b/docs/api/utility-process.md new file mode 100644 index 0000000000..147f970810 --- /dev/null +++ b/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)
+ +## 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 diff --git a/docs/glossary.md b/docs/glossary.md index 893c598c52..01ba457ff8 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -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 diff --git a/filenames.auto.gni b/filenames.auto.gni index a7c71081f4..f34e4c88cc 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -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", + ] } diff --git a/filenames.gni b/filenames.gni index 994fc5275a..bd2f91fd6a 100644 --- a/filenames.gni +++ b/filenames.gni @@ -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", ] diff --git a/lib/browser/api/module-list.ts b/lib/browser/api/module-list.ts index be516aec28..88c8ea7e33 100644 --- a/lib/browser/api/module-list.ts +++ b/lib/browser/api/module-list.ts @@ -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') }, diff --git a/lib/browser/api/utility-process.ts b/lib/browser/api/utility-process.ts new file mode 100644 index 0000000000..8027a548eb --- /dev/null +++ b/lib/browser/api/utility-process.ts @@ -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); +} diff --git a/lib/common/init.ts b/lib/common/init.ts index 8e1245dd6a..630a5f910e 100644 --- a/lib/common/init.ts +++ b/lib/common/init.ts @@ -33,20 +33,29 @@ function wrap (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; } diff --git a/lib/utility/.eslintrc.json b/lib/utility/.eslintrc.json new file mode 100644 index 0000000000..dab1dafc3f --- /dev/null +++ b/lib/utility/.eslintrc.json @@ -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/*" + ] + } + ] + } +} diff --git a/lib/utility/api/exports/electron.ts b/lib/utility/api/exports/electron.ts new file mode 100644 index 0000000000..3e37918b66 --- /dev/null +++ b/lib/utility/api/exports/electron.ts @@ -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); diff --git a/lib/utility/api/module-list.ts b/lib/utility/api/module-list.ts new file mode 100644 index 0000000000..53f2e5d9bf --- /dev/null +++ b/lib/utility/api/module-list.ts @@ -0,0 +1,2 @@ +// Utility side modules, please sort alphabetically. +export const utilityNodeModuleList: ElectronInternal.ModuleEntry[] = []; diff --git a/lib/utility/init.ts b/lib/utility/init.ts new file mode 100644 index 0000000000..d360dd514e --- /dev/null +++ b/lib/utility/init.ts @@ -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); diff --git a/lib/utility/parent-port.ts b/lib/utility/parent-port.ts new file mode 100644 index 0000000000..ba0f2fd871 --- /dev/null +++ b/lib/utility/parent-port.ts @@ -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); + } +} diff --git a/patches/chromium/.patches b/patches/chromium/.patches index bae4246c94..9e25d21ab8 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -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 diff --git a/patches/chromium/allow_new_privileges_in_unsandboxed_child_processes.patch b/patches/chromium/allow_new_privileges_in_unsandboxed_child_processes.patch index 84f527f561..1e8e701c77 100644 --- a/patches/chromium/allow_new_privileges_in_unsandboxed_child_processes.patch +++ b/patches/chromium/allow_new_privileges_in_unsandboxed_child_processes.patch @@ -3,28 +3,28 @@ From: Jeremy Apthorp 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); diff --git a/patches/chromium/feat_configure_launch_options_for_service_process.patch b/patches/chromium/feat_configure_launch_options_for_service_process.patch new file mode 100644 index 0000000000..b349ea8dff --- /dev/null +++ b/patches/chromium/feat_configure_launch_options_for_service_process.patch @@ -0,0 +1,671 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: deepak1556 +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 ++ + 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(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 delegate = + std::make_unique( +- 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 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 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 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 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); + ++ // 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 child_flags; + std::vector 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 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); + diff --git a/script/gen-filenames.ts b/script/gen-filenames.ts index 96812d82a3..3e1f41bcc8 100644 --- a/script/gen-filenames.ts +++ b/script/gen-filenames.ts @@ -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' } ]; diff --git a/shell/browser/api/electron_api_app.cc b/shell/browser/api/electron_api_app.cc index 2ba1e0adbe..319b59fab5 100644 --- a/shell/browser/api/electron_api_app.cc +++ b/shell/browser/api/electron_api_app.cc @@ -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); } diff --git a/shell/browser/api/electron_api_utility_process.cc b/shell/browser/api/electron_api_utility_process.cc new file mode 100644 index 0000000000..f70cac01c6 --- /dev/null +++ b/shell/browser/api/electron_api_utility_process.cc @@ -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 +#include + +#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 +#include +#include "base/win/windows_types.h" +#endif + +namespace electron { + +base::IDMap& +GetAllUtilityProcessWrappers() { + static base::NoDestructor< + base::IDMap> + 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 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(read), _O_RDONLY); + } else if (io_handle == IOHandle::STDERR) { + stderr_write.Set(write); + stderr_read_handle_ = read; + stderr_read_fd_ = + _open_osfhandle(reinterpret_cast(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 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( + 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 message_value; + if (args->GetNext(&message_value)) { + if (!electron::SerializeV8Value(args->isolate(), message_value, + &transferable_message)) { + // SerializeV8Value sets an exception. + return; + } + } + + v8::Local transferables; + std::vector> 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 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 message_value = + electron::DeserializeV8Value(isolate, message); + EmitWithoutCustomEvent("message", message_value); + return true; +} + +// static +raw_ptr UtilityProcessWrapper::FromProcessId( + base::ProcessId pid) { + auto* utility_process_wrapper = GetAllUtilityProcessWrappers().Lookup(pid); + return !!utility_process_wrapper ? utility_process_wrapper : nullptr; +} + +// static +gin::Handle UtilityProcessWrapper::Create( + gin::Arguments* args) { + gin_helper::Dictionary dict; + if (!args->GetNext(&dict)) { + args->ThrowTypeError("Options must be an object."); + return gin::Handle(); + } + + std::u16string display_name; + bool use_plugin_helper = false; + std::map 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(); + } + + 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(); + } + + if (opts.Has("execArgv") && !opts.Get("execArgv", ¶ms->exec_args)) { + args->ThrowTypeError("Invalid value for execArgv"); + return gin::Handle(); + } + + opts.Get("serviceName", &display_name); + opts.Get("cwd", ¤t_working_directory); + + std::vector 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(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 exports, + v8::Local unused, + v8::Local 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) diff --git a/shell/browser/api/electron_api_utility_process.h b/shell/browser/api/electron_api_utility_process.h new file mode 100644 index 0000000000..482437e571 --- /dev/null +++ b/shell/browser/api/electron_api_utility_process.h @@ -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 +#include +#include +#include + +#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 +class Handle; +} // namespace gin + +namespace base { +class Process; +} // namespace base + +namespace electron { + +namespace api { + +class UtilityProcessWrapper + : public gin::Wrappable, + public gin_helper::Pinnable, + public gin_helper::EventEmitterMixin, + 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 Create(gin::Arguments* args); + static raw_ptr 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 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 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 connector_; + blink::MessagePortDescriptor host_port_; + mojo::Remote node_service_remote_; + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace api + +} // namespace electron + +#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_UTILITY_PROCESS_H_ diff --git a/shell/browser/electron_browser_main_parts.cc b/shell/browser/electron_browser_main_parts.cc index a74c932ef7..1a962b4c67 100644 --- a/shell/browser/electron_browser_main_parts.cc +++ b/shell/browser/electron_browser_main_parts.cc @@ -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& 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(); diff --git a/shell/browser/event_emitter_mixin.h b/shell/browser/event_emitter_mixin.h index 4f796c55ea..54d6f55c12 100644 --- a/shell/browser/event_emitter_mixin.h +++ b/shell/browser/event_emitter_mixin.h @@ -38,6 +38,17 @@ class EventEmitterMixin { std::forward(args)...); } + // this.emit(name, args...); + template + void EmitWithoutCustomEvent(base::StringPiece name, Args&&... args) { + v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate(); + v8::HandleScope handle_scope(isolate); + v8::Local wrapper; + if (!static_cast(this)->GetWrapper(isolate).ToLocal(&wrapper)) + return; + gin_helper::EmitEvent(isolate, wrapper, name, std::forward(args)...); + } + // this.emit(name, event, args...); template bool EmitCustomEvent(base::StringPiece name, diff --git a/shell/browser/javascript_environment.cc b/shell/browser/javascript_environment.cc index 2acd06cbba..c1fab43482 100644 --- a/shell/browser/javascript_environment.cc +++ b/shell/browser/javascript_environment.cc @@ -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(isolate()); base::CurrentThread::Get()->AddTaskObserver(microtasks_runner_.get()); } -void JavascriptEnvironment::OnMessageLoopDestroying() { +void JavascriptEnvironment::DestroyMicrotasksRunner() { DCHECK(microtasks_runner_); { v8::HandleScope scope(isolate_); diff --git a/shell/browser/javascript_environment.h b/shell/browser/javascript_environment.h index 31ca854490..d8f91525b4 100644 --- a/shell/browser/javascript_environment.h +++ b/shell/browser/javascript_environment.h @@ -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_; } diff --git a/shell/common/node_bindings.cc b/shell/common/node_bindings.cc index f6b548215f..c291875556 100644 --- a/shell/common/node_bindings.cc +++ b/shell/common/node_bindings.cc @@ -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 argv = {"electron"}; std::vector exec_argv; std::vector 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 context, - node::MultiIsolatePlatform* platform) { -#if BUILDFLAG(IS_WIN) - auto& atom_args = ElectronCommandLine::argv(); - std::vector 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 args, + std::vector 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 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 context, + node::MultiIsolatePlatform* platform) { +#if BUILDFLAG(IS_WIN) + auto& electron_args = ElectronCommandLine::argv(); + std::vector 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"); diff --git a/shell/common/node_bindings.h b/shell/common/node_bindings.h index 54bed07a39..c56e1aecd5 100644 --- a/shell/common/node_bindings.h +++ b/shell/common/node_bindings.h @@ -5,7 +5,9 @@ #ifndef ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_ #define ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_ +#include #include +#include #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 context, + node::MultiIsolatePlatform* platform, + std::vector args, + std::vector exec_args); node::Environment* CreateEnvironment(v8::Handle context, node::MultiIsolatePlatform* platform); diff --git a/shell/services/node/node_service.cc b/shell/services/node/node_service.cc new file mode 100644 index 0000000000..ffd2f6966f --- /dev/null +++ b/shell/services/node/node_service.cc @@ -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 +#include + +#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 receiver) + : node_bindings_( + NodeBindings::Create(NodeBindings::BrowserEnvironment::kUtility)), + electron_bindings_( + std::make_unique(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(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(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 diff --git a/shell/services/node/node_service.h b/shell/services/node/node_service.h new file mode 100644 index 0000000000..9683977595 --- /dev/null +++ b/shell/services/node/node_service.h @@ -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 + +#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 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 js_env_; + std::unique_ptr node_bindings_; + std::unique_ptr electron_bindings_; + std::unique_ptr node_env_; + mojo::Receiver receiver_{this}; +}; + +} // namespace electron + +#endif // ELECTRON_SHELL_SERVICES_NODE_NODE_SERVICE_H_ diff --git a/shell/services/node/parent_port.cc b/shell/services/node/parent_port.cc new file mode 100644 index 0000000000..59e2623dd7 --- /dev/null +++ b/shell/services/node/parent_port.cc @@ -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 + +#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 instance; + return instance.get(); +} + +ParentPort::ParentPort() = default; +ParentPort::~ParentPort() = default; + +void ParentPort::Initialize(blink::MessagePortDescriptor port) { + port_ = std::move(port); + connector_ = std::make_unique( + 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 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 message_value = + electron::DeserializeV8Value(isolate, message); + v8::Local 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::Create(v8::Isolate* isolate) { + return gin::CreateHandle(isolate, ParentPort::GetInstance()); +} + +// static +gin::ObjectTemplateBuilder ParentPort::GetObjectTemplateBuilder( + v8::Isolate* isolate) { + return gin::Wrappable::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 exports, + v8::Local unused, + v8::Local 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) diff --git a/shell/services/node/parent_port.h b/shell/services/node/parent_port.h new file mode 100644 index 0000000000..bb56886573 --- /dev/null +++ b/shell/services/node/parent_port.h @@ -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 + +#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 Local; +class Value; +class Isolate; +} // namespace v8 + +namespace gin { +class Arguments; +template +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, + public mojo::MessageReceiver { + public: + static ParentPort* GetInstance(); + static gin::Handle 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 message_value); + void Close(); + void Start(); + void Pause(); + + // mojo::MessageReceiver + bool Accept(mojo::Message* mojo_message) override; + + bool connector_closed_ = false; + std::unique_ptr connector_; + blink::MessagePortDescriptor port_; +}; + +} // namespace electron + +#endif // ELECTRON_SHELL_SERVICES_NODE_PARENT_PORT_H_ diff --git a/shell/services/node/public/mojom/BUILD.gn b/shell/services/node/public/mojom/BUILD.gn new file mode 100644 index 0000000000..5f82edb29c --- /dev/null +++ b/shell/services/node/public/mojom/BUILD.gn @@ -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", + ] +} diff --git a/shell/services/node/public/mojom/node_service.mojom b/shell/services/node/public/mojom/node_service.mojom new file mode 100644 index 0000000000..02dacc8a67 --- /dev/null +++ b/shell/services/node/public/mojom/node_service.mojom @@ -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 args; + array exec_args; + blink.mojom.MessagePortDescriptor port; +}; + +[ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox] +interface NodeService { + Initialize(NodeServiceParams params); +}; diff --git a/shell/utility/electron_content_utility_client.cc b/shell/utility/electron_content_utility_client.cc index 0a711e13e8..a21b3543a3 100644 --- a/shell/utility/electron_content_utility_client.cc +++ b/shell/utility/electron_content_utility_client.cc @@ -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 receiver) { + return std::make_unique(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( diff --git a/spec/api-utility-process-spec.ts b/spec/api-utility-process-spec.ts new file mode 100644 index 0000000000..bbdab8eea8 --- /dev/null +++ b/spec/api-utility-process-spec.ts @@ -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'); + }); + }); +}); diff --git a/spec/fixtures/api/utility-process/crash.js b/spec/fixtures/api/utility-process/crash.js new file mode 100644 index 0000000000..f55d42eb6d --- /dev/null +++ b/spec/fixtures/api/utility-process/crash.js @@ -0,0 +1 @@ +process.crash(); diff --git a/spec/fixtures/api/utility-process/custom-exit.js b/spec/fixtures/api/utility-process/custom-exit.js new file mode 100644 index 0000000000..a403aa1140 --- /dev/null +++ b/spec/fixtures/api/utility-process/custom-exit.js @@ -0,0 +1,3 @@ +const arg = process.argv[2]; +const code = arg.split('=')[1]; +process.exit(code); diff --git a/spec/fixtures/api/utility-process/empty.js b/spec/fixtures/api/utility-process/empty.js new file mode 100644 index 0000000000..dcbbff6c93 --- /dev/null +++ b/spec/fixtures/api/utility-process/empty.js @@ -0,0 +1 @@ +process.exit(0); diff --git a/spec/fixtures/api/utility-process/endless.js b/spec/fixtures/api/utility-process/endless.js new file mode 100644 index 0000000000..3af355c7b8 --- /dev/null +++ b/spec/fixtures/api/utility-process/endless.js @@ -0,0 +1 @@ +setInterval(() => {}, 2000); diff --git a/spec/fixtures/api/utility-process/env-app/main.js b/spec/fixtures/api/utility-process/env-app/main.js new file mode 100644 index 0000000000..df4d078cd4 --- /dev/null +++ b/spec/fixtures/api/utility-process/env-app/main.js @@ -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(); + }); +}); diff --git a/spec/fixtures/api/utility-process/env-app/package.json b/spec/fixtures/api/utility-process/env-app/package.json new file mode 100644 index 0000000000..f706e18582 --- /dev/null +++ b/spec/fixtures/api/utility-process/env-app/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-utility-process-env-app", + "main": "main.js" +} diff --git a/spec/fixtures/api/utility-process/env-app/test.js b/spec/fixtures/api/utility-process/env-app/test.js new file mode 100644 index 0000000000..a4daff9f3c --- /dev/null +++ b/spec/fixtures/api/utility-process/env-app/test.js @@ -0,0 +1,2 @@ +process.parentPort.postMessage(process.env.FROM); +process.exit(0); diff --git a/spec/fixtures/api/utility-process/exception.js b/spec/fixtures/api/utility-process/exception.js new file mode 100644 index 0000000000..5f3b3af5ac --- /dev/null +++ b/spec/fixtures/api/utility-process/exception.js @@ -0,0 +1 @@ +nonExistingFunc(); // eslint-disable-line diff --git a/spec/fixtures/api/utility-process/inherit-stderr/main.js b/spec/fixtures/api/utility-process/inherit-stderr/main.js new file mode 100644 index 0000000000..65510c45b7 --- /dev/null +++ b/spec/fixtures/api/utility-process/inherit-stderr/main.js @@ -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(); + }); +}); diff --git a/spec/fixtures/api/utility-process/inherit-stderr/package.json b/spec/fixtures/api/utility-process/inherit-stderr/package.json new file mode 100644 index 0000000000..eab8b4655c --- /dev/null +++ b/spec/fixtures/api/utility-process/inherit-stderr/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-utility-process-inherit-stderr", + "main": "main.js" +} diff --git a/spec/fixtures/api/utility-process/inherit-stderr/test.js b/spec/fixtures/api/utility-process/inherit-stderr/test.js new file mode 100644 index 0000000000..88b99060f0 --- /dev/null +++ b/spec/fixtures/api/utility-process/inherit-stderr/test.js @@ -0,0 +1,3 @@ +process.stderr.write(process.argv[2].split('--payload=')[1]); +process.stderr.end(); +process.exit(0); diff --git a/spec/fixtures/api/utility-process/inherit-stdout/main.js b/spec/fixtures/api/utility-process/inherit-stdout/main.js new file mode 100644 index 0000000000..65510c45b7 --- /dev/null +++ b/spec/fixtures/api/utility-process/inherit-stdout/main.js @@ -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(); + }); +}); diff --git a/spec/fixtures/api/utility-process/inherit-stdout/package.json b/spec/fixtures/api/utility-process/inherit-stdout/package.json new file mode 100644 index 0000000000..c067207f88 --- /dev/null +++ b/spec/fixtures/api/utility-process/inherit-stdout/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-utility-process-inherit-stdout", + "main": "main.js" +} diff --git a/spec/fixtures/api/utility-process/inherit-stdout/test.js b/spec/fixtures/api/utility-process/inherit-stdout/test.js new file mode 100644 index 0000000000..07d5efa8eb --- /dev/null +++ b/spec/fixtures/api/utility-process/inherit-stdout/test.js @@ -0,0 +1,3 @@ +process.stdout.write(process.argv[2].split('--payload=')[1]); +process.stdout.end(); +process.exit(0); diff --git a/spec/fixtures/api/utility-process/log.js b/spec/fixtures/api/utility-process/log.js new file mode 100644 index 0000000000..87a0ceafdf --- /dev/null +++ b/spec/fixtures/api/utility-process/log.js @@ -0,0 +1,3 @@ +console.log('hello'); +process.stderr.write('world'); +process.exit(0); diff --git a/spec/fixtures/api/utility-process/post-message-queue.js b/spec/fixtures/api/utility-process/post-message-queue.js new file mode 100644 index 0000000000..9a03adaea8 --- /dev/null +++ b/spec/fixtures/api/utility-process/post-message-queue.js @@ -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); diff --git a/spec/fixtures/api/utility-process/post-message.js b/spec/fixtures/api/utility-process/post-message.js new file mode 100644 index 0000000000..1e8180870f --- /dev/null +++ b/spec/fixtures/api/utility-process/post-message.js @@ -0,0 +1,3 @@ +process.parentPort.on('message', (e) => { + process.parentPort.postMessage(e.data); +}); diff --git a/spec/fixtures/api/utility-process/preload.js b/spec/fixtures/api/utility-process/preload.js new file mode 100644 index 0000000000..0c17f5586a --- /dev/null +++ b/spec/fixtures/api/utility-process/preload.js @@ -0,0 +1,5 @@ +const { ipcRenderer } = require('electron'); + +ipcRenderer.on('port', (e, msg) => { + e.ports[0].postMessage(msg); +}); diff --git a/spec/fixtures/api/utility-process/receive-message.js b/spec/fixtures/api/utility-process/receive-message.js new file mode 100644 index 0000000000..021a8ef010 --- /dev/null +++ b/spec/fixtures/api/utility-process/receive-message.js @@ -0,0 +1,6 @@ +process.parentPort.on('message', (e) => { + e.ports[0].on('message', (ev) => { + process.parentPort.postMessage(ev.data); + }); + e.ports[0].start(); +}); diff --git a/spec/fixtures/api/utility-process/suid.js b/spec/fixtures/api/utility-process/suid.js new file mode 100644 index 0000000000..178a52618e --- /dev/null +++ b/spec/fixtures/api/utility-process/suid.js @@ -0,0 +1,2 @@ +const result = require('child_process').execSync('sudo --help'); +process.parentPort.postMessage(result); diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts index 6dcf6de2ca..d972d03eda 100644 --- a/typings/internal-ambient.d.ts +++ b/typings/internal-ambient.d.ts @@ -251,6 +251,7 @@ declare namespace NodeJS { // Additional properties _firstFileName?: string; + _serviceStartupScript: string; helperExecPath: string; mainModule?: NodeJS.Module | undefined; diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts index 00478dad54..df753fd471 100644 --- a/typings/internal-electron.d.ts +++ b/typings/internal-electron.d.ts @@ -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;