diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index e151f5e4c2b31..f80b127889692 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -732,7 +732,7 @@ export class MainThreadTask extends Disposable implements MainThreadTaskShape { default: platform = Platform.platform; } - this._taskService.registerTaskSystem(key, { + this._register(this._taskService.registerTaskSystem(key, { platform: platform, uriProvider: (path: string): URI => { return URI.from({ scheme: info.scheme, authority: info.authority, path }); @@ -777,7 +777,7 @@ export class MainThreadTask extends Disposable implements MainThreadTaskShape { findExecutable: (command: string, cwd?: string, paths?: string[]): Promise => { return this._proxy.$findExecutable(command, cwd, paths); } - }); + })); } async $registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): Promise { diff --git a/src/vs/workbench/api/test/browser/mainThreadTask.test.ts b/src/vs/workbench/api/test/browser/mainThreadTask.test.ts new file mode 100644 index 0000000000000..e895e2a1108ba --- /dev/null +++ b/src/vs/workbench/api/test/browser/mainThreadTask.test.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Event } from '../../../../base/common/event.js'; +import { IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { ITaskService } from '../../../contrib/tasks/common/taskService.js'; +import { MainThreadTask } from '../../browser/mainThreadTask.js'; +import { SingleProxyRPCProtocol } from '../common/testRPCProtocol.js'; + +suite('MainThreadTask', function () { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('unregisters task systems on dispose', function () { + let registrations = 0; + let disposals = 0; + const taskService = new class extends mock() { + override readonly onDidStateChange = Event.None; + + override registerTaskSystem(): IDisposable { + registrations++; + return toDisposable(() => disposals++); + } + }; + const mainThreadTask = new MainThreadTask(SingleProxyRPCProtocol(null), taskService, undefined!, undefined!); + + mainThreadTask.$registerTaskSystem('file', { scheme: 'file', authority: '', platform: 'linux' }); + assert.strictEqual(registrations, 1); + + mainThreadTask.dispose(); + assert.strictEqual(disposals, 1); + }); +}); diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index a3079cd7a81c0..612a9db8fb593 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -878,8 +878,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return infosCount > 0; } - public registerTaskSystem(key: string, info: ITaskSystemInfo): void { - // Ideally the Web caller of registerRegisterTaskSystem would use the correct key. + public registerTaskSystem(key: string, info: ITaskSystemInfo): IDisposable { + // Ideally the Web caller of registerTaskSystem would use the correct key. // However, the caller doesn't know about the workspace folders at the time of the call, even though we know about them here. if (info.platform === Platform.Platform.Web) { key = this.workspaceFolders.length ? this.workspaceFolders[0].uri.scheme : key; @@ -899,6 +899,22 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (this.hasTaskSystemInfo) { this._onDidChangeTaskSystemInfo.fire(); } + + return toDisposable(() => { + const infos = this._taskSystemInfos.get(key); + if (!infos) { + return; + } + const index = infos.indexOf(info); + if (index === -1) { + return; + } + infos.splice(index, 1); + if (infos.length === 0) { + this._taskSystemInfos.delete(key); + } + this._onDidChangeTaskSystemInfo.fire(); + }); } private _getTaskSystemInfo(key: string): ITaskSystemInfo | undefined { diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index ec8414ee339f4..e665a8da0ccb1 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -104,7 +104,7 @@ export interface ITaskService { registerTaskProvider(taskProvider: ITaskProvider, type: string): IDisposable; - registerTaskSystem(scheme: string, taskSystemInfo: ITaskSystemInfo): void; + registerTaskSystem(scheme: string, taskSystemInfo: ITaskSystemInfo): IDisposable; readonly onDidChangeTaskSystemInfo: Event; readonly onDidChangeTaskConfig: Event; readonly hasTaskSystemInfo: boolean; diff --git a/src/vs/workbench/contrib/tasks/test/browser/abstractTaskService.test.ts b/src/vs/workbench/contrib/tasks/test/browser/abstractTaskService.test.ts new file mode 100644 index 0000000000000..47dc6a0a0a967 --- /dev/null +++ b/src/vs/workbench/contrib/tasks/test/browser/abstractTaskService.test.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Emitter } from '../../../../../base/common/event.js'; +import * as Platform from '../../../../../base/common/platform.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { AbstractTaskService } from '../../browser/abstractTaskService.js'; +import { ITaskSystemInfo } from '../../common/taskSystem.js'; + +suite('AbstractTaskService', function () { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('removes task system info and emits a change when disposed', function () { + const taskSystemInfoEmitter = store.add(new Emitter()); + const taskService = Object.create(AbstractTaskService.prototype) as AbstractTaskService; + Reflect.set(taskService, '_taskSystemInfos', new Map()); + Reflect.set(taskService, '_environmentService', { remoteAuthority: undefined }); + Reflect.set(taskService, '_onDidChangeTaskSystemInfo', taskSystemInfoEmitter); + Reflect.set(taskService, 'onDidChangeTaskSystemInfo', taskSystemInfoEmitter.event); + + const states: boolean[] = []; + store.add(taskService.onDidChangeTaskSystemInfo(() => states.push(taskService.hasTaskSystemInfo))); + const registration = store.add(taskService.registerTaskSystem('file', { + platform: Platform.Platform.Linux, + context: undefined, + uriProvider: path => URI.file(path), + resolveVariables: async () => undefined, + findExecutable: async () => undefined + })); + + registration.dispose(); + + assert.deepStrictEqual(states, [true, false]); + }); +});