electron/lib/browser/api/utility-process.ts
Robo da0fd286b4
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>
2022-10-19 22:49:49 -07:00

150 lines
4.5 KiB
TypeScript

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);
}