Skip to content

Commit

Permalink
Introduce proper custom exceution lifecycle
Browse files Browse the repository at this point in the history
Signed-off-by: Thomas Mäder <[email protected]>
  • Loading branch information
tsmaeder committed Jun 7, 2021
1 parent 9b9cc2b commit 2300dd9
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 92 deletions.
2 changes: 1 addition & 1 deletion packages/debug/src/browser/debug-session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ export class DebugSessionManager {
return true;
}

const taskInfo = await this.taskService.runWorkspaceTask(this.taskService.startUserAction(), workspaceFolderUri, taskName);
const taskInfo = await this.taskService.runWorkspaceTask(await this.taskService.startUserAction(), workspaceFolderUri, taskName);
if (!checkErrors) {
return true;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1758,10 +1758,11 @@ export const MAIN_RPC_CONTEXT = {
};

export interface TasksExt {
$onDidStartUserInteraction(): Promise<void>;
$provideTasks(handle: number, token?: CancellationToken): Promise<TaskDto[] | undefined>;
$resolveTask(handle: number, task: TaskDto, token?: CancellationToken): Promise<TaskDto | undefined>;
$onDidStartTask(execution: TaskExecutionDto, terminalId: number): void;
$onDidEndTask(id: number): void;
$onDidEndTask(execution: TaskExecutionDto): void;
$onDidStartTaskProcess(processId: number | undefined, execution: TaskExecutionDto): void;
$onDidEndTaskProcess(exitCode: number | undefined, taskId: number): void;
}
Expand Down
13 changes: 11 additions & 2 deletions packages/plugin-ext/src/main/browser/tasks-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { TaskInfo, TaskExitedEvent, TaskConfiguration, TaskCustomization, TaskOu
import { TaskWatcher } from '@theia/task/lib/common/task-watcher';
import { TaskService } from '@theia/task/lib/browser/task-service';
import { TaskDefinitionRegistry } from '@theia/task/lib/browser';
import { ProvidedTaskConfigurations, TaskStartUserInteractionEvent } from '@theia/task/lib/browser/provided-task-configurations';

const revealKindMap = new Map<number | RevealKind, RevealKind | number>(
[
Expand Down Expand Up @@ -72,6 +73,11 @@ export class TasksMainImpl implements TasksMain, Disposable {
this.taskService = container.get(TaskService);
this.taskDefinitionRegistry = container.get(TaskDefinitionRegistry);

this.toDispose.push(container.get(ProvidedTaskConfigurations).onStartUserInteraction((event: TaskStartUserInteractionEvent) => {
// we must wait with further processing until the plugin side has had time to clean up it's garbage
event.waitUntil(this.proxy.$onDidStartUserInteraction());
}));

this.toDispose.push(this.taskWatcher.onTaskCreated((event: TaskInfo) => {
this.proxy.$onDidStartTask({
id: event.taskId,
Expand All @@ -80,7 +86,10 @@ export class TasksMainImpl implements TasksMain, Disposable {
}));

this.toDispose.push(this.taskWatcher.onTaskExit((event: TaskExitedEvent) => {
this.proxy.$onDidEndTask(event.taskId);
this.proxy.$onDidEndTask({
id: event.taskId,
task: this.fromTaskConfiguration(event.config)
});
}));

this.toDispose.push(this.taskWatcher.onDidStartTaskProcess((event: TaskInfo) => {
Expand Down Expand Up @@ -128,7 +137,7 @@ export class TasksMainImpl implements TasksMain, Disposable {
return [];
}

const token: number = this.taskService.startUserAction();
const token: number = await this.taskService.startUserAction();
const [configured, provided] = await Promise.all([
this.taskService.getConfiguredTasks(token),
this.taskService.getProvidedTasks(token)
Expand Down
159 changes: 101 additions & 58 deletions packages/plugin-ext/src/plugin/tasks/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,19 @@ import * as theia from '@theia/plugin';
import * as converter from '../type-converters';
import { CustomExecution, Disposable } from '../types-impl';
import { RPCProtocol, ConnectionClosedError } from '../../common/rpc-protocol';
import { TaskProviderAdapter } from './task-provider';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { TerminalServiceExtImpl } from '../terminal-ext';
import { UUID } from '@theia/core/shared/@phosphor/coreutils';

type ExecutionCallback = (resolvedDefintion: theia.TaskDefinition) => Thenable<theia.Pseudoterminal>;
export class TasksExtImpl implements TasksExt {
private proxy: TasksMain;

private callId = 0;
private adaptersMap = new Map<number, TaskProviderAdapter>();
private providersByHandle = new Map<number, theia.TaskProvider>();
private executions = new Map<number, theia.TaskExecution>();
protected callbackIdBase: string = UUID.uuid4();
protected callbackId: number;
protected customExecutionIds: Map<ExecutionCallback, string> = new Map();
protected customExecutionFunctions: Map<string, ExecutionCallback> = new Map();
protected callbackId: number = 0;
protected providedCustomExecutions: Map<string, theia.CustomExecution> = new Map();
protected oneOffCustomExecutions: Map<string, theia.CustomExecution> = new Map();
protected lastStartedTask: number | undefined;

private readonly onDidExecuteTask: Emitter<theia.TaskStartEvent> = new Emitter<theia.TaskStartEvent>();
Expand All @@ -67,11 +64,38 @@ export class TasksExtImpl implements TasksExt {
return this.onDidExecuteTask.event;
}

async $onDidStartUserInteraction(): Promise<void> {
console.info(`$onDidStartUserInteraction: clearing ${this.providedCustomExecutions.size} custom executions`);
this.providedCustomExecutions.clear();
return Promise.resolve();
}

private getCustomExecution(id: string | undefined): theia.CustomExecution | undefined {
if (!id) {
return undefined;
}
const result = this.providedCustomExecutions.get(id);
return result ? result : this.oneOffCustomExecutions.get(id);
}

private addProvidedCustomExecution(execution: theia.CustomExecution): string {
const id = this.nextCallbackId();
this.providedCustomExecutions.set(id, execution);
return id;
}

private addOneOffCustomExecution(execution: theia.CustomExecution): string {
const id = this.nextCallbackId();
this.oneOffCustomExecutions.set(id, execution);
return id;
}

async $onDidStartTask(execution: TaskExecutionDto, terminalId: number): Promise<void> {
const customExecution = this.customExecutionFunctions.get(execution.task.executionId || '');
const customExecution = this.getCustomExecution(execution.task.executionId);
if (customExecution) {
console.info(`running custom execution with id ${execution.task.executionId}`);
const taskDefinition = converter.toTask(execution.task).definition;
const pty = await customExecution(taskDefinition);
const pty = await customExecution.callback(taskDefinition);
this.terminalExt.attachPtyToTerminal(terminalId, pty);
if (pty.onDidClose) {
const disposable = pty.onDidClose((e: number | void = undefined) => {
Expand All @@ -92,13 +116,19 @@ export class TasksExtImpl implements TasksExt {
return this.onDidTerminateTask.event;
}

$onDidEndTask(id: number): void {
const taskExecution = this.executions.get(id);
$onDidEndTask(executionDto: TaskExecutionDto): void {
const taskExecution = this.executions.get(executionDto.id);
if (!taskExecution) {
throw new Error(`Task execution with id ${id} is not found`);
throw new Error(`Task execution with id ${executionDto.id} is not found`);
}

if (executionDto.task.executionId) {
if (this.oneOffCustomExecutions.delete(executionDto.task.executionId)) {
console.info(`removed one-off custom execution with id ${executionDto.task.executionId}`);
}
}

this.executions.delete(id);
this.executions.delete(executionDto.id);

this.onDidTerminateTask.fire({
execution: taskExecution
Expand Down Expand Up @@ -133,7 +163,7 @@ export class TasksExtImpl implements TasksExt {
}

registerTaskProvider(type: string, provider: theia.TaskProvider): theia.Disposable {
const callId = this.addNewAdapter(new TaskProviderAdapter(provider));
const callId = this.addProvider(provider);
this.proxy.$registerTaskProvider(callId, type);
return this.createDisposable(callId);
}
Expand All @@ -152,7 +182,7 @@ export class TasksExtImpl implements TasksExt {
// in the provided custom execution map that is cleaned up after the
// task is executed.
if (CustomExecution.is(task.execution!)) {
taskDto.executionId = this.addCustomExecution(task.execution!.callback);
taskDto.executionId = this.addOneOffCustomExecution(task.execution);
}
const executionDto = await this.proxy.$executeTask(taskDto);
if (executionDto) {
Expand All @@ -165,52 +195,67 @@ export class TasksExtImpl implements TasksExt {
}

$provideTasks(handle: number, token: theia.CancellationToken): Promise<TaskDto[] | undefined> {
const adapter = this.adaptersMap.get(handle);
if (adapter) {
return adapter.provideTasks(token).then(tasks => {
const provider = this.providersByHandle.get(handle);
let addedExecutions = 0;
if (provider) {
return Promise.resolve(provider.provideTasks(token)).then(tasks => {
if (tasks) {
for (const task of tasks) {
if (task.taskType === 'customExecution') {
task.executionId = this.addCustomExecution(task.callback);
task.callback = undefined;
return tasks.map(task => {
const dto = converter.fromTask(task);
if (dto && CustomExecution.is(task.execution!)) {
dto.executionId = this.addProvidedCustomExecution(task.execution);
addedExecutions++;
}
}
return dto;
}).filter(task => !!task).map(task => task!);
} else {
return undefined;
}
}).then(tasks => {
console.info(`provideTasks: added ${addedExecutions} executions`);
return tasks;
});
} else {
return Promise.reject(new Error('No adapter found to provide tasks'));
return Promise.reject(new Error(`No task provider found for handle ${handle} `));
}
}

$resolveTask(handle: number, task: TaskDto, token: theia.CancellationToken): Promise<TaskDto | undefined> {
const adapter = this.adaptersMap.get(handle);
if (adapter) {
return adapter.resolveTask(task, token).then(resolvedTask => {
if (resolvedTask && resolvedTask.taskType === 'customExecution') {
resolvedTask.executionId = this.addCustomExecution(resolvedTask.callback);
resolvedTask.callback = undefined;
$resolveTask(handle: number, dto: TaskDto, token: theia.CancellationToken): Promise<TaskDto | undefined> {
const provider = this.providersByHandle.get(handle);
if (provider) {
const task = converter.toTask(dto);
if (task) {
const resolvedTask = provider.resolveTask(task, token);
if (resolvedTask) {
return Promise.resolve(resolvedTask).then(maybeResolvedTask => {
if (maybeResolvedTask) {
const resolvedDto = converter.fromTask(maybeResolvedTask);
if (resolvedDto && CustomExecution.is(maybeResolvedTask.execution)) {
resolvedDto.executionId = this.addProvidedCustomExecution(maybeResolvedTask.execution);
console.info('resolveTask: added custom execution');
}
return resolvedDto;
}
return undefined;
});
}
return resolvedTask;
});
}
return Promise.resolve(undefined);

} else {
return Promise.reject(new Error('No adapter found to resolve task'));
return Promise.reject(new Error('No provider found to resolve task'));
}
}

private addNewAdapter(adapter: TaskProviderAdapter): number {
const callId = this.nextCallId();
this.adaptersMap.set(callId, adapter);
private addProvider(provider: theia.TaskProvider): number {
const callId = this.callId++;
this.providersByHandle.set(callId, provider);
return callId;
}

private nextCallId(): number {
return this.callId++;
}

private createDisposable(callId: number): theia.Disposable {
return new Disposable(() => {
this.adaptersMap.delete(callId);
this.providersByHandle.delete(callId);
this.proxy.$unregister(callId);
});
}
Expand All @@ -227,33 +272,31 @@ export class TasksExtImpl implements TasksExt {
}
}

private toTaskExecution(execution: TaskExecutionDto): theia.TaskExecution {
const result = {
task: converter.toTask(execution.task),
terminate: () => {
this.proxy.$terminateTask(execution.id);
}
};
if (execution.task.executionId) {
result.task.execution = this.getCustomExecution(execution.task.executionId);
}
return result;
}

private getTaskExecution(execution: TaskExecutionDto): theia.TaskExecution {
const executionId = execution.id;
let result: theia.TaskExecution | undefined = this.executions.get(executionId);
if (result) {
return result;
}

result = {
task: converter.toTask(execution.task),
terminate: () => {
this.proxy.$terminateTask(executionId);
}
};
result = this.toTaskExecution(execution);
this.executions.set(executionId, result);
return result;
}

private addCustomExecution(callback: ExecutionCallback): string {
let id = this.customExecutionIds.get(callback);
if (!id) {
id = this.nextCallbackId();
this.customExecutionIds.set(callback, id);
this.customExecutionFunctions.set(id, callback);
}
return id;
}

private nextCallbackId(): string {
return this.callbackIdBase + this.callbackId++;
}
Expand Down
23 changes: 7 additions & 16 deletions packages/plugin-ext/src/plugin/type-converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,9 @@ export function fromRangeOrRangeWithMessage(ranges: theia.Range[] | theia.Decora
});
} else {
return ranges.map((r): DecorationOptions =>
({
range: fromRange(r)!
}));
({
range: fromRange(r)!
}));
}
}

Expand Down Expand Up @@ -803,10 +803,10 @@ export function toTask(taskDto: TaskDto): theia.Task {
result.execution = getShellExecution(taskDto);
}

if (taskType === 'customExecution' || types.CustomExecution.is(execution)) {
result.execution = getCustomExecution(taskDto);
if (taskType === 'customExecution') {
// if taskType is customExecution, we need to put all the information into taskDefinition,
// because some parameters may be in taskDefinition.
// we cannot assign a custom execution because these have assigned ids which we don't have in this stateless converter
taskDefinition.label = label;
taskDefinition.command = command;
taskDefinition.args = args;
Expand Down Expand Up @@ -874,14 +874,9 @@ export function fromShellExecution(execution: theia.ShellExecution, taskDto: Tas
}

export function fromCustomExecution(execution: theia.CustomExecution, taskDto: TaskDto): TaskDto {
// handling of the execution id is must be done independently
taskDto.taskType = 'customExecution';
const callback = execution.callback;
if (callback) {
taskDto.callback = callback;
return taskDto;
} else {
throw new Error('Converting CustomExecution callback is not implemented');
}
return taskDto;
}

export function getProcessExecution(taskDto: TaskDto): theia.ProcessExecution {
Expand All @@ -903,10 +898,6 @@ export function getShellExecution(taskDto: TaskDto): theia.ShellExecution {
taskDto.options || {});
}

export function getCustomExecution(taskDto: TaskDto): theia.CustomExecution {
return new types.CustomExecution(taskDto.callback);
}

export function getShellArgs(args: undefined | (string | theia.ShellQuotedString)[]): string[] {
if (!args || args.length === 0) {
return [];
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-ext/src/plugin/types-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1756,7 +1756,7 @@ export class CustomExecution {
return this._callback;
}

public static is(value: theia.ShellExecution | theia.ProcessExecution | theia.CustomExecution): value is CustomExecution {
public static is(value: theia.ShellExecution | theia.ProcessExecution | theia.CustomExecution | undefined): value is CustomExecution {
const candidate = value as CustomExecution;
return candidate && (!!candidate._callback);
}
Expand Down
Loading

0 comments on commit 2300dd9

Please sign in to comment.