/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import path = require('path'); import * as cp from 'child_process'; import ChildProcess = cp.ChildProcess; import exec = cp.exec; import spawn = cp.spawn; import { PassThrough } from 'stream'; import { fork } from './stdFork'; import nls = require('vs/nls'); import { PPromise, Promise, TPromise, TValueCallback, TProgressCallback, ErrorCallback } from 'vs/base/common/winjs.base'; import * as Types from 'vs/base/common/types'; import { IStringDictionary } from 'vs/base/common/collections'; import URI from 'vs/base/common/uri'; import * as Objects from 'vs/base/common/objects'; import * as TPath from 'vs/base/common/paths'; import * as Platform from 'vs/base/common/platform'; import { LineDecoder } from 'vs/base/node/decoder'; import { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, Executable } from 'vs/base/common/processes'; export { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse }; export interface LineData { line: string; source: Source; } export interface BufferData { data: Buffer; source: Source; } export interface StreamData { stdin: NodeJS.WritableStream; stdout: NodeJS.ReadableStream; stderr: NodeJS.ReadableStream; } export function terminateProcess(process: ChildProcess, cwd?: string): TerminateResponse { if (Platform.isWindows) { try { // This we run in Electron execFileSync is available. // Ignore stderr since this is otherwise piped to parent.stderr // which might be already closed. let options:any = { stdio: ['pipe', 'pipe', 'ignore'] }; if (cwd) { options.cwd = cwd; } (cp).execFileSync('taskkill', ['/T', '/F', '/PID', process.pid.toString()], options); } catch (err) { return { success: false, error: err }; } } else if (Platform.isLinux || Platform.isMacintosh) { try { let cmd = URI.parse(require.toUrl('vs/base/node/terminateProcess.sh')).fsPath; let result = (cp).spawnSync(cmd, [process.pid.toString()]); if (result.error) { return { success: false, error: result.error }; } } catch (err) { return { success: false, error: err }; } } else { process.kill('SIGKILL'); } return { success: true }; } export function getWindowsShell(): string { return process.env['comspec'] || 'cmd.exe'; } export abstract class AbstractProcess { private cmd: string; private module: string; private args: string[]; private options: CommandOptions | ForkOptions; protected shell: boolean; private childProcess: ChildProcess; protected childProcessPromise: TPromise; protected terminateRequested:boolean; private static WellKnowCommands: IStringDictionary = { 'ant': true, 'cmake': true, 'eslint': true, 'gradle': true, 'grunt': true, 'gulp': true, 'jake': true, 'jenkins': true, 'jshint': true, 'make': true, 'maven': true, 'msbuild': true, 'msc': true, 'nmake': true, 'npm': true, 'rake': true, 'tsc': true, 'xbuild': true }; public constructor(executable: Executable); public constructor(cmd: string, args: string[], shell: boolean, options: CommandOptions); public constructor(module: string, args: string[], options: ForkOptions); public constructor(arg1: string | Executable, arg2?: string[], arg3?: boolean | ForkOptions, arg4?: CommandOptions) { if (arg4) { this.cmd = arg1; this.args = arg2; this.shell = arg3; this.options = arg4; } else if (arg3 && arg2) { this.module = arg1; this.args = arg2; this.shell = false; this.options = arg3; } else { let executable = arg1; this.cmd = executable.command; this.shell = executable.isShellCommand; this.args = executable.args.slice(0); this.options = executable.options || {}; } this.childProcess = null; this.terminateRequested = false; if (this.options.env) { let newEnv: IStringDictionary = Object.create(null); Object.keys(process.env).forEach((key) => { newEnv[key] = process.env[key]; }); Object.keys(this.options.env).forEach((key) => { newEnv[key] = this.options.env[key]; }); this.options.env = newEnv; } } public getSanitizedCommand(): string { let result = this.cmd.toLowerCase(); let index = result.lastIndexOf(path.sep); if (index !== -1) { result = result.substring(index + 1); } if (AbstractProcess.WellKnowCommands[result]) { return result; } return 'other'; } public start(): PPromise { if (Platform.isWindows && ((this.options && this.options.cwd && TPath.isUNC(this.options.cwd)) || !this.options && !this.options.cwd && TPath.isUNC(process.cwd()))) { return Promise.wrapError(nls.localize('TaskRunner.UNC', 'Can\'t execute a shell command on an UNC drive.')); } return this.useExec().then((useExec) => { let cc: TValueCallback; let ee: ErrorCallback; let pp: TProgressCallback; let result = new PPromise((c, e, p) => { cc = c; ee = e; pp = p; }); if (useExec) { let cmd: string = this.cmd; if (this.args) { cmd = cmd + ' ' + this.args.join(' '); } this.childProcess = exec(cmd, this.options, (error, stdout, stderr) => { this.childProcess = null; let err:any = error; // This is tricky since executing a command shell reports error back in case the executed command return an // error or the command didn't exist at all. So we can't blindly treat an error as a failed command. So we // always parse the output and report success unless the job got killed. if (err && err.killed) { ee({ killed: this.terminateRequested, stdout: stdout.toString(), stderr: stderr.toString() }); } else { this.handleExec(cc, pp, error, stdout, stderr); } }); } else { let childProcess: ChildProcess = null; let closeHandler = (data: any) => { this.childProcess = null; this.childProcessPromise = null; this.handleClose(data, cc, pp, ee); let result: SuccessData = { terminated: this.terminateRequested }; if (this.shell && Platform.isWindows && Types.isNumber(data)) { result.cmdCode = data; } cc(result); }; if (this.shell && Platform.isWindows) { let options: any = Objects.clone(this.options); options.windowsVerbatimArguments = true; options.detached = false; let quotedCommand: boolean = false; let quotedArg: boolean = false; let commandLine: string[] = []; let quoted = this.ensureQuotes(this.cmd); commandLine.push(quoted.value); quotedCommand = quoted.quoted; if (this.args) { this.args.forEach((elem) => { quoted = this.ensureQuotes(elem); commandLine.push(quoted.value); quotedArg = quotedArg && quoted.quoted; }); } let args: string[] = [ '/s', '/c', ]; if (quotedCommand) { if (quotedArg) { args.push('"' + commandLine.join(' ') + '"'); } else if (commandLine.length > 1) { args.push('"' + commandLine[0] + '"' + ' ' + commandLine.slice(1).join(' ')); } else { args.push('"' + commandLine[0] + '"'); } } else { args.push(commandLine.join(' ')); } childProcess = spawn(getWindowsShell(), args, options); } else { if (this.cmd) { childProcess = spawn(this.cmd, this.args, this.options); } else if (this.module) { this.childProcessPromise = new TPromise((c, e, p) => { fork(this.module, this.args, this.options, (error: any, childProcess: ChildProcess) => { if (error) { e(error); ee({ terminated: this.terminateRequested, error: error }); return; } this.childProcess = childProcess; this.childProcess.on('close', closeHandler); this.handleSpawn(childProcess, cc, pp, ee, false); c(childProcess); }); }); } } if (childProcess) { this.childProcess = childProcess; this.childProcessPromise = TPromise.as(childProcess); childProcess.on('error', (error:Error) => { this.childProcess = null; ee({ terminated: this.terminateRequested, error: error}); }); if (childProcess.pid) { this.childProcess.on('close', closeHandler); this.handleSpawn(childProcess, cc, pp, ee, true); } } } return result; }); } protected abstract handleExec(cc: TValueCallback, pp: TProgressCallback, error: Error, stdout: Buffer, stderr: Buffer): void; protected abstract handleSpawn(childProcess: ChildProcess, cc: TValueCallback, pp: TProgressCallback, ee: ErrorCallback, sync: boolean): void; protected handleClose(data: any, cc: TValueCallback, pp: TProgressCallback, ee: ErrorCallback): void { // Default is to do nothing. } private static regexp = /^[^"].* .*[^"]/; private ensureQuotes(value: string) { if (AbstractProcess.regexp.test(value)) { return { value: '"' + value + '"', //`"${value}"`, quoted: true }; } else { return { value: value, quoted: value.length > 0 && value[0] === '"' && value[value.length - 1] === '"' }; } } public isRunning(): boolean { return this.childProcessPromise !== null; } public get pid(): TPromise { return this.childProcessPromise.then(childProcess => childProcess.pid, err => -1); } public terminate(): TPromise { if (!this.childProcessPromise) { return TPromise.as({ success: true }); } return this.childProcessPromise.then((childProcess) => { this.terminateRequested = true; let result = terminateProcess(childProcess, this.options.cwd); if (result.success) { this.childProcess = null; } return result; }, (err) => { return { success: true }; }); } private useExec(): TPromise { return new TPromise((c, e, p) => { if (!this.shell || !Platform.isWindows) { c(false); } let cmdShell = spawn(getWindowsShell(), ['/s', '/c']); cmdShell.on('error', (error:Error) => { c(true); }); cmdShell.on('exit', (data:any) => { c(false); }); }); } } export class LineProcess extends AbstractProcess { private stdoutLineDecoder: LineDecoder; private stderrLineDecoder: LineDecoder; public constructor(executable: Executable); public constructor(cmd: string, args: string[], shell: boolean, options: CommandOptions); public constructor(module: string, args: string[], options: ForkOptions); public constructor(arg1: string | Executable, arg2?: string[], arg3?: boolean | ForkOptions, arg4?: CommandOptions) { super(arg1, arg2, arg3, arg4); } protected handleExec(cc: TValueCallback, pp: TProgressCallback, error: Error, stdout: Buffer, stderr: Buffer) { [stdout, stderr].forEach((buffer:Buffer, index:number) => { let lineDecoder = new LineDecoder(); let lines = lineDecoder.write(buffer); lines.forEach((line) => { pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr }); }); let line = lineDecoder.end(); if (line) { pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr }); } }); cc({ terminated: this.terminateRequested, error: error }); } protected handleSpawn(childProcess: ChildProcess, cc: TValueCallback, pp: TProgressCallback, ee: ErrorCallback, sync: boolean): void { this.stdoutLineDecoder = new LineDecoder(); this.stderrLineDecoder = new LineDecoder(); childProcess.stdout.on('data', (data:Buffer) => { let lines = this.stdoutLineDecoder.write(data); lines.forEach(line => pp({ line: line, source: Source.stdout })); }); childProcess.stderr.on('data', (data:Buffer) => { let lines = this.stderrLineDecoder.write(data); lines.forEach(line => pp({ line: line, source: Source.stderr })); }); } protected handleClose(data: any, cc: TValueCallback, pp: TProgressCallback, ee: ErrorCallback): void { [this.stdoutLineDecoder.end(), this.stderrLineDecoder.end()].forEach((line, index) => { if (line) { pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr }); } }); } } export class BufferProcess extends AbstractProcess { public constructor(executable: Executable); public constructor(cmd: string, args: string[], shell: boolean, options: CommandOptions); public constructor(module: string, args: string[], options: ForkOptions); public constructor(arg1: string | Executable, arg2?: string[], arg3?: boolean | ForkOptions, arg4?: CommandOptions) { super(arg1, arg2, arg3, arg4); } protected handleExec(cc: TValueCallback, pp: TProgressCallback, error: Error, stdout: Buffer, stderr: Buffer): void { pp({ data: stdout, source: Source.stdout }); pp({ data: stderr, source: Source.stderr }); cc({ terminated: this.terminateRequested, error: error }); } protected handleSpawn(childProcess: ChildProcess, cc: TValueCallback, pp: TProgressCallback, ee: ErrorCallback, sync: boolean): void { childProcess.stdout.on('data', (data:Buffer) => { pp({ data: data, source: Source.stdout }); }); childProcess.stderr.on('data', (data:Buffer) => { pp({ data: data, source: Source.stderr }); }); } } export class StreamProcess extends AbstractProcess { public constructor(executable: Executable); public constructor(cmd: string, args: string[], shell: boolean, options: CommandOptions); public constructor(module: string, args: string[], options: ForkOptions); public constructor(arg1: string | Executable, arg2?: string[], arg3?: boolean | ForkOptions, arg4?: CommandOptions) { super(arg1, arg2, arg3, arg4); } protected handleExec(cc: TValueCallback, pp: TProgressCallback, error: Error, stdout: Buffer, stderr: Buffer): void { let stdoutStream = new PassThrough(); stdoutStream.end(stdout); let stderrStream = new PassThrough(); stderrStream.end(stderr); pp({ stdin: null, stdout: stdoutStream, stderr: stderrStream }); cc({ terminated: this.terminateRequested, error: error }); } protected handleSpawn(childProcess: ChildProcess, cc: TValueCallback, pp: TProgressCallback, ee: ErrorCallback, sync: boolean): void { if (sync) { process.nextTick(() => { pp({ stdin: childProcess.stdin, stdout: childProcess.stdout, stderr: childProcess.stderr }); }); } else { pp({ stdin: childProcess.stdin, stdout: childProcess.stdout, stderr: childProcess.stderr }); } } }