Skip to content

Commit e0a65a9

Browse files
authored
support watch task reconnection (microsoft#155120)
1 parent 23f95b1 commit e0a65a9

File tree

11 files changed

+154
-59
lines changed

11 files changed

+154
-59
lines changed

src/vs/platform/terminal/common/terminal.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ export interface IPtyHostAttachTarget {
167167
icon: TerminalIcon | undefined;
168168
fixedDimensions: IFixedTerminalDimensions | undefined;
169169
environmentVariableCollections: ISerializableEnvironmentVariableCollections | undefined;
170+
reconnectionOwner?: string;
171+
task?: { label: string; id: string; lastTask?: string; group?: string };
170172
}
171173

172174
export enum TitleEventSource {
@@ -438,6 +440,11 @@ export interface IShellLaunchConfig {
438440
*/
439441
ignoreConfigurationCwd?: boolean;
440442

443+
/**
444+
* The owner of this terminal for reconnection.
445+
*/
446+
reconnectionOwner?: string;
447+
441448
/** Whether to wait for a key press before closing the terminal. */
442449
waitOnExit?: boolean | string | ((exitCode: number) => string);
443450

@@ -462,7 +469,7 @@ export interface IShellLaunchConfig {
462469
/**
463470
* This is a terminal that attaches to an already running terminal.
464471
*/
465-
attachPersistentProcess?: { id: number; findRevivedId?: boolean; pid: number; title: string; titleSource: TitleEventSource; cwd: string; icon?: TerminalIcon; color?: string; hasChildProcesses?: boolean; fixedDimensions?: IFixedTerminalDimensions; environmentVariableCollections?: ISerializableEnvironmentVariableCollections };
472+
attachPersistentProcess?: { id: number; findRevivedId?: boolean; pid: number; title: string; titleSource: TitleEventSource; cwd: string; icon?: TerminalIcon; color?: string; hasChildProcesses?: boolean; fixedDimensions?: IFixedTerminalDimensions; environmentVariableCollections?: ISerializableEnvironmentVariableCollections; reconnectionOwner?: string; task?: { label: string; id: string; lastTask?: string; group?: string } };
466473

467474
/**
468475
* Whether the terminal process environment should be exactly as provided in
@@ -533,6 +540,11 @@ export interface IShellLaunchConfig {
533540
* Create a terminal without shell integration even when it's enabled
534541
*/
535542
ignoreShellIntegration?: boolean;
543+
544+
/**
545+
* The task associated with this terminal
546+
*/
547+
task?: { lastTask?: string; group?: string; label: string; id: string };
536548
}
537549

538550
export interface ICreateContributedTerminalProfileOptions {

src/vs/platform/terminal/common/terminalProcess.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export interface IProcessDetails {
6060
color: string | undefined;
6161
fixedDimensions: IFixedTerminalDimensions | undefined;
6262
environmentVariableCollections: ISerializableEnvironmentVariableCollections | undefined;
63+
reconnectionOwner?: string;
64+
task?: { label: string; id: string; lastTask?: string; group?: string };
6365
}
6466

6567
export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo<IProcessDetails>;

src/vs/platform/terminal/node/ptyService.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ export class PtyService extends Disposable implements IPtyService {
347347
}
348348

349349
async setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void> {
350+
this._logService.trace('ptyService#setLayoutInfo', args.tabs);
350351
this._workspaceLayoutInfos.set(args.workspaceId, args);
351352
}
352353

@@ -408,7 +409,9 @@ export class PtyService extends Disposable implements IPtyService {
408409
icon: persistentProcess.icon,
409410
color: persistentProcess.color,
410411
fixedDimensions: persistentProcess.fixedDimensions,
411-
environmentVariableCollections: persistentProcess.processLaunchOptions.options.environmentVariableCollections
412+
environmentVariableCollections: persistentProcess.processLaunchOptions.options.environmentVariableCollections,
413+
reconnectionOwner: persistentProcess.shellLaunchConfig.reconnectionOwner,
414+
task: persistentProcess.shellLaunchConfig.task
412415
};
413416
}
414417

src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,23 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
339339
this._onDidRegisterSupportedExecutions.fire();
340340
}
341341

342+
private async _restartTasks(): Promise<void> {
343+
const recentlyUsedTasks = await this.readRecentTasks();
344+
if (!recentlyUsedTasks) {
345+
return;
346+
}
347+
for (const task of recentlyUsedTasks) {
348+
if (ConfiguringTask.is(task)) {
349+
const resolved = await this.tryResolveTask(task);
350+
if (resolved) {
351+
this.run(resolved, undefined, TaskRunSource.Reconnect);
352+
}
353+
} else {
354+
this.run(task, undefined, TaskRunSource.Reconnect);
355+
}
356+
}
357+
}
358+
342359
public get onDidStateChange(): Event<ITaskEvent> {
343360
return this._onDidStateChange.event;
344361
}
@@ -405,7 +422,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
405422
this._runTerminateCommand(arg);
406423
}
407424
});
408-
409425
CommandsRegistry.registerCommand('workbench.action.tasks.showLog', () => {
410426
if (!this._canRunCommand()) {
411427
return;
@@ -602,7 +618,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
602618
return infosCount > 0;
603619
}
604620

605-
public registerTaskSystem(key: string, info: ITaskSystemInfo): void {
621+
public async registerTaskSystem(key: string, info: ITaskSystemInfo): Promise<void> {
606622
// Ideally the Web caller of registerRegisterTaskSystem would use the correct key.
607623
// However, the caller doesn't know about the workspace folders at the time of the call, even though we know about them here.
608624
if (info.platform === Platform.Platform.Web) {
@@ -622,6 +638,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
622638

623639
if (this.hasTaskSystemInfo) {
624640
this._onDidChangeTaskSystemInfo.fire();
641+
await this._restartTasks();
625642
}
626643
}
627644

@@ -661,7 +678,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
661678
}
662679
}
663680
}
664-
665681
return result;
666682
}
667683

@@ -917,7 +933,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
917933
map.get(folder).push(task);
918934
}
919935
}
920-
921936
for (const entry of recentlyUsedTasks.entries()) {
922937
const key = entry[0];
923938
const task = JSON.parse(entry[1]);
@@ -926,6 +941,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
926941
}
927942

928943
const readTasksMap: Map<string, (Task | ConfiguringTask)> = new Map();
944+
929945
async function readTasks(that: AbstractTaskService, map: Map<string, any>, isWorkspaceFile: boolean) {
930946
for (const key of map.keys()) {
931947
const custom: CustomTask[] = [];
@@ -954,7 +970,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
954970
}
955971
await readTasks(this, folderToTasksMap, false);
956972
await readTasks(this, workspaceToTaskMap, true);
957-
958973
for (const key of recentlyUsedTasks.keys()) {
959974
if (readTasksMap.has(key)) {
960975
tasks.push(readTasksMap.get(key)!);
@@ -1749,8 +1764,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
17491764
? await this.getTask(taskFolder, taskIdentifier) : task) ?? task;
17501765
}
17511766
await ProblemMatcherRegistry.onReady();
1752-
const executeResult = this._getTaskSystem().run(taskToRun, resolver);
1753-
return this._handleExecuteResult(executeResult, runSource);
1767+
const executeResult = runSource === TaskRunSource.Reconnect ? this._getTaskSystem().reconnect(taskToRun, resolver) : this._getTaskSystem().run(taskToRun, resolver);
1768+
if (executeResult) {
1769+
return this._handleExecuteResult(executeResult, runSource);
1770+
}
1771+
return { exitCode: 0 };
17541772
}
17551773

17561774
private async _handleExecuteResult(executeResult: ITaskExecuteResult, runSource?: TaskRunSource): Promise<ITaskSummary> {
@@ -1781,6 +1799,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
17811799
throw new TaskError(Severity.Warning, nls.localize('TaskSystem.active', 'There is already a task running. Terminate it first before executing another task.'), TaskErrors.RunningTask);
17821800
}
17831801
}
1802+
this._setRecentlyUsedTask(executeResult.task);
17841803
return executeResult.promise;
17851804
}
17861805

src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts

Lines changed: 73 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,52 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import * as path from 'vs/base/common/path';
7-
import * as nls from 'vs/nls';
8-
import * as Objects from 'vs/base/common/objects';
9-
import * as Types from 'vs/base/common/types';
10-
import * as Platform from 'vs/base/common/platform';
116
import * as Async from 'vs/base/common/async';
12-
import * as resources from 'vs/base/common/resources';
137
import { IStringDictionary } from 'vs/base/common/collections';
8+
import { Emitter, Event } from 'vs/base/common/event';
9+
import { isUNC } from 'vs/base/common/extpath';
10+
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
1411
import { LinkedMap, Touch } from 'vs/base/common/map';
12+
import * as Objects from 'vs/base/common/objects';
13+
import * as path from 'vs/base/common/path';
14+
import * as Platform from 'vs/base/common/platform';
15+
import * as resources from 'vs/base/common/resources';
1516
import Severity from 'vs/base/common/severity';
16-
import { Event, Emitter } from 'vs/base/common/event';
17-
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
18-
import { isUNC } from 'vs/base/common/extpath';
17+
import * as Types from 'vs/base/common/types';
18+
import * as nls from 'vs/nls';
1919

20+
import { IModelService } from 'vs/editor/common/services/model';
2021
import { IFileService } from 'vs/platform/files/common/files';
2122
import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers';
22-
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
23-
import { IModelService } from 'vs/editor/common/services/model';
24-
import { ProblemMatcher, ProblemMatcherRegistry /*, ProblemPattern, getResource */ } from 'vs/workbench/contrib/tasks/common/problemMatcher';
23+
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace';
2524
import { Markers } from 'vs/workbench/contrib/markers/common/markers';
25+
import { ProblemMatcher, ProblemMatcherRegistry /*, ProblemPattern, getResource */ } from 'vs/workbench/contrib/tasks/common/problemMatcher';
2626

27-
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
28-
import { ITerminalProfileResolverService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
29-
import { ITerminalService, ITerminalInstance, ITerminalGroupService } from 'vs/workbench/contrib/terminal/browser/terminal';
30-
import { IOutputService } from 'vs/workbench/services/output/common/output';
31-
import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEventKind, ProblemHandlingStrategy } from 'vs/workbench/contrib/tasks/common/problemCollectors';
32-
import {
33-
Task, CustomTask, ContributedTask, RevealKind, CommandOptions, IShellConfiguration, RuntimeType, PanelKind,
34-
TaskEvent, TaskEventKind, IShellQuotingOptions, ShellQuoting, CommandString, ICommandConfiguration, IExtensionTaskSource, TaskScope, RevealProblemKind, DependsOrder, TaskSourceKind, InMemoryTask, ITaskEvent, TaskSettingId
35-
} from 'vs/workbench/contrib/tasks/common/tasks';
36-
import {
37-
ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, ITaskResolver,
38-
Triggers, ITaskTerminateResponse, ITaskSystemInfoResolver, ITaskSystemInfo, IResolveSet, IResolvedVariables
39-
} from 'vs/workbench/contrib/tasks/common/taskSystem';
40-
import { URI } from 'vs/base/common/uri';
41-
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
27+
import { Codicon } from 'vs/base/common/codicons';
4228
import { Schemas } from 'vs/base/common/network';
43-
import { IPathService } from 'vs/workbench/services/path/common/pathService';
44-
import { IViewsService, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views';
45-
import { ILogService } from 'vs/platform/log/common/log';
29+
import { URI } from 'vs/base/common/uri';
4630
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
47-
import { IShellLaunchConfig, TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
48-
import { TerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy';
49-
import { TaskTerminalStatus } from 'vs/workbench/contrib/tasks/browser/taskTerminalStatus';
50-
import { ITaskService } from 'vs/workbench/contrib/tasks/common/taskService';
51-
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
31+
import { ILogService } from 'vs/platform/log/common/log';
5232
import { INotificationService } from 'vs/platform/notification/common/notification';
53-
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
33+
import { IShellLaunchConfig, TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
5434
import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings';
35+
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
36+
import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views';
37+
import { TaskTerminalStatus } from 'vs/workbench/contrib/tasks/browser/taskTerminalStatus';
38+
import { ProblemCollectorEventKind, ProblemHandlingStrategy, StartStopProblemCollector, WatchingProblemCollector } from 'vs/workbench/contrib/tasks/common/problemCollectors';
5539
import { GroupKind } from 'vs/workbench/contrib/tasks/common/taskConfiguration';
56-
import { Codicon } from 'vs/base/common/codicons';
40+
import { CommandOptions, CommandString, ContributedTask, CustomTask, DependsOrder, ICommandConfiguration, IExtensionTaskSource, InMemoryTask, IShellConfiguration, IShellQuotingOptions, ITaskEvent, PanelKind, RevealKind, RevealProblemKind, RuntimeType, ShellQuoting, Task, TaskEvent, TaskEventKind, TaskScope, TaskSettingId, TaskSourceKind } from 'vs/workbench/contrib/tasks/common/tasks';
41+
import { ITaskService } from 'vs/workbench/contrib/tasks/common/taskService';
42+
import { IResolvedVariables, IResolveSet, ITaskExecuteResult, ITaskResolver, ITaskSummary, ITaskSystem, ITaskSystemInfo, ITaskSystemInfoResolver, ITaskTerminateResponse, TaskError, TaskErrors, TaskExecuteKind, Triggers } from 'vs/workbench/contrib/tasks/common/taskSystem';
43+
import { ITerminalGroupService, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
5744
import { VSCodeOscProperty, VSCodeOscPt, VSCodeSequence } from 'vs/workbench/contrib/terminal/browser/terminalEscapeSequences';
45+
import { TerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy';
46+
import { ITerminalProfileResolverService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
47+
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
48+
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
49+
import { IOutputService } from 'vs/workbench/services/output/common/output';
50+
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
51+
import { IPathService } from 'vs/workbench/services/path/common/pathService';
5852

5953
interface ITerminalData {
6054
terminal: ITerminalInstance;
@@ -205,7 +199,8 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
205199
private _previousTerminalInstance: ITerminalInstance | undefined;
206200
private _terminalStatusManager: TaskTerminalStatus;
207201
private _terminalCreationQueue: Promise<ITerminalInstance | void> = Promise.resolve();
208-
202+
private _hasReconnected: boolean = false;
203+
private _tasksToReconnect: string[] = [];
209204
private readonly _onDidStateChange: Emitter<ITaskEvent>;
210205

211206
get taskShellIntegrationStartSequence(): string {
@@ -245,12 +240,23 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
245240
this._terminals = Object.create(null);
246241
this._idleTaskTerminals = new LinkedMap<string, string>();
247242
this._sameTaskTerminals = Object.create(null);
248-
249243
this._onDidStateChange = new Emitter();
250244
this._taskSystemInfoResolver = taskSystemInfoResolver;
251245
this._register(this._terminalStatusManager = new TaskTerminalStatus(taskService));
252246
}
253247

248+
private _reconnectToTerminals(terminals: ITerminalInstance[]): void {
249+
for (const terminal of terminals) {
250+
const taskForTerminal = terminal.shellLaunchConfig.attachPersistentProcess?.task;
251+
if (taskForTerminal?.id && taskForTerminal?.lastTask) {
252+
this._tasksToReconnect.push(taskForTerminal.id);
253+
this._terminals[terminal.instanceId] = { terminal, lastTask: taskForTerminal.lastTask, group: taskForTerminal.group };
254+
} else {
255+
this._logService.trace(`Could not reconnect to terminal ${terminal.instanceId} with process details ${terminal.shellLaunchConfig.attachPersistentProcess}`);
256+
}
257+
}
258+
}
259+
254260
public get onDidStateChange(): Event<ITaskEvent> {
255261
return this._onDidStateChange.event;
256262
}
@@ -263,6 +269,19 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
263269
this._outputService.showChannel(this._outputChannelId, true);
264270
}
265271

272+
public reconnect(task: Task, resolver: ITaskResolver, trigger: string = Triggers.command): ITaskExecuteResult | undefined {
273+
const terminals = this._terminalService.getReconnectedTerminals('Task');
274+
if (!this._hasReconnected && terminals && terminals.length > 0) {
275+
this._reconnectToTerminals(terminals);
276+
this._hasReconnected = true;
277+
}
278+
if (this._tasksToReconnect.includes(task._id)) {
279+
this._lastTask = new VerifiedTask(task, resolver, trigger);
280+
this.rerun();
281+
}
282+
return undefined;
283+
}
284+
266285
public run(task: Task, resolver: ITaskResolver, trigger: string = Triggers.command): ITaskExecuteResult {
267286
task = task.clone(); // A small amount of task state is stored in the task (instance) and tasks passed in to run may have that set already.
268287
const recentTaskKey = task.getRecentlyUsedKey() ?? '';
@@ -1269,7 +1288,18 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
12691288
return createdTerminal;
12701289
}
12711290

1291+
private _reviveTerminals(): void {
1292+
if (Object.entries(this._terminals).length === 0) {
1293+
for (const terminal of this._terminalService.instances) {
1294+
if (terminal.shellLaunchConfig.attachPersistentProcess?.task?.lastTask) {
1295+
this._terminals[terminal.instanceId] = { lastTask: terminal.shellLaunchConfig.attachPersistentProcess.task.lastTask, group: terminal.shellLaunchConfig.attachPersistentProcess.task.group, terminal };
1296+
}
1297+
}
1298+
}
1299+
}
1300+
12721301
private async _createTerminal(task: CustomTask | ContributedTask, resolver: VariableResolver, workspaceFolder: IWorkspaceFolder | undefined): Promise<[ITerminalInstance | undefined, TaskError | undefined]> {
1302+
this._reviveTerminals();
12731303
const platform = resolver.taskSystemInfo ? resolver.taskSystemInfo.platform : Platform.platform;
12741304
const options = await this._resolveOptions(resolver, task.command.options);
12751305
const presentationOptions = task.command.presentation;
@@ -1308,7 +1338,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
13081338
}, 'Executing task: {0}', task._label), { excludeLeadingNewLine: true }) : undefined,
13091339
isFeatureTerminal: true,
13101340
icon: task.configurationProperties.icon?.id ? ThemeIcon.fromId(task.configurationProperties.icon.id) : undefined,
1311-
color: task.configurationProperties.icon?.color || undefined,
1341+
color: task.configurationProperties.icon?.color || undefined
13121342
};
13131343
} else {
13141344
const resolvedResult: { command: CommandString; args: CommandString[] } = await this._resolveCommandAndArgs(resolver, task.command);
@@ -1369,9 +1399,10 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
13691399

13701400
this._terminalCreationQueue = this._terminalCreationQueue.then(() => this._doCreateTerminal(group, launchConfigs!));
13711401
const result: ITerminalInstance = (await this._terminalCreationQueue)!;
1372-
1402+
result.shellLaunchConfig.task = { lastTask: taskKey, group, label: task._label, id: task._id };
1403+
result.shellLaunchConfig.reconnectionOwner = 'Task';
13731404
const terminalKey = result.instanceId.toString();
1374-
result.onDisposed((terminal) => {
1405+
result.onDisposed(() => {
13751406
const terminalData = this._terminals[terminalKey];
13761407
if (terminalData) {
13771408
delete this._terminals[terminalKey];

src/vs/workbench/contrib/tasks/common/taskSystem.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export interface ITaskSystemInfoResolver {
102102
export interface ITaskSystem {
103103
onDidStateChange: Event<ITaskEvent>;
104104
run(task: Task, resolver: ITaskResolver): ITaskExecuteResult;
105+
reconnect(task: Task, resolver: ITaskResolver): ITaskExecuteResult | undefined;
105106
rerun(): ITaskExecuteResult | undefined;
106107
isActive(): Promise<boolean>;
107108
isActiveSync(): boolean;

0 commit comments

Comments
 (0)