Browse Source

feat: UtilityProcess API (#36089)

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

Co-authored-by: Robo <[email protected]>

* chore: update patches

* docs: add utility process info to tutorial docs (#36074)

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Robo <[email protected]>
trop[bot] 2 years ago
parent
commit
d843ae327d
60 changed files with 2717 additions and 54 deletions
  1. 12 0
      BUILD.gn
  2. 4 0
      build/webpack/webpack.config.utility.js
  3. 46 0
      docs/api/parent-port.md
  4. 6 0
      docs/api/process.md
  5. 136 0
      docs/api/utility-process.md
  6. 10 0
      docs/glossary.md
  7. 17 0
      docs/tutorial/process-model.md
  8. 19 0
      filenames.auto.gni
  9. 6 0
      filenames.gni
  10. 1 0
      lib/browser/api/module-list.ts
  11. 150 0
      lib/browser/api/utility-process.ts
  12. 13 4
      lib/common/init.ts
  13. 21 0
      lib/utility/.eslintrc.json
  14. 6 0
      lib/utility/api/exports/electron.ts
  15. 2 0
      lib/utility/api/module-list.ts
  16. 38 0
      lib/utility/init.ts
  17. 30 0
      lib/utility/parent-port.ts
  18. 1 0
      patches/chromium/.patches
  19. 17 17
      patches/chromium/allow_new_privileges_in_unsandboxed_child_processes.patch
  20. 671 0
      patches/chromium/feat_configure_launch_options_for_service_process.patch
  21. 4 0
      script/gen-filenames.ts
  22. 7 0
      shell/browser/api/electron_api_app.cc
  23. 420 0
      shell/browser/api/electron_api_utility_process.cc
  24. 100 0
      shell/browser/api/electron_api_utility_process.h
  25. 40 4
      shell/browser/electron_browser_main_parts.cc
  26. 11 0
      shell/browser/event_emitter_mixin.h
  27. 2 2
      shell/browser/javascript_environment.cc
  28. 2 2
      shell/browser/javascript_environment.h
  29. 49 23
      shell/common/node_bindings.cc
  30. 7 1
      shell/common/node_bindings.h
  31. 104 0
      shell/services/node/node_service.cc
  32. 44 0
      shell/services/node/node_service.h
  33. 133 0
      shell/services/node/parent_port.cc
  34. 68 0
      shell/services/node/parent_port.h
  35. 14 0
      shell/services/node/public/mojom/BUILD.gn
  36. 21 0
      shell/services/node/public/mojom/node_service.mojom
  37. 8 0
      shell/utility/electron_content_utility_client.cc
  38. 364 0
      spec/api-utility-process-spec.ts
  39. 1 0
      spec/fixtures/api/utility-process/crash.js
  40. 3 0
      spec/fixtures/api/utility-process/custom-exit.js
  41. 1 0
      spec/fixtures/api/utility-process/empty.js
  42. 1 0
      spec/fixtures/api/utility-process/endless.js
  43. 22 0
      spec/fixtures/api/utility-process/env-app/main.js
  44. 4 0
      spec/fixtures/api/utility-process/env-app/package.json
  45. 2 0
      spec/fixtures/api/utility-process/env-app/test.js
  46. 1 0
      spec/fixtures/api/utility-process/exception.js
  47. 10 0
      spec/fixtures/api/utility-process/inherit-stderr/main.js
  48. 4 0
      spec/fixtures/api/utility-process/inherit-stderr/package.json
  49. 3 0
      spec/fixtures/api/utility-process/inherit-stderr/test.js
  50. 10 0
      spec/fixtures/api/utility-process/inherit-stdout/main.js
  51. 4 0
      spec/fixtures/api/utility-process/inherit-stdout/package.json
  52. 3 0
      spec/fixtures/api/utility-process/inherit-stdout/test.js
  53. 3 0
      spec/fixtures/api/utility-process/log.js
  54. 10 0
      spec/fixtures/api/utility-process/post-message-queue.js
  55. 3 0
      spec/fixtures/api/utility-process/post-message.js
  56. 5 0
      spec/fixtures/api/utility-process/preload.js
  57. 6 0
      spec/fixtures/api/utility-process/receive-message.js
  58. 2 0
      spec/fixtures/api/utility-process/suid.js
  59. 1 0
      typings/internal-ambient.d.ts
  60. 14 1
      typings/internal-electron.d.ts

+ 12 - 0
BUILD.gn

@@ -210,6 +210,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",
@@ -217,6 +226,7 @@ action("electron_js2c") {
     ":electron_isolated_renderer_bundle",
     ":electron_renderer_bundle",
     ":electron_sandboxed_renderer_bundle",
+    ":electron_utility_bundle",
     ":electron_worker_bundle",
   ]
 
@@ -226,6 +236,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",
   ]
 
@@ -406,6 +417,7 @@ source_set("electron_lib") {
     "chromium_src:chrome",
     "chromium_src:chrome_spellchecker",
     "shell/common/api:mojo",
+    "shell/services/node/public/mojom",
     "//base:base_static",
     "//base/allocator:buildflags",
     "//chrome:strings",

+ 4 - 0
build/webpack/webpack.config.utility.js

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

+ 46 - 0
docs/api/parent-port.md

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

+ 6 - 0
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:

+ 136 - 0
docs/api/utility-process.md

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

+ 10 - 0
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

+ 17 - 0
docs/tutorial/process-model.md

@@ -214,8 +214,25 @@ This feature is incredibly useful for two main purposes:
   URL, you can add custom properties onto the renderer's `window` global that can
   be used for desktop-only logic on the web client's side.
 
+## The utility process
+
+Each Electron app can spawn multiple child processes from the main process using
+the [UtilityProcess][] API. The utility process runs in a Node.js environment,
+meaning it has the ability to `require` modules and use all of Node.js APIs.
+The utility process can be used to host for example: untrusted services,
+CPU intensive tasks or crash prone components which would have previously
+been hosted in the main process or process spawned with Node.js [`child_process.fork`][] API.
+The primary difference between the utility process and process spawned by Node.js
+child_process module is that the utility process can establish a communication
+channel with a renderer process using [`MessagePort`][]s. An Electron app can
+always prefer the [UtilityProcess][] API over Node.js [`child_process.fork`][] API when
+there is need to fork a child process from the main process.
+
 [window-mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Window
+[`MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
+[`child_process.fork`]: https://nodejs.org/dist/latest-v16.x/docs/api/child_process.html#child_processforkmodulepath-args-options
 [context-isolation]: ./context-isolation.md
 [context-bridge]: ../api/context-bridge.md
 [ipcrenderer]: ../api/ipc-renderer.md
+[UtilityProcess]: ../api/utility-process.md
 [tutorial]: ./tutorial-1-prerequisites.md

+ 19 - 0
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",
+  ]
 }

+ 6 - 0
filenames.gni

@@ -313,6 +313,8 @@ filenames = {
     "shell/browser/api/electron_api_tray.h",
     "shell/browser/api/electron_api_url_loader.cc",
     "shell/browser/api/electron_api_url_loader.h",
+    "shell/browser/api/electron_api_utility_process.cc",
+    "shell/browser/api/electron_api_utility_process.h",
     "shell/browser/api/electron_api_view.cc",
     "shell/browser/api/electron_api_view.h",
     "shell/browser/api/electron_api_web_contents.cc",
@@ -678,6 +680,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",
   ]

+ 1 - 0
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') },

+ 150 - 0
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);
+}

+ 13 - 4
lib/common/init.ts

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

+ 21 - 0
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/*"
+        ]
+      }
+    ]
+  }
+}

+ 6 - 0
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);

+ 2 - 0
lib/utility/api/module-list.ts

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

+ 38 - 0
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);

+ 30 - 0
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);
+  }
+}

+ 1 - 0
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
 fix_on-screen-keyboard_hides_on_input_blur_in_webview.patch
 preconnect_manager.patch
 fix_remove_caption-removing_style_call.patch

+ 17 - 17
patches/chromium/allow_new_privileges_in_unsandboxed_child_processes.patch

@@ -3,28 +3,28 @@ From: Jeremy Apthorp <[email protected]>
 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);

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

+ 4 - 0
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'
     }
   ];
 

+ 7 - 0
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"
@@ -923,6 +924,12 @@ void App::BrowserChildProcessCrashedOrKilled(
   if (!data.name.empty()) {
     details.Set("name", data.name);
   }
+  if (data.process_type == content::PROCESS_TYPE_UTILITY) {
+    base::ProcessId pid = data.GetProcess().Pid();
+    auto utility_process_wrapper = UtilityProcessWrapper::FromProcessId(pid);
+    if (utility_process_wrapper)
+      utility_process_wrapper->Shutdown(info.exit_code);
+  }
   Emit("child-process-gone", details);
 }
 

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

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

+ 40 - 4
shell/browser/electron_browser_main_parts.cc

@@ -25,13 +25,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"
@@ -40,6 +44,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"
@@ -279,12 +284,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
@@ -521,7 +529,6 @@ int ElectronBrowserMainParts::PreMainMessageLoopRun() {
 
 void ElectronBrowserMainParts::WillRunMainMessageLoop(
     std::unique_ptr<base::RunLoop>& run_loop) {
-  js_env_->OnMessageLoopCreated();
   exit_code_ = content::RESULT_CODE_NORMAL_EXIT;
   Browser::Get()->SetMainMessageLoopQuitClosure(
       run_loop->QuitWhenIdleClosure());
@@ -583,10 +590,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();
 

+ 11 - 0
shell/browser/event_emitter_mixin.h

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

+ 2 - 2
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<MicrotasksRunner>(isolate());
   base::CurrentThread::Get()->AddTaskObserver(microtasks_runner_.get());
 }
 
-void JavascriptEnvironment::OnMessageLoopDestroying() {
+void JavascriptEnvironment::DestroyMicrotasksRunner() {
   DCHECK(microtasks_runner_);
   {
     v8::HandleScope scope(isolate_);

+ 2 - 2
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_; }

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

+ 7 - 1
shell/common/node_bindings.h

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

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

+ 44 - 0
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 <memory>
+
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "shell/services/node/public/mojom/node_service.mojom.h"
+
+namespace electron {
+
+class ElectronBindings;
+class JavascriptEnvironment;
+class NodeBindings;
+class NodeEnvironment;
+
+class NodeService : public node::mojom::NodeService {
+ public:
+  explicit NodeService(
+      mojo::PendingReceiver<node::mojom::NodeService> receiver);
+  ~NodeService() override;
+
+  NodeService(const NodeService&) = delete;
+  NodeService& operator=(const NodeService&) = delete;
+
+  // mojom::NodeService implementation:
+  void Initialize(node::mojom::NodeServiceParamsPtr params) override;
+
+ private:
+  bool node_env_stopped_ = false;
+  std::unique_ptr<JavascriptEnvironment> js_env_;
+  std::unique_ptr<NodeBindings> node_bindings_;
+  std::unique_ptr<ElectronBindings> electron_bindings_;
+  std::unique_ptr<NodeEnvironment> node_env_;
+  mojo::Receiver<node::mojom::NodeService> receiver_{this};
+};
+
+}  // namespace electron
+
+#endif  // ELECTRON_SHELL_SERVICES_NODE_NODE_SERVICE_H_

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

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

+ 14 - 0
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",
+  ]
+}

+ 21 - 0
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<string> args;
+  array<string> exec_args;
+  blink.mojom.MessagePortDescriptor port;
+};
+
+[ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox]
+interface NodeService {
+  Initialize(NodeServiceParams params);
+};

+ 8 - 0
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<node::mojom::NodeService> receiver) {
+  return std::make_unique<electron::NodeService>(std::move(receiver));
+}
+
 }  // namespace
 
 ElectronContentUtilityClient::ElectronContentUtilityClient() = default;
@@ -115,6 +121,8 @@ void ElectronContentUtilityClient::RegisterMainThreadServices(
     (BUILDFLAG(ENABLE_PRINTING) && BUILDFLAG(IS_WIN))
   services.Add(RunPrintingService);
 #endif
+
+  services.Add(RunNodeService);
 }
 
 void ElectronContentUtilityClient::RegisterIOThreadServices(

+ 364 - 0
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');
+    });
+  });
+});

+ 1 - 0
spec/fixtures/api/utility-process/crash.js

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

+ 3 - 0
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);

+ 1 - 0
spec/fixtures/api/utility-process/empty.js

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

+ 1 - 0
spec/fixtures/api/utility-process/endless.js

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

+ 22 - 0
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();
+  });
+});

+ 4 - 0
spec/fixtures/api/utility-process/env-app/package.json

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

+ 2 - 0
spec/fixtures/api/utility-process/env-app/test.js

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

+ 1 - 0
spec/fixtures/api/utility-process/exception.js

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

+ 10 - 0
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();
+  });
+});

+ 4 - 0
spec/fixtures/api/utility-process/inherit-stderr/package.json

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

+ 3 - 0
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);

+ 10 - 0
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();
+  });
+});

+ 4 - 0
spec/fixtures/api/utility-process/inherit-stdout/package.json

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

+ 3 - 0
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);

+ 3 - 0
spec/fixtures/api/utility-process/log.js

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

+ 10 - 0
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);

+ 3 - 0
spec/fixtures/api/utility-process/post-message.js

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

+ 5 - 0
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);
+});

+ 6 - 0
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();
+});

+ 2 - 0
spec/fixtures/api/utility-process/suid.js

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

+ 1 - 0
typings/internal-ambient.d.ts

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

+ 14 - 1
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<string>;