UNPKG

@theia/process

Version:
208 lines (180 loc) 7.24 kB
// ***************************************************************************** // 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, unmanaged } from '@theia/core/shared/inversify'; import { ILogger, Emitter, Event, isObject } from '@theia/core/lib/common'; import { FileUri } from '@theia/core/lib/node'; import { isOSX, isWindows } from '@theia/core'; import { Readable, Writable } from 'stream'; import { exec } from 'child_process'; import * as fs from 'fs'; import { IProcessStartEvent, IProcessExitEvent, ProcessErrorEvent, ProcessType, ManagedProcessManager, ManagedProcess } from '../common/process-manager-types'; export { IProcessStartEvent, IProcessExitEvent, ProcessErrorEvent, ProcessType }; /** * 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 ProcessOptions { readonly command: string, args?: string[], options?: { // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any } } /** * 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 ForkOptions { readonly modulePath: string, args?: string[], options?: object } @injectable() export abstract class Process implements ManagedProcess { readonly id: number; protected readonly startEmitter: Emitter<IProcessStartEvent> = new Emitter<IProcessStartEvent>(); protected readonly exitEmitter: Emitter<IProcessExitEvent> = new Emitter<IProcessExitEvent>(); protected readonly closeEmitter: Emitter<IProcessExitEvent> = new Emitter<IProcessExitEvent>(); protected readonly errorEmitter: Emitter<ProcessErrorEvent> = new Emitter<ProcessErrorEvent>(); protected _killed = false; /** * The OS process id. */ abstract readonly pid: number; /** * The stdout stream. */ abstract readonly outputStream: Readable; /** * The stderr stream. */ abstract readonly errorStream: Readable; /** * The stdin stream. */ abstract readonly inputStream: Writable; constructor( @unmanaged() protected readonly processManager: ManagedProcessManager, @unmanaged() protected readonly logger: ILogger, @unmanaged() protected readonly type: ProcessType, @unmanaged() protected readonly options: ProcessOptions | ForkOptions ) { this.id = this.processManager.register(this); this.initialCwd = options && options.options && 'cwd' in options.options && options.options['cwd'].toString() || __dirname; } abstract kill(signal?: string): void; get killed(): boolean { return this._killed; } get onStart(): Event<IProcessStartEvent> { return this.startEmitter.event; } /** * Wait for the process to exit, streams can still emit data. */ get onExit(): Event<IProcessExitEvent> { return this.exitEmitter.event; } get onError(): Event<ProcessErrorEvent> { return this.errorEmitter.event; } /** * Waits for both process exit and for all the streams to be closed. */ get onClose(): Event<IProcessExitEvent> { return this.closeEmitter.event; } protected emitOnStarted(): void { this.startEmitter.fire({}); } /** * Emit the onExit event for this process. Only one of code and signal * should be defined. */ protected emitOnExit(code?: number, signal?: string): void { const exitEvent: IProcessExitEvent = { code, signal }; this.handleOnExit(exitEvent); this.exitEmitter.fire(exitEvent); } /** * Emit the onClose event for this process. Only one of code and signal * should be defined. */ protected emitOnClose(code?: number, signal?: string): void { this.closeEmitter.fire({ code, signal }); } protected handleOnExit(event: IProcessExitEvent): void { this._killed = true; const signalSuffix = event.signal ? `, signal: ${event.signal}` : ''; const executable = this.isForkOptions(this.options) ? this.options.modulePath : this.options.command; this.logger.debug(`Process ${this.pid} has exited with code ${event.code}${signalSuffix}.`, executable, this.options.args); } protected emitOnError(err: ProcessErrorEvent): void { this.handleOnError(err); this.errorEmitter.fire(err); } protected async emitOnErrorAsync(error: ProcessErrorEvent): Promise<void> { process.nextTick(this.emitOnError.bind(this), error); } protected handleOnError(error: ProcessErrorEvent): void { this._killed = true; this.logger.error(error); } protected isForkOptions(options: unknown): options is ForkOptions { return isObject<ForkOptions>(options) && !!options.modulePath; } protected readonly initialCwd: string; /** * @returns the current working directory as a URI (usually file:// URI) */ public getCwdURI(): Promise<string> { if (isOSX) { return new Promise<string>(resolve => { exec('lsof -OPln -p ' + this.pid + ' | grep cwd', (error, stdout, stderr) => { if (stdout !== '') { resolve(FileUri.create(stdout.substring(stdout.indexOf('/'), stdout.length - 1)).toString()); } else { resolve(FileUri.create(this.initialCwd).toString()); } }); }); } else if (!isWindows) { return new Promise<string>(resolve => { fs.readlink('/proc/' + this.pid + '/cwd', (err, linkedstr) => { if (err || !linkedstr) { resolve(FileUri.create(this.initialCwd).toString()); } else { resolve(FileUri.create(linkedstr).toString()); } }); }); } else { return new Promise<string>(resolve => { resolve(FileUri.create(this.initialCwd).toString()); }); } } }