diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index 38f414b2ffa..ea32b4dce2f 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -79,6 +79,7 @@ export abstract class AbstractProcess { private childProcess: cp.ChildProcess; protected childProcessPromise: TPromise; + private pidResolve: TValueCallback; protected terminateRequested: boolean; private static WellKnowCommands: IStringDictionary = { @@ -241,6 +242,10 @@ export abstract class AbstractProcess { return; } this.childProcess = childProcess; + if (this.pidResolve) { + this.pidResolve(Types.isNumber(childProcess.pid) ? childProcess.pid : -1); + this.pidResolve = undefined; + } this.childProcess.on('close', closeHandler); this.handleSpawn(childProcess, cc, pp, ee, false); c(childProcess); @@ -251,6 +256,10 @@ export abstract class AbstractProcess { if (childProcess) { this.childProcess = childProcess; this.childProcessPromise = TPromise.as(childProcess); + if (this.pidResolve) { + this.pidResolve(Types.isNumber(childProcess.pid) ? childProcess.pid : -1); + this.pidResolve = undefined; + } childProcess.on('error', (error: Error) => { this.childProcess = null; ee({ terminated: this.terminateRequested, error: error }); @@ -288,7 +297,13 @@ export abstract class AbstractProcess { } public get pid(): TPromise { - return this.childProcessPromise.then(childProcess => childProcess.pid, err => -1); + if (this.childProcessPromise) { + return this.childProcessPromise.then(childProcess => childProcess.pid, err => -1); + } else { + return new TPromise((resolve) => { + this.pidResolve = resolve; + }); + } } public terminate(): TPromise { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 0f5543d1bae..c6b6eb6f6d4 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -334,6 +334,40 @@ declare module 'vscode' { //#region Tasks + /** + * An event signaling the start of a process execution + * triggered through a task + */ + export interface TaskProcessStartEvent { + + /** + * The task execution for which the process got started. + */ + execution: TaskExecution; + + /** + * The underlying process id. + */ + processId: number; + } + + /** + * An event signaling the end of a process execution + * triggered through a task + */ + export interface TaskProcessEndEvent { + + /** + * The task execution for which the process got started. + */ + execution: TaskExecution; + + /** + * The process's exit code. + */ + exitCode: number; + } + /** * An object representing an executed Task. It can be used * to terminate a task. @@ -346,6 +380,20 @@ declare module 'vscode' { */ task: Task; + /** + * Fires when the underlying process has been started. + * This event might not fire for tasks that don't + * execute an underlying process. + */ + onDidStartProcess: Event; + + /** + * Fires when the underlying process has ended. + * This event might not fire for tasks that don't + * execute an underlying process. + */ + onDidEndProcess: Event; + /** * Terminates the task execution. */ @@ -392,7 +440,7 @@ declare module 'vscode' { export namespace workspace { /** - * Fetches all task available in the systems. Thisweweb includes tasks + * Fetches all tasks available in the systems. This includes tasks * from `tasks.json` files as well as tasks from task providers * contributed through extensions. * diff --git a/src/vs/workbench/api/electron-browser/mainThreadTask.ts b/src/vs/workbench/api/electron-browser/mainThreadTask.ts index 75f08197085..4eca8900871 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTask.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTask.ts @@ -26,7 +26,7 @@ import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostC import { ExtHostContext, MainThreadTaskShape, ExtHostTaskShape, MainContext, IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; import { TaskDefinitionDTO, TaskExecutionDTO, ProcessExecutionOptionsDTO, TaskPresentationOptionsDTO, - ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO + ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO } from 'vs/workbench/api/shared/tasks'; export { TaskDTO, TaskHandleDTO, TaskExecutionDTO, TaskFilterDTO }; @@ -46,6 +46,24 @@ namespace TaskExecutionDTO { } } +namespace TaskProcessStartedDTO { + export function from(value: TaskExecution, processId: number): TaskProcessStartedDTO { + return { + id: value.id, + processId + }; + } +} + +namespace TaskProcessEndedDTO { + export function from(value: TaskExecution, exitCode: number): TaskProcessEndedDTO { + return { + id: value.id, + exitCode + }; + } +} + namespace TaskDefinitionDTO { export function from(value: TaskIdentifier): TaskDefinitionDTO { let result = Objects.assign(Object.create(null), value); @@ -352,9 +370,13 @@ export class MainThreadTask implements MainThreadTaskShape { this._taskService.onDidStateChange((event: TaskEvent) => { let task = event.__task; if (event.kind === TaskEventKind.Start) { - this._proxy.$taskStarted(TaskExecutionDTO.from(Task.getTaskExecution(task))); + this._proxy.$onDidStartTask(TaskExecutionDTO.from(Task.getTaskExecution(task))); + } else if (event.kind === TaskEventKind.ProcessStarted) { + this._proxy.$onDidStartTaskProcess(TaskProcessStartedDTO.from(Task.getTaskExecution(task), event.processId)); + } else if (event.kind === TaskEventKind.ProcessEnded) { + this._proxy.$onDidEndTaskProcess(TaskProcessEndedDTO.from(Task.getTaskExecution(task), event.exitCode)); } else if (event.kind === TaskEventKind.End) { - this._proxy.$taskEnded(TaskExecutionDTO.from(Task.getTaskExecution(task))); + this._proxy.$OnDidEndTask(TaskExecutionDTO.from(Task.getTaskExecution(task))); } }); } @@ -417,7 +439,7 @@ export class MainThreadTask implements MainThreadTaskShape { task: TaskDTO.from(task) }; resolve(result); - }, (error) => { + }, (_error) => { reject(new Error('Task not found')); }); } else { diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index fcdd2ea5485..55bc964bcf0 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -48,7 +48,7 @@ import { CommentRule, CharacterPair, EnterAction } from 'vs/editor/common/modes/ import { ISingleEditOperation } from 'vs/editor/common/model'; import { IPatternInfo, IRawSearchQuery, IRawFileMatch2 } from 'vs/platform/search/common/search'; import { LogLevel } from 'vs/platform/log/common/log'; -import { TaskExecutionDTO, TaskDTO, TaskHandleDTO, TaskFilterDTO } from 'vs/workbench/api/shared/tasks'; +import { TaskExecutionDTO, TaskDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO } from 'vs/workbench/api/shared/tasks'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -773,8 +773,10 @@ export interface ExtHostSCMShape { export interface ExtHostTaskShape { $provideTasks(handle: number): TPromise; - $taskStarted(execution: TaskExecutionDTO): void; - $taskEnded(execution: TaskExecutionDTO): void; + $onDidStartTask(execution: TaskExecutionDTO): void; + $onDidStartTaskProcess(value: TaskProcessStartedDTO): void; + $onDidEndTaskProcess(value: TaskProcessEndedDTO): void; + $OnDidEndTask(execution: TaskExecutionDTO): void; } export interface IBreakpointDto { diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 194121f17cc..578c65590ae 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -21,7 +21,7 @@ import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import * as vscode from 'vscode'; import { TaskDefinitionDTO, TaskExecutionDTO, TaskPresentationOptionsDTO, ProcessExecutionOptionsDTO, ProcessExecutionDTO, - ShellExecutionOptionsDTO, ShellExecutionDTO, TaskDTO, TaskHandleDTO, TaskFilterDTO + ShellExecutionOptionsDTO, ShellExecutionDTO, TaskDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO } from '../shared/tasks'; export { TaskExecutionDTO }; @@ -689,21 +689,47 @@ namespace TaskFilterDTO { } class TaskExecutionImpl implements vscode.TaskExecution { - constructor(readonly _id: string, private readonly _task: vscode.Task, private readonly _tasks: ExtHostTask) { + + private readonly _onDidTaskProcessStarted: Emitter = new Emitter(); + private readonly _onDidTaskProcessEnded: Emitter = new Emitter(); + + constructor(private readonly _tasks: ExtHostTask, readonly _id: string, private readonly _task: vscode.Task) { } - get task(): vscode.Task { + public get task(): vscode.Task { return this._task; } public terminate(): void { this._tasks.terminateTask(this); } + + public get onDidStartProcess(): Event { + return this._onDidTaskProcessStarted.event; + } + + public fireDidStartProcess(value: TaskProcessStartedDTO): void { + this._onDidTaskProcessStarted.fire({ + execution: this, + processId: value.processId + }); + } + + public get onDidEndProcess(): Event { + return this._onDidTaskProcessEnded.event; + } + + public fireDidEndProcess(value: TaskProcessEndedDTO): void { + this._onDidTaskProcessEnded.fire({ + execution: this, + exitCode: value.exitCode + }); + } } namespace TaskExecutionDTO { export function to(value: TaskExecutionDTO, tasks: ExtHostTask): vscode.TaskExecution { - return new TaskExecutionImpl(value.id, TaskDTO.to(value.task, tasks.extHostWorkspace), tasks); + return new TaskExecutionImpl(tasks, value.id, TaskDTO.to(value.task, tasks.extHostWorkspace)); } export function from(value: vscode.TaskExecution): TaskExecutionDTO { return { @@ -781,12 +807,26 @@ export class ExtHostTask implements ExtHostTaskShape { } } - public $taskStarted(execution: TaskExecutionDTO): void { + public $onDidStartTask(execution: TaskExecutionDTO): void { this._onDidExecuteTask.fire({ execution: this.getTaskExecution(execution) }); } + public $onDidStartTaskProcess(value: TaskProcessStartedDTO): void { + const execution = this.getTaskExecution(value.id); + if (execution) { + execution.fireDidStartProcess(value); + } + } + + public $onDidEndTaskProcess(value: TaskProcessEndedDTO): void { + const execution = this.getTaskExecution(value.id); + if (execution) { + execution.fireDidEndProcess(value); + } + } + get taskExecutions(): vscode.TaskExecution[] { let result: vscode.TaskExecution[] = []; this._taskExecutions.forEach(value => result.push(value)); @@ -804,7 +844,7 @@ export class ExtHostTask implements ExtHostTaskShape { return this._proxy.$terminateTask((execution as TaskExecutionImpl)._id); } - public $taskEnded(execution: TaskExecutionDTO): void { + public $OnDidEndTask(execution: TaskExecutionDTO): void { const _execution = this.getTaskExecution(execution); this._taskExecutions.delete(execution.id); this._onDidTerminateTask.fire({ @@ -834,12 +874,16 @@ export class ExtHostTask implements ExtHostTaskShape { return this._handleCounter++; } - private getTaskExecution(execution: TaskExecutionDTO, task?: vscode.Task): TaskExecutionImpl { + private getTaskExecution(execution: TaskExecutionDTO | string, task?: vscode.Task): TaskExecutionImpl { + if (typeof execution === 'string') { + return this._taskExecutions.get(execution); + } + let result: TaskExecutionImpl = this._taskExecutions.get(execution.id); if (result) { return result; } - result = new TaskExecutionImpl(execution.id, task ? task : TaskDTO.to(execution.task, this._extHostWorkspace), this); + result = new TaskExecutionImpl(this, execution.id, task ? task : TaskDTO.to(execution.task, this._extHostWorkspace)); this._taskExecutions.set(execution.id, result); return result; } diff --git a/src/vs/workbench/api/shared/tasks.ts b/src/vs/workbench/api/shared/tasks.ts index 7e070957c48..29ac9b413ec 100644 --- a/src/vs/workbench/api/shared/tasks.ts +++ b/src/vs/workbench/api/shared/tasks.ts @@ -88,6 +88,17 @@ export interface TaskExecutionDTO { task: TaskDTO; } +export interface TaskProcessStartedDTO { + id: string; + processId: number; +} + +export interface TaskProcessEndedDTO { + id: string; + exitCode: number; +} + + export interface TaskFilterDTO { version?: string; type?: string; diff --git a/src/vs/workbench/parts/tasks/common/tasks.ts b/src/vs/workbench/parts/tasks/common/tasks.ts index bfdbdad02e7..5ae480532ab 100644 --- a/src/vs/workbench/parts/tasks/common/tasks.ts +++ b/src/vs/workbench/parts/tasks/common/tasks.ts @@ -695,10 +695,12 @@ export class TaskSorter { export enum TaskEventKind { Start = 'start', + ProcessStarted = 'processStarted', Active = 'active', Inactive = 'inactive', Changed = 'changed', Terminated = 'terminated', + ProcessEnded = 'processEnded', End = 'end' } @@ -714,22 +716,34 @@ export interface TaskEvent { taskName?: string; runType?: TaskRunType; group?: string; + processId?: number; + exitCode?: number; __task?: Task; } export namespace TaskEvent { - export function create(kind: TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.Terminated | TaskEventKind.Start | TaskEventKind.End, task: Task); - export function create(kind: TaskEventKind.Changed); - export function create(kind: TaskEventKind, task?: Task): TaskEvent { + export function create(kind: TaskEventKind.ProcessStarted, task: Task, processId: number): TaskEvent; + export function create(kind: TaskEventKind.ProcessEnded, task: Task, exitCode: number): TaskEvent; + export function create(kind: TaskEventKind.Start | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.Terminated | TaskEventKind.End, task: Task): TaskEvent; + export function create(kind: TaskEventKind.Changed): TaskEvent; + export function create(kind: TaskEventKind, task?: Task, processIdOrExitCode?: number): TaskEvent { if (task) { - return Object.freeze({ + let result = { kind: kind, taskId: task._id, taskName: task.name, runType: task.isBackground ? TaskRunType.Background : TaskRunType.SingleRun, group: task.group, + processId: undefined, + exitCode: undefined, __task: task, - }); + }; + if (kind === TaskEventKind.ProcessStarted) { + result.processId = processIdOrExitCode; + } else if (kind === TaskEventKind.ProcessEnded) { + result.exitCode = processIdOrExitCode; + } + return Object.freeze(result); } else { return Object.freeze({ kind: TaskEventKind.Changed }); } diff --git a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts index 1753670ee47..71dcb179c46 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts @@ -305,6 +305,13 @@ export class TerminalTaskSystem implements ITaskSystem { if (error || !terminal) { return; } + let processStartedSignaled: boolean = false; + terminal.processReady.done(() => { + processStartedSignaled = true; + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal.processId)); + }, (_error) => { + // The process never got ready. Need to think how to handle this. + }); this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Start, task)); const registeredLinkMatchers = this.registerLinkMatchers(terminal, problemMatchers); const onData = terminal.onLineData((line) => { @@ -339,6 +346,9 @@ export class TerminalTaskSystem implements ITaskSystem { watchingProblemMatcher.done(); watchingProblemMatcher.dispose(); registeredLinkMatchers.forEach(handle => terminal.deregisterLinkMatcher(handle)); + if (processStartedSignaled) { + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode)); + } toUnbind = dispose(toUnbind); toUnbind = null; for (let i = 0; i < eventCounter; i++) { @@ -356,6 +366,13 @@ export class TerminalTaskSystem implements ITaskSystem { if (error || !terminal) { return; } + let processStartedSignaled: boolean = false; + terminal.processReady.done(() => { + processStartedSignaled = true; + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal.processId)); + }, (_error) => { + // The process never got ready. Need to think how to handle this. + }); this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Start, task)); this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Active, task)); let problemMatchers = this.resolveMatchers(task, task.problemMatchers); @@ -386,6 +403,9 @@ export class TerminalTaskSystem implements ITaskSystem { startStopProblemMatcher.done(); startStopProblemMatcher.dispose(); registeredLinkMatchers.forEach(handle => terminal.deregisterLinkMatcher(handle)); + if (processStartedSignaled) { + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode)); + } this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Inactive, task)); this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task)); resolve({ exitCode }); diff --git a/src/vs/workbench/parts/tasks/node/processTaskSystem.ts b/src/vs/workbench/parts/tasks/node/processTaskSystem.ts index 8e2310d9f23..881e681044f 100644 --- a/src/vs/workbench/parts/tasks/node/processTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/node/processTaskSystem.ts @@ -248,10 +248,21 @@ export class ProcessTaskSystem implements ITaskSystem { let delayer: Async.Delayer = null; this.activeTask = task; const inactiveEvent = TaskEvent.create(TaskEventKind.Inactive, task); - this.activeTaskPromise = this.childProcess.start().then((success): ITaskSummary => { + let processStartedSignaled: boolean = false; + const startPromise = this.childProcess.start(); + this.childProcess.pid.then(pid => { + if (pid !== -1) { + processStartedSignaled = true; + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, pid)); + } + }); + this.activeTaskPromise = startPromise.then((success): ITaskSummary => { this.childProcessEnded(); watchingProblemMatcher.done(); watchingProblemMatcher.dispose(); + if (processStartedSignaled) { + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, success.cmdCode)); + } toUnbind = dispose(toUnbind); toUnbind = null; for (let i = 0; i < eventCounter; i++) { @@ -295,16 +306,29 @@ export class ProcessTaskSystem implements ITaskSystem { : { kind: TaskExecuteKind.Started, started: {}, promise: this.activeTaskPromise }; return result; } else { + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Start, task)); this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Active, task)); let startStopProblemMatcher = new StartStopProblemCollector(this.resolveMatchers(task, task.problemMatchers), this.markerService, this.modelService); this.activeTask = task; const inactiveEvent = TaskEvent.create(TaskEventKind.Inactive, task); - this.activeTaskPromise = this.childProcess.start().then((success): ITaskSummary => { + let processStartedSignaled: boolean = false; + const startPromise = this.childProcess.start(); + this.childProcess.pid.then(pid => { + if (pid !== -1) { + processStartedSignaled = true; + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, pid)); + } + }); + this.activeTaskPromise = startPromise.then((success): ITaskSummary => { this.childProcessEnded(); startStopProblemMatcher.done(); startStopProblemMatcher.dispose(); this.checkTerminated(task, success); + if (processStartedSignaled) { + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, success.cmdCode)); + } this._onDidStateChange.fire(inactiveEvent); + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task)); if (success.cmdCode && success.cmdCode === 1 && startStopProblemMatcher.numberOfMatches === 0 && reveal !== RevealKind.Never) { this.showOutput(); } @@ -314,6 +338,7 @@ export class ProcessTaskSystem implements ITaskSystem { this.childProcessEnded(); startStopProblemMatcher.dispose(); this._onDidStateChange.fire(inactiveEvent); + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task)); return this.handleError(task, error); }, (progress) => { let line = Strings.removeAnsiEscapeCodes(progress.line);