/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; import severity from 'vs/base/common/severity'; import { Event } from 'vs/base/common/event'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; import { ITaskService, ITaskSummary } from 'vs/workbench/contrib/tasks/common/taskService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; import { TaskEvent, TaskEventKind, TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IAction } from 'vs/base/common/actions'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { IViewsService } from 'vs/workbench/common/views'; function once(match: (e: TaskEvent) => boolean, event: Event): Event { return (listener, thisArgs = null, disposables?) => { const result = event(e => { if (match(e)) { result.dispose(); return listener.call(thisArgs, e); } }, null, disposables); return result; }; } export const enum TaskRunResult { Failure, Success } export class DebugTaskRunner { private canceled = false; constructor( @ITaskService private readonly taskService: ITaskService, @IMarkerService private readonly markerService: IMarkerService, @IConfigurationService private readonly configurationService: IConfigurationService, @IViewsService private readonly viewsService: IViewsService, @IDialogService private readonly dialogService: IDialogService, ) { } cancel(): void { this.canceled = true; } async runTaskAndCheckErrors(root: IWorkspaceFolder | IWorkspace | undefined, taskId: string | TaskIdentifier | undefined, onError: (msg: string, actions: IAction[]) => Promise): Promise { try { this.canceled = false; const taskSummary = await this.runTask(root, taskId); if (this.canceled || (taskSummary && taskSummary.exitCode === undefined)) { // User canceled, either debugging, or the prelaunch task return TaskRunResult.Failure; } const errorCount = taskId ? this.markerService.getStatistics().errors : 0; const successExitCode = taskSummary && taskSummary.exitCode === 0; const failureExitCode = taskSummary && taskSummary.exitCode !== 0; const onTaskErrors = this.configurationService.getValue('debug').onTaskErrors; if (successExitCode || onTaskErrors === 'debugAnyway' || (errorCount === 0 && !failureExitCode)) { return TaskRunResult.Success; } if (onTaskErrors === 'showErrors') { await this.viewsService.openView(Constants.MARKERS_VIEW_ID); return Promise.resolve(TaskRunResult.Failure); } if (onTaskErrors === 'cancel') { return Promise.resolve(TaskRunResult.Failure); } const taskLabel = typeof taskId === 'string' ? taskId : taskId ? taskId.name : ''; const message = errorCount > 1 ? nls.localize('preLaunchTaskErrors', "Errors exist after running preLaunchTask '{0}'.", taskLabel) : errorCount === 1 ? nls.localize('preLaunchTaskError', "Error exists after running preLaunchTask '{0}'.", taskLabel) : taskSummary && typeof taskSummary.exitCode === 'number' ? nls.localize('preLaunchTaskExitCode', "The preLaunchTask '{0}' terminated with exit code {1}.", taskLabel, taskSummary.exitCode) : nls.localize('preLaunchTaskTerminated', "The preLaunchTask '{0}' terminated.", taskLabel); const result = await this.dialogService.show(severity.Warning, message, [nls.localize('debugAnyway', "Debug Anyway"), nls.localize('showErrors', "Show Errors"), nls.localize('cancel', "Cancel")], { checkbox: { label: nls.localize('remember', "Remember my choice in user settings"), }, cancelId: 2 }); const debugAnyway = result.choice === 0; const cancel = result.choice = 2; if (result.checkboxChecked) { this.configurationService.updateValue('debug.onTaskErrors', result.choice === 0 ? 'debugAnyway' : cancel ? 'cancel' : 'showErrors'); } if (cancel) { return Promise.resolve(TaskRunResult.Failure); } if (debugAnyway) { return TaskRunResult.Success; } await this.viewsService.openView(Constants.MARKERS_VIEW_ID); return Promise.resolve(TaskRunResult.Failure); } catch (err) { await onError(err.message, [this.taskService.configureAction()]); return TaskRunResult.Failure; } } async runTask(root: IWorkspace | IWorkspaceFolder | undefined, taskId: string | TaskIdentifier | undefined): Promise { if (!taskId) { return Promise.resolve(null); } if (!root) { return Promise.reject(new Error(nls.localize('invalidTaskReference', "Task '{0}' can not be referenced from a launch configuration that is in a different workspace folder.", typeof taskId === 'string' ? taskId : taskId.type))); } // run a task before starting a debug session const task = await this.taskService.getTask(root, taskId); if (!task) { const errorMessage = typeof taskId === 'string' ? nls.localize('DebugTaskNotFoundWithTaskId', "Could not find the task '{0}'.", taskId) : nls.localize('DebugTaskNotFound', "Could not find the specified task."); return Promise.reject(createErrorWithActions(errorMessage)); } // If a task is missing the problem matcher the promise will never complete, so we need to have a workaround #35340 let taskStarted = false; const inactivePromise: Promise = new Promise((c, e) => once(e => { // When a task isBackground it will go inactive when it is safe to launch. // But when a background task is terminated by the user, it will also fire an inactive event. // This means that we will not get to see the real exit code from running the task (undefined when terminated by the user). // Catch the ProcessEnded event here, which occurs before inactive, and capture the exit code to prevent this. return (e.kind === TaskEventKind.Inactive || (e.kind === TaskEventKind.ProcessEnded && e.exitCode === undefined)) && e.taskId === task._id; }, this.taskService.onDidStateChange)(e => { taskStarted = true; c(e.kind === TaskEventKind.ProcessEnded ? { exitCode: e.exitCode } : null); })); const promise: Promise = this.taskService.getActiveTasks().then(async (tasks): Promise => { if (tasks.filter(t => t._id === task._id).length) { // Check that the task isn't busy and if it is, wait for it const busyTasks = await this.taskService.getBusyTasks(); if (busyTasks.filter(t => t._id === task._id).length) { taskStarted = true; return inactivePromise; } // task is already running and isn't busy - nothing to do. return Promise.resolve(null); } once(e => ((e.kind === TaskEventKind.Active) || (e.kind === TaskEventKind.DependsOnStarted)) && e.taskId === task._id, this.taskService.onDidStateChange)(() => { // Task is active, so everything seems to be fine, no need to prompt after 10 seconds // Use case being a slow running task should not be prompted even though it takes more than 10 seconds taskStarted = true; }); const taskPromise = this.taskService.run(task); if (task.configurationProperties.isBackground) { return inactivePromise; } return taskPromise.then(withUndefinedAsNull); }); return new Promise((c, e) => { promise.then(result => { taskStarted = true; c(result); }, error => e(error)); setTimeout(() => { if (!taskStarted) { const errorMessage = typeof taskId === 'string' ? nls.localize('taskNotTrackedWithTaskId', "The specified task cannot be tracked.") : nls.localize('taskNotTracked', "The task '{0}' cannot be tracked.", JSON.stringify(taskId)); e({ severity: severity.Error, message: errorMessage }); } }, 10000); }); } }