@theia/process
Version:
Theia process support.
157 lines (135 loc) • 6.44 kB
text/typescript
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { ProcessManager } from './process-manager';
import { ILogger } from '@theia/core/lib/common';
import { Process, ProcessType, ProcessOptions, ForkOptions, ProcessErrorEvent } from './process';
import { ChildProcess, spawn, fork } from 'child_process';
import * as stream from 'stream';
// The class was here before, exporting to not break anything.
export { DevNullStream } from './dev-null-stream';
import { DevNullStream } from './dev-null-stream';
export const RawProcessOptions = Symbol('RawProcessOptions');
/**
* Options to spawn a new process (`spawn`).
*
* For more information please refer to the spawn function of Node's
* child_process module:
*
* https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
*/
export interface RawProcessOptions extends ProcessOptions {
}
/**
* Options to fork a new process using the current Node interpreter (`fork`).
*
* For more information please refer to the fork function of Node's
* `child_process` module:
*
* https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options
*/
export interface RawForkOptions extends ForkOptions {
}
export const RawProcessFactory = Symbol('RawProcessFactory');
export interface RawProcessFactory {
(options: RawProcessOptions | RawForkOptions): RawProcess;
}
export class RawProcess extends Process {
/**
* If the process fails to launch, it will be undefined.
*/
readonly process: ChildProcess | undefined;
readonly outputStream: stream.Readable;
readonly errorStream: stream.Readable;
readonly inputStream: stream.Writable;
constructor( // eslint-disable-next-line @typescript-eslint/indent
options: RawProcessOptions | RawForkOptions,
processManager: ProcessManager,
logger: ILogger
) {
super(processManager, logger, ProcessType.Raw, options);
const executable = this.isForkOptions(options) ? options.modulePath : options.command;
this.logger.debug(`Starting raw process: ${executable},`
+ ` with args: ${options.args ? options.args.join(' ') : ''}, `
+ ` with options: ${JSON.stringify(options.options)}`);
// About catching errors: spawn will sometimes throw directly
// (EACCES on Linux), sometimes return a Process object with the pid
// property undefined (ENOENT on Linux) and then emit an 'error' event.
// For now, we try to normalize that into always emitting an 'error'
// event.
try {
if (this.isForkOptions(options)) {
this.process = fork(
options.modulePath,
options.args || [],
options.options || {});
} else {
this.process = spawn(
options.command,
options.args || [],
options.options || {});
}
this.process.on('error', (error: NodeJS.ErrnoException) => {
error.code = error.code || 'Unknown error';
this.emitOnError(error as ProcessErrorEvent);
});
// When no stdio option is passed, it is null by default.
this.outputStream = this.process.stdout || new DevNullStream({ autoDestroy: true });
this.inputStream = this.process.stdin || new DevNullStream({ autoDestroy: true });
this.errorStream = this.process.stderr || new DevNullStream({ autoDestroy: true });
this.process.on('exit', (exitCode, signal) => {
// node's child_process exit sets the unused parameter to null,
// but we want it to be undefined instead.
this.emitOnExit(
typeof exitCode === 'number' ? exitCode : undefined,
typeof signal === 'string' ? signal : undefined,
);
this.processManager.unregister(this);
});
this.process.on('close', (exitCode, signal) => {
// node's child_process exit sets the unused parameter to null,
// but we want it to be undefined instead.
this.emitOnClose(
typeof exitCode === 'number' ? exitCode : undefined,
typeof signal === 'string' ? signal : undefined,
);
});
if (this.process.pid !== undefined) {
process.nextTick(this.emitOnStarted.bind(this));
}
} catch (error) {
/* When an error is thrown, set up some fake streams, so the client
code doesn't break because these field are undefined. */
this.outputStream = new DevNullStream({ autoDestroy: true });
this.inputStream = new DevNullStream({ autoDestroy: true });
this.errorStream = new DevNullStream({ autoDestroy: true });
/* Call the client error handler, but first give them a chance to register it. */
this.emitOnErrorAsync(error);
}
}
get pid(): number {
if (!this.process || !this.process.pid) {
throw new Error('process did not start correctly');
}
return this.process.pid;
}
kill(signal?: string): void {
if (this.process && this.killed === false) {
this.process.kill(signal as NodeJS.Signals);
}
}
}