From ad80a054eab110b136bb99e34c64f053515e50f7 Mon Sep 17 00:00:00 2001 From: Shimon Ben Yair Date: Wed, 18 Dec 2019 14:59:19 +0200 Subject: [PATCH] Support Compound Tasks, Background Tasks and TaskIdentifier, Fixes eclipse-theia#5517 and also Fixes eclipse-theia#6534 Signed-off-by: Shimon Ben Yair --- .../src/browser/debug-session-manager.ts | 24 +- .../debug/src/common/debug-configuration.ts | 6 +- packages/task/src/browser/task-node.ts | 37 +++ .../task/src/browser/task-schema-updater.ts | 48 +++ packages/task/src/browser/task-service.ts | 276 +++++++++++++++++- packages/task/src/common/task-protocol.ts | 26 ++ packages/task/src/common/task-watcher.ts | 10 +- packages/task/src/node/task-line-matchers.ts | 6 +- .../task/src/node/task-problem-collector.ts | 17 +- packages/task/src/node/task-server.ts | 45 +++ .../src/browser/variable-resolver-service.ts | 4 +- 11 files changed, 467 insertions(+), 32 deletions(-) create mode 100644 packages/task/src/browser/task-node.ts diff --git a/packages/debug/src/browser/debug-session-manager.ts b/packages/debug/src/browser/debug-session-manager.ts index ece6394526099..00efeb4da401b 100644 --- a/packages/debug/src/browser/debug-session-manager.ts +++ b/packages/debug/src/browser/debug-session-manager.ts @@ -22,7 +22,7 @@ import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-k import URI from '@theia/core/lib/common/uri'; import { EditorManager } from '@theia/editor/lib/browser'; import { QuickOpenTask } from '@theia/task/lib/browser/quick-open-task'; -import { TaskService } from '@theia/task/lib/browser/task-service'; +import { TaskService, TaskEndedInfo, TaskEndedTypes } from '@theia/task/lib/browser/task-service'; import { VariableResolverService } from '@theia/variable-resolver/lib/browser'; import { inject, injectable, postConstruct } from 'inversify'; import { DebugConfiguration } from '../common/debug-common'; @@ -35,6 +35,7 @@ import { DebugSessionOptions, InternalDebugSessionOptions } from './debug-sessio import { DebugBreakpoint } from './model/debug-breakpoint'; import { DebugStackFrame } from './model/debug-stack-frame'; import { DebugThread } from './model/debug-thread'; +import { TaskIdentifier } from '@theia/task/lib/common'; export interface WillStartDebugSession extends WaitUntilEvent { } @@ -410,7 +411,7 @@ export class DebugSessionManager { * @param taskName the task name to run, see [TaskNameResolver](#TaskNameResolver) * @return true if it allowed to continue debugging otherwise it returns false */ - protected async runTask(workspaceFolderUri: string | undefined, taskName: string | undefined, checkErrors?: boolean): Promise { + protected async runTask(workspaceFolderUri: string | undefined, taskName: string | TaskIdentifier | undefined, checkErrors?: boolean): Promise { if (!taskName) { return true; } @@ -424,11 +425,22 @@ export class DebugSessionManager { return this.doPostTaskAction(`Could not run the task '${taskName}'.`); } - const code = await this.taskService.getExitCode(taskInfo.taskId); - if (code === 0) { + const getExitCodePromise: Promise = this.taskService.getExitCode(taskInfo.taskId).then(result => + ({ taskEndedType: TaskEndedTypes.TaskExited, value: result })); + const isBackgroundTaskEndedPromise: Promise = this.taskService.isBackgroundTaskEnded(taskInfo.taskId).then(result => + ({ taskEndedType: TaskEndedTypes.BackgroundTaskEnded, value: result })); + + // After start running the task, we wait for the task process to exit and if it is a background task, we also wait for a feedback + // that a background task is active, as soon as one of the promises fulfills, we can continue and analyze the results. + const taskEndedInfo: TaskEndedInfo = await Promise.race([getExitCodePromise, isBackgroundTaskEndedPromise]); + + if (taskEndedInfo.taskEndedType === TaskEndedTypes.BackgroundTaskEnded && taskEndedInfo.value) { + return true; + } + if (taskEndedInfo.taskEndedType === TaskEndedTypes.TaskExited && taskEndedInfo.value === 0) { return true; - } else if (code !== undefined) { - return this.doPostTaskAction(`Task '${taskName}' terminated with exit code ${code}.`); + } else if (taskEndedInfo.taskEndedType === TaskEndedTypes.TaskExited && taskEndedInfo.value !== undefined) { + return this.doPostTaskAction(`Task '${taskName}' terminated with exit code ${taskEndedInfo.value}.`); } else { const signal = await this.taskService.getTerminateSignal(taskInfo.taskId); if (signal !== undefined) { diff --git a/packages/debug/src/common/debug-configuration.ts b/packages/debug/src/common/debug-configuration.ts index 4515b8af1dcc0..a8fe876d9cdd4 100644 --- a/packages/debug/src/common/debug-configuration.ts +++ b/packages/debug/src/common/debug-configuration.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ - +import { TaskIdentifier } from '@theia/task/lib/common'; // tslint:disable:no-any export type DebugViewLocation = 'default' | 'left' | 'right' | 'bottom'; @@ -64,10 +64,10 @@ export interface DebugConfiguration { internalConsoleOptions?: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart' /** Task to run before debug session starts */ - preLaunchTask?: string; + preLaunchTask?: string | TaskIdentifier; /** Task to run after debug session ends */ - postDebugTask?: string; + postDebugTask?: string | TaskIdentifier; } export namespace DebugConfiguration { export function is(arg: DebugConfiguration | any): arg is DebugConfiguration { diff --git a/packages/task/src/browser/task-node.ts b/packages/task/src/browser/task-node.ts new file mode 100644 index 0000000000000..3f6bc8cfd6f53 --- /dev/null +++ b/packages/task/src/browser/task-node.ts @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (c) 2019 SAP SE or an SAP affiliate company 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 { TaskConfiguration } from '../common'; + +export class TaskNode { + + taskId: TaskConfiguration; + childTasks: TaskNode[]; + parentsID: TaskConfiguration[]; + + constructor(taskId: TaskConfiguration, childTasks: TaskNode[], parentsID: TaskConfiguration[]) { + this.taskId = taskId; + this.childTasks = childTasks; + this.parentsID = parentsID; + } + + addChildDependency(node: TaskNode): void { + this.childTasks.push(node); + } + + addParentDependency(parentId: TaskConfiguration): void { + this.parentsID.push(parentId); + } +} diff --git a/packages/task/src/browser/task-schema-updater.ts b/packages/task/src/browser/task-schema-updater.ts index 76b7a7cea1e41..16601d614949d 100644 --- a/packages/task/src/browser/task-schema-updater.ts +++ b/packages/task/src/browser/task-schema-updater.ts @@ -532,6 +532,17 @@ const problemMatcher = { ] }; +const taskIdentifier: IJSONSchema = { + type: 'object', + additionalProperties: true, + properties: { + type: { + type: 'string', + description: 'The task identifier.' + } + } +}; + const processTaskConfigurationSchema: IJSONSchema = { type: 'object', required: ['type', 'label', 'command'], @@ -539,6 +550,43 @@ const processTaskConfigurationSchema: IJSONSchema = { label: taskLabel, type: defaultTaskType, ...commandAndArgs, + isBackground: { + type: 'boolean', + default: false, + description: 'Whether the executed task is kept alive and is running in the background.' + }, + dependsOn: { + anyOf: [ + { + type: 'string', + description: 'Another task this task depends on.' + }, + taskIdentifier, + { + type: 'array', + description: 'The other tasks this task depends on.', + items: { + anyOf: [ + { + type: 'string' + }, + taskIdentifier + ] + } + } + ], + description: 'Either a string representing another task or an array of other tasks that this task depends on.' + }, + dependsOrder: { + type: 'string', + enum: ['parallel', 'sequence'], + enumDescriptions: [ + 'Run all dependsOn tasks in parallel.', + 'Run all dependsOn tasks in sequence.' + ], + default: 'parallel', + description: 'Determines the order of the dependsOn tasks for this task. Note that this property is not recursive.' + }, windows: { type: 'object', description: 'Windows specific command configuration that overrides the command, args, and options', diff --git a/packages/task/src/browser/task-service.ts b/packages/task/src/browser/task-service.ts index 7b6a00b39a1d6..5b67008b77d09 100644 --- a/packages/task/src/browser/task-service.ts +++ b/packages/task/src/browser/task-service.ts @@ -41,7 +41,11 @@ import { TaskExitedEvent, TaskInfo, TaskOutputProcessedEvent, - TaskServer + BackgroundTaskEndedEvent, + TaskDefinition, + TaskServer, + TaskIdentifier, + DependsOrder } from '../common'; import { TaskWatcher } from '../common/task-watcher'; import { ProvidedTaskConfigurations } from './provided-task-configurations'; @@ -54,12 +58,28 @@ import { ProblemMatcherRegistry } from './task-problem-matcher-registry'; import { TaskSchemaUpdater } from './task-schema-updater'; import { TaskConfigurationManager } from './task-configuration-manager'; import { PROBLEMS_WIDGET_ID, ProblemWidget } from '@theia/markers/lib/browser/problem/problem-widget'; +import { TaskNode } from './task-node'; export interface QuickPickProblemMatcherItem { problemMatchers: NamedProblemMatcher[] | undefined; learnMore?: boolean; } +interface TaskGraphNode { + taskConfiguration: TaskConfiguration; + node: TaskNode; +} + +export enum TaskEndedTypes { + TaskExited, + BackgroundTaskEnded +} + +export interface TaskEndedInfo { + taskEndedType: TaskEndedTypes, + value: number | boolean | undefined +} + @injectable() export class TaskService implements TaskConfigurationClient { @@ -70,7 +90,8 @@ export class TaskService implements TaskConfigurationClient { protected cachedRecentTasks: TaskConfiguration[] = []; protected runningTasks = new Map, - terminateSignal: Deferred + terminateSignal: Deferred, + isBackgroundTaskEnded: Deferred }>(); @inject(FrontendApplication) @@ -158,7 +179,10 @@ export class TaskService implements TaskConfigurationClient { this.getRunningTasks().then(tasks => tasks.forEach(task => { if (!this.runningTasks.has(task.taskId)) { - this.runningTasks.set(task.taskId, { exitCode: new Deferred(), terminateSignal: new Deferred() }); + this.runningTasks.set(task.taskId, { + exitCode: new Deferred(), terminateSignal: new Deferred(), + isBackgroundTaskEnded: new Deferred() + }); } })); @@ -167,7 +191,11 @@ export class TaskService implements TaskConfigurationClient { if (!this.isEventForThisClient(event.ctx)) { return; } - this.runningTasks.set(event.taskId, { exitCode: new Deferred(), terminateSignal: new Deferred() }); + this.runningTasks.set(event.taskId, { + exitCode: new Deferred(), + terminateSignal: new Deferred(), + isBackgroundTaskEnded: new Deferred() + }); const taskConfig = event.config; const taskIdentifier = taskConfig ? this.getTaskIdentifier(taskConfig) : event.taskId.toString(); this.messageService.info(`Task '${taskIdentifier}' has been started.`); @@ -202,13 +230,32 @@ export class TaskService implements TaskConfigurationClient { } }); + this.taskWatcher.onBackgroundTaskEnded((event: BackgroundTaskEndedEvent) => { + if (!this.isEventForThisClient(event.ctx)) { + return; + } + + if (!this.runningTasks.has(event.taskId)) { + this.runningTasks.set(event.taskId, { + exitCode: new Deferred(), + terminateSignal: new Deferred(), + isBackgroundTaskEnded: new Deferred() + }); + } + this.runningTasks.get(event.taskId)!.isBackgroundTaskEnded.resolve(true); + }); + // notify user that task has finished this.taskWatcher.onTaskExit((event: TaskExitedEvent) => { if (!this.isEventForThisClient(event.ctx)) { return; } if (!this.runningTasks.has(event.taskId)) { - this.runningTasks.set(event.taskId, { exitCode: new Deferred(), terminateSignal: new Deferred() }); + this.runningTasks.set(event.taskId, { + exitCode: new Deferred(), + terminateSignal: new Deferred(), + isBackgroundTaskEnded: new Deferred() + }); } this.runningTasks.get(event.taskId)!.exitCode.resolve(event.code); this.runningTasks.get(event.taskId)!.terminateSignal.resolve(event.signal); @@ -408,12 +455,193 @@ export class TaskService implements TaskConfigurationClient { } } + const tasks = await this.getWorkspaceTasks(task._scope); const resolvedMatchers = await this.resolveProblemMatchers(task, customizationObject); - return this.runTask(task, { + try { + const rootNode = new TaskNode(task, [], []); + this.detectDirectedAcyclicGraph(task, rootNode, tasks); + } catch (error) { + this.logger.error(error.message); + this.messageService.error(error.message); + return undefined; + } + return this.runTasksGraph(task, tasks, { customization: { ...customizationObject, ...{ problemMatcher: resolvedMatchers } } + }).catch(error => { + console.log(error.message); + return undefined; }); } + /** + * A recursive function that runs a task and all its sub tasks that it depends on. + * A task can be executed only when all of its dependencies have been executed, or when it doesn’t have any dependencies at all. + */ + async runTasksGraph(task: TaskConfiguration, tasks: TaskConfiguration[], option?: RunTaskOption): Promise { + if (task && task.dependsOn) { + // In case it is an array of task dependencies + if (Array.isArray(task.dependsOn) && task.dependsOn.length > 0) { + const dependentTasks: { 'task': TaskConfiguration; 'taskCustomization': TaskCustomization; 'resolvedMatchers': ProblemMatcher[] | undefined }[] = []; + for (let i = 0; i < task.dependsOn.length; i++) { + // It may be a string (a task label) or a JSON object which represents a TaskIdentifier (e.g. {"type":"npm", "script":"script1"}) + const taskIdentifier = task.dependsOn[i]; + const dependentTask = this.getDependentTask(taskIdentifier, tasks); + const taskCustomization = await this.getTaskCustomization(dependentTask); + const resolvedMatchers = await this.resolveProblemMatchers(dependentTask, taskCustomization); + dependentTasks.push({ 'task': dependentTask, 'taskCustomization': taskCustomization, 'resolvedMatchers': resolvedMatchers }); + // In case the 'dependsOrder' is 'sequence' + if (task.dependsOrder && task.dependsOrder === DependsOrder.Sequence) { + await this.runTasksGraph(dependentTask, tasks, { + customization: { ...taskCustomization, ...{ problemMatcher: resolvedMatchers } } + }); + } + } + // In case the 'dependsOrder' is 'parallel' + if (((!task.dependsOrder) || (task.dependsOrder && task.dependsOrder === DependsOrder.Parallel))) { + const promises = dependentTasks.map(item => + this.runTasksGraph(item.task, tasks, { + customization: { ...item.taskCustomization, ...{ problemMatcher: item.resolvedMatchers } } + }) + ); + await Promise.all(promises); + } + } else if (!Array.isArray(task.dependsOn)) { + // In case it is a string (a task label) or a JSON object which represents a TaskIdentifier (e.g. {"type":"npm", "script":"script1"}) + const taskIdentifier = task.dependsOn; + const dependentTask = this.getDependentTask(taskIdentifier, tasks); + const taskCustomization = await this.getTaskCustomization(dependentTask); + const resolvedMatchers = await this.resolveProblemMatchers(dependentTask, taskCustomization); + await this.runTasksGraph(dependentTask, tasks, { + customization: { ...taskCustomization, ...{ problemMatcher: resolvedMatchers } } + }); + } + } + + const taskInfo = await this.runTask(task, option); + if (taskInfo) { + const getExitCodePromise: Promise = this.getExitCode(taskInfo.taskId).then(result => ({ taskEndedType: TaskEndedTypes.TaskExited, value: result })); + const isBackgroundTaskEndedPromise: Promise = this.isBackgroundTaskEnded(taskInfo.taskId).then(result => + ({ taskEndedType: TaskEndedTypes.BackgroundTaskEnded, value: result })); + + // After start running the task, we wait for the task process to exit and if it is a background task, we also wait for a feedback + // that a background task is active, as soon as one of the promises fulfills, we can continue and analyze the results. + const taskEndedInfo: TaskEndedInfo = await Promise.race([getExitCodePromise, isBackgroundTaskEndedPromise]); + + if ((taskEndedInfo.taskEndedType === TaskEndedTypes.TaskExited && taskEndedInfo.value !== 0) || + (taskEndedInfo.taskEndedType === TaskEndedTypes.BackgroundTaskEnded && !taskEndedInfo.value)) { + throw new Error('The task: ' + task.label + ' terminated with exit code ' + taskEndedInfo.value + '.'); + } + } + return taskInfo; + } + + /** + * Creates a graph of dependencies tasks from the root task and verify there is no DAG (Directed Acyclic Graph). + * In case of detection of a circular dependency, an error is thrown with a message which describes the detected circular reference. + */ + detectDirectedAcyclicGraph(task: TaskConfiguration, taskNode: TaskNode, tasks: TaskConfiguration[]): void { + if (task && task.dependsOn) { + // In case the 'dependsOn' is an array + if (Array.isArray(task.dependsOn) && task.dependsOn.length > 0) { + for (let i = 0; i < task.dependsOn.length; i++) { + const childNode = this.createChildTaskNode(task, taskNode, task.dependsOn[i], tasks); + this.detectDirectedAcyclicGraph(childNode.taskConfiguration, childNode.node, tasks); + } + } else if (!Array.isArray(task.dependsOn)) { + const childNode = this.createChildTaskNode(task, taskNode, task.dependsOn, tasks); + this.detectDirectedAcyclicGraph(childNode.taskConfiguration, childNode.node, tasks); + } + } + } + + // 'childTaskIdentifier' may be a string (a task label) or a JSON object which represents a TaskIdentifier (e.g. {"type":"npm", "script":"script1"}) + createChildTaskNode(task: TaskConfiguration, taskNode: TaskNode, childTaskIdentifier: string | TaskIdentifier, tasks: TaskConfiguration[]): TaskGraphNode { + const childTaskConfiguration = this.getDependentTask(childTaskIdentifier, tasks); + + // If current task and child task are identical or if + // one of the child tasks is identical to one of the current task ancestors, then raise an error + if (this.taskDefinitionRegistry.compareTasks(task, childTaskConfiguration) || + taskNode.parentsID.filter(t => this.taskDefinitionRegistry.compareTasks(childTaskConfiguration, t)).length > 0) { + const fromNode = task.label; + const toNode = childTaskConfiguration.label; + throw new Error('Circular reference detected: ' + fromNode + ' --> ' + toNode); + } + const childNode = new TaskNode(childTaskConfiguration, [], Object.assign([], taskNode.parentsID)); + childNode.addParentDependency(taskNode.taskId); + taskNode.addChildDependency(childNode); + return { 'taskConfiguration': childTaskConfiguration, 'node': childNode }; + } + + /** + * Gets task configuration by task label or by a JSON object which represents a task identifier + * + * @param taskIdentifier The task label (string) or a JSON object which represents a TaskIdentifier (e.g. {"type":"npm", "script":"script1"}) + * @param tasks an array of the task configurations + * @returns the correct TaskConfiguration object which matches the taskIdentifier + */ + getDependentTask(taskIdentifier: string | TaskIdentifier, tasks: TaskConfiguration[]): TaskConfiguration { + const notEnoughDataError = 'The information provided in the "dependsOn" is not enough for matching the correct task !'; + let currentTaskChildConfiguration: TaskConfiguration; + if (typeof (taskIdentifier) !== 'string') { + // TaskIdentifier object does not support tasks of type 'shell' (The same behavior as in VS Code). + // So if we want the 'dependsOn' property to include tasks of type 'shell', + // then we must mention their labels (in the 'dependsOn' property) and not to create a task identifier object for them. + const taskDefinition = this.taskDefinitionRegistry.getDefinition(taskIdentifier); + if (taskDefinition) { + currentTaskChildConfiguration = this.getTaskByTaskIdentifierAndTaskDefinition(taskDefinition, taskIdentifier, tasks); + if (!currentTaskChildConfiguration.type) { + this.messageService.error(notEnoughDataError); + throw new Error(notEnoughDataError); + } + return currentTaskChildConfiguration; + } else { + this.messageService.error(notEnoughDataError); + throw new Error(notEnoughDataError); + } + } else { + currentTaskChildConfiguration = tasks.filter(t => taskIdentifier === this.taskNameResolver.resolve(t))[0]; + return currentTaskChildConfiguration; + } + } + + /** + * Gets the matched task from an array of task configurations by TaskDefinition and TaskIdentifier. + * In case that more than one task configuration matches, we returns the first one. + * + * @param taskDefinition The task definition for the task configuration. + * @param taskIdentifier The task label (string) or a JSON object which represents a TaskIdentifier (e.g. {"type":"npm", "script":"script1"}) + * @param tasks An array of task configurations. + * @returns The correct TaskConfiguration object which matches the taskDefinition and taskIdentifier. + */ + getTaskByTaskIdentifierAndTaskDefinition(taskDefinition: TaskDefinition | undefined, taskIdentifier: TaskIdentifier, tasks: TaskConfiguration[]): TaskConfiguration { + const identifierProperties: string[] = []; + let relevantTasks = tasks.filter(t => + taskDefinition && t.hasOwnProperty('taskType') && + taskDefinition['taskType'] === t['taskType'] && + t.hasOwnProperty('source') && + taskDefinition['source'] === t['source']); + + Object.keys(taskIdentifier).forEach(key => { + identifierProperties.push(key); + }); + + identifierProperties.forEach(key => { + if (key === 'type' || key === 'taskType') { + relevantTasks = relevantTasks.filter(t => (t.hasOwnProperty('type') || t.hasOwnProperty('taskType')) && + ((taskIdentifier[key] === t['type']) || (taskIdentifier[key] === t['taskType']))); + } else { + relevantTasks = relevantTasks.filter(t => t.hasOwnProperty(key) && taskIdentifier[key] === t[key]); + } + }); + + if (relevantTasks.length > 0) { + return relevantTasks[0]; + } else { + // return empty TaskConfiguration + return { 'label': '', '_scope': '', 'type': '' }; + } + } + async runTask(task: TaskConfiguration, option?: RunTaskOption): Promise { const runningTasksInfo: TaskInfo[] = await this.getRunningTasks(); @@ -488,22 +716,31 @@ export class TaskService implements TaskConfigurationClient { return this.runTask(task); } } - return; } - async runWorkspaceTask(workspaceFolderUri: string | undefined, taskName: string): Promise { + async runWorkspaceTask(workspaceFolderUri: string | undefined, taskIdentifier: string | TaskIdentifier): Promise { const tasks = await this.getWorkspaceTasks(workspaceFolderUri); - const task = tasks.filter(t => taskName === this.taskNameResolver.resolve(t))[0]; + const task = this.getDependentTask(taskIdentifier, tasks); if (!task) { return undefined; } const taskCustomization = await this.getTaskCustomization(task); const resolvedMatchers = await this.resolveProblemMatchers(task, taskCustomization); - - return this.runTask(task, { + try { + const rootNode = new TaskNode(task, [], []); + this.detectDirectedAcyclicGraph(task, rootNode, tasks); + } catch (error) { + this.logger.error(error.message); + this.messageService.error(error.message); + return undefined; + } + return this.runTasksGraph(task, tasks, { customization: { ...taskCustomization, ...{ problemMatcher: resolvedMatchers } } + }).catch(error => { + console.log(error.message); + return undefined; }); } @@ -597,16 +834,18 @@ export class TaskService implements TaskConfigurationClient { } private async getResolvedTask(task: TaskConfiguration): Promise { - const resolver = await this.taskResolverRegistry.getResolver(task.type); + let resolver = undefined; + let resolvedTask: TaskConfiguration; try { - const resolvedTask = resolver ? await resolver.resolveTask(task) : task; - this.addRecentTasks(task); - return resolvedTask; + resolver = await this.taskResolverRegistry.getResolver(task.type); + resolvedTask = resolver ? await resolver.resolveTask(task) : task; } catch (error) { const errMessage = `Error resolving task '${task.label}': ${error}`; this.logger.error(errMessage); - this.messageService.error(errMessage); + resolvedTask = task; } + this.addRecentTasks(task); + return resolvedTask; } /** @@ -747,6 +986,11 @@ export class TaskService implements TaskConfigurationClient { this.logger.debug(`Task killed. Task id: ${id}`); } + async isBackgroundTaskEnded(id: number): Promise { + const completedTask = this.runningTasks.get(id); + return completedTask && completedTask.isBackgroundTaskEnded!.promise; + } + async getExitCode(id: number): Promise { const completedTask = this.runningTasks.get(id); return completedTask && completedTask.exitCode.promise; diff --git a/packages/task/src/common/task-protocol.ts b/packages/task/src/common/task-protocol.ts index c623b723f9f35..163c83bcfc26d 100644 --- a/packages/task/src/common/task-protocol.ts +++ b/packages/task/src/common/task-protocol.ts @@ -22,11 +22,25 @@ export const taskPath = '/services/task'; export const TaskServer = Symbol('TaskServer'); export const TaskClient = Symbol('TaskClient'); +export enum DependsOrder { + Sequence = 'sequence', + Parallel = 'parallel', +} export interface TaskCustomization { type: string; group?: 'build' | 'test' | 'none' | { kind: 'build' | 'test' | 'none', isDefault: true }; problemMatcher?: string | ProblemMatcherContribution | (string | ProblemMatcherContribution)[]; + + /** Whether the task is a background task or not. */ + isBackground?: boolean; + + /** The other tasks the task depend on. */ + dependsOn?: string | TaskIdentifier | Array; + + /** The order the dependsOn tasks should be executed in. */ + dependsOrder?: DependsOrder; + // tslint:disable-next-line:no-any [name: string]: any; } @@ -68,6 +82,12 @@ export interface ContributedTaskConfiguration extends TaskConfiguration { readonly _source: string; } +/** A task identifier */ +export interface TaskIdentifier { + type: string; + [name: string]: string; +} + /** Runtime information about Task. */ export interface TaskInfo { /** internal unique task id */ @@ -138,12 +158,18 @@ export interface TaskOutputProcessedEvent { readonly problems?: ProblemMatch[]; } +export interface BackgroundTaskEndedEvent { + readonly taskId: number; + readonly ctx?: string; +} + export interface TaskClient { onTaskExit(event: TaskExitedEvent): void; onTaskCreated(event: TaskInfo): void; onDidStartTaskProcess(event: TaskInfo): void; onDidEndTaskProcess(event: TaskExitedEvent): void; onDidProcessTaskOutput(event: TaskOutputProcessedEvent): void; + onBackgroundTaskEnded(event: BackgroundTaskEndedEvent): void; } export interface TaskDefinition { diff --git a/packages/task/src/common/task-watcher.ts b/packages/task/src/common/task-watcher.ts index cb04340fa493c..7215699d911cc 100644 --- a/packages/task/src/common/task-watcher.ts +++ b/packages/task/src/common/task-watcher.ts @@ -16,7 +16,7 @@ import { injectable } from 'inversify'; import { Emitter, Event } from '@theia/core/lib/common/event'; -import { TaskClient, TaskExitedEvent, TaskInfo, TaskOutputProcessedEvent } from './task-protocol'; +import { TaskClient, TaskExitedEvent, TaskInfo, TaskOutputProcessedEvent, BackgroundTaskEndedEvent } from './task-protocol'; @injectable() export class TaskWatcher { @@ -27,6 +27,7 @@ export class TaskWatcher { const taskProcessStartedEmitter = this.onDidStartTaskProcessEmitter; const taskProcessEndedEmitter = this.onDidEndTaskProcessEmitter; const outputProcessedEmitter = this.onOutputProcessedEmitter; + const backgroundTaskEndedEmitter = this.onBackgroundTaskEndedEmitter; return { onTaskCreated(event: TaskInfo): void { newTaskEmitter.fire(event); @@ -42,6 +43,9 @@ export class TaskWatcher { }, onDidProcessTaskOutput(event: TaskOutputProcessedEvent): void { outputProcessedEmitter.fire(event); + }, + onBackgroundTaskEnded(event: BackgroundTaskEndedEvent): void { + backgroundTaskEndedEmitter.fire(event); } }; } @@ -51,6 +55,7 @@ export class TaskWatcher { protected onDidStartTaskProcessEmitter = new Emitter(); protected onDidEndTaskProcessEmitter = new Emitter(); protected onOutputProcessedEmitter = new Emitter(); + protected onBackgroundTaskEndedEmitter = new Emitter(); get onTaskCreated(): Event { return this.onTaskCreatedEmitter.event; @@ -67,4 +72,7 @@ export class TaskWatcher { get onOutputProcessed(): Event { return this.onOutputProcessedEmitter.event; } + get onBackgroundTaskEnded(): Event { + return this.onBackgroundTaskEndedEmitter.event; + } } diff --git a/packages/task/src/node/task-line-matchers.ts b/packages/task/src/node/task-line-matchers.ts index e306dbec0b53b..c43fe6cf91d92 100644 --- a/packages/task/src/node/task-line-matchers.ts +++ b/packages/task/src/node/task-line-matchers.ts @@ -67,7 +67,7 @@ export class WatchModeLineMatcher extends StartStopLineMatcher { private beginsPattern: WatchingPattern; private endsPattern: WatchingPattern; - private activeOnStart: boolean = false; + activeOnStart: boolean = false; constructor( protected matcher: ProblemMatcher @@ -108,7 +108,7 @@ export class WatchModeLineMatcher extends StartStopLineMatcher { return undefined; } - private matchBegin(line: string): boolean { + matchBegin(line: string): boolean { const beginRegexp = new RegExp(this.beginsPattern.regexp); const regexMatches = beginRegexp.exec(line); if (regexMatches) { @@ -119,7 +119,7 @@ export class WatchModeLineMatcher extends StartStopLineMatcher { return false; } - private matchEnd(line: string): boolean { + matchEnd(line: string): boolean { const endRegexp = new RegExp(this.endsPattern.regexp); const match = endRegexp.exec(line); if (match) { diff --git a/packages/task/src/node/task-problem-collector.ts b/packages/task/src/node/task-problem-collector.ts index de3a5401a5eb5..f9d381adf0325 100644 --- a/packages/task/src/node/task-problem-collector.ts +++ b/packages/task/src/node/task-problem-collector.ts @@ -23,7 +23,7 @@ export class ProblemCollector { private lineMatchers: AbstractLineMatcher[] = []; constructor( - protected problemMatchers: ProblemMatcher[] + public problemMatchers: ProblemMatcher[] ) { for (const matcher of problemMatchers) { if (ProblemMatcher.isWatchModeWatcher(matcher)) { @@ -44,4 +44,19 @@ export class ProblemCollector { }); return markers; } + + isTaskActiveOnStart(): boolean { + const activeOnStart = this.lineMatchers.some(lineMatcher => (lineMatcher instanceof WatchModeLineMatcher) && lineMatcher.activeOnStart); + return activeOnStart; + } + + matchBeginMatcher(line: string): boolean { + const match = this.lineMatchers.some(lineMatcher => (lineMatcher instanceof WatchModeLineMatcher) && lineMatcher.matchBegin(line)); + return match; + } + + matchEndMatcher(line: string): boolean { + const match = this.lineMatchers.some(lineMatcher => (lineMatcher instanceof WatchModeLineMatcher) && lineMatcher.matchEnd(line)); + return match; + } } diff --git a/packages/task/src/node/task-server.ts b/packages/task/src/node/task-server.ts index f81c2e13d85be..bdbdec8d621e3 100644 --- a/packages/task/src/node/task-server.ts +++ b/packages/task/src/node/task-server.ts @@ -24,6 +24,7 @@ import { TaskConfiguration, TaskOutputProcessedEvent, RunTaskOption, + BackgroundTaskEndedEvent } from '../common'; import { TaskManager } from './task-manager'; import { TaskRunnerRegistry } from './task-runner'; @@ -39,6 +40,10 @@ export class TaskServerImpl implements TaskServer, Disposable { /** Map of task id and task disposable */ protected readonly toDispose = new Map(); + /** Map of task id and task background status. */ + // Currently there is only one property ('isActive'), but in the future we may want to store more properties + protected readonly backgroundTaskStatusMap = new Map(); + @inject(ILogger) @named('task') protected readonly logger: ILogger; @@ -56,6 +61,7 @@ export class TaskServerImpl implements TaskServer, Disposable { toDispose.dispose(); } this.toDispose.clear(); + this.backgroundTaskStatusMap.clear(); } protected disposeByTaskId(taskId: number): void { @@ -63,6 +69,10 @@ export class TaskServerImpl implements TaskServer, Disposable { this.toDispose.get(taskId)!.dispose(); this.toDispose.delete(taskId); } + + if (this.backgroundTaskStatusMap.has(taskId)) { + this.backgroundTaskStatusMap.delete(taskId); + } } async getTasks(context?: string): Promise { @@ -85,6 +95,11 @@ export class TaskServerImpl implements TaskServer, Disposable { if (!this.toDispose.has(task.id)) { this.toDispose.set(task.id, new DisposableCollection()); } + + if (taskConfiguration.isBackground && !this.backgroundTaskStatusMap.has(task.id)) { + this.backgroundTaskStatusMap.set(task.id, { 'isActive': false }); + } + this.toDispose.get(task.id)!.push( task.onExit(event => { this.taskManager.delete(task); @@ -112,6 +127,32 @@ export class TaskServerImpl implements TaskServer, Disposable { problems }); } + if (taskConfiguration.isBackground) { + const backgroundTaskStatus = this.backgroundTaskStatusMap.get(event.taskId)!; + if (!backgroundTaskStatus.isActive) { + // Get the 'activeOnStart' value of the problem matcher 'background' property + const activeOnStart = collector.isTaskActiveOnStart(); + if (activeOnStart) { + backgroundTaskStatus.isActive = true; + } else { + const isBeginsPatternMatch = collector.matchBeginMatcher(event.line); + if (isBeginsPatternMatch) { + backgroundTaskStatus.isActive = true; + } + } + } + + if (backgroundTaskStatus.isActive) { + const isEndsPatternMatch = collector.matchEndMatcher(event.line); + // Mark ends pattern as matches, only after begins pattern matches + if (isEndsPatternMatch) { + this.fireBackgroundTaskEndedEvent({ + taskId: event.taskId, + ctx: event.ctx + }); + } + } + } }) ); } @@ -158,6 +199,10 @@ export class TaskServerImpl implements TaskServer, Disposable { this.clients.forEach(client => client.onDidProcessTaskOutput(event)); } + protected fireBackgroundTaskEndedEvent(event: BackgroundTaskEndedEvent): void { + this.clients.forEach(client => client.onBackgroundTaskEnded(event)); + } + /** Kill task for a given id. Rejects if task is not found */ async kill(id: number): Promise { const taskToKill = this.taskManager.get(id); diff --git a/packages/variable-resolver/src/browser/variable-resolver-service.ts b/packages/variable-resolver/src/browser/variable-resolver-service.ts index 0b0fb157bc112..a88a05cd44956 100644 --- a/packages/variable-resolver/src/browser/variable-resolver-service.ts +++ b/packages/variable-resolver/src/browser/variable-resolver-service.ts @@ -109,13 +109,13 @@ export class VariableResolverService { } protected async resolveVariables(value: string, context: VariableResolverService.Context): Promise { + const variableRegExp = new RegExp(VariableResolverService.VAR_REGEXP); let match; - while ((match = VariableResolverService.VAR_REGEXP.exec(value)) !== null) { + while ((match = variableRegExp.exec(value)) !== null) { const variableName = match[1]; await context.resolve(variableName); } } - } export namespace VariableResolverService { export class Context {