Skip to content

Commit 20cb29b

Browse files
committed
Fixes microsoft#125430: Add a lock around the extension registry.
The problem was that `restartExtensionHost()` was called and immediately afterwards enablement changed events were coming in. This would lead to the delta extensions queue being handled while the extension host was starting, making the extension host start with newly enabled extensions, which were then attempted to be enabled via the delta extensions queue. The solution is to make starting and enablement changes handling exclusive via a lock
1 parent 383ae36 commit 20cb29b

File tree

2 files changed

+93
-14
lines changed

2 files changed

+93
-14
lines changed

src/vs/workbench/services/extensions/common/abstractExtensionService.ts

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as nls from 'vs/nls';
77
import { isNonEmptyArray } from 'vs/base/common/arrays';
88
import { Barrier } from 'vs/base/common/async';
99
import { Emitter, Event } from 'vs/base/common/event';
10-
import { Disposable } from 'vs/base/common/lifecycle';
10+
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
1111
import * as perf from 'vs/base/common/performance';
1212
import { isEqualOrParent } from 'vs/base/common/resources';
1313
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@@ -68,6 +68,69 @@ export const enum ExtensionRunningPreference {
6868
Remote
6969
}
7070

71+
class LockCustomer {
72+
public readonly promise: Promise<IDisposable>;
73+
private _resolve!: (value: IDisposable) => void;
74+
75+
constructor(
76+
public readonly name: string
77+
) {
78+
this.promise = new Promise<IDisposable>((resolve, reject) => {
79+
this._resolve = resolve;
80+
});
81+
}
82+
83+
resolve(value: IDisposable): void {
84+
this._resolve(value);
85+
}
86+
}
87+
88+
class Lock {
89+
private readonly _pendingCustomers: LockCustomer[] = [];
90+
private _isLocked = false;
91+
92+
public async acquire(customerName: string): Promise<IDisposable> {
93+
const customer = new LockCustomer(customerName);
94+
this._pendingCustomers.push(customer);
95+
this._advance();
96+
return customer.promise;
97+
}
98+
99+
private _advance(): void {
100+
if (this._isLocked) {
101+
// cannot advance yet
102+
return;
103+
}
104+
if (this._pendingCustomers.length === 0) {
105+
// no more waiting customers
106+
return;
107+
}
108+
109+
const customer = this._pendingCustomers.shift()!;
110+
111+
this._isLocked = true;
112+
let customerHoldsLock = true;
113+
114+
let logLongRunningCustomerTimeout = setTimeout(() => {
115+
if (customerHoldsLock) {
116+
console.warn(`The customer named ${customer.name} has been holding on to the lock for 30s. This might be a problem.`);
117+
}
118+
}, 30 * 1000 /* 30 seconds */);
119+
120+
const releaseLock = () => {
121+
if (!customerHoldsLock) {
122+
return;
123+
}
124+
clearTimeout(logLongRunningCustomerTimeout);
125+
customerHoldsLock = false;
126+
this._isLocked = false;
127+
this._advance();
128+
};
129+
130+
customer.resolve(toDisposable(releaseLock));
131+
}
132+
}
133+
71134
export abstract class AbstractExtensionService extends Disposable implements IExtensionService {
72135

73136
public _serviceBrand: undefined;
@@ -88,6 +151,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
88151
public readonly onDidChangeResponsiveChange: Event<IResponsiveStateChangeEvent> = this._onDidChangeResponsiveChange.event;
89152

90153
protected readonly _registry: ExtensionDescriptionRegistry;
154+
private readonly _registryLock: Lock;
155+
91156
private readonly _installedExtensionsReady: Barrier;
92157
protected readonly _isDev: boolean;
93158
private readonly _extensionsMessages: Map<string, IMessage[]>;
@@ -98,7 +163,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
98163

99164
private _deltaExtensionsQueue: DeltaExtensionsQueueItem[];
100165
private _inHandleDeltaExtensions: boolean;
101-
private readonly _onDidFinishHandleDeltaExtensions = this._register(new Emitter<void>());
102166

103167
protected _runningLocation: Map<string, ExtensionRunningLocation>;
104168

@@ -130,6 +194,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
130194
}));
131195

132196
this._registry = new ExtensionDescriptionRegistry([]);
197+
this._registryLock = new Lock();
198+
133199
this._installedExtensionsReady = new Barrier();
134200
this._isDev = !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment;
135201
this._extensionsMessages = new Map<string, IMessage[]>();
@@ -207,17 +273,20 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
207273
return;
208274
}
209275

210-
while (this._deltaExtensionsQueue.length > 0) {
211-
const item = this._deltaExtensionsQueue.shift()!;
212-
try {
213-
this._inHandleDeltaExtensions = true;
276+
let lock: IDisposable | null = null;
277+
try {
278+
this._inHandleDeltaExtensions = true;
279+
lock = await this._registryLock.acquire('handleDeltaExtensions');
280+
while (this._deltaExtensionsQueue.length > 0) {
281+
const item = this._deltaExtensionsQueue.shift()!;
214282
await this._deltaExtensions(item.toAdd, item.toRemove);
215-
} finally {
216-
this._inHandleDeltaExtensions = false;
283+
}
284+
} finally {
285+
this._inHandleDeltaExtensions = false;
286+
if (lock) {
287+
lock.dispose();
217288
}
218289
}
219-
220-
this._onDidFinishHandleDeltaExtensions.fire();
221290
}
222291

223292
private async _deltaExtensions(_toAdd: IExtension[], _toRemove: string[] | IExtension[]): Promise<void> {
@@ -566,11 +635,17 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
566635
public async startExtensionHosts(): Promise<void> {
567636
this.stopExtensionHosts();
568637

569-
if (this._inHandleDeltaExtensions) {
570-
await Event.toPromise(this._onDidFinishHandleDeltaExtensions.event);
571-
}
638+
const lock = await this._registryLock.acquire('startExtensionHosts');
639+
try {
640+
this._startExtensionHosts(false, Array.from(this._allRequestedActivateEvents.keys()));
572641

573-
this._startExtensionHosts(false, Array.from(this._allRequestedActivateEvents.keys()));
642+
const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess);
643+
if (localProcessExtensionHost) {
644+
await localProcessExtensionHost.ready();
645+
}
646+
} finally {
647+
lock.dispose();
648+
}
574649
}
575650

576651
public async restartExtensionHost(): Promise<void> {

src/vs/workbench/services/extensions/common/extensionHostManager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ export class ExtensionHostManager extends Disposable {
133133
return p.value;
134134
}
135135

136+
public async ready(): Promise<void> {
137+
await this._getProxy();
138+
}
139+
136140
private async _measureLatency(proxy: ExtHostExtensionServiceShape): Promise<number> {
137141
const COUNT = 10;
138142

0 commit comments

Comments
 (0)