From 387b3679e45665e4520a2cad5706d5bb028e5f31 Mon Sep 17 00:00:00 2001 From: Lewin Tan Date: Sun, 14 Mar 2021 10:51:31 +0800 Subject: [PATCH] Implement CustomExecution extension API. issue: #7185, #8767 Signed-off-by: Lewin Tan --- .../plugin-ext/src/common/plugin-api-rpc.ts | 60 ++++++++++++-- .../plugin-ext/src/main/browser/tasks-main.ts | 16 +++- .../src/main/browser/terminal-main.ts | 56 ++++++++++++- .../plugin-ext/src/plugin/plugin-context.ts | 4 +- packages/plugin-ext/src/plugin/tasks/tasks.ts | 47 ++++++++++- .../plugin-ext/src/plugin/terminal-ext.ts | 34 ++++++-- .../plugin-ext/src/plugin/type-converters.ts | 22 ++++++ packages/plugin-ext/src/plugin/types-impl.ts | 51 ++++++++++-- packages/plugin/src/theia.d.ts | 20 ++++- packages/task/src/browser/task-service.ts | 4 + packages/task/src/common/task-protocol.ts | 2 + .../custom-task-runner-backend-module.ts | 37 +++++++++ .../custom/custom-task-runner-contribution.ts | 30 +++++++ .../src/node/custom/custom-task-runner.ts | 77 ++++++++++++++++++ packages/task/src/node/custom/custom-task.ts | 78 +++++++++++++++++++ packages/task/src/node/task-backend-module.ts | 2 + packages/task/src/node/task-server.ts | 7 ++ .../src/browser/base/terminal-widget.ts | 2 + .../src/browser/terminal-widget-impl.ts | 28 ++++++- 19 files changed, 544 insertions(+), 33 deletions(-) create mode 100644 packages/task/src/node/custom/custom-task-runner-backend-module.ts create mode 100644 packages/task/src/node/custom/custom-task-runner-contribution.ts create mode 100644 packages/task/src/node/custom/custom-task-runner.ts create mode 100644 packages/task/src/node/custom/custom-task.ts diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 552ab5e1db69b..300c23d789240 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -248,7 +248,7 @@ export interface CommandRegistryExt { export interface TerminalServiceExt { $terminalCreated(id: string, name: string): void; $terminalNameChanged(id: string, name: string): void; - $terminalOpened(id: string, processId: number, cols: number, rows: number): void; + $terminalOpened(id: string, processId: number, terminalId: number, cols: number, rows: number): void; $terminalClosed(id: string): void; $terminalOnInput(id: string, data: string): void; $terminalSizeChanged(id: string, cols: number, rows: number): void; @@ -283,7 +283,7 @@ export interface TerminalServiceMain { /** * Send text to the terminal by id. - * @param id - terminal id. + * @param id - terminal widget id. * @param text - text content. * @param addNewLine - in case true - add new line after the text, otherwise - don't apply new line. */ @@ -291,14 +291,14 @@ export interface TerminalServiceMain { /** * Write data to the terminal by id. - * @param id - terminal id. + * @param id - terminal widget id. * @param data - data. */ $write(id: string, data: string): void; /** * Resize the terminal by id. - * @param id - terminal id. + * @param id - terminal widget id. * @param cols - columns. * @param rows - rows. */ @@ -306,23 +306,66 @@ export interface TerminalServiceMain { /** * Show terminal on the UI panel. - * @param id - terminal id. + * @param id - terminal widget id. * @param preserveFocus - set terminal focus in case true value, and don't set focus otherwise. */ $show(id: string, preserveFocus?: boolean): void; /** * Hide UI panel where is located terminal widget. - * @param id - terminal id. + * @param id - terminal widget id. */ $hide(id: string): void; /** * Destroy terminal. - * @param id - terminal id. + * @param id - terminal widget id. */ $dispose(id: string): void; + /** + * Send text to the terminal by id. + * @param id - terminal id. + * @param text - text content. + * @param addNewLine - in case true - add new line after the text, otherwise - don't apply new line. + */ + $sendTextByTerminalId(id: number, text: string, addNewLine?: boolean): void; + + /** + * Write data to the terminal by id. + * @param id - terminal id. + * @param data - data. + */ + $writeByTerminalId(id: number, data: string): void; + + /** + * Resize the terminal by id. + * @param id - terminal id. + * @param cols - columns. + * @param rows - rows. + */ + $resizeByTerminalId(id: number, cols: number, rows: number): void; + + /** + * Show terminal on the UI panel. + * @param id - terminal id. + * @param preserveFocus - set terminal focus in case true value, and don't set focus otherwise. + */ + $showByTerminalId(id: number, preserveFocus?: boolean): void; + + /** + * Hide UI panel where is located terminal widget. + * @param id - terminal id. + */ + $hideByTerminalId(id: number): void; + + /** + * Destroy terminal. + * @param id - terminal id. + * @param waitOnExit - Whether to wait for a key press before closing the terminal. + */ + $disposeByTerminalId(id: number, waitOnExit?: boolean | string): void; + $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection | undefined): void; } @@ -1718,7 +1761,7 @@ export const MAIN_RPC_CONTEXT = { export interface TasksExt { $provideTasks(handle: number, token?: CancellationToken): Promise; $resolveTask(handle: number, task: TaskDto, token?: CancellationToken): Promise; - $onDidStartTask(execution: TaskExecutionDto): void; + $onDidStartTask(execution: TaskExecutionDto, terminalId: number, resolvedDefinition: theia.TaskDefinition): void; $onDidEndTask(id: number): void; $onDidStartTaskProcess(processId: number | undefined, execution: TaskExecutionDto): void; $onDidEndTaskProcess(exitCode: number | undefined, taskId: number): void; @@ -1731,6 +1774,7 @@ export interface TasksMain { $taskExecutions(): Promise; $unregister(handle: number): void; $terminateTask(id: number): void; + $customExecutionComplete(id: number, exitCode: number | undefined): void; } export interface AuthenticationExt { diff --git a/packages/plugin-ext/src/main/browser/tasks-main.ts b/packages/plugin-ext/src/main/browser/tasks-main.ts index 7cb79fa8f839e..2ef9ce7f3ea48 100644 --- a/packages/plugin-ext/src/main/browser/tasks-main.ts +++ b/packages/plugin-ext/src/main/browser/tasks-main.ts @@ -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 * as theia from '@theia/plugin'; const revealKindMap = new Map( [ @@ -73,10 +74,19 @@ export class TasksMainImpl implements TasksMain, Disposable { this.taskDefinitionRegistry = container.get(TaskDefinitionRegistry); this.toDispose.push(this.taskWatcher.onTaskCreated((event: TaskInfo) => { + const taskDefinition = { + type: event.config.type + } as theia.TaskDefinition; + const { type, ...properties } = event.config; + for (const key in properties) { + if (properties.hasOwnProperty(key)) { + taskDefinition[key] = properties[key]; + } + } this.proxy.$onDidStartTask({ id: event.taskId, task: this.fromTaskConfiguration(event.config) - }); + }, event.terminalId!!, taskDefinition); })); this.toDispose.push(this.taskWatcher.onTaskExit((event: TaskExitedEvent) => { @@ -177,6 +187,10 @@ export class TasksMainImpl implements TasksMain, Disposable { this.taskService.kill(id); } + async $customExecutionComplete(id: number, exitCode: number | undefined): Promise { + this.taskService.customExecutionComplete(id, exitCode); + } + protected createTaskProvider(handle: number): TaskProvider { return { provideTasks: () => diff --git a/packages/plugin-ext/src/main/browser/terminal-main.ts b/packages/plugin-ext/src/main/browser/terminal-main.ts index 9ab198ce42281..083fe0e6800f1 100644 --- a/packages/plugin-ext/src/main/browser/terminal-main.ts +++ b/packages/plugin-ext/src/main/browser/terminal-main.ts @@ -85,7 +85,7 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, Disposable this.toDispose.push(Disposable.create(() => terminal.title.changed.disconnect(updateTitle))); const updateProcessId = () => terminal.processId.then( - processId => this.extProxy.$terminalOpened(terminal.id, processId, terminal.dimensions.cols, terminal.dimensions.rows), + processId => this.extProxy.$terminalOpened(terminal.id, processId, terminal.terminalId, terminal.dimensions.cols, terminal.dimensions.rows), () => {/* no-op */ } ); updateProcessId(); @@ -174,4 +174,58 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, Disposable terminal.dispose(); } } + + $sendTextByTerminalId(id: number, text: string, addNewLine?: boolean): void { + const terminal = this.terminals.getByTerminalId(id); + if (terminal) { + text = text.replace(/\r?\n/g, '\r'); + if (addNewLine && text.charAt(text.length - 1) !== '\r') { + text += '\r'; + } + terminal.sendText(text); + } + } + $writeByTerminalId(id: number, data: string): void { + const terminal = this.terminals.getByTerminalId(id); + if (!terminal) { + return; + } + terminal.write(data); + } + $resizeByTerminalId(id: number, cols: number, rows: number): void { + const terminal = this.terminals.getByTerminalId(id); + if (!terminal) { + return; + } + terminal.resize(cols, rows); + } + $showByTerminalId(id: number, preserveFocus?: boolean): void { + const terminal = this.terminals.getByTerminalId(id); + if (terminal) { + const options: WidgetOpenerOptions = {}; + if (preserveFocus) { + options.mode = 'reveal'; + } + this.terminals.open(terminal, options); + } + } + $hideByTerminalId(id: number): void { + const terminal = this.terminals.getByTerminalId(id); + if (terminal && terminal.isVisible) { + const area = this.shell.getAreaFor(terminal); + if (area) { + this.shell.collapsePanel(area); + } + } + } + $disposeByTerminalId(id: number, waitOnExit?: boolean | string): void { + const terminal = this.terminals.getByTerminalId(id); + if (terminal) { + if (waitOnExit) { + terminal.waitOnExit(waitOnExit); + return; + } + terminal.dispose(); + } + } } diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 76b343fef1379..08969dd8b77b8 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -95,6 +95,7 @@ import { ShellQuoting, ShellExecution, ProcessExecution, + CustomExecution, TaskScope, TaskPanelKind, TaskRevealKind, @@ -194,7 +195,7 @@ export function createAPIFactory( const outputChannelRegistryExt = rpc.set(MAIN_RPC_CONTEXT.OUTPUT_CHANNEL_REGISTRY_EXT, new OutputChannelRegistryExtImpl(rpc)); const languagesExt = rpc.set(MAIN_RPC_CONTEXT.LANGUAGES_EXT, new LanguagesExtImpl(rpc, documents, commandRegistry)); const treeViewsExt = rpc.set(MAIN_RPC_CONTEXT.TREE_VIEWS_EXT, new TreeViewsExtImpl(rpc, commandRegistry)); - const tasksExt = rpc.set(MAIN_RPC_CONTEXT.TASKS_EXT, new TasksExtImpl(rpc)); + const tasksExt = rpc.set(MAIN_RPC_CONTEXT.TASKS_EXT, new TasksExtImpl(rpc, terminalExt)); const connectionExt = rpc.set(MAIN_RPC_CONTEXT.CONNECTION_EXT, new ConnectionExtImpl(rpc)); const fileSystemExt = rpc.set(MAIN_RPC_CONTEXT.FILE_SYSTEM_EXT, new FileSystemExtImpl(rpc, languagesExt)); const extHostFileSystemEvent = rpc.set(MAIN_RPC_CONTEXT.ExtHostFileSystemEventService, new ExtHostFileSystemEventService(rpc, editorsAndDocumentsExt)); @@ -910,6 +911,7 @@ export function createAPIFactory( ShellQuoting, ShellExecution, ProcessExecution, + CustomExecution, TaskScope, TaskRevealKind, TaskPanelKind, diff --git a/packages/plugin-ext/src/plugin/tasks/tasks.ts b/packages/plugin-ext/src/plugin/tasks/tasks.ts index 1e5ea1dbd3035..2494915b2c911 100644 --- a/packages/plugin-ext/src/plugin/tasks/tasks.ts +++ b/packages/plugin-ext/src/plugin/tasks/tasks.ts @@ -23,10 +23,11 @@ import { } from '../../common/plugin-api-rpc'; import * as theia from '@theia/plugin'; import * as converter from '../type-converters'; -import { Disposable } from '../types-impl'; +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'; export class TasksExtImpl implements TasksExt { private proxy: TasksMain; @@ -34,6 +35,8 @@ export class TasksExtImpl implements TasksExt { private callId = 0; private adaptersMap = new Map(); private executions = new Map(); + protected providedCustomExecutions: Map; + protected activeCustomExecutions: Map; private readonly onDidExecuteTask: Emitter = new Emitter(); private readonly onDidTerminateTask: Emitter = new Emitter(); @@ -42,8 +45,10 @@ export class TasksExtImpl implements TasksExt { private disposed = false; - constructor(rpc: RPCProtocol) { + constructor(rpc: RPCProtocol, readonly terminalExt: TerminalServiceExtImpl) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.TASKS_MAIN); + this.providedCustomExecutions = new Map(); + this.activeCustomExecutions = new Map(); this.fetchTaskExecutions(); } @@ -59,7 +64,24 @@ export class TasksExtImpl implements TasksExt { return this.onDidExecuteTask.event; } - $onDidStartTask(execution: TaskExecutionDto): void { + async $onDidStartTask(execution: TaskExecutionDto, terminalId: number, resolvedDefinition: theia.TaskDefinition): Promise { + const customExecution: CustomExecution | undefined = this.providedCustomExecutions.get(execution.task.id); + if (customExecution) { + if (this.activeCustomExecutions.get(execution.id) !== undefined) { + throw new Error('We should not be trying to start the same custom task executions twice.'); + } + + // Clone the custom execution to keep the original untouched. This is important for multiple runs of the same task. + this.activeCustomExecutions.set(execution.id, customExecution); + const pty = await customExecution.callback(resolvedDefinition); + this.terminalExt.attachPtyToTerminal(terminalId, pty); + if (pty.onDidClose) { + pty.onDidClose((e: number | void = undefined) => { + // eslint-disable-next-line no-void + this.proxy.$customExecutionComplete(execution.id, e === void 0 ? undefined : e); + }); + } + } this.onDidExecuteTask.fire({ execution: this.getTaskExecution(execution) }); @@ -76,6 +98,7 @@ export class TasksExtImpl implements TasksExt { } this.executions.delete(id); + this.customExecutionComplete(id); this.onDidTerminateTask.fire({ execution: taskExecution @@ -138,7 +161,16 @@ export class TasksExtImpl implements TasksExt { $provideTasks(handle: number, token: theia.CancellationToken): Promise { const adapter = this.adaptersMap.get(handle); if (adapter) { - return adapter.provideTasks(token); + return adapter.provideTasks(token).then(tasks => { + if (tasks) { + for (const task of tasks) { + if (task.type === 'customExecution') { + this.providedCustomExecutions.set(task.id, new CustomExecution(task.callback)); + } + } + } + return tasks; + }); } else { return Promise.reject(new Error('No adapter found to provide tasks')); } @@ -198,4 +230,11 @@ export class TasksExtImpl implements TasksExt { this.executions.set(executionId, result); return result; } + + private customExecutionComplete(id: number): void { + const extensionCallback2: CustomExecution | undefined = this.activeCustomExecutions.get(id); + if (extensionCallback2) { + this.activeCustomExecutions.delete(id); + } + } } diff --git a/packages/plugin-ext/src/plugin/terminal-ext.ts b/packages/plugin-ext/src/plugin/terminal-ext.ts index a2f576deee453..4546588aaa983 100644 --- a/packages/plugin-ext/src/plugin/terminal-ext.ts +++ b/packages/plugin-ext/src/plugin/terminal-ext.ts @@ -79,6 +79,10 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { return this.obtainTerminal(id, options.name || 'Terminal'); } + public attachPtyToTerminal(terminalId: number, pty: theia.Pseudoterminal): void { + this._pseudoTerminals.set(terminalId.toString(), new PseudoTerminal(terminalId, this.proxy, pty, true)); + } + protected obtainTerminal(id: string, name: string): TerminalExtImpl { let terminal = this._terminals.get(id); if (!terminal) { @@ -118,7 +122,7 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { } } - $terminalOpened(id: string, processId: number, cols: number, rows: number): void { + $terminalOpened(id: string, processId: number, terminalId: number, cols: number, rows: number): void { const terminal = this._terminals.get(id); if (terminal) { // resolve for existing clients @@ -127,7 +131,7 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { terminal.deferredProcessId = new Deferred(); terminal.deferredProcessId.resolve(processId); } - const pseudoTerminal = this._pseudoTerminals.get(id); + const pseudoTerminal = this._pseudoTerminals.get(terminalId.toString()); if (pseudoTerminal) { pseudoTerminal.emitOnOpen(cols, rows); } @@ -292,22 +296,36 @@ export class TerminalExtImpl implements Terminal { export class PseudoTerminal { constructor( - id: string, + id: string | number, private readonly proxy: TerminalServiceMain, - private readonly pseudoTerminal: theia.Pseudoterminal + private readonly pseudoTerminal: theia.Pseudoterminal, + waitOnExit?: boolean | string ) { + pseudoTerminal.onDidWrite(data => { - this.proxy.$write(id, data); + if (typeof id === 'string') { + this.proxy.$write(id, data); + } else { + this.proxy.$writeByTerminalId(id, data); + } }); if (pseudoTerminal.onDidClose) { - pseudoTerminal.onDidClose(() => { - this.proxy.$dispose(id); + pseudoTerminal.onDidClose((e: number | void = undefined) => { + if (typeof id === 'string') { + this.proxy.$dispose(id); + } else { + this.proxy.$disposeByTerminalId(id, waitOnExit); + } }); } if (pseudoTerminal.onDidOverrideDimensions) { pseudoTerminal.onDidOverrideDimensions(e => { if (e) { - this.proxy.$resize(id, e.columns, e.rows); + if (typeof id === 'string') { + this.proxy.$resize(id, e.columns, e.rows); + } else { + this.proxy.$resizeByTerminalId(id, e.columns, e.rows); + } } }); } diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index da2fb3ceacbf6..44b413d7bb4c1 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -758,6 +758,10 @@ export function fromTask(task: theia.Task): TaskDto | undefined { return fromProcessExecution(execution, taskDto); } + if (taskDefinition.type === 'customExecution' || types.CustomExecution.is(execution)) { + return fromCustomExecution(execution, taskDto); + } + return taskDto; } @@ -800,6 +804,10 @@ export function toTask(taskDto: TaskDto): theia.Task { result.execution = getShellExecution(taskDto); } + if (taskType === 'customExecution' || types.CustomExecution.is(execution)) { + result.execution = getCustomExecution(taskDto); + } + if (group) { if (group === BUILD_GROUP) { result.group = TaskGroup.Build; @@ -858,6 +866,16 @@ export function fromShellExecution(execution: theia.ShellExecution, taskDto: Tas } } +export function fromCustomExecution(execution: theia.CustomExecution, taskDto: TaskDto): TaskDto { + const callback = execution.callback; + if (callback) { + taskDto.callback = callback; + return taskDto; + } else { + throw new Error('Converting CustomExecution callback is not implemented'); + } +} + export function getProcessExecution(taskDto: TaskDto): theia.ProcessExecution { return new types.ProcessExecution( taskDto.command, @@ -877,6 +895,10 @@ 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 []; diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 88e224f1a0215..76f54d87b5ce2 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1543,7 +1543,7 @@ export class ProcessExecution { return computeTaskExecutionId(props); } - public static is(value: theia.ShellExecution | theia.ProcessExecution): boolean { + public static is(value: theia.ShellExecution | theia.ProcessExecution | theia.CustomExecution): boolean { const candidate = value as ProcessExecution; return candidate && !!candidate.process; } @@ -1651,12 +1651,35 @@ export class ShellExecution { return computeTaskExecutionId(props); } - public static is(value: theia.ShellExecution | theia.ProcessExecution): boolean { + public static is(value: theia.ShellExecution | theia.ProcessExecution | theia.CustomExecution): boolean { const candidate = value as ShellExecution; return candidate && (!!candidate.commandLine || !!candidate.command); } } +export class CustomExecution { + private _callback: (resolvedDefintion: theia.TaskDefinition) => Thenable; + constructor(callback: (resolvedDefintion: theia.TaskDefinition) => Thenable) { + this._callback = callback; + } + public computeId(): string { + return 'customExecution' + UUID.uuid4(); + } + + public set callback(value: (resolvedDefintion: theia.TaskDefinition) => Thenable) { + this._callback = value; + } + + public get callback(): ((resolvedDefintion: theia.TaskDefinition) => Thenable) { + return this._callback; + } + + public static is(value: theia.ShellExecution | theia.ProcessExecution | theia.CustomExecution): boolean { + const candidate = value as CustomExecution; + return candidate && (!!candidate._callback); + } +} + export class TaskGroup { private groupId: string; @@ -1704,7 +1727,7 @@ export class Task { private taskDefinition: theia.TaskDefinition; private taskScope: theia.TaskScope.Global | theia.TaskScope.Workspace | theia.WorkspaceFolder | undefined; private taskName: string; - private taskExecution: ProcessExecution | ShellExecution | undefined; + private taskExecution: ProcessExecution | ShellExecution | CustomExecution | undefined; private taskProblemMatchers: string[]; private hasTaskProblemMatchers: boolean; private isTaskBackground: boolean; @@ -1716,7 +1739,7 @@ export class Task { scope: theia.WorkspaceFolder | theia.TaskScope.Global | theia.TaskScope.Workspace, name: string, source: string, - execution?: ProcessExecution | ShellExecution, + execution?: ProcessExecution | ShellExecution | CustomExecution, problemMatchers?: string | string[] ); @@ -1725,7 +1748,7 @@ export class Task { taskDefinition: theia.TaskDefinition, name: string, source: string, - execution?: ProcessExecution | ShellExecution, + execution?: ProcessExecution | ShellExecution | CustomExecution, problemMatchers?: string | string[], ); @@ -1735,7 +1758,7 @@ export class Task { let scope: theia.WorkspaceFolder | theia.TaskScope.Global | theia.TaskScope.Workspace | undefined; let name: string; let source: string; - let execution: ProcessExecution | ShellExecution | undefined; + let execution: ProcessExecution | ShellExecution | CustomExecution | undefined; let problemMatchers: string | string[] | undefined; if (typeof args[1] === 'string') { @@ -1810,11 +1833,11 @@ export class Task { this.taskName = value; } - get execution(): ProcessExecution | ShellExecution | undefined { + get execution(): ProcessExecution | ShellExecution | CustomExecution | undefined { return this.taskExecution; } - set execution(value: ProcessExecution | ShellExecution | undefined) { + set execution(value: ProcessExecution | ShellExecution | CustomExecution | undefined) { if (value === null) { value = undefined; } @@ -1898,6 +1921,18 @@ export class Task { id: this.taskExecution.computeId(), taskType: this.taskDefinition!.type }); + } else if (this.taskExecution instanceof CustomExecution) { + Object.assign(this.taskDefinition, { + type: 'customExecution', + id: this.taskExecution.computeId(), + taskType: this.taskDefinition!.type + }); + } else { + Object.assign(this.taskDefinition, { + type: '$empty', + id: UUID.uuid4(), + taskType: this.taskDefinition!.type + }); } } } diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index dc2c67e7ee3ce..cba7376e81b18 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -9763,6 +9763,24 @@ declare module '@theia/plugin' { [name: string]: any; } + /** + * Class used to execute an extension callback as a task. + */ + export class CustomExecution { + /** + * Constructs a CustomExecution task object. The callback will be executed when the task is run, at which point the + * extension should return the Pseudoterminal it will "run in". The task should wait to do further execution until + * [Pseudoterminal.open](#Pseudoterminal.open) is called. Task cancellation should be handled using + * [Pseudoterminal.close](#Pseudoterminal.close). When the task is complete fire + * [Pseudoterminal.onDidClose](#Pseudoterminal.onDidClose). + * @param callback The callback that will be called when the task is started by a user. Any ${} style variables that + * were in the task definition will be resolved and passed into the callback as `resolvedDefinition`. + */ + constructor(callback: (resolvedDefinition: TaskDefinition) => Thenable); + + readonly callback; + } + export enum TaskScope { /** The task is a global task. Global tasks are currently not supported. */ Global = 1, @@ -9898,7 +9916,7 @@ declare module '@theia/plugin' { scope?: TaskScope.Global | TaskScope.Workspace | WorkspaceFolder; /** The task's execution engine */ - execution?: ProcessExecution | ShellExecution; + execution?: ProcessExecution | ShellExecution | CustomExecution; /** Whether the task is a background task or not. */ isBackground?: boolean; diff --git a/packages/task/src/browser/task-service.ts b/packages/task/src/browser/task-service.ts index 21b47f2e166af..7fb2e214c4a5c 100644 --- a/packages/task/src/browser/task-service.ts +++ b/packages/task/src/browser/task-service.ts @@ -450,6 +450,10 @@ export class TaskService implements TaskConfigurationClient { return this.taskServer.getTasks(this.getContext()); } + async customExecutionComplete(id: number, exitCode: number | undefined): Promise { + return this.taskServer.customExecutionComplete(id, exitCode); + } + /** Returns an array of task types that are registered, including the default types */ getRegisteredTaskTypes(): Promise { return this.taskSchemaUpdater.getRegisteredTaskTypes(); diff --git a/packages/task/src/common/task-protocol.ts b/packages/task/src/common/task-protocol.ts index 871386b6e2377..548851ec312ec 100644 --- a/packages/task/src/common/task-protocol.ts +++ b/packages/task/src/common/task-protocol.ts @@ -213,6 +213,8 @@ export interface TaskServer extends JsonRpcServer { /** Returns the list of default and registered task runners */ getRegisteredTaskTypes(): Promise + /** plugin callback task complete */ + customExecutionComplete(id: number, exitCode: number | undefined): Promise } export interface TaskCustomizationData { diff --git a/packages/task/src/node/custom/custom-task-runner-backend-module.ts b/packages/task/src/node/custom/custom-task-runner-backend-module.ts new file mode 100644 index 0000000000000..63dc426a30426 --- /dev/null +++ b/packages/task/src/node/custom/custom-task-runner-backend-module.ts @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { interfaces, Container } from 'inversify'; +import { CustomTask, TaskFactory, TaskCustomOptions } from './custom-task'; +import { CustomTaskRunner } from './custom-task-runner'; +import { CustomTaskRunnerContribution } from './custom-task-runner-contribution'; +import { TaskRunnerContribution } from '../task-runner'; + +export function bindCustomTaskRunnerModule(bind: interfaces.Bind): void { + + bind(CustomTask).toSelf().inTransientScope(); + bind(TaskFactory).toFactory(ctx => + (options: TaskCustomOptions) => { + const child = new Container({ defaultScope: 'Singleton' }); + child.parent = ctx.container; + child.bind(TaskCustomOptions).toConstantValue(options); + return child.get(CustomTask); + } + ); + bind(CustomTaskRunner).toSelf().inSingletonScope(); + bind(CustomTaskRunnerContribution).toSelf().inSingletonScope(); + bind(TaskRunnerContribution).toService(CustomTaskRunnerContribution); +} diff --git a/packages/task/src/node/custom/custom-task-runner-contribution.ts b/packages/task/src/node/custom/custom-task-runner-contribution.ts new file mode 100644 index 0000000000000..a15e76772d20e --- /dev/null +++ b/packages/task/src/node/custom/custom-task-runner-contribution.ts @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { CustomTaskRunner } from './custom-task-runner'; +import { TaskRunnerContribution, TaskRunnerRegistry } from '../task-runner'; + +@injectable() +export class CustomTaskRunnerContribution implements TaskRunnerContribution { + + @inject(CustomTaskRunner) + protected readonly customTaskRunner: CustomTaskRunner; + + registerRunner(runners: TaskRunnerRegistry): void { + runners.registerRunner('customExecution', this.customTaskRunner); + } +} diff --git a/packages/task/src/node/custom/custom-task-runner.ts b/packages/task/src/node/custom/custom-task-runner.ts new file mode 100644 index 0000000000000..427adee154e2c --- /dev/null +++ b/packages/task/src/node/custom/custom-task-runner.ts @@ -0,0 +1,77 @@ +/******************************************************************************** + * Copyright (C) 2017-2019 Ericsson and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TaskConfiguration } from '../../common'; +import { Task } from '../task'; +import { TaskRunner } from '../task-runner'; +import { injectable, inject, named } from 'inversify'; +import { ILogger } from '@theia/core'; +import { TaskFactory } from './custom-task'; +import { + RawProcessFactory, + TerminalProcessFactory, + Process, + TerminalProcessOptions, +} from '@theia/process/lib/node'; + +/** + * Task runner that runs a task as a pseudoterminal open. + */ +@injectable() +export class CustomTaskRunner implements TaskRunner { + + @inject(ILogger) @named('task') + protected readonly logger: ILogger; + + @inject(RawProcessFactory) + protected readonly rawProcessFactory: RawProcessFactory; + + @inject(TerminalProcessFactory) + protected readonly terminalProcessFactory: TerminalProcessFactory; + + @inject(TaskFactory) + protected readonly taskFactory: TaskFactory; + + async run(tskConfig: TaskConfiguration, ctx?: string): Promise { + try { + const terminalProcessOptions = { isPseudo: true } as TerminalProcessOptions; + const terminal: Process = this.terminalProcessFactory(terminalProcessOptions); + + // Wait for the confirmation that the process is successfully started, or has failed to start. + // await new Promise((resolve, reject) => { + // terminal.onStart(resolve); + // terminal.onError((error: ProcessErrorEvent) => { + // reject(ProcessTaskError.CouldNotRun(error.code)); + // }); + // }); + + return Promise.resolve(this.taskFactory({ + context: ctx, + config: tskConfig, + label: tskConfig.label, + process: terminal, + })); + } catch (error) { + this.logger.error(`Error occurred while creating task: ${error}`); + throw error; + } + } +} diff --git a/packages/task/src/node/custom/custom-task.ts b/packages/task/src/node/custom/custom-task.ts new file mode 100644 index 0000000000000..d1453a8f0fc77 --- /dev/null +++ b/packages/task/src/node/custom/custom-task.ts @@ -0,0 +1,78 @@ +/******************************************************************************** + * Copyright (C) 2017 Ericsson and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { injectable, inject, named } from 'inversify'; +import { ILogger, MaybePromise } from '@theia/core/lib/common/'; +import { Task, TaskOptions } from '../task'; +import { TaskManager } from '../task-manager'; +import { TaskInfo } from '../../common/task-protocol'; +import { Process } from '@theia/process/lib/node'; + +export const TaskCustomOptions = Symbol('TaskCustomOptions'); +export interface TaskCustomOptions extends TaskOptions { + process: Process +} + +export const TaskFactory = Symbol('TaskFactory'); +export type TaskFactory = (options: TaskCustomOptions) => CustomTask; + +/** Represents a Task launched as a fake process by `CustomTaskRunner`. */ +@injectable() +export class CustomTask extends Task { + + constructor( + @inject(TaskManager) protected readonly taskManager: TaskManager, + @inject(ILogger) @named('task') protected readonly logger: ILogger, + @inject(TaskCustomOptions) protected readonly options: TaskCustomOptions + ) { + super(taskManager, logger, options); + this.logger.info(`Created new custom task, id: ${this.id}, context: ${this.context}`); + } + + kill(): Promise { + return Promise.resolve(); + } + + getRuntimeInfo(): MaybePromise { + return { + taskId: this.id, + ctx: this.context, + config: this.options.config, + terminalId: this.process.id, + processId: this.process.id, + }; + } + + public callbackTaskComplete(exitCode: number | undefined): MaybePromise { + this.fireTaskExited({ + taskId: this.taskId, + ctx: this.context, + config: this.options.config, + terminalId: this.process.id, + processId: this.process.id, + code: exitCode, + }); + } + + get process(): Process { + return this.options.process; + } +} diff --git a/packages/task/src/node/task-backend-module.ts b/packages/task/src/node/task-backend-module.ts index 77b0a709027b2..e92f8d06d6b0a 100644 --- a/packages/task/src/node/task-backend-module.ts +++ b/packages/task/src/node/task-backend-module.ts @@ -19,6 +19,7 @@ import { bindContributionProvider } from '@theia/core'; import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core/lib/common/messaging'; import { BackendApplicationContribution } from '@theia/core/lib/node'; import { bindProcessTaskRunnerModule } from './process/process-task-runner-backend-module'; +import { bindCustomTaskRunnerModule } from './custom/custom-task-runner-backend-module'; import { TaskBackendApplicationContribution } from './task-backend-application-contribution'; import { TaskManager } from './task-manager'; import { TaskRunnerContribution, TaskRunnerRegistry } from './task-runner'; @@ -52,4 +53,5 @@ export default new ContainerModule(bind => { bind(BackendApplicationContribution).toService(TaskBackendApplicationContribution); bindProcessTaskRunnerModule(bind); + bindCustomTaskRunnerModule(bind); }); diff --git a/packages/task/src/node/task-server.ts b/packages/task/src/node/task-server.ts index 8e7d8ca8ac665..376ac226a5a43 100644 --- a/packages/task/src/node/task-server.ts +++ b/packages/task/src/node/task-server.ts @@ -31,6 +31,7 @@ import { TaskRunnerRegistry } from './task-runner'; import { Task } from './task'; import { ProcessTask } from './process/process-task'; import { ProblemCollector } from './task-problem-collector'; +import { CustomTask } from './custom/custom-task'; @injectable() export class TaskServerImpl implements TaskServer, Disposable { @@ -168,6 +169,12 @@ export class TaskServerImpl implements TaskServer, Disposable { return this.runnerRegistry.getRunnerTypes(); } + async customExecutionComplete(id: number, exitCode: number | undefined): Promise { + const task = this.taskManager.get(id) as CustomTask; + await task.callbackTaskComplete(exitCode); + return; + } + protected fireTaskExitedEvent(event: TaskExitedEvent, task?: Task): void { this.logger.debug(log => log('task has exited:', event)); diff --git a/packages/terminal/src/browser/base/terminal-widget.ts b/packages/terminal/src/browser/base/terminal-widget.ts index d2efff2c86247..a0f95911bcc1c 100644 --- a/packages/terminal/src/browser/base/terminal-widget.ts +++ b/packages/terminal/src/browser/base/terminal-widget.ts @@ -119,6 +119,8 @@ export abstract class TerminalWidget extends BaseWidget { abstract hasChildProcesses(): Promise; abstract setTitle(title: string): void; + + abstract waitOnExit(waitOnExit?: boolean | string): void; } /** diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index c4910bb1c6601..dee26879fa2db 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -36,7 +36,7 @@ import { TerminalSearchWidgetFactory, TerminalSearchWidget } from './search/term import { TerminalCopyOnSelectionHandler } from './terminal-copy-on-selection-handler'; import { TerminalThemeService } from './terminal-theme-service'; import { CommandLineOptions, ShellCommandBuilder } from '@theia/process/lib/common/shell-command-builder'; - +import { Key } from '@theia/core/lib/browser/keys'; export const TERMINAL_WIDGET_FACTORY_ID = 'terminal'; export interface TerminalWidgetFactoryOptions extends Partial { @@ -670,4 +670,30 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget this.title.caption = title; this.title.label = title; } + + waitOnExit(waitOnExit?: boolean | string): void { + if (waitOnExit) { + if (typeof waitOnExit === 'string') { + let message = waitOnExit; + // Bold the message and add an extra new line to make it stand out from the rest of the output + message = `\r\n\x1b[1m${message}\x1b[0m`; + this.write(message); + } + if (this.closeOnDispose === true && typeof this.terminalId === 'number') { + this.shellTerminalServer.close(this.terminalId); + this.onTermDidClose.fire(this); + } + this.attachPressAnyKeyToCloseListener(this.term); + return; + } + return this.dispose(); + } + + private attachPressAnyKeyToCloseListener(term: Terminal): void { + if (term.textarea) { + this.addKeyListener(term.textarea, Key.ENTER, (event: KeyboardEvent) => { + this.dispose(); + }); + } + } }