Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Action } from '../../../../base/common/actions.js';
7
import { disposableTimeout } from '../../../../base/common/async.js';
8
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
9
import { createErrorWithActions } from '../../../../base/common/errorMessage.js';
10
import { Emitter, Event } from '../../../../base/common/event.js';
11
import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
12
import severity from '../../../../base/common/severity.js';
13
import * as nls from '../../../../nls.js';
14
import { ICommandService } from '../../../../platform/commands/common/commands.js';
15
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
16
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
17
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js';
18
import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';
19
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
20
import { IWorkspace, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
21
import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from './debugCommands.js';
22
import { IDebugConfiguration } from '../common/debug.js';
23
import { Markers } from '../../markers/common/markers.js';
24
import { ConfiguringTask, CustomTask, ITaskEvent, ITaskIdentifier, Task, TaskEventKind } from '../../tasks/common/tasks.js';
25
import { ITaskService, ITaskSummary } from '../../tasks/common/taskService.js';
26
import { IViewsService } from '../../../services/views/common/viewsService.js';
27
28
const onceFilter = (event: Event<ITaskEvent>, filter: (e: ITaskEvent) => boolean) => Event.once(Event.filter(event, filter));
29
30
export const enum TaskRunResult {
31
Failure,
32
Success
33
}
34
35
const DEBUG_TASK_ERROR_CHOICE_KEY = 'debug.taskerrorchoice';
36
const ABORT_LABEL = nls.localize('abort', "Abort");
37
const DEBUG_ANYWAY_LABEL = nls.localize({ key: 'debugAnyway', comment: ['&& denotes a mnemonic'] }, "&&Debug Anyway");
38
const DEBUG_ANYWAY_LABEL_NO_MEMO = nls.localize('debugAnywayNoMemo', "Debug Anyway");
39
40
interface IRunnerTaskSummary extends ITaskSummary {
41
cancelled?: boolean;
42
}
43
44
export class DebugTaskRunner implements IDisposable {
45
46
private globalCancellation = new CancellationTokenSource();
47
48
constructor(
49
@ITaskService private readonly taskService: ITaskService,
50
@IMarkerService private readonly markerService: IMarkerService,
51
@IConfigurationService private readonly configurationService: IConfigurationService,
52
@IViewsService private readonly viewsService: IViewsService,
53
@IDialogService private readonly dialogService: IDialogService,
54
@IStorageService private readonly storageService: IStorageService,
55
@ICommandService private readonly commandService: ICommandService,
56
@IProgressService private readonly progressService: IProgressService,
57
) { }
58
59
cancel(): void {
60
this.globalCancellation.dispose(true);
61
this.globalCancellation = new CancellationTokenSource();
62
}
63
64
public dispose(): void {
65
this.globalCancellation.dispose(true);
66
}
67
68
async runTaskAndCheckErrors(
69
root: IWorkspaceFolder | IWorkspace | undefined,
70
taskId: string | ITaskIdentifier | undefined,
71
): Promise<TaskRunResult> {
72
try {
73
const taskSummary = await this.runTask(root, taskId, this.globalCancellation.token);
74
if (taskSummary && (taskSummary.exitCode === undefined || taskSummary.cancelled)) {
75
// User canceled, either debugging, or the prelaunch task
76
return TaskRunResult.Failure;
77
}
78
79
const errorCount = taskId ? this.markerService.read({ severities: MarkerSeverity.Error, take: 2 }).length : 0;
80
const successExitCode = taskSummary && taskSummary.exitCode === 0;
81
const failureExitCode = taskSummary && taskSummary.exitCode !== 0;
82
const onTaskErrors = this.configurationService.getValue<IDebugConfiguration>('debug').onTaskErrors;
83
if (successExitCode || onTaskErrors === 'debugAnyway' || (errorCount === 0 && !failureExitCode)) {
84
return TaskRunResult.Success;
85
}
86
if (onTaskErrors === 'showErrors') {
87
await this.viewsService.openView(Markers.MARKERS_VIEW_ID, true);
88
return Promise.resolve(TaskRunResult.Failure);
89
}
90
if (onTaskErrors === 'abort') {
91
return Promise.resolve(TaskRunResult.Failure);
92
}
93
94
const taskLabel = typeof taskId === 'string' ? taskId : taskId ? taskId.name : '';
95
const message = errorCount > 1
96
? nls.localize('preLaunchTaskErrors', "Errors exist after running preLaunchTask '{0}'.", taskLabel)
97
: errorCount === 1
98
? nls.localize('preLaunchTaskError', "Error exists after running preLaunchTask '{0}'.", taskLabel)
99
: taskSummary && typeof taskSummary.exitCode === 'number'
100
? nls.localize('preLaunchTaskExitCode', "The preLaunchTask '{0}' terminated with exit code {1}.", taskLabel, taskSummary.exitCode)
101
: nls.localize('preLaunchTaskTerminated', "The preLaunchTask '{0}' terminated.", taskLabel);
102
103
enum DebugChoice {
104
DebugAnyway = 1,
105
ShowErrors = 2,
106
Cancel = 0
107
}
108
const { result, checkboxChecked } = await this.dialogService.prompt<DebugChoice>({
109
type: severity.Warning,
110
message,
111
buttons: [
112
{
113
label: DEBUG_ANYWAY_LABEL,
114
run: () => DebugChoice.DebugAnyway
115
},
116
{
117
label: nls.localize({ key: 'showErrors', comment: ['&& denotes a mnemonic'] }, "&&Show Errors"),
118
run: () => DebugChoice.ShowErrors
119
}
120
],
121
cancelButton: {
122
label: ABORT_LABEL,
123
run: () => DebugChoice.Cancel
124
},
125
checkbox: {
126
label: nls.localize('remember', "Remember my choice in user settings"),
127
}
128
});
129
130
131
const debugAnyway = result === DebugChoice.DebugAnyway;
132
const abort = result === DebugChoice.Cancel;
133
if (checkboxChecked) {
134
this.configurationService.updateValue('debug.onTaskErrors', result === DebugChoice.DebugAnyway ? 'debugAnyway' : abort ? 'abort' : 'showErrors');
135
}
136
137
if (abort) {
138
return Promise.resolve(TaskRunResult.Failure);
139
}
140
if (debugAnyway) {
141
return TaskRunResult.Success;
142
}
143
144
await this.viewsService.openView(Markers.MARKERS_VIEW_ID, true);
145
return Promise.resolve(TaskRunResult.Failure);
146
} catch (err) {
147
const taskConfigureAction = this.taskService.configureAction();
148
const choiceMap: { [key: string]: number } = JSON.parse(this.storageService.get(DEBUG_TASK_ERROR_CHOICE_KEY, StorageScope.WORKSPACE, '{}'));
149
150
let choice = -1;
151
enum DebugChoice {
152
DebugAnyway = 0,
153
ConfigureTask = 1,
154
Cancel = 2
155
}
156
if (choiceMap[err.message] !== undefined) {
157
choice = choiceMap[err.message];
158
} else {
159
const { result, checkboxChecked } = await this.dialogService.prompt<DebugChoice>({
160
type: severity.Error,
161
message: err.message,
162
buttons: [
163
{
164
label: nls.localize({ key: 'debugAnyway', comment: ['&& denotes a mnemonic'] }, "&&Debug Anyway"),
165
run: () => DebugChoice.DebugAnyway
166
},
167
{
168
label: taskConfigureAction.label,
169
run: () => DebugChoice.ConfigureTask
170
}
171
],
172
cancelButton: {
173
run: () => DebugChoice.Cancel
174
},
175
checkbox: {
176
label: nls.localize('rememberTask', "Remember my choice for this task")
177
}
178
});
179
choice = result;
180
if (checkboxChecked) {
181
choiceMap[err.message] = choice;
182
this.storageService.store(DEBUG_TASK_ERROR_CHOICE_KEY, JSON.stringify(choiceMap), StorageScope.WORKSPACE, StorageTarget.MACHINE);
183
}
184
}
185
186
if (choice === DebugChoice.ConfigureTask) {
187
await taskConfigureAction.run();
188
}
189
190
return choice === DebugChoice.DebugAnyway ? TaskRunResult.Success : TaskRunResult.Failure;
191
}
192
}
193
194
async runTask(root: IWorkspace | IWorkspaceFolder | undefined, taskId: string | ITaskIdentifier | undefined, token = this.globalCancellation.token): Promise<IRunnerTaskSummary | null> {
195
if (!taskId) {
196
return Promise.resolve(null);
197
}
198
if (!root) {
199
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)));
200
}
201
// run a task before starting a debug session
202
const task = await this.taskService.getTask(root, taskId);
203
if (!task) {
204
const errorMessage = typeof taskId === 'string'
205
? nls.localize('DebugTaskNotFoundWithTaskId', "Could not find the task '{0}'.", taskId)
206
: nls.localize('DebugTaskNotFound', "Could not find the specified task.");
207
return Promise.reject(createErrorWithActions(errorMessage, [new Action(DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, undefined, true, () => this.commandService.executeCommand(DEBUG_CONFIGURE_COMMAND_ID))]));
208
}
209
210
// If a task is missing the problem matcher the promise will never complete, so we need to have a workaround #35340
211
let taskStarted = false;
212
const store = new DisposableStore();
213
const getTaskKey = (t: Task) => t.getKey() ?? t.getMapKey();
214
const taskKey = getTaskKey(task);
215
const inactivePromise: Promise<ITaskSummary | null> = new Promise((resolve) => store.add(
216
onceFilter(this.taskService.onDidStateChange, e => {
217
// When a task isBackground it will go inactive when it is safe to launch.
218
// But when a background task is terminated by the user, it will also fire an inactive event.
219
// This means that we will not get to see the real exit code from running the task (undefined when terminated by the user).
220
// Catch the ProcessEnded event here, which occurs before inactive, and capture the exit code to prevent this.
221
return (e.kind === TaskEventKind.Inactive
222
|| (e.kind === TaskEventKind.ProcessEnded && e.exitCode === undefined))
223
&& getTaskKey(e.__task) === taskKey;
224
})(e => {
225
taskStarted = true;
226
resolve(e.kind === TaskEventKind.ProcessEnded ? { exitCode: e.exitCode } : null);
227
}),
228
));
229
230
store.add(
231
onceFilter(this.taskService.onDidStateChange, e => ((e.kind === TaskEventKind.Active) || (e.kind === TaskEventKind.DependsOnStarted)) && getTaskKey(e.__task) === taskKey
232
)(() => {
233
// Task is active, so everything seems to be fine, no need to prompt after 10 seconds
234
// Use case being a slow running task should not be prompted even though it takes more than 10 seconds
235
taskStarted = true;
236
})
237
);
238
239
const didAcquireInput = store.add(new Emitter<void>());
240
store.add(onceFilter(
241
this.taskService.onDidStateChange,
242
e => (e.kind === TaskEventKind.AcquiredInput) && getTaskKey(e.__task) === taskKey
243
)(() => didAcquireInput.fire()));
244
245
const taskDonePromise: Promise<ITaskSummary | null> = this.taskService.getActiveTasks().then(async (tasks): Promise<ITaskSummary | null> => {
246
if (tasks.find(t => getTaskKey(t) === taskKey)) {
247
didAcquireInput.fire();
248
// Check that the task isn't busy and if it is, wait for it
249
const busyTasks = await this.taskService.getBusyTasks();
250
if (busyTasks.find(t => getTaskKey(t) === taskKey)) {
251
taskStarted = true;
252
return inactivePromise;
253
}
254
// task is already running and isn't busy - nothing to do.
255
return Promise.resolve(null);
256
}
257
258
const taskPromise = this.taskService.run(task);
259
if (task.configurationProperties.isBackground) {
260
return inactivePromise;
261
}
262
263
return taskPromise.then(x => x ?? null);
264
});
265
266
const result = new Promise<IRunnerTaskSummary | null>((resolve, reject) => {
267
taskDonePromise.then(result => {
268
taskStarted = true;
269
resolve(result);
270
}, error => reject(error));
271
272
store.add(token.onCancellationRequested(() => {
273
resolve({ exitCode: undefined, cancelled: true });
274
this.taskService.terminate(task).catch(() => { });
275
}));
276
277
// Start the timeouts once a terminal has been acquired
278
store.add(didAcquireInput.event(() => {
279
const waitTime = task.configurationProperties.isBackground ? 5000 : 10000;
280
281
// Error shown if there's a background task with no problem matcher that doesn't exit quickly
282
store.add(disposableTimeout(() => {
283
if (!taskStarted) {
284
const 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));
285
reject({ severity: severity.Error, message: errorMessage });
286
}
287
}, waitTime));
288
289
const hideSlowPreLaunchWarning = this.configurationService.getValue<IDebugConfiguration>('debug').hideSlowPreLaunchWarning;
290
if (!hideSlowPreLaunchWarning) {
291
// Notification shown on any task taking a while to resolve
292
store.add(disposableTimeout(() => {
293
const message = nls.localize('runningTask', "Waiting for preLaunchTask '{0}'...", task.configurationProperties.name);
294
const buttons = [DEBUG_ANYWAY_LABEL_NO_MEMO, ABORT_LABEL];
295
const canConfigure = task instanceof CustomTask || task instanceof ConfiguringTask;
296
if (canConfigure) {
297
buttons.splice(1, 0, nls.localize('configureTask', "Configure Task"));
298
}
299
300
this.progressService.withProgress(
301
{ location: ProgressLocation.Notification, title: message, buttons },
302
() => result.catch(() => { }),
303
(choice) => {
304
if (choice === undefined) {
305
// no-op, keep waiting
306
} else if (choice === 0) { // debug anyway
307
resolve({ exitCode: 0 });
308
} else { // abort or configure
309
resolve({ exitCode: undefined, cancelled: true });
310
this.taskService.terminate(task).catch(() => { });
311
if (canConfigure && choice === 1) { // configure
312
this.taskService.openConfig(task as CustomTask);
313
}
314
}
315
}
316
);
317
}, 10_000));
318
}
319
}));
320
});
321
322
return result.finally(() => store.dispose());
323
}
324
}
325
326