Repository: microsoft/vscode (TypeScript)
Date: 2022-03-09 20:43:36+00:00
Task type: Bug Fix

Issue:
Problem with git extension and vscode-icons
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- 🕮 Read our guide about submitting issues: https://github.com/microsoft/vscode/wiki/Submitting-Bugs-and-Suggestions -->
<!-- 🔎 Search existing issues to avoid creating duplicates. -->
<!-- 🧪 Test using the latest Insiders build to see if your issue has already been fixed: https://code.visualstudio.com/insiders/ -->
<!-- 💡 Instead of creating your report here, use 'Report Issue' from the 'Help' menu in VS Code to pre-fill useful information. -->
<!-- 🔧 Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: No

<!-- 🪓 If you answered No above, use 'Help: Start Extension Bisect' from Command Palette to try to identify the cause. -->
<!-- 📣 Issues caused by an extension need to be reported directly to the extension publisher. The 'Help > Report Issue' dialog can assist with this. -->
- VS Code Version: 1.65.0
- OS Version: Fedora 35

Steps to Reproduce:

1. Install `vscode-icons` extension (v11.10.0)
2. Reload vscode, git extension is not initialized
3. Disable `vscode-icons` and restart, git extension works again

With vscode-icons disabled
```
[2022-03-06 19:04:34.182] [exthost] [info] Extension host with pid 14818 started
[2022-03-06 19:04:34.182] [exthost] [info] Skipping acquiring lock for /home/nenadfilipovic/.config/Code/User/workspaceStorage/244820f36cea0bfb0fd2cf8309ec405e.
[2022-03-06 19:04:34.354] [exthost] [info] ExtensionService#_doActivateExtension vscode.microsoft-authentication, startup: false, activationEvent: 'onAuthenticationRequest:microsoft'
[2022-03-06 19:04:34.398] [exthost] [info] ExtensionService#_doActivateExtension vscode.debug-auto-launch, startup: true, activationEvent: '*'
[2022-03-06 19:04:34.401] [exthost] [info] ExtensionService#_doActivateExtension vscode.git-base, startup: true, activationEvent: '*', root cause: vscode.github
[2022-03-06 19:04:34.404] [exthost] [info] ExtensionService#_doActivateExtension vscode.ipynb, startup: true, activationEvent: '*'
[2022-03-06 19:04:34.408] [exthost] [info] ExtensionService#_doActivateExtension monokai.theme-monokai-pro-vscode, startup: true, activationEvent: '*'
[2022-03-06 19:04:34.427] [exthost] [info] ExtensionService#_doActivateExtension vscode.npm, startup: true, activationEvent: 'workspaceContains:package.json'
[2022-03-06 19:04:34.501] [exthost] [info] ExtensionService#_doActivateExtension vscode.git, startup: true, activationEvent: '*'
[2022-03-06 19:04:34.519] [exthost] [info] ExtensionService#_doActivateExtension vscode.github, startup: true, activationEvent: '*'
[2022-03-06 19:04:34.613] [exthost] [info] ExtensionService#_doActivateExtension vscode.github-authentication, startup: false, activationEvent: 'onAuthenticationRequest:github'
[2022-03-06 19:04:34.729] [exthost] [info] Eager extensions activated
[2022-03-06 19:04:34.785] [exthost] [info] ExtensionService#_doActivateExtension vscode.emmet, startup: false, activationEvent: 'onStartupFinished'
[2022-03-06 19:04:34.791] [exthost] [info] ExtensionService#_doActivateExtension vscode.merge-conflict, startup: false, activationEvent: 'onStartupFinished'
[2022-03-06 19:04:34.794] [exthost] [info] ExtensionService#_doActivateExtension dbaeumer.vscode-eslint, startup: false, activationEvent: 'onStartupFinished'
[2022-03-06 19:04:34.806] [exthost] [info] ExtensionService#_doActivateExtension esbenp.prettier-vscode, startup: false, activationEvent: 'onStartupFinished'
[2022-03-06 19:04:35.020] [exthost] [info] ExtensionService#_doActivateExtension ms-vscode-remote.remote-containers, startup: false, activationEvent: 'onStartupFinished'
```

With vscode-icons enabled
```
[2022-03-06 19:17:28.353] [exthost] [info] Extension host with pid 15578 started
[2022-03-06 19:17:28.353] [exthost] [info] Skipping acquiring lock for /home/nenadfilipovic/.config/Code/User/workspaceStorage/244820f36cea0bfb0fd2cf8309ec405e.
[2022-03-06 19:17:28.710] [exthost] [info] ExtensionService#_doActivateExtension vscode.microsoft-authentication, startup: false, activationEvent: 'onAuthenticationRequest:microsoft'
[2022-03-06 19:17:28.733] [exthost] [info] ExtensionService#_doActivateExtension vscode.debug-auto-launch, startup: true, activationEvent: '*'
[2022-03-06 19:17:28.736] [exthost] [info] ExtensionService#_doActivateExtension vscode.git-base, startup: true, activationEvent: '*', root cause: vscode.github
[2022-03-06 19:17:28.740] [exthost] [info] ExtensionService#_doActivateExtension vscode.ipynb, startup: true, activationEvent: '*'
[2022-03-06 19:17:28.747] [exthost] [info] ExtensionService#_doActivateExtension monokai.theme-monokai-pro-vscode, startup: true, activationEvent: '*'
[2022-03-06 19:17:28.752] [exthost] [info] ExtensionService#_doActivateExtension vscode-icons-team.vscode-icons, startup: true, activationEvent: '*'
[2022-03-06 19:17:28.775] [exthost] [info] ExtensionService#_doActivateExtension vscode.npm, startup: true, activationEvent: 'workspaceContains:package.json'
[2022-03-06 19:17:29.356] [exthost] [info] ExtensionService#_doActivateExtension vscode.github-authentication, startup: false, activationEvent: 'onAuthenticationRequest:github'
```


Issue discussion:
Reproduces with the following extension:
* `package.json`:
```json
{
    "publisher": "vscode-icons-team",
    "name": "144518",
    "version": "0.0.0",
    "engines": {
        "vscode": "^1.0.0"
    },
    "activationEvents": ["*"],
    "main": "index.js"
}
```
* `index.js`:
```js
exports.activate = function() {
    return new Promise((resolve, reject) => {
        // never resolve
    });
};
```

Solution patch:
diff --git a/src/vs/workbench/api/common/extHostExtensionActivator.ts b/src/vs/workbench/api/common/extHostExtensionActivator.ts
--- a/src/vs/workbench/api/common/extHostExtensionActivator.ts
+++ b/src/vs/workbench/api/common/extHostExtensionActivator.ts
@@ -10,8 +10,7 @@ import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/c
 import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
 import { ExtensionActivationReason, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions';
 import { ILogService } from 'vs/platform/log/common/log';
-
-const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
+import { Barrier } from 'vs/base/common/async';
 
 /**
  * Represents the source code (module) of an extension.
@@ -166,14 +165,11 @@ type ActivationIdAndReason = { id: ExtensionIdentifier; reason: ExtensionActivat
 
 export class ExtensionsActivator implements IDisposable {
 
-	private _isDisposed: boolean;
-
 	private readonly _registry: ExtensionDescriptionRegistry;
 	private readonly _resolvedExtensionsSet: Set<string>;
 	private readonly _externalExtensionsMap: Map<string, ExtensionIdentifier>;
 	private readonly _host: IExtensionsActivatorHost;
-	private readonly _activatingExtensions: Map<string, Promise<void>>;
-	private readonly _activatedExtensions: Map<string, ActivatedExtension>;
+	private readonly _operations: Map<string, ActivationOperation>;
 	/**
 	 * A map of already activated events to speed things up if the same activation event is triggered multiple times.
 	 */
@@ -186,126 +182,111 @@ export class ExtensionsActivator implements IDisposable {
 		host: IExtensionsActivatorHost,
 		@ILogService private readonly _logService: ILogService
 	) {
-		this._isDisposed = false;
 		this._registry = registry;
 		this._resolvedExtensionsSet = new Set<string>();
 		resolvedExtensions.forEach((extensionId) => this._resolvedExtensionsSet.add(ExtensionIdentifier.toKey(extensionId)));
 		this._externalExtensionsMap = new Map<string, ExtensionIdentifier>();
 		externalExtensions.forEach((extensionId) => this._externalExtensionsMap.set(ExtensionIdentifier.toKey(extensionId), extensionId));
 		this._host = host;
-		this._activatingExtensions = new Map<string, Promise<void>>();
-		this._activatedExtensions = new Map<string, ActivatedExtension>();
+		this._operations = new Map<string, ActivationOperation>();
 		this._alreadyActivatedEvents = Object.create(null);
 	}
 
 	public dispose(): void {
-		this._isDisposed = true;
+		for (const [_, op] of this._operations) {
+			op.dispose();
+		}
 	}
 
 	public isActivated(extensionId: ExtensionIdentifier): boolean {
-		const extensionKey = ExtensionIdentifier.toKey(extensionId);
-
-		return this._activatedExtensions.has(extensionKey);
+		const op = this._operations.get(ExtensionIdentifier.toKey(extensionId));
+		return Boolean(op && op.value);
 	}
 
 	public getActivatedExtension(extensionId: ExtensionIdentifier): ActivatedExtension {
-		const extensionKey = ExtensionIdentifier.toKey(extensionId);
-
-		const activatedExtension = this._activatedExtensions.get(extensionKey);
-		if (!activatedExtension) {
-			throw new Error('Extension `' + extensionId.value + '` is not known or not activated');
+		const op = this._operations.get(ExtensionIdentifier.toKey(extensionId));
+		if (!op || !op.value) {
+			throw new Error(`Extension '${extensionId.value}' is not known or not activated`);
 		}
-		return activatedExtension;
+		return op.value;
 	}
 
-	public activateByEvent(activationEvent: string, startup: boolean): Promise<void> {
+	public async activateByEvent(activationEvent: string, startup: boolean): Promise<void> {
 		if (this._alreadyActivatedEvents[activationEvent]) {
-			return NO_OP_VOID_PROMISE;
+			return;
 		}
+
 		const activateExtensions = this._registry.getExtensionDescriptionsForActivationEvent(activationEvent);
-		return this._activateExtensions(activateExtensions.map(e => ({
+		await this._activateExtensions(activateExtensions.map(e => ({
 			id: e.identifier,
 			reason: { startup, extensionId: e.identifier, activationEvent }
-		}))).then(() => {
-			this._alreadyActivatedEvents[activationEvent] = true;
-		});
+		})));
+
+		this._alreadyActivatedEvents[activationEvent] = true;
 	}
 
 	public activateById(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void> {
 		const desc = this._registry.getExtensionDescription(extensionId);
 		if (!desc) {
-			throw new Error('Extension `' + extensionId + '` is not known');
+			throw new Error(`Extension '${extensionId}' is not known`);
 		}
+		return this._activateExtensions([{ id: desc.identifier, reason }]);
+	}
 
-		return this._activateExtensions([{
-			id: desc.identifier,
-			reason
-		}]);
+	private async _activateExtensions(extensions: ActivationIdAndReason[]): Promise<void> {
+		const operations = extensions
+			.filter((p) => !this.isActivated(p.id))
+			.map(ext => this._handleActivationRequest(ext));
+		await Promise.all(operations.map(op => op.wait()));
 	}
 
 	/**
 	 * Handle semantics related to dependencies for `currentExtension`.
-	 * semantics: `redExtensions` must wait for `greenExtensions`.
+	 * We don't need to worry about dependency loops because they are handled by the registry.
 	 */
-	private _handleActivateRequest(currentActivation: ActivationIdAndReason, greenExtensions: { [id: string]: ActivationIdAndReason }, redExtensions: ActivationIdAndReason[]): void {
+	private _handleActivationRequest(currentActivation: ActivationIdAndReason): ActivationOperation {
+		if (this._operations.has(ExtensionIdentifier.toKey(currentActivation.id))) {
+			return this._operations.get(ExtensionIdentifier.toKey(currentActivation.id))!;
+		}
+
 		if (this._externalExtensionsMap.has(ExtensionIdentifier.toKey(currentActivation.id))) {
-			greenExtensions[ExtensionIdentifier.toKey(currentActivation.id)] = currentActivation;
-			return;
+			return this._createAndSaveOperation(currentActivation, null, [], null);
 		}
 
 		const currentExtension = this._registry.getExtensionDescription(currentActivation.id);
 		if (!currentExtension) {
 			// Error condition 0: unknown extension
 			const error = new Error(`Cannot activate unknown extension '${currentActivation.id.value}'`);
+			const result = this._createAndSaveOperation(currentActivation, null, [], new FailedExtension(error));
 			this._host.onExtensionActivationError(
 				currentActivation.id,
 				error,
 				new MissingExtensionDependency(currentActivation.id.value)
 			);
-			this._activatedExtensions.set(ExtensionIdentifier.toKey(currentActivation.id), new FailedExtension(error));
-			return;
+			return result;
 		}
 
+		const deps: ActivationOperation[] = [];
 		const depIds = (typeof currentExtension.extensionDependencies === 'undefined' ? [] : currentExtension.extensionDependencies);
-		let currentExtensionGetsGreenLight = true;
-
-		for (let j = 0, lenJ = depIds.length; j < lenJ; j++) {
-			const depId = depIds[j];
+		for (const depId of depIds) {
 
 			if (this._resolvedExtensionsSet.has(ExtensionIdentifier.toKey(depId))) {
 				// This dependency is already resolved
 				continue;
 			}
 
-			const dep = this._activatedExtensions.get(ExtensionIdentifier.toKey(depId));
-			if (dep && !dep.activationFailed) {
-				// the dependency is already activated OK
+			const dep = this._operations.get(ExtensionIdentifier.toKey(depId));
+			if (dep) {
+				deps.push(dep);
 				continue;
 			}
 
-			if (dep && dep.activationFailed) {
-				// Error condition 2: a dependency has already failed activation
-				const currentExtensionFriendlyName = currentExtension.displayName || currentExtension.identifier.value;
-				const depDesc = this._registry.getExtensionDescription(depId);
-				const depFriendlyName = (depDesc ? depDesc.displayName || depId : depId);
-				const error = new Error(`Cannot activate the '${currentExtensionFriendlyName}' extension because its dependency '${depFriendlyName}' failed to activate`);
-				(<any>error).detail = dep.activationFailedError;
-				this._host.onExtensionActivationError(
-					currentExtension.identifier,
-					error,
-					null
-				);
-				this._activatedExtensions.set(ExtensionIdentifier.toKey(currentExtension.identifier), new FailedExtension(error));
-				return;
-			}
-
 			if (this._externalExtensionsMap.has(ExtensionIdentifier.toKey(depId))) {
 				// must first wait for the dependency to activate
-				currentExtensionGetsGreenLight = false;
-				greenExtensions[ExtensionIdentifier.toKey(depId)] = {
+				deps.push(this._handleActivationRequest({
 					id: this._externalExtensionsMap.get(ExtensionIdentifier.toKey(depId))!,
 					reason: currentActivation.reason
-				};
+				}));
 				continue;
 			}
 
@@ -317,118 +298,140 @@ export class ExtensionsActivator implements IDisposable {
 				}
 
 				// must first wait for the dependency to activate
-				currentExtensionGetsGreenLight = false;
-				greenExtensions[ExtensionIdentifier.toKey(depId)] = {
+				deps.push(this._handleActivationRequest({
 					id: depDesc.identifier,
 					reason: currentActivation.reason
-				};
+				}));
 				continue;
 			}
 
 			// Error condition 1: unknown dependency
 			const currentExtensionFriendlyName = currentExtension.displayName || currentExtension.identifier.value;
 			const error = new Error(`Cannot activate the '${currentExtensionFriendlyName}' extension because it depends on unknown extension '${depId}'`);
+			const result = this._createAndSaveOperation(currentActivation, currentExtension.displayName, [], new FailedExtension(error));
 			this._host.onExtensionActivationError(
 				currentExtension.identifier,
 				error,
 				new MissingExtensionDependency(depId)
 			);
-			this._activatedExtensions.set(ExtensionIdentifier.toKey(currentExtension.identifier), new FailedExtension(error));
-			return;
+			return result;
 		}
 
-		if (currentExtensionGetsGreenLight) {
-			greenExtensions[ExtensionIdentifier.toKey(currentExtension.identifier)] = currentActivation;
-		} else {
-			redExtensions.push(currentActivation);
-		}
+		return this._createAndSaveOperation(currentActivation, currentExtension.displayName, deps, null);
 	}
 
-	private _activateExtensions(extensions: ActivationIdAndReason[]): Promise<void> {
-		if (extensions.length === 0) {
-			return Promise.resolve(undefined);
-		}
+	private _createAndSaveOperation(activation: ActivationIdAndReason, displayName: string | null | undefined, deps: ActivationOperation[], value: ActivatedExtension | null): ActivationOperation {
+		const operation = new ActivationOperation(activation.id, displayName, activation.reason, deps, value, this._host, this._logService);
+		this._operations.set(ExtensionIdentifier.toKey(activation.id), operation);
+		return operation;
+	}
+}
 
-		extensions = extensions.filter((p) => !this._activatedExtensions.has(ExtensionIdentifier.toKey(p.id)));
-		if (extensions.length === 0) {
-			return Promise.resolve(undefined);
-		}
+class ActivationOperation {
 
-		const greenMap: { [id: string]: ActivationIdAndReason } = Object.create(null),
-			red: ActivationIdAndReason[] = [];
+	private readonly _barrier = new Barrier();
+	private _isDisposed = false;
 
-		for (let i = 0, len = extensions.length; i < len; i++) {
-			this._handleActivateRequest(extensions[i], greenMap, red);
-		}
+	public get value(): ActivatedExtension | null {
+		return this._value;
+	}
 
-		// Make sure no red is also green
-		for (let i = 0, len = red.length; i < len; i++) {
-			const redExtensionKey = ExtensionIdentifier.toKey(red[i].id);
-			if (greenMap[redExtensionKey]) {
-				delete greenMap[redExtensionKey];
-			}
-		}
+	public get friendlyName(): string {
+		return this._displayName || this._id.value;
+	}
 
-		const green = Object.keys(greenMap).map(id => greenMap[id]);
+	constructor(
+		private readonly _id: ExtensionIdentifier,
+		private readonly _displayName: string | null | undefined,
+		private readonly _reason: ExtensionActivationReason,
+		private readonly _deps: ActivationOperation[],
+		private _value: ActivatedExtension | null,
+		private readonly _host: IExtensionsActivatorHost,
+		@ILogService private readonly _logService: ILogService
+	) {
+		this._initialize();
+	}
 
-		if (red.length === 0) {
-			// Finally reached only leafs!
-			return Promise.all(green.map((p) => this._activateExtension(p.id, p.reason))).then(_ => undefined);
-		}
+	public dispose(): void {
+		this._isDisposed = true;
+	}
 
-		return this._activateExtensions(green).then(_ => {
-			return this._activateExtensions(red);
-		});
+	public wait() {
+		return this._barrier.wait();
 	}
 
-	private _activateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void> {
-		const extensionKey = ExtensionIdentifier.toKey(extensionId);
+	private async _initialize(): Promise<void> {
+		await this._waitForDepsThenActivate();
+		this._barrier.open();
+	}
 
-		if (this._activatedExtensions.has(extensionKey)) {
-			return Promise.resolve(undefined);
+	private async _waitForDepsThenActivate(): Promise<void> {
+		if (this._value) {
+			// this operation is already finished
+			return;
 		}
 
-		const currentlyActivatingExtension = this._activatingExtensions.get(extensionKey);
-		if (currentlyActivatingExtension) {
-			return currentlyActivatingExtension;
+		while (this._deps.length > 0) {
+			// remove completed deps
+			for (let i = 0; i < this._deps.length; i++) {
+				const dep = this._deps[i];
+
+				if (dep.value && !dep.value.activationFailed) {
+					// the dependency is already activated OK
+					this._deps.splice(i, 1);
+					i--;
+					continue;
+				}
+
+				if (dep.value && dep.value.activationFailed) {
+					// Error condition 2: a dependency has already failed activation
+					const error = new Error(`Cannot activate the '${this.friendlyName}' extension because its dependency '${dep.friendlyName}' failed to activate`);
+					(<any>error).detail = dep.value.activationFailedError;
+					this._value = new FailedExtension(error);
+					this._host.onExtensionActivationError(this._id, error, null);
+					return;
+				}
+			}
+
+			if (this._deps.length > 0) {
+				// wait for one dependency
+				await Promise.race(this._deps.map(dep => dep.wait()));
+			}
 		}
 
-		const newlyActivatingExtension = this._host.actualActivateExtension(extensionId, reason).then(undefined, (err) => {
+		await this._activate();
+	}
+
+	private async _activate(): Promise<void> {
+		try {
+			this._value = await this._host.actualActivateExtension(this._id, this._reason);
+		} catch (err) {
 
 			const error = new Error();
 			if (err && err.name) {
 				error.name = err.name;
 			}
 			if (err && err.message) {
-				error.message = `Activating extension '${extensionId.value}' failed: ${err.message}.`;
+				error.message = `Activating extension '${this._id.value}' failed: ${err.message}.`;
 			} else {
-				error.message = `Activating extension '${extensionId.value}' failed: ${err}.`;
+				error.message = `Activating extension '${this._id.value}' failed: ${err}.`;
 			}
 			if (err && err.stack) {
 				error.stack = err.stack;
 			}
 
+			// Treat the extension as being empty
+			this._value = new FailedExtension(error);
+
 			if (this._isDisposed && errors.isCancellationError(err)) {
 				// It is expected for ongoing activations to fail if the extension host is going down
 				// So simply ignore and don't log canceled errors in this case
-				return new FailedExtension(err);
+				return;
 			}
 
-			this._host.onExtensionActivationError(
-				extensionId,
-				error,
-				null
-			);
-			this._logService.error(`Activating extension ${extensionId.value} failed due to an error:`);
+			this._host.onExtensionActivationError(this._id, error, null);
+			this._logService.error(`Activating extension ${this._id.value} failed due to an error:`);
 			this._logService.error(err);
-			// Treat the extension as being empty
-			return new FailedExtension(err);
-		}).then((x: ActivatedExtension) => {
-			this._activatedExtensions.set(extensionKey, x);
-			this._activatingExtensions.delete(extensionKey);
-		});
-
-		this._activatingExtensions.set(extensionKey, newlyActivatingExtension);
-		return newlyActivatingExtension;
+		}
 	}
 }


Test patch:
diff --git a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts
--- a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts
+++ b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts
@@ -164,6 +164,31 @@ suite('ExtensionsActivator', () => {
 		assert.deepStrictEqual(host.errors[1][0], idA);
 	});
 
+	test('issue #144518: Problem with git extension and vscode-icons', async () => {
+		const extActivationA = new ExtensionActivationPromiseSource();
+		const extActivationB = new ExtensionActivationPromiseSource();
+		const extActivationC = new ExtensionActivationPromiseSource();
+		const host = new PromiseExtensionsActivatorHost([
+			[idA, extActivationA],
+			[idB, extActivationB],
+			[idC, extActivationC]
+		]);
+		const activator = createActivator(host, [
+			desc(idA, [idB]),
+			desc(idB),
+			desc(idC),
+		]);
+
+		activator.activateByEvent('*', false);
+		assert.deepStrictEqual(host.activateCalls, [idB, idC]);
+
+		extActivationB.resolve();
+		await timeout(0);
+
+		assert.deepStrictEqual(host.activateCalls, [idB, idC, idA]);
+		extActivationA.resolve();
+	});
+
 	class SimpleExtensionsActivatorHost implements IExtensionsActivatorHost {
 		public readonly activateCalls: ExtensionIdentifier[] = [];
 		public readonly errors: [ExtensionIdentifier, Error | null, MissingExtensionDependency | null][] = [];