Path: blob/main/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Action } from '../../../../base/common/actions.js';6import { disposableTimeout } from '../../../../base/common/async.js';7import { CancellationTokenSource } from '../../../../base/common/cancellation.js';8import { createErrorWithActions } from '../../../../base/common/errorMessage.js';9import { Emitter, Event } from '../../../../base/common/event.js';10import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';11import severity from '../../../../base/common/severity.js';12import * as nls from '../../../../nls.js';13import { ICommandService } from '../../../../platform/commands/common/commands.js';14import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';15import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';16import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js';17import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';18import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';19import { IWorkspace, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';20import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from './debugCommands.js';21import { IDebugConfiguration } from '../common/debug.js';22import { Markers } from '../../markers/common/markers.js';23import { ConfiguringTask, CustomTask, ITaskEvent, ITaskIdentifier, Task, TaskEventKind } from '../../tasks/common/tasks.js';24import { ITaskService, ITaskSummary } from '../../tasks/common/taskService.js';25import { IViewsService } from '../../../services/views/common/viewsService.js';2627const onceFilter = (event: Event<ITaskEvent>, filter: (e: ITaskEvent) => boolean) => Event.once(Event.filter(event, filter));2829export const enum TaskRunResult {30Failure,31Success32}3334const DEBUG_TASK_ERROR_CHOICE_KEY = 'debug.taskerrorchoice';35const ABORT_LABEL = nls.localize('abort', "Abort");36const DEBUG_ANYWAY_LABEL = nls.localize({ key: 'debugAnyway', comment: ['&& denotes a mnemonic'] }, "&&Debug Anyway");37const DEBUG_ANYWAY_LABEL_NO_MEMO = nls.localize('debugAnywayNoMemo', "Debug Anyway");3839interface IRunnerTaskSummary extends ITaskSummary {40cancelled?: boolean;41}4243export class DebugTaskRunner implements IDisposable {4445private globalCancellation = new CancellationTokenSource();4647constructor(48@ITaskService private readonly taskService: ITaskService,49@IMarkerService private readonly markerService: IMarkerService,50@IConfigurationService private readonly configurationService: IConfigurationService,51@IViewsService private readonly viewsService: IViewsService,52@IDialogService private readonly dialogService: IDialogService,53@IStorageService private readonly storageService: IStorageService,54@ICommandService private readonly commandService: ICommandService,55@IProgressService private readonly progressService: IProgressService,56) { }5758cancel(): void {59this.globalCancellation.dispose(true);60this.globalCancellation = new CancellationTokenSource();61}6263public dispose(): void {64this.globalCancellation.dispose(true);65}6667async runTaskAndCheckErrors(68root: IWorkspaceFolder | IWorkspace | undefined,69taskId: string | ITaskIdentifier | undefined,70): Promise<TaskRunResult> {71try {72const taskSummary = await this.runTask(root, taskId, this.globalCancellation.token);73if (taskSummary && (taskSummary.exitCode === undefined || taskSummary.cancelled)) {74// User canceled, either debugging, or the prelaunch task75return TaskRunResult.Failure;76}7778const errorCount = taskId ? this.markerService.read({ severities: MarkerSeverity.Error, take: 2 }).length : 0;79const successExitCode = taskSummary && taskSummary.exitCode === 0;80const failureExitCode = taskSummary && taskSummary.exitCode !== 0;81const onTaskErrors = this.configurationService.getValue<IDebugConfiguration>('debug').onTaskErrors;82if (successExitCode || onTaskErrors === 'debugAnyway' || (errorCount === 0 && !failureExitCode)) {83return TaskRunResult.Success;84}85if (onTaskErrors === 'showErrors') {86await this.viewsService.openView(Markers.MARKERS_VIEW_ID, true);87return Promise.resolve(TaskRunResult.Failure);88}89if (onTaskErrors === 'abort') {90return Promise.resolve(TaskRunResult.Failure);91}9293const taskLabel = typeof taskId === 'string' ? taskId : taskId ? taskId.name : '';94const message = errorCount > 195? nls.localize('preLaunchTaskErrors', "Errors exist after running preLaunchTask '{0}'.", taskLabel)96: errorCount === 197? nls.localize('preLaunchTaskError', "Error exists after running preLaunchTask '{0}'.", taskLabel)98: taskSummary && typeof taskSummary.exitCode === 'number'99? nls.localize('preLaunchTaskExitCode', "The preLaunchTask '{0}' terminated with exit code {1}.", taskLabel, taskSummary.exitCode)100: nls.localize('preLaunchTaskTerminated', "The preLaunchTask '{0}' terminated.", taskLabel);101102enum DebugChoice {103DebugAnyway = 1,104ShowErrors = 2,105Cancel = 0106}107const { result, checkboxChecked } = await this.dialogService.prompt<DebugChoice>({108type: severity.Warning,109message,110buttons: [111{112label: DEBUG_ANYWAY_LABEL,113run: () => DebugChoice.DebugAnyway114},115{116label: nls.localize({ key: 'showErrors', comment: ['&& denotes a mnemonic'] }, "&&Show Errors"),117run: () => DebugChoice.ShowErrors118}119],120cancelButton: {121label: ABORT_LABEL,122run: () => DebugChoice.Cancel123},124checkbox: {125label: nls.localize('remember', "Remember my choice in user settings"),126}127});128129130const debugAnyway = result === DebugChoice.DebugAnyway;131const abort = result === DebugChoice.Cancel;132if (checkboxChecked) {133this.configurationService.updateValue('debug.onTaskErrors', result === DebugChoice.DebugAnyway ? 'debugAnyway' : abort ? 'abort' : 'showErrors');134}135136if (abort) {137return Promise.resolve(TaskRunResult.Failure);138}139if (debugAnyway) {140return TaskRunResult.Success;141}142143await this.viewsService.openView(Markers.MARKERS_VIEW_ID, true);144return Promise.resolve(TaskRunResult.Failure);145} catch (err) {146const taskConfigureAction = this.taskService.configureAction();147const choiceMap: { [key: string]: number } = JSON.parse(this.storageService.get(DEBUG_TASK_ERROR_CHOICE_KEY, StorageScope.WORKSPACE, '{}'));148149let choice = -1;150enum DebugChoice {151DebugAnyway = 0,152ConfigureTask = 1,153Cancel = 2154}155if (choiceMap[err.message] !== undefined) {156choice = choiceMap[err.message];157} else {158const { result, checkboxChecked } = await this.dialogService.prompt<DebugChoice>({159type: severity.Error,160message: err.message,161buttons: [162{163label: nls.localize({ key: 'debugAnyway', comment: ['&& denotes a mnemonic'] }, "&&Debug Anyway"),164run: () => DebugChoice.DebugAnyway165},166{167label: taskConfigureAction.label,168run: () => DebugChoice.ConfigureTask169}170],171cancelButton: {172run: () => DebugChoice.Cancel173},174checkbox: {175label: nls.localize('rememberTask', "Remember my choice for this task")176}177});178choice = result;179if (checkboxChecked) {180choiceMap[err.message] = choice;181this.storageService.store(DEBUG_TASK_ERROR_CHOICE_KEY, JSON.stringify(choiceMap), StorageScope.WORKSPACE, StorageTarget.MACHINE);182}183}184185if (choice === DebugChoice.ConfigureTask) {186await taskConfigureAction.run();187}188189return choice === DebugChoice.DebugAnyway ? TaskRunResult.Success : TaskRunResult.Failure;190}191}192193async runTask(root: IWorkspace | IWorkspaceFolder | undefined, taskId: string | ITaskIdentifier | undefined, token = this.globalCancellation.token): Promise<IRunnerTaskSummary | null> {194if (!taskId) {195return Promise.resolve(null);196}197if (!root) {198return 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)));199}200// run a task before starting a debug session201const task = await this.taskService.getTask(root, taskId);202if (!task) {203const errorMessage = typeof taskId === 'string'204? nls.localize('DebugTaskNotFoundWithTaskId', "Could not find the task '{0}'.", taskId)205: nls.localize('DebugTaskNotFound', "Could not find the specified task.");206return Promise.reject(createErrorWithActions(errorMessage, [new Action(DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, undefined, true, () => this.commandService.executeCommand(DEBUG_CONFIGURE_COMMAND_ID))]));207}208209// If a task is missing the problem matcher the promise will never complete, so we need to have a workaround #35340210let taskStarted = false;211const store = new DisposableStore();212const getTaskKey = (t: Task) => t.getKey() ?? t.getMapKey();213const taskKey = getTaskKey(task);214const inactivePromise: Promise<ITaskSummary | null> = new Promise((resolve) => store.add(215onceFilter(this.taskService.onDidStateChange, e => {216// When a task isBackground it will go inactive when it is safe to launch.217// But when a background task is terminated by the user, it will also fire an inactive event.218// This means that we will not get to see the real exit code from running the task (undefined when terminated by the user).219// Catch the ProcessEnded event here, which occurs before inactive, and capture the exit code to prevent this.220return (e.kind === TaskEventKind.Inactive221|| (e.kind === TaskEventKind.ProcessEnded && e.exitCode === undefined))222&& getTaskKey(e.__task) === taskKey;223})(e => {224taskStarted = true;225resolve(e.kind === TaskEventKind.ProcessEnded ? { exitCode: e.exitCode } : null);226}),227));228229store.add(230onceFilter(this.taskService.onDidStateChange, e => ((e.kind === TaskEventKind.Active) || (e.kind === TaskEventKind.DependsOnStarted)) && getTaskKey(e.__task) === taskKey231)(() => {232// Task is active, so everything seems to be fine, no need to prompt after 10 seconds233// Use case being a slow running task should not be prompted even though it takes more than 10 seconds234taskStarted = true;235})236);237238const didAcquireInput = store.add(new Emitter<void>());239store.add(onceFilter(240this.taskService.onDidStateChange,241e => (e.kind === TaskEventKind.AcquiredInput) && getTaskKey(e.__task) === taskKey242)(() => didAcquireInput.fire()));243244const taskDonePromise: Promise<ITaskSummary | null> = this.taskService.getActiveTasks().then(async (tasks): Promise<ITaskSummary | null> => {245if (tasks.find(t => getTaskKey(t) === taskKey)) {246didAcquireInput.fire();247// Check that the task isn't busy and if it is, wait for it248const busyTasks = await this.taskService.getBusyTasks();249if (busyTasks.find(t => getTaskKey(t) === taskKey)) {250taskStarted = true;251return inactivePromise;252}253// task is already running and isn't busy - nothing to do.254return Promise.resolve(null);255}256257const taskPromise = this.taskService.run(task);258if (task.configurationProperties.isBackground) {259return inactivePromise;260}261262return taskPromise.then(x => x ?? null);263});264265const result = new Promise<IRunnerTaskSummary | null>((resolve, reject) => {266taskDonePromise.then(result => {267taskStarted = true;268resolve(result);269}, error => reject(error));270271store.add(token.onCancellationRequested(() => {272resolve({ exitCode: undefined, cancelled: true });273this.taskService.terminate(task).catch(() => { });274}));275276// Start the timeouts once a terminal has been acquired277store.add(didAcquireInput.event(() => {278const waitTime = task.configurationProperties.isBackground ? 5000 : 10000;279280// Error shown if there's a background task with no problem matcher that doesn't exit quickly281store.add(disposableTimeout(() => {282if (!taskStarted) {283const errorMessage = nls.localize('taskNotTracked', "The task '{0}' has not exited and doesn't have a 'problemMatcher' defined. Make sure to define a problem matcher for watch tasks.", typeof taskId === 'string' ? taskId : JSON.stringify(taskId));284reject({ severity: severity.Error, message: errorMessage });285}286}, waitTime));287288const hideSlowPreLaunchWarning = this.configurationService.getValue<IDebugConfiguration>('debug').hideSlowPreLaunchWarning;289if (!hideSlowPreLaunchWarning) {290// Notification shown on any task taking a while to resolve291store.add(disposableTimeout(() => {292const message = nls.localize('runningTask', "Waiting for preLaunchTask '{0}'...", task.configurationProperties.name);293const buttons = [DEBUG_ANYWAY_LABEL_NO_MEMO, ABORT_LABEL];294const canConfigure = task instanceof CustomTask || task instanceof ConfiguringTask;295if (canConfigure) {296buttons.splice(1, 0, nls.localize('configureTask', "Configure Task"));297}298299this.progressService.withProgress(300{ location: ProgressLocation.Notification, title: message, buttons },301() => result.catch(() => { }),302(choice) => {303if (choice === undefined) {304// no-op, keep waiting305} else if (choice === 0) { // debug anyway306resolve({ exitCode: 0 });307} else { // abort or configure308resolve({ exitCode: undefined, cancelled: true });309this.taskService.terminate(task).catch(() => { });310if (canConfigure && choice === 1) { // configure311this.taskService.openConfig(task as CustomTask);312}313}314}315);316}, 10_000));317}318}));319});320321return result.finally(() => store.dispose());322}323}324325326