diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 9bd5eb6dbbe..c38664ea320 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1143,7 +1143,7 @@ declare module 'vscode' { readonly onDidAcceptInput: Event; /** - * An event which fires when the [maximum dimensions](#TerminalRenderer.maimumDimensions) of + * An event which fires when the [maximum dimensions](#TerminalRenderer.maximumDimensions) of * the terminal renderer change. */ readonly onDidChangeMaximumDimensions: Event; @@ -1238,6 +1238,48 @@ declare module 'vscode' { } //#endregion + /** + * Class used to execute an extension callback as a task. + */ + export class CustomExecution { + /** + * @param callback The callback that will be called when the extension callback task is executed. + */ + constructor(callback: (terminalRenderer: TerminalRenderer, cancellationToken: CancellationToken, thisArg?: any) => Thenable); + + /** + * The callback used to execute the task. + * @param terminalRenderer Used by the task to render output and receive input. + * @param cancellationToken Cancellation used to signal a cancel request to the executing task. + * @returns The callback should return '0' for success and a non-zero value for failure. + */ + callback: (terminalRenderer: TerminalRenderer, cancellationToken: CancellationToken, thisArg?: any) => Thenable; + } + + /** + * A task to execute + */ + export class Task2 extends Task { + /** + * Creates a new task. + * + * @param definition The task definition as defined in the taskDefinitions extension point. + * @param scope Specifies the task's scope. It is either a global or a workspace task or a task for a specific workspace folder. + * @param name The task's name. Is presented in the user interface. + * @param source The task's source (e.g. 'gulp', 'npm', ...). Is presented in the user interface. + * @param execution The process or shell execution. + * @param problemMatchers the names of problem matchers to use, like '$tsc' + * or '$eslint'. Problem matchers can be contributed by an extension using + * the `problemMatchers` extension point. + */ + constructor(taskDefinition: TaskDefinition, scope: WorkspaceFolder | TaskScope.Global | TaskScope.Workspace, name: string, source: string, execution?: ProcessExecution | ShellExecution | CustomExecution, problemMatchers?: string | string[]); + + /** + * The task's execution engine + */ + execution2?: ProcessExecution | ShellExecution | CustomExecution; + } + //#region Tasks export interface TaskPresentationOptions { /** diff --git a/src/vs/workbench/api/electron-browser/mainThreadTask.ts b/src/vs/workbench/api/electron-browser/mainThreadTask.ts index 47bc0be5ea3..8f9279e0877 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTask.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTask.ts @@ -30,7 +30,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, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO, + ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, CustomExecutionDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO, RunOptionsDTO } from 'vs/workbench/api/shared/tasks'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; @@ -138,7 +138,7 @@ namespace ProcessExecutionOptionsDTO { } namespace ProcessExecutionDTO { - export function is(value: ShellExecutionDTO | ProcessExecutionDTO): value is ProcessExecutionDTO { + export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is ProcessExecutionDTO { const candidate = value as ProcessExecutionDTO; return candidate && !!candidate.process; } @@ -206,7 +206,7 @@ namespace ShellExecutionOptionsDTO { } namespace ShellExecutionDTO { - export function is(value: ShellExecutionDTO | ProcessExecutionDTO): value is ShellExecutionDTO { + export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is ShellExecutionDTO { const candidate = value as ShellExecutionDTO; return candidate && (!!candidate.commandLine || !!candidate.command); } @@ -237,6 +237,26 @@ namespace ShellExecutionDTO { } } +namespace CustomExecutionDTO { + export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is CustomExecutionDTO { + const candidate = value as CustomExecutionDTO; + return candidate && candidate.customExecution === 'customExecution'; + } + + export function from(value: CommandConfiguration): CustomExecutionDTO { + return { + customExecution: 'customExecution' + }; + } + + export function to(value: CustomExecutionDTO): CommandConfiguration { + return { + runtime: RuntimeType.CustomExecution, + presentation: undefined + }; + } +} + namespace TaskSourceDTO { export function from(value: TaskSource): TaskSourceDTO { const result: TaskSourceDTO = { @@ -331,12 +351,15 @@ namespace TaskDTO { if (typeof task.name !== 'string') { return undefined; } + let command: CommandConfiguration | undefined; if (task.execution) { if (ShellExecutionDTO.is(task.execution)) { command = ShellExecutionDTO.to(task.execution); } else if (ProcessExecutionDTO.is(task.execution)) { command = ProcessExecutionDTO.to(task.execution); + } else if (CustomExecutionDTO.is(task.execution)) { + command = CustomExecutionDTO.to(task.execution); } } @@ -397,7 +420,7 @@ export class MainThreadTask implements MainThreadTaskShape { this._taskService.onDidStateChange((event: TaskEvent) => { const task = event.__task!; if (event.kind === TaskEventKind.Start) { - this._proxy.$onDidStartTask(TaskExecutionDTO.from(task.getTaskExecution())); + this._proxy.$onDidStartTask(TaskExecutionDTO.from(task.getTaskExecution()), event.terminalId); } else if (event.kind === TaskEventKind.ProcessStarted) { this._proxy.$onDidStartTaskProcess(TaskProcessStartedDTO.from(task.getTaskExecution(), event.processId!)); } else if (event.kind === TaskEventKind.ProcessEnded) { @@ -415,6 +438,13 @@ export class MainThreadTask implements MainThreadTaskShape { this._providers.clear(); } + $createTaskId(taskDTO: TaskDTO): Promise { + return new Promise((resolve) => { + let task = TaskDTO.to(taskDTO, this._workspaceContextServer, true); + resolve(task._id); + }); + } + public $registerTaskProvider(handle: number): Promise { const provider: ITaskProvider = { provideTasks: (validTypes: IStringDictionary) => { @@ -489,6 +519,24 @@ export class MainThreadTask implements MainThreadTaskShape { }); } + public $customExecutionComplete(id: string, result?: number): Promise { + return new Promise((resolve, reject) => { + this._taskService.getActiveTasks().then((tasks) => { + for (let task of tasks) { + if (id === task._id) { + this._taskService.extensionCallbackTaskComplete(task, result).then((value) => { + resolve(undefined); + }, (error) => { + reject(error); + }); + return; + } + } + reject(new Error('Task to mark as complete not found')); + }); + }); + } + public $terminateTask(id: string): Promise { return new Promise((resolve, reject) => { this._taskService.getActiveTasks().then((tasks) => { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 3a9655de101..27432553426 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -119,7 +119,7 @@ export function createApiFactory( const extHostSCM = rpcProtocol.set(ExtHostContext.ExtHostSCM, new ExtHostSCM(rpcProtocol, extHostCommands, extHostLogService)); const extHostComment = rpcProtocol.set(ExtHostContext.ExtHostComments, new ExtHostComments(rpcProtocol, extHostCommands, extHostDocuments)); const extHostSearch = rpcProtocol.set(ExtHostContext.ExtHostSearch, new ExtHostSearch(rpcProtocol, schemeTransformer, extHostLogService)); - const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, new ExtHostTask(rpcProtocol, extHostWorkspace, extHostDocumentsAndEditors, extHostConfiguration)); + const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, new ExtHostTask(rpcProtocol, extHostWorkspace, extHostDocumentsAndEditors, extHostConfiguration, extHostTerminalService)); const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); @@ -777,6 +777,7 @@ export function createApiFactory( DocumentSymbol: extHostTypes.DocumentSymbol, EndOfLine: extHostTypes.EndOfLine, EventEmitter: Emitter, + CustomExecution: extHostTypes.CustomExecution, FileChangeType: extHostTypes.FileChangeType, FileSystemError: extHostTypes.FileSystemError, FileType: files.FileType, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 8be2c244442..2c21e157732 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -558,12 +558,14 @@ export interface MainThreadSearchShape extends IDisposable { } export interface MainThreadTaskShape extends IDisposable { + $createTaskId(task: TaskDTO): Promise; $registerTaskProvider(handle: number): Promise; $unregisterTaskProvider(handle: number): Promise; $fetchTasks(filter?: TaskFilterDTO): Promise; $executeTask(task: TaskHandleDTO | TaskDTO): Promise; $terminateTask(id: string): Promise; $registerTaskSystem(scheme: string, info: TaskSystemInfoDTO): void; + $customExecutionComplete(id: string, result?: number): Promise; } export interface MainThreadExtensionServiceShape extends IDisposable { @@ -995,7 +997,7 @@ export interface ExtHostSCMShape { export interface ExtHostTaskShape { $provideTasks(handle: number, validTypes: { [key: string]: boolean; }): Thenable; - $onDidStartTask(execution: TaskExecutionDTO): void; + $onDidStartTask(execution: TaskExecutionDTO, terminalId: number): void; $onDidStartTaskProcess(value: TaskProcessStartedDTO): void; $onDidEndTaskProcess(value: TaskProcessEndedDTO): void; $OnDidEndTask(execution: TaskExecutionDTO): void; diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 8d2fc278b45..f0184406cab 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -19,14 +19,19 @@ import * as types from 'vs/workbench/api/node/extHostTypes'; import { ExtHostWorkspace, IExtHostWorkspaceProvider } from 'vs/workbench/api/node/extHostWorkspace'; import * as vscode from 'vscode'; import { - TaskDefinitionDTO, TaskExecutionDTO, TaskPresentationOptionsDTO, ProcessExecutionOptionsDTO, ProcessExecutionDTO, - ShellExecutionOptionsDTO, ShellExecutionDTO, TaskDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO, TaskSetDTO + TaskDefinitionDTO, TaskExecutionDTO, TaskPresentationOptionsDTO, + ProcessExecutionOptionsDTO, ProcessExecutionDTO, + ShellExecutionOptionsDTO, ShellExecutionDTO, + CustomExecutionDTO, + TaskDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO, TaskSetDTO } from '../shared/tasks'; import { ExtHostVariableResolverService } from 'vs/workbench/api/node/extHostDebugService'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; +import { ExtHostTerminalService, ExtHostTerminal } from 'vs/workbench/api/node/extHostTerminalService'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; namespace TaskDefinitionDTO { export function from(value: vscode.TaskDefinition): TaskDefinitionDTO { @@ -74,7 +79,7 @@ namespace ProcessExecutionOptionsDTO { } namespace ProcessExecutionDTO { - export function is(value: ShellExecutionDTO | ProcessExecutionDTO): value is ProcessExecutionDTO { + export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is ProcessExecutionDTO { const candidate = value as ProcessExecutionDTO; return candidate && !!candidate.process; } @@ -115,7 +120,7 @@ namespace ShellExecutionOptionsDTO { } namespace ShellExecutionDTO { - export function is(value: ShellExecutionDTO | ProcessExecutionDTO): value is ShellExecutionDTO { + export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is ShellExecutionDTO { const candidate = value as ShellExecutionDTO; return candidate && (!!candidate.commandLine || !!candidate.command); } @@ -148,6 +153,19 @@ namespace ShellExecutionDTO { } } +namespace CustomExecutionDTO { + export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is CustomExecutionDTO { + let candidate = value as CustomExecutionDTO; + return candidate && candidate.customExecution === 'customExecution'; + } + + export function from(value: vscode.CustomExecution): CustomExecutionDTO { + return { + customExecution: 'customExecution' + }; + } +} + namespace TaskHandleDTO { export function from(value: types.Task): TaskHandleDTO { let folder: UriComponents | undefined; @@ -181,11 +199,13 @@ namespace TaskDTO { if (value === undefined || value === null) { return undefined; } - let execution: ShellExecutionDTO | ProcessExecutionDTO | undefined; + let execution: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO | undefined; if (value.execution instanceof types.ProcessExecution) { execution = ProcessExecutionDTO.from(value.execution); } else if (value.execution instanceof types.ShellExecution) { execution = ShellExecutionDTO.from(value.execution); + } else if ((value).execution2 && (value).execution2 instanceof types.CustomExecution) { + execution = CustomExecutionDTO.from((value).execution2); } const definition: TaskDefinitionDTO = TaskDefinitionDTO.from(value.definition); let scope: number | UriComponents; @@ -315,15 +335,121 @@ interface HandlerData { extension: IExtensionDescription; } +class CustomExecutionData implements IDisposable { + private static waitForDimensionsTimeoutInMs: number = 5000; + private _cancellationSource?: CancellationTokenSource; + private readonly _onTaskExecutionComplete: Emitter = new Emitter(); + private readonly _disposables: IDisposable[] = []; + private terminal?: vscode.Terminal; + private terminalId?: number; + public result: number | undefined; + + constructor( + private readonly customExecution: vscode.CustomExecution, + private readonly terminalService: ExtHostTerminalService) { + } + + public dispose(): void { + dispose(this._disposables); + } + + public get onTaskExecutionComplete(): Event { + return this._onTaskExecutionComplete.event; + } + + private onDidCloseTerminal(terminal: vscode.Terminal): void { + if (this.terminal === terminal) { + this._cancellationSource.cancel(); + } + } + + private onDidOpenTerminal(terminal: vscode.Terminal): void { + if (!(terminal instanceof ExtHostTerminal)) { + throw new Error('How could this not be a extension host terminal?'); + } + + if (this.terminalId && terminal._id === this.terminalId) { + this.startCallback(this.terminalId); + } + } + + public async startCallback(terminalId: number): Promise { + this.terminalId = terminalId; + + // If we have already started the extension task callback, then + // do not start it again. + // It is completely valid for multiple terminals to be opened + // before the one for our task. + if (this._cancellationSource) { + return undefined; + } + + const callbackTerminals: vscode.Terminal[] = this.terminalService.terminals.filter((terminal) => terminal._id === terminalId); + + if (!callbackTerminals || callbackTerminals.length === 0) { + this._disposables.push(this.terminalService.onDidOpenTerminal(this.onDidOpenTerminal.bind(this))); + return; + } + + if (callbackTerminals.length !== 1) { + throw new Error(`Expected to only have one terminal at this point`); + } + + this.terminal = callbackTerminals[0]; + const terminalRenderer: vscode.TerminalRenderer = await this.terminalService.resolveTerminalRenderer(terminalId); + + // If we don't have the maximum dimensions yet, then we need to wait for them (but not indefinitely). + // Custom executions will expect the dimensions to be set properly before they are launched. + // BUT, due to the API contract VSCode has for terminals and dimensions, they are still responsible for + // handling cases where they are not set. + if (!terminalRenderer.maximumDimensions) { + const dimensionTimeout: Promise = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, CustomExecutionData.waitForDimensionsTimeoutInMs); + }); + + let dimensionsRegistration: IDisposable | undefined; + const dimensionsPromise: Promise = new Promise((resolve) => { + dimensionsRegistration = terminalRenderer.onDidChangeMaximumDimensions((newDimensions) => { + resolve(); + }); + }); + + await Promise.race([dimensionTimeout, dimensionsPromise]); + if (dimensionsRegistration) { + dimensionsRegistration.dispose(); + } + } + + this._cancellationSource = new CancellationTokenSource(); + this._disposables.push(this._cancellationSource); + + this._disposables.push(this.terminalService.onDidCloseTerminal(this.onDidCloseTerminal.bind(this))); + + // Regardless of how the task completes, we are done with this custom execution task. + this.customExecution.callback(terminalRenderer, this._cancellationSource.token).then( + (success) => { + this.result = success; + this._onTaskExecutionComplete.fire(this); + }, (rejected) => { + this._onTaskExecutionComplete.fire(this); + }); + } +} + export class ExtHostTask implements ExtHostTaskShape { private _proxy: MainThreadTaskShape; private _workspaceProvider: IExtHostWorkspaceProvider; private _editorService: ExtHostDocumentsAndEditors; private _configurationService: ExtHostConfiguration; + private _terminalService: ExtHostTerminalService; private _handleCounter: number; private _handlers: Map; private _taskExecutions: Map; + private _providedCustomExecutions: Map; + private _activeCustomExecutions: Map; private readonly _onDidExecuteTask: Emitter = new Emitter(); private readonly _onDidTerminateTask: Emitter = new Emitter(); @@ -331,14 +457,22 @@ export class ExtHostTask implements ExtHostTaskShape { private readonly _onDidTaskProcessStarted: Emitter = new Emitter(); private readonly _onDidTaskProcessEnded: Emitter = new Emitter(); - constructor(mainContext: IMainContext, workspaceService: ExtHostWorkspace, editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfiguration) { + constructor( + mainContext: IMainContext, + workspaceService: ExtHostWorkspace, + editorService: ExtHostDocumentsAndEditors, + configurationService: ExtHostConfiguration, + extHostTerminalService: ExtHostTerminalService) { this._proxy = mainContext.getProxy(MainContext.MainThreadTask); this._workspaceProvider = workspaceService; this._editorService = editorService; this._configurationService = configurationService; + this._terminalService = extHostTerminalService; this._handleCounter = 0; this._handlers = new Map(); this._taskExecutions = new Map(); + this._providedCustomExecutions = new Map(); + this._activeCustomExecutions = new Map(); } public registerTaskProvider(extension: IExtensionDescription, provider: vscode.TaskProvider): vscode.Disposable { @@ -402,7 +536,26 @@ export class ExtHostTask implements ExtHostTaskShape { return this._onDidExecuteTask.event; } - public async $onDidStartTask(execution: TaskExecutionDTO): Promise { + public async $onDidStartTask(execution: TaskExecutionDTO, terminalId: number): Promise { + // Once a terminal is spun up for the custom execution task this event will be fired. + // At that point, we need to actually start the callback, but + // only if it hasn't already begun. + const extensionCallback: CustomExecutionData | undefined = this._providedCustomExecutions.get(execution.id); + if (extensionCallback) { + if (this._activeCustomExecutions.get(execution.id) !== undefined) { + throw new Error('We should not be trying to start the same custom task executions twice.'); + } + + this._activeCustomExecutions.set(execution.id, extensionCallback); + + const taskExecutionComplete: IDisposable = extensionCallback.onTaskExecutionComplete(() => { + this.customExecutionComplete(execution); + taskExecutionComplete.dispose(); + }); + + extensionCallback.startCallback(terminalId); + } + this._onDidExecuteTask.fire({ execution: await this.getTaskExecution(execution) }); @@ -415,6 +568,7 @@ export class ExtHostTask implements ExtHostTaskShape { public async $OnDidEndTask(execution: TaskExecutionDTO): Promise { const _execution = await this.getTaskExecution(execution); this._taskExecutions.delete(execution.id); + this.customExecutionComplete(execution); this._onDidTerminateTask.fire({ execution: _execution }); @@ -453,21 +607,57 @@ export class ExtHostTask implements ExtHostTaskShape { if (!handler) { return Promise.reject(new Error('no handler found')); } - return asPromise(() => handler.provider.provideTasks(CancellationToken.None)).then(value => { - const sanitized: vscode.Task[] = []; + + // For custom execution tasks, we need to store the execution objects locally + // since we obviously cannot send callback functions through the proxy. + // So, clear out any existing ones. + this._providedCustomExecutions.clear(); + + // Set up a list of task ID promises that we can wait on + // before returning the provided tasks. The ensures that + // our task IDs are calculated for any custom execution tasks. + // Knowing this ID ahead of time is needed because when a task + // start event is fired this is when the custom execution is called. + // The task start event is also the first time we see the ID from the main + // thread, which is too late for us because we need to save an map + // from an ID to the custom execution function. (Kind of a cart before the horse problem). + const taskIdPromises: Promise[] = []; + const fetchPromise = asPromise(() => handler.provider.provideTasks(CancellationToken.None)).then(value => { + const taskDTOs: TaskDTO[] = []; for (let task of value) { - if (task.definition && validTypes[task.definition.type] === true) { - sanitized.push(task); - } else { - sanitized.push(task); + if (!task.definition || !validTypes[task.definition.type]) { console.warn(`The task [${task.source}, ${task.name}] uses an undefined task type. The task will be ignored in the future.`); } + + const taskDTO: TaskDTO = TaskDTO.from(task, handler.extension); + taskDTOs.push(taskDTO); + + if (CustomExecutionDTO.is(taskDTO.execution)) { + taskIdPromises.push(new Promise((resolve) => { + // The ID is calculated on the main thread task side, so, let's call into it here. + // We need the task id's pre-computed for custom task executions because when OnDidStartTask + // is invoked, we have to be able to map it back to our data. + this._proxy.$createTaskId(taskDTO).then((taskId) => { + this._providedCustomExecutions.set(taskId, new CustomExecutionData((task).execution2, this._terminalService)); + resolve(); + }); + })); + } } + return { - tasks: TaskDTO.fromMany(sanitized, handler.extension), + tasks: taskDTOs, extension: handler.extension }; }); + + return new Promise((resolve) => { + fetchPromise.then((result) => { + Promise.all(taskIdPromises).then(() => { + resolve(result); + }); + }); + }); } public async $resolveVariables(uriComponents: UriComponents, toResolve: { process?: { name: string; cwd?: string; path?: string }, variables: string[] }): Promise<{ process?: string, variables: { [key: string]: string; } }> { @@ -525,4 +715,13 @@ export class ExtHostTask implements ExtHostTaskShape { this._taskExecutions.set(execution.id, result); return result; } + + private customExecutionComplete(execution: TaskExecutionDTO): void { + const extensionCallback: CustomExecutionData | undefined = this._activeCustomExecutions.get(execution.id); + if (extensionCallback) { + this._activeCustomExecutions.delete(execution.id); + this._proxy.$customExecutionComplete(execution.id, extensionCallback.result); + extensionCallback.dispose(); + } + } } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 45d953bb808..115b263be66 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -233,13 +233,17 @@ export class ExtHostTerminalRenderer extends BaseExtHostTerminal implements vsco constructor( proxy: MainThreadTerminalServiceShape, private _name: string, - private _terminal: ExtHostTerminal + private _terminal: ExtHostTerminal, + id?: number ) { - super(proxy); - this._proxy.$createTerminalRenderer(this._name).then(id => { - this._runQueuedRequests(id); - (this._terminal)._runQueuedRequests(id); - }); + super(proxy, id); + + if (!id) { + this._proxy.$createTerminalRenderer(this._name).then(id => { + this._runQueuedRequests(id); + (this._terminal)._runQueuedRequests(id); + }); + } } public write(data: string): void { @@ -313,6 +317,21 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { return renderer; } + public async resolveTerminalRenderer(id: number): Promise { + // Check to see if the extension host already knows about this terminal. + for (const terminalRenderer of this._terminalRenderers) { + if (terminalRenderer._id === id) { + return terminalRenderer; + } + } + + const terminal = this._getTerminalById(id); + const renderer = new ExtHostTerminalRenderer(this._proxy, terminal.name, terminal, terminal._id); + this._terminalRenderers.push(renderer); + + return renderer; + } + public $acceptActiveTerminalChanged(id: number | null): void { const original = this._activeTerminal; if (id === null) { @@ -372,11 +391,10 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { public $acceptTerminalClosed(id: number): void { const index = this._getTerminalObjectIndexById(this.terminals, id); - if (index === null) { - return; + if (index !== null) { + const terminal = this._terminals.splice(index, 1)[0]; + this._onDidCloseTerminal.fire(terminal); } - const terminal = this._terminals.splice(index, 1)[0]; - this._onDidCloseTerminal.fire(terminal); } public $acceptTerminalOpened(id: number, name: string): void { @@ -386,6 +404,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { this._onDidOpenTerminal.fire(this.terminals[index]); return; } + const renderer = this._getTerminalRendererById(id); const terminal = new ExtHostTerminal(this._proxy, name, id, renderer ? RENDERER_NO_PROCESS_ID : undefined); this._terminals.push(terminal); diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 535dadfb0c8..26d40372a34 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -1708,9 +1708,33 @@ export enum TaskScope { Workspace = 2 } -@es5ClassCompat -export class Task implements vscode.Task { +export class CustomExecution implements vscode.CustomExecution { + private _callback: (args: vscode.TerminalRenderer, cancellationToken: vscode.CancellationToken) => Thenable; + constructor(callback: (args: vscode.TerminalRenderer, cancellationToken: vscode.CancellationToken) => Thenable) { + this._callback = callback; + } + + public computeId(): string { + const hash = crypto.createHash('md5'); + hash.update('customExecution'); + hash.update(generateUuid()); + return hash.digest('hex'); + } + + public set callback(value: (args: vscode.TerminalRenderer, cancellationToken: vscode.CancellationToken) => Thenable) { + this._callback = value; + } + + public get callback(): (args: vscode.TerminalRenderer, cancellationToken: vscode.CancellationToken) => Thenable { + return this._callback; + } +} + +@es5ClassCompat +export class Task implements vscode.Task2 { + + private static ExtensionCallbackType: string = 'customExecution'; private static ProcessType: string = 'process'; private static ShellType: string = 'shell'; private static EmptyType: string = '$empty'; @@ -1720,7 +1744,7 @@ export class Task implements vscode.Task { private _definition: vscode.TaskDefinition; private _scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder | undefined; private _name: string; - private _execution: ProcessExecution | ShellExecution | undefined; + private _execution: ProcessExecution | ShellExecution | CustomExecution | undefined; private _problemMatchers: string[]; private _hasDefinedMatchers: boolean; private _isBackground: boolean; @@ -1729,8 +1753,8 @@ export class Task implements vscode.Task { private _presentationOptions: vscode.TaskPresentationOptions; private _runOptions: vscode.RunOptions; - constructor(definition: vscode.TaskDefinition, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]); - constructor(definition: vscode.TaskDefinition, scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]); + constructor(definition: vscode.TaskDefinition, name: string, source: string, execution?: ProcessExecution | ShellExecution | CustomExecution, problemMatchers?: string | string[]); + constructor(definition: vscode.TaskDefinition, scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder, name: string, source: string, execution?: ProcessExecution | ShellExecution | CustomExecution, problemMatchers?: string | string[]); constructor(definition: vscode.TaskDefinition, arg2: string | (vscode.TaskScope.Global | vscode.TaskScope.Workspace) | vscode.WorkspaceFolder, arg3: any, arg4?: any, arg5?: any, arg6?: any) { this.definition = definition; let problemMatchers: string | string[]; @@ -1795,6 +1819,11 @@ export class Task implements vscode.Task { type: Task.ShellType, id: this._execution.computeId() }; + } else if (this._execution instanceof CustomExecution) { + this._definition = { + type: Task.ExtensionCallbackType, + id: this._execution.computeId() + }; } else { this._definition = { type: Task.EmptyType, @@ -1837,17 +1866,25 @@ export class Task implements vscode.Task { } get execution(): ProcessExecution | ShellExecution | undefined { - return this._execution; + return (this._execution instanceof CustomExecution) ? undefined : this._execution; } set execution(value: ProcessExecution | ShellExecution | undefined) { + this.execution2 = value; + } + + get execution2(): ProcessExecution | ShellExecution | CustomExecution | undefined { + return this._execution; + } + + set execution2(value: ProcessExecution | ShellExecution | CustomExecution | undefined) { if (value === null) { value = undefined; } this.clear(); this._execution = value; const type = this._definition.type; - if (Task.EmptyType === type || Task.ProcessType === type || Task.ShellType === type) { + if (Task.EmptyType === type || Task.ProcessType === type || Task.ShellType === type || Task.ExtensionCallbackType === type) { this.computeDefinitionBasedOnExecution(); } } diff --git a/src/vs/workbench/api/shared/tasks.ts b/src/vs/workbench/api/shared/tasks.ts index e260f14f1f5..0abf582d133 100644 --- a/src/vs/workbench/api/shared/tasks.ts +++ b/src/vs/workbench/api/shared/tasks.ts @@ -66,6 +66,10 @@ export interface ShellExecutionDTO { options?: ShellExecutionOptionsDTO; } +export interface CustomExecutionDTO { + customExecution: 'customExecution'; +} + export interface TaskSourceDTO { label: string; extensionId?: string; @@ -80,7 +84,7 @@ export interface TaskHandleDTO { export interface TaskDTO { _id: string; name?: string; - execution?: ProcessExecutionDTO | ShellExecutionDTO; + execution: ProcessExecutionDTO | ShellExecutionDTO | CustomExecutionDTO; definition: TaskDefinitionDTO; isBackground?: boolean; source: TaskSourceDTO; diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index c592c6d64f6..83180e1f43b 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -82,4 +82,6 @@ export interface ITaskService { registerTaskProvider(taskProvider: ITaskProvider): IDisposable; registerTaskSystem(scheme: string, taskSystemInfo: TaskSystemInfo): void; + + extensionCallbackTaskComplete(task: Task, result: number | undefined): Promise; } diff --git a/src/vs/workbench/contrib/tasks/common/taskSystem.ts b/src/vs/workbench/contrib/tasks/common/taskSystem.ts index 9a2fb0787e4..3c462491828 100644 --- a/src/vs/workbench/contrib/tasks/common/taskSystem.ts +++ b/src/vs/workbench/contrib/tasks/common/taskSystem.ts @@ -136,4 +136,5 @@ export interface ITaskSystem { terminate(task: Task): Promise; terminateAll(): Promise; revealTask(task: Task): boolean; + customExecutionComplete(task: Task, result: number): Promise; } \ No newline at end of file diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index e8489a6f345..c3b29e5449b 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -229,7 +229,8 @@ export namespace PresentationOptions { export enum RuntimeType { Shell = 1, - Process = 2 + Process = 2, + CustomExecution = 3 } export namespace RuntimeType { @@ -239,6 +240,8 @@ export namespace RuntimeType { return RuntimeType.Shell; case 'process': return RuntimeType.Process; + case 'customExecution': + return RuntimeType.CustomExecution; default: return RuntimeType.Process; } @@ -595,7 +598,23 @@ export class CustomTask extends CommonTask { } else { let type: string; if (this.command !== undefined) { - type = this.command.runtime === RuntimeType.Shell ? 'shell' : 'process'; + switch (this.command.runtime) { + case RuntimeType.Shell: + type = 'shell'; + break; + + case RuntimeType.Process: + type = 'process'; + break; + + case RuntimeType.CustomExecution: + type = 'customExecution'; + break; + + default: + throw new Error('Unexpected task runtime'); + break; + } } else { type = '$composite'; } @@ -861,6 +880,7 @@ export interface TaskEvent { group?: string; processId?: number; exitCode?: number; + terminalId?: number; __task?: Task; } @@ -871,12 +891,13 @@ export const enum TaskRunSource { } export namespace TaskEvent { - export function create(kind: TaskEventKind.ProcessStarted | TaskEventKind.ProcessEnded, task: Task, processIdOrExitCode: number): TaskEvent; + export function create(kind: TaskEventKind.ProcessStarted | TaskEventKind.ProcessEnded, task: Task, processIdOrExitCode?: number): TaskEvent; + export function create(kind: TaskEventKind.Start, task: Task, terminalId?: number): TaskEvent; export function create(kind: TaskEventKind.DependsOnStarted | 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 { + export function create(kind: TaskEventKind, task?: Task, processIdOrExitCodeOrTerminalId?: number): TaskEvent { if (task) { - let result = { + let result: TaskEvent = { kind: kind, taskId: task._id, taskName: task.configurationProperties.name, @@ -884,12 +905,15 @@ export namespace TaskEvent { group: task.configurationProperties.group, processId: undefined as number | undefined, exitCode: undefined as number | undefined, + terminalId: undefined as number | undefined, __task: task, }; - if (kind === TaskEventKind.ProcessStarted) { - result.processId = processIdOrExitCode; + if (kind === TaskEventKind.Start) { + result.terminalId = processIdOrExitCodeOrTerminalId; + } else if (kind === TaskEventKind.ProcessStarted) { + result.processId = processIdOrExitCodeOrTerminalId; } else if (kind === TaskEventKind.ProcessEnded) { - result.exitCode = processIdOrExitCode; + result.exitCode = processIdOrExitCodeOrTerminalId; } return Object.freeze(result); } else { diff --git a/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts index 0b427eff30f..a6e5f6106e2 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts @@ -718,6 +718,13 @@ class TaskService extends Disposable implements ITaskService { this._taskSystemInfos.set(key, info); } + public extensionCallbackTaskComplete(task: Task, result: number): Promise { + if (!this._taskSystem) { + return Promise.resolve(); + } + return this._taskSystem.customExecutionComplete(task, result); + } + public getTask(folder: IWorkspaceFolder | string, identifier: string | TaskIdentifier, compareId: boolean = false): Promise { const name = Types.isString(folder) ? folder : folder.name; if (this.ignoredWorkspaceFolders.some(ignored => ignored.name === name)) { diff --git a/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts index 025c9479cab..5ded7e4a924 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts @@ -269,6 +269,18 @@ export class TerminalTaskSystem implements ITaskSystem { return Object.keys(this.activeTasks).map(key => this.activeTasks[key].task); } + public customExecutionComplete(task: Task, result: number): Promise { + let activeTerminal = this.activeTasks[task.getMapKey()]; + if (!activeTerminal) { + return Promise.reject(new Error('Expected to have a terminal for an custom execution task')); + } + + return new Promise((resolve) => { + activeTerminal.terminal.rendererExit(result); + resolve(); + }); + } + public terminate(task: Task): Promise { let activeTerminal = this.activeTasks[task.getMapKey()]; if (!activeTerminal) { @@ -276,6 +288,7 @@ export class TerminalTaskSystem implements ITaskSystem { } return new Promise((resolve, reject) => { let terminal = activeTerminal.terminal; + const onExit = terminal.onExit(() => { let task = activeTerminal.task; try { @@ -519,13 +532,15 @@ export class TerminalTaskSystem implements ITaskSystem { let processStartedSignaled = false; terminal.processReady.then(() => { if (!processStartedSignaled) { - this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!)); + if (task.command.runtime !== RuntimeType.CustomExecution) { + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!)); + } processStartedSignaled = true; } }, (_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.Start, task, terminal.id)); const registeredLinkMatchers = this.registerLinkMatchers(terminal, problemMatchers); const onData = terminal.onLineData((line) => { watchingProblemMatcher.processLine(line); @@ -567,7 +582,11 @@ export class TerminalTaskSystem implements ITaskSystem { this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!)); processStartedSignaled = true; } - this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode)); + + if (task.command.runtime !== RuntimeType.CustomExecution) { + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode)); + } + for (let i = 0; i < eventCounter; i++) { let event = TaskEvent.create(TaskEventKind.Inactive, task); this._onDidStateChange.fire(event); @@ -589,13 +608,15 @@ export class TerminalTaskSystem implements ITaskSystem { let processStartedSignaled = false; terminal.processReady.then(() => { if (!processStartedSignaled) { - this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!)); + if (task.command.runtime !== RuntimeType.CustomExecution) { + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!)); + } processStartedSignaled = true; } }, (_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.Start, task, terminal.id)); this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Active, task)); let problemMatchers = this.resolveMatchers(resolver, task.configurationProperties.problemMatchers); let startStopProblemMatcher = new StartStopProblemCollector(problemMatchers, this.markerService, this.modelService); @@ -637,7 +658,9 @@ export class TerminalTaskSystem implements ITaskSystem { this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal.processId!)); processStartedSignaled = true; } - this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode)); + if (task.command.runtime !== RuntimeType.CustomExecution) { + 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 }); @@ -700,11 +723,16 @@ export class TerminalTaskSystem implements ITaskSystem { }); } + private createTerminalName(task: CustomTask | ContributedTask): string { + const needsFolderQualification = this.currentTask.workspaceFolder && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; + return nls.localize('TerminalTaskSystem.terminalName', 'Task - {0}', needsFolderQualification ? task.getQualifiedLabel() : task.configurationProperties.name); + } + private createShellLaunchConfig(task: CustomTask | ContributedTask, variableResolver: VariableResolver, platform: Platform.Platform, options: CommandOptions, command: CommandString, args: CommandString[], waitOnExit: boolean | string): IShellLaunchConfig | undefined { let shellLaunchConfig: IShellLaunchConfig; let isShellCommand = task.command.runtime === RuntimeType.Shell; let needsFolderQualification = this.currentTask.workspaceFolder && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; - let terminalName = nls.localize('TerminalTaskSystem.terminalName', 'Task - {0}', needsFolderQualification ? task.getQualifiedLabel() : task.configurationProperties.name); + let terminalName = this.createTerminalName(task); let originalCommand = task.command.name; if (isShellCommand) { shellLaunchConfig = { name: terminalName, executable: undefined, args: undefined, waitOnExit }; @@ -786,7 +814,7 @@ export class TerminalTaskSystem implements ITaskSystem { } } } else { - let commandExecutable = CommandString.value(command); + let commandExecutable = task.command.runtime !== RuntimeType.CustomExecution ? CommandString.value(command) : undefined; let executable = !isShellCommand ? this.resolveVariable(variableResolver, '${' + TerminalTaskSystem.ProcessVarName + '}') : commandExecutable; @@ -837,9 +865,8 @@ export class TerminalTaskSystem implements ITaskSystem { private createTerminal(task: CustomTask | ContributedTask, resolver: VariableResolver): [ITerminalInstance | undefined, string | undefined, TaskError | undefined] { let platform = resolver.taskSystemInfo ? resolver.taskSystemInfo.platform : Platform.platform; let options = this.resolveOptions(resolver, task.command.options); + let waitOnExit: boolean | string = false; - let { command, args } = this.resolveCommandAndArgs(resolver, task.command); - let commandExecutable = CommandString.value(command); const presentationOptions = task.command.presentation; if (!presentationOptions) { throw new Error('Task presentation options should not be undefined here.'); @@ -854,10 +881,29 @@ export class TerminalTaskSystem implements ITaskSystem { waitOnExit = true; } } - this.currentTask.shellLaunchConfig = this.isRerun ? this.lastTask.getVerifiedTask().shellLaunchConfig : this.createShellLaunchConfig(task, resolver, platform, options, command, args, waitOnExit); - if (this.currentTask.shellLaunchConfig === undefined) { - return [undefined, undefined, new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive using cmd.exe.'), TaskErrors.UnknownError)]; + + let commandExecutable: string | undefined; + let command: CommandString | undefined; + let args: CommandString[] | undefined; + + if (task.command.runtime === RuntimeType.CustomExecution) { + this.currentTask.shellLaunchConfig = { + isRendererOnly: true, + waitOnExit, + name: this.createTerminalName(task) + }; + } else { + let resolvedResult: { command: CommandString, args: CommandString[] } = this.resolveCommandAndArgs(resolver, task.command); + command = resolvedResult.command; + args = resolvedResult.args; + commandExecutable = CommandString.value(command); + + this.currentTask.shellLaunchConfig = this.isRerun ? this.lastTask.getVerifiedTask().shellLaunchConfig : this.createShellLaunchConfig(task, resolver, platform, options, command, args, waitOnExit); + if (this.currentTask.shellLaunchConfig === undefined) { + return [undefined, undefined, new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive using cmd.exe.'), TaskErrors.UnknownError)]; + } } + let prefersSameTerminal = presentationOptions.panel === PanelKind.Dedicated; let allowsSharedTerminal = presentationOptions.panel === PanelKind.Shared; let group = presentationOptions.group; @@ -890,7 +936,12 @@ export class TerminalTaskSystem implements ITaskSystem { } } if (terminalToReuse) { + if (!this.currentTask.shellLaunchConfig) { + throw new Error('Task shell launch configuration should not be undefined here.'); + } + terminalToReuse.terminal.reuseTerminal(this.currentTask.shellLaunchConfig); + if (task.command.presentation && task.command.presentation.clear) { terminalToReuse.terminal.clear(); } @@ -1053,6 +1104,12 @@ export class TerminalTaskSystem implements ITaskSystem { } private collectCommandVariables(variables: Set, command: CommandConfiguration, task: CustomTask | ContributedTask): void { + // The custom execution should have everything it needs already as it provided + // the callback. + if (command.runtime === RuntimeType.CustomExecution) { + return; + } + if (command.name === undefined) { throw new Error('Command name should never be undefined here.'); } diff --git a/src/vs/workbench/contrib/tasks/node/processTaskSystem.ts b/src/vs/workbench/contrib/tasks/node/processTaskSystem.ts index 4e128d5ded5..f8c795e968e 100644 --- a/src/vs/workbench/contrib/tasks/node/processTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/node/processTaskSystem.ts @@ -102,6 +102,10 @@ export class ProcessTaskSystem implements ITaskSystem { return true; } + public customExecutionComplete(task: Task, result?: number): Promise { + throw new TaskError(Severity.Error, 'Custom execution task completion is never expected in the process task system.', TaskErrors.UnknownError); + } + public hasErrors(value: boolean): void { this.errorsShown = !value; } @@ -276,7 +280,7 @@ export class ProcessTaskSystem implements ITaskSystem { this.childProcessEnded(); watchingProblemMatcher.done(); watchingProblemMatcher.dispose(); - if (processStartedSignaled) { + if (processStartedSignaled && task.command.runtime !== RuntimeType.CustomExecution) { this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, success.cmdCode!)); } toDispose = dispose(toDispose!); @@ -332,7 +336,7 @@ export class ProcessTaskSystem implements ITaskSystem { startStopProblemMatcher.done(); startStopProblemMatcher.dispose(); this.checkTerminated(task, success); - if (processStartedSignaled) { + if (processStartedSignaled && task.command.runtime !== RuntimeType.CustomExecution) { this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, success.cmdCode!)); } this._onDidStateChange.fire(inactiveEvent); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 08845402186..b8ef5a55e61 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -155,6 +155,7 @@ export class TerminalInstance implements ITerminalInstance { private static _idCounter = 1; private _processManager: ITerminalProcessManager | undefined; + private _pressAnyKeyToCloseListener: lifecycle.IDisposable | undefined; private _id: number; private _isExiting: boolean; @@ -725,9 +726,21 @@ export class TerminalInstance implements ITerminalInstance { this._sendLineData(buffer, buffer.ybase + buffer.y); this._xterm.dispose(); } + + if (this._pressAnyKeyToCloseListener) { + this._pressAnyKeyToCloseListener.dispose(); + this._pressAnyKeyToCloseListener = undefined; + } + if (this._processManager) { this._processManager.dispose(immediate); + } else { + // In cases where there is no associated process (for example executing an extension callback task) + // consumers still expect on onExit event to be fired. An example of this is terminating the extension callback + // task. + this._onExit.fire(0); } + if (!this._isDisposed) { this._isDisposed = true; this._onDisposed.fire(this); @@ -735,6 +748,16 @@ export class TerminalInstance implements ITerminalInstance { this._disposables = lifecycle.dispose(this._disposables); } + public rendererExit(exitCode: number): void { + // The use of this API is for cases where there is no backing process behind a terminal + // instance (eg. a custom execution task). + if (!this.shellLaunchConfig.isRendererOnly) { + throw new Error('rendererExit is only expected to be called on a renderer only terminal'); + } + + return this._onProcessExit(exitCode); + } + public forceRedraw(): void { this._xterm.refresh(0, this._xterm.rows - 1); } @@ -894,7 +917,13 @@ export class TerminalInstance implements ITerminalInstance { } } - private _onProcessExit(exitCode: number): void { + /** + * Called when either a process tied to a terminal has exited or when a terminal renderer + * simulates a process exiting (eg. custom execution task). + * @param exitCode The exit code of the process, this is undefined when the terminal was exited + * through user action. + */ + private _onProcessExit(exitCode?: number): void { this._logService.debug(`Terminal process exit (id: ${this.id}) with code ${exitCode}`); // Prevent dispose functions being triggered multiple times @@ -909,11 +938,11 @@ export class TerminalInstance implements ITerminalInstance { exitCodeMessage = nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode); } - this._logService.debug(`Terminal process exit (id: ${this.id}) state ${this._processManager!.processState}`); + this._logService.debug(`Terminal process exit (id: ${this.id})${this._processManager ? ' state ' + this._processManager.processState : ''}`); // Only trigger wait on exit when the exit was *not* triggered by the // user (via the `workbench.action.terminal.kill` command). - if (this._shellLaunchConfig.waitOnExit && this._processManager!.processState !== ProcessState.KILLED_BY_USER) { + if (this._shellLaunchConfig.waitOnExit && (!this._processManager || this._processManager.processState !== ProcessState.KILLED_BY_USER)) { if (exitCode) { this._xterm.writeln(exitCodeMessage!); } @@ -931,7 +960,7 @@ export class TerminalInstance implements ITerminalInstance { } else { this.dispose(); if (exitCode) { - if (this._processManager!.processState === ProcessState.KILLED_DURING_LAUNCH) { + if (this._processManager && this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH) { let args = ''; if (typeof this._shellLaunchConfig.args === 'string') { args = this._shellLaunchConfig.args; @@ -958,19 +987,34 @@ export class TerminalInstance implements ITerminalInstance { } } - this._onExit.fire(exitCode); + this._onExit.fire(exitCode || 0); } private _attachPressAnyKeyToCloseListener() { - this._processManager!.addDisposable(dom.addDisposableListener(this._xterm.textarea, 'keypress', (event: KeyboardEvent) => { - this.dispose(); - event.preventDefault(); - })); + if (!this._pressAnyKeyToCloseListener) { + this._pressAnyKeyToCloseListener = dom.addDisposableListener(this._xterm.textarea, 'keypress', (event: KeyboardEvent) => { + if (this._pressAnyKeyToCloseListener) { + this._pressAnyKeyToCloseListener.dispose(); + this._pressAnyKeyToCloseListener = undefined; + this.dispose(); + event.preventDefault(); + } + }); + } } public reuseTerminal(shell: IShellLaunchConfig): void { + // Unsubscribe any key listener we may have. + if (this._pressAnyKeyToCloseListener) { + this._pressAnyKeyToCloseListener.dispose(); + this._pressAnyKeyToCloseListener = undefined; + } + // Kill and clear up the process, making the process manager ready for a new process - this._processManager!.dispose(); + if (this._processManager) { + this._processManager.dispose(); + this._processManager = undefined; + } // Ensure new processes' output starts at start of new line this._xterm.write('\n\x1b[G'); @@ -989,12 +1033,24 @@ export class TerminalInstance implements ITerminalInstance { // Set the new shell launch config this._shellLaunchConfig = shell; // Must be done before calling _createProcess() - // Initialize new process - this._createProcess(); + + // Launch the process unless this is only a renderer. + // In the renderer only cases, we still need to set the title correctly. + if (!this._shellLaunchConfig.isRendererOnly) { + this._createProcess(); + } else if (this._shellLaunchConfig.name) { + this.setTitle(this._shellLaunchConfig.name, false); + } + if (oldTitle !== this._title) { this.setTitle(this._title, true); } - this._processManager!.onProcessData(data => this._onProcessData(data)); + + if (this._processManager) { + // The "!" operator is required here because _processManager is set to undefiend earlier + // and TS does not know that createProcess sets it. + this._processManager!.onProcessData(data => this._onProcessData(data)); + } } private _sendRendererInput(input: string): void { diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index ec0303a0ea3..b718dba571e 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -223,6 +223,7 @@ export interface ITerminalService { * @param name The name of the terminal. */ createTerminalRenderer(name: string): ITerminalInstance; + /** * Creates a raw terminal instance, this should not be used outside of the terminal part. */ @@ -438,6 +439,14 @@ export interface ITerminalInstance { */ dispose(immediate?: boolean): void; + /** + * Indicates that a consumer of a renderer only terminal is finished with it. + * + * @param exitCode The exit code of the terminal. Zero indicates success, non-zero indicates + * failure. + */ + rendererExit(exitCode: number): void; + /** * Forces the terminal to redraw its viewport. */