Skip to content

Commit

Permalink
support watch task reconnection (#155120)
Browse files Browse the repository at this point in the history
  • Loading branch information
meganrogge authored Jul 14, 2022
1 parent 23f95b1 commit e0a65a9
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 59 deletions.
14 changes: 13 additions & 1 deletion src/vs/platform/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ export interface IPtyHostAttachTarget {
icon: TerminalIcon | undefined;
fixedDimensions: IFixedTerminalDimensions | undefined;
environmentVariableCollections: ISerializableEnvironmentVariableCollections | undefined;
reconnectionOwner?: string;
task?: { label: string; id: string; lastTask?: string; group?: string };
}

export enum TitleEventSource {
Expand Down Expand Up @@ -438,6 +440,11 @@ export interface IShellLaunchConfig {
*/
ignoreConfigurationCwd?: boolean;

/**
* The owner of this terminal for reconnection.
*/
reconnectionOwner?: string;

/** Whether to wait for a key press before closing the terminal. */
waitOnExit?: boolean | string | ((exitCode: number) => string);

Expand All @@ -462,7 +469,7 @@ export interface IShellLaunchConfig {
/**
* This is a terminal that attaches to an already running terminal.
*/
attachPersistentProcess?: { id: number; findRevivedId?: boolean; pid: number; title: string; titleSource: TitleEventSource; cwd: string; icon?: TerminalIcon; color?: string; hasChildProcesses?: boolean; fixedDimensions?: IFixedTerminalDimensions; environmentVariableCollections?: ISerializableEnvironmentVariableCollections };
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 } };

/**
* Whether the terminal process environment should be exactly as provided in
Expand Down Expand Up @@ -533,6 +540,11 @@ export interface IShellLaunchConfig {
* Create a terminal without shell integration even when it's enabled
*/
ignoreShellIntegration?: boolean;

/**
* The task associated with this terminal
*/
task?: { lastTask?: string; group?: string; label: string; id: string };
}

export interface ICreateContributedTerminalProfileOptions {
Expand Down
2 changes: 2 additions & 0 deletions src/vs/platform/terminal/common/terminalProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export interface IProcessDetails {
color: string | undefined;
fixedDimensions: IFixedTerminalDimensions | undefined;
environmentVariableCollections: ISerializableEnvironmentVariableCollections | undefined;
reconnectionOwner?: string;
task?: { label: string; id: string; lastTask?: string; group?: string };
}

export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo<IProcessDetails>;
Expand Down
5 changes: 4 additions & 1 deletion src/vs/platform/terminal/node/ptyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ export class PtyService extends Disposable implements IPtyService {
}

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

Expand Down Expand Up @@ -408,7 +409,9 @@ export class PtyService extends Disposable implements IPtyService {
icon: persistentProcess.icon,
color: persistentProcess.color,
fixedDimensions: persistentProcess.fixedDimensions,
environmentVariableCollections: persistentProcess.processLaunchOptions.options.environmentVariableCollections
environmentVariableCollections: persistentProcess.processLaunchOptions.options.environmentVariableCollections,
reconnectionOwner: persistentProcess.shellLaunchConfig.reconnectionOwner,
task: persistentProcess.shellLaunchConfig.task
};
}

Expand Down
33 changes: 26 additions & 7 deletions src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,23 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
this._onDidRegisterSupportedExecutions.fire();
}

private async _restartTasks(): Promise<void> {
const recentlyUsedTasks = await this.readRecentTasks();
if (!recentlyUsedTasks) {
return;
}
for (const task of recentlyUsedTasks) {
if (ConfiguringTask.is(task)) {
const resolved = await this.tryResolveTask(task);
if (resolved) {
this.run(resolved, undefined, TaskRunSource.Reconnect);
}
} else {
this.run(task, undefined, TaskRunSource.Reconnect);
}
}
}

public get onDidStateChange(): Event<ITaskEvent> {
return this._onDidStateChange.event;
}
Expand Down Expand Up @@ -405,7 +422,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
this._runTerminateCommand(arg);
}
});

CommandsRegistry.registerCommand('workbench.action.tasks.showLog', () => {
if (!this._canRunCommand()) {
return;
Expand Down Expand Up @@ -602,7 +618,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
return infosCount > 0;
}

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

if (this.hasTaskSystemInfo) {
this._onDidChangeTaskSystemInfo.fire();
await this._restartTasks();
}
}

Expand Down Expand Up @@ -661,7 +678,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
}
}
}

return result;
}

Expand Down Expand Up @@ -917,7 +933,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
map.get(folder).push(task);
}
}

for (const entry of recentlyUsedTasks.entries()) {
const key = entry[0];
const task = JSON.parse(entry[1]);
Expand All @@ -926,6 +941,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
}

const readTasksMap: Map<string, (Task | ConfiguringTask)> = new Map();

async function readTasks(that: AbstractTaskService, map: Map<string, any>, isWorkspaceFile: boolean) {
for (const key of map.keys()) {
const custom: CustomTask[] = [];
Expand Down Expand Up @@ -954,7 +970,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
}
await readTasks(this, folderToTasksMap, false);
await readTasks(this, workspaceToTaskMap, true);

for (const key of recentlyUsedTasks.keys()) {
if (readTasksMap.has(key)) {
tasks.push(readTasksMap.get(key)!);
Expand Down Expand Up @@ -1749,8 +1764,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
? await this.getTask(taskFolder, taskIdentifier) : task) ?? task;
}
await ProblemMatcherRegistry.onReady();
const executeResult = this._getTaskSystem().run(taskToRun, resolver);
return this._handleExecuteResult(executeResult, runSource);
const executeResult = runSource === TaskRunSource.Reconnect ? this._getTaskSystem().reconnect(taskToRun, resolver) : this._getTaskSystem().run(taskToRun, resolver);
if (executeResult) {
return this._handleExecuteResult(executeResult, runSource);
}
return { exitCode: 0 };
}

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

Expand Down
115 changes: 73 additions & 42 deletions src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,52 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as path from 'vs/base/common/path';
import * as nls from 'vs/nls';
import * as Objects from 'vs/base/common/objects';
import * as Types from 'vs/base/common/types';
import * as Platform from 'vs/base/common/platform';
import * as Async from 'vs/base/common/async';
import * as resources from 'vs/base/common/resources';
import { IStringDictionary } from 'vs/base/common/collections';
import { Emitter, Event } from 'vs/base/common/event';
import { isUNC } from 'vs/base/common/extpath';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { LinkedMap, Touch } from 'vs/base/common/map';
import * as Objects from 'vs/base/common/objects';
import * as path from 'vs/base/common/path';
import * as Platform from 'vs/base/common/platform';
import * as resources from 'vs/base/common/resources';
import Severity from 'vs/base/common/severity';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { isUNC } from 'vs/base/common/extpath';
import * as Types from 'vs/base/common/types';
import * as nls from 'vs/nls';

import { IModelService } from 'vs/editor/common/services/model';
import { IFileService } from 'vs/platform/files/common/files';
import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers';
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IModelService } from 'vs/editor/common/services/model';
import { ProblemMatcher, ProblemMatcherRegistry /*, ProblemPattern, getResource */ } from 'vs/workbench/contrib/tasks/common/problemMatcher';
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { Markers } from 'vs/workbench/contrib/markers/common/markers';
import { ProblemMatcher, ProblemMatcherRegistry /*, ProblemPattern, getResource */ } from 'vs/workbench/contrib/tasks/common/problemMatcher';

import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { ITerminalProfileResolverService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITerminalService, ITerminalInstance, ITerminalGroupService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IOutputService } from 'vs/workbench/services/output/common/output';
import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEventKind, ProblemHandlingStrategy } from 'vs/workbench/contrib/tasks/common/problemCollectors';
import {
Task, CustomTask, ContributedTask, RevealKind, CommandOptions, IShellConfiguration, RuntimeType, PanelKind,
TaskEvent, TaskEventKind, IShellQuotingOptions, ShellQuoting, CommandString, ICommandConfiguration, IExtensionTaskSource, TaskScope, RevealProblemKind, DependsOrder, TaskSourceKind, InMemoryTask, ITaskEvent, TaskSettingId
} from 'vs/workbench/contrib/tasks/common/tasks';
import {
ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, ITaskResolver,
Triggers, ITaskTerminateResponse, ITaskSystemInfoResolver, ITaskSystemInfo, IResolveSet, IResolvedVariables
} from 'vs/workbench/contrib/tasks/common/taskSystem';
import { URI } from 'vs/base/common/uri';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { Codicon } from 'vs/base/common/codicons';
import { Schemas } from 'vs/base/common/network';
import { IPathService } from 'vs/workbench/services/path/common/pathService';
import { IViewsService, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views';
import { ILogService } from 'vs/platform/log/common/log';
import { URI } from 'vs/base/common/uri';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IShellLaunchConfig, TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { TerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy';
import { TaskTerminalStatus } from 'vs/workbench/contrib/tasks/browser/taskTerminalStatus';
import { ITaskService } from 'vs/workbench/contrib/tasks/common/taskService';
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IShellLaunchConfig, TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views';
import { TaskTerminalStatus } from 'vs/workbench/contrib/tasks/browser/taskTerminalStatus';
import { ProblemCollectorEventKind, ProblemHandlingStrategy, StartStopProblemCollector, WatchingProblemCollector } from 'vs/workbench/contrib/tasks/common/problemCollectors';
import { GroupKind } from 'vs/workbench/contrib/tasks/common/taskConfiguration';
import { Codicon } from 'vs/base/common/codicons';
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';
import { ITaskService } from 'vs/workbench/contrib/tasks/common/taskService';
import { IResolvedVariables, IResolveSet, ITaskExecuteResult, ITaskResolver, ITaskSummary, ITaskSystem, ITaskSystemInfo, ITaskSystemInfoResolver, ITaskTerminateResponse, TaskError, TaskErrors, TaskExecuteKind, Triggers } from 'vs/workbench/contrib/tasks/common/taskSystem';
import { ITerminalGroupService, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { VSCodeOscProperty, VSCodeOscPt, VSCodeSequence } from 'vs/workbench/contrib/terminal/browser/terminalEscapeSequences';
import { TerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy';
import { ITerminalProfileResolverService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IOutputService } from 'vs/workbench/services/output/common/output';
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
import { IPathService } from 'vs/workbench/services/path/common/pathService';

interface ITerminalData {
terminal: ITerminalInstance;
Expand Down Expand Up @@ -205,7 +199,8 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
private _previousTerminalInstance: ITerminalInstance | undefined;
private _terminalStatusManager: TaskTerminalStatus;
private _terminalCreationQueue: Promise<ITerminalInstance | void> = Promise.resolve();

private _hasReconnected: boolean = false;
private _tasksToReconnect: string[] = [];
private readonly _onDidStateChange: Emitter<ITaskEvent>;

get taskShellIntegrationStartSequence(): string {
Expand Down Expand Up @@ -245,12 +240,23 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
this._terminals = Object.create(null);
this._idleTaskTerminals = new LinkedMap<string, string>();
this._sameTaskTerminals = Object.create(null);

this._onDidStateChange = new Emitter();
this._taskSystemInfoResolver = taskSystemInfoResolver;
this._register(this._terminalStatusManager = new TaskTerminalStatus(taskService));
}

private _reconnectToTerminals(terminals: ITerminalInstance[]): void {
for (const terminal of terminals) {
const taskForTerminal = terminal.shellLaunchConfig.attachPersistentProcess?.task;
if (taskForTerminal?.id && taskForTerminal?.lastTask) {
this._tasksToReconnect.push(taskForTerminal.id);
this._terminals[terminal.instanceId] = { terminal, lastTask: taskForTerminal.lastTask, group: taskForTerminal.group };
} else {
this._logService.trace(`Could not reconnect to terminal ${terminal.instanceId} with process details ${terminal.shellLaunchConfig.attachPersistentProcess}`);
}
}
}

public get onDidStateChange(): Event<ITaskEvent> {
return this._onDidStateChange.event;
}
Expand All @@ -263,6 +269,19 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
this._outputService.showChannel(this._outputChannelId, true);
}

public reconnect(task: Task, resolver: ITaskResolver, trigger: string = Triggers.command): ITaskExecuteResult | undefined {
const terminals = this._terminalService.getReconnectedTerminals('Task');
if (!this._hasReconnected && terminals && terminals.length > 0) {
this._reconnectToTerminals(terminals);
this._hasReconnected = true;
}
if (this._tasksToReconnect.includes(task._id)) {
this._lastTask = new VerifiedTask(task, resolver, trigger);
this.rerun();
}
return undefined;
}

public run(task: Task, resolver: ITaskResolver, trigger: string = Triggers.command): ITaskExecuteResult {
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.
const recentTaskKey = task.getRecentlyUsedKey() ?? '';
Expand Down Expand Up @@ -1269,7 +1288,18 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
return createdTerminal;
}

private _reviveTerminals(): void {
if (Object.entries(this._terminals).length === 0) {
for (const terminal of this._terminalService.instances) {
if (terminal.shellLaunchConfig.attachPersistentProcess?.task?.lastTask) {
this._terminals[terminal.instanceId] = { lastTask: terminal.shellLaunchConfig.attachPersistentProcess.task.lastTask, group: terminal.shellLaunchConfig.attachPersistentProcess.task.group, terminal };
}
}
}
}

private async _createTerminal(task: CustomTask | ContributedTask, resolver: VariableResolver, workspaceFolder: IWorkspaceFolder | undefined): Promise<[ITerminalInstance | undefined, TaskError | undefined]> {
this._reviveTerminals();
const platform = resolver.taskSystemInfo ? resolver.taskSystemInfo.platform : Platform.platform;
const options = await this._resolveOptions(resolver, task.command.options);
const presentationOptions = task.command.presentation;
Expand Down Expand Up @@ -1308,7 +1338,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
}, 'Executing task: {0}', task._label), { excludeLeadingNewLine: true }) : undefined,
isFeatureTerminal: true,
icon: task.configurationProperties.icon?.id ? ThemeIcon.fromId(task.configurationProperties.icon.id) : undefined,
color: task.configurationProperties.icon?.color || undefined,
color: task.configurationProperties.icon?.color || undefined
};
} else {
const resolvedResult: { command: CommandString; args: CommandString[] } = await this._resolveCommandAndArgs(resolver, task.command);
Expand Down Expand Up @@ -1369,9 +1399,10 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {

this._terminalCreationQueue = this._terminalCreationQueue.then(() => this._doCreateTerminal(group, launchConfigs!));
const result: ITerminalInstance = (await this._terminalCreationQueue)!;

result.shellLaunchConfig.task = { lastTask: taskKey, group, label: task._label, id: task._id };
result.shellLaunchConfig.reconnectionOwner = 'Task';
const terminalKey = result.instanceId.toString();
result.onDisposed((terminal) => {
result.onDisposed(() => {
const terminalData = this._terminals[terminalKey];
if (terminalData) {
delete this._terminals[terminalKey];
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/contrib/tasks/common/taskSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export interface ITaskSystemInfoResolver {
export interface ITaskSystem {
onDidStateChange: Event<ITaskEvent>;
run(task: Task, resolver: ITaskResolver): ITaskExecuteResult;
reconnect(task: Task, resolver: ITaskResolver): ITaskExecuteResult | undefined;
rerun(): ITaskExecuteResult | undefined;
isActive(): Promise<boolean>;
isActiveSync(): boolean;
Expand Down
Loading

0 comments on commit e0a65a9

Please sign in to comment.