Skip to content

Commit 3680622

Browse files
authored
Defend against missing extension paths (#13649)
Prior to this change, Positron could hang during startup with the UI stuck in "Preparing", the extension host process alive but no extensions activating, and most of the workbench frozen. The trigger was a stale extension directory on disk, typically a bootstrap extension whose old versioned folder (e.g. `posit.air-vscode-0.24.0-darwin-arm64`) had been removed but was still referenced by the in-memory extension registry after a version update. The root cause was in the extension host's `$startExtensionHost`: it built a path index by calling `realpath` on every registered extension's location inside a `Promise.all`. A single missing directory threw `ENOENT`, rejected the whole `Promise.all`, and prevented `_startExtensionHost()` from ever running ... which meant the `_readyToRunExtensions` barrier never opened, so every subsequent `$activate` RPC waited forever. On the renderer side, the Positron-added `Promise.all(allExtensionHostStartups).then(...)` had no `.catch`, so the failure didn't even surface as a visible error. We now defend against this in three ways: 1. `_createExtensionPathIndex` now skips and logs individual extensions whose paths can't be resolved instead of failing the whole index build. 2. `$startExtensionHost` wraps its registry-prep preamble in try/catch so `_startExtensionHost()` is always called and the host always becomes ready, even if the path index build hits an unexpected error. 3. The renderer-side `Promise.all` on extension-host startups is now `Promise.allSettled`, with each rejection logged and the `_allExtensionHostsStarted` barrier always opened so failures are observable instead of silent. Fixes #12864. ### Release Notes <!-- Optionally, replace `N/A` with text to be included in the next release notes. The `N/A` bullets are ignored. If you refer to one or more Positron issues, these issues are used to collect information about the feature or bugfix, such as the relevant language pack as determined by Github labels of type `lang: `. The note will automatically be tagged with the language. These notes are typically filled by the Positron team. If you are an external contributor, you may ignore this section. --> #### New Features - N/A #### Bug Fixes - Address occasional hang in "Preparing" when starting Positron after updating to a new version (#12864) ### Validation Steps Manually deleting an installed extension's directory while leaving its entry in the registry; confirm Positron now starts normally with a warning in the log rather than hanging in "Preparing".
1 parent 39c9c83 commit 3680622

2 files changed

Lines changed: 42 additions & 13 deletions

File tree

src/vs/workbench/api/common/extHostExtensionService.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -367,12 +367,23 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
367367
return extUriBiasedIgnorePathCase.ignorePathCasing(key);
368368
});
369369
// const tst = TernarySearchTree.forUris<IExtensionDescription>(key => true);
370+
// --- Start Positron ---
371+
// Skip extensions whose location can't be resolved (e.g. directory was
372+
// removed after a version swap). A single missing path used to reject
373+
// the whole index and prevent the extension host from ever becoming
374+
// ready.
370375
await Promise.all(extensions.map(async (ext) => {
371-
if (this._getEntryPoint(ext)) {
376+
if (!this._getEntryPoint(ext)) {
377+
return;
378+
}
379+
try {
372380
const uri = await this._realPathExtensionUri(ext.extensionLocation);
373381
tst.set(uri, ext);
382+
} catch (err) {
383+
this._logService.warn(`Skipping extension '${ext.identifier.value}' in path index; cannot resolve ${ext.extensionLocation.toString()}: ${err}`);
374384
}
375385
}));
386+
// --- End Positron ---
376387
return tst;
377388
}
378389

@@ -1013,19 +1024,30 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
10131024
// eslint-disable-next-line local/code-no-any-casts
10141025
extensionsDelta.toAdd.forEach((extension) => (<any>extension).extensionLocation = URI.revive(extension.extensionLocation));
10151026

1016-
const { globalRegistry, myExtensions } = applyExtensionsDelta(this._activationEventsReader, this._globalRegistry, this._myRegistry, extensionsDelta);
1017-
const newSearchTree = await this._createExtensionPathIndex(myExtensions);
1018-
const extensionsPaths = await this.getExtensionPathIndex();
1019-
extensionsPaths.setSearchTree(newSearchTree);
1020-
this._globalRegistry.set(globalRegistry.getAllExtensionDescriptions());
1021-
this._myRegistry.set(myExtensions);
1022-
1023-
if (isCI) {
1024-
this._logService.info(`$startExtensionHost: global extensions: ${printExtIds(this._globalRegistry)}`);
1025-
this._logService.info(`$startExtensionHost: local extensions: ${printExtIds(this._myRegistry)}`);
1027+
// --- Start Positron ---
1028+
// Defend against the preamble throwing. If we let an error escape here,
1029+
// `_startExtensionHost()` is never called, `_readyToRunExtensions` is
1030+
// never opened, and every subsequent `$activate*` RPC hangs forever --
1031+
// the IDE stalls in "Preparing" with the extension host alive but
1032+
// useless.
1033+
try {
1034+
const { globalRegistry, myExtensions } = applyExtensionsDelta(this._activationEventsReader, this._globalRegistry, this._myRegistry, extensionsDelta);
1035+
const newSearchTree = await this._createExtensionPathIndex(myExtensions);
1036+
const extensionsPaths = await this.getExtensionPathIndex();
1037+
extensionsPaths.setSearchTree(newSearchTree);
1038+
this._globalRegistry.set(globalRegistry.getAllExtensionDescriptions());
1039+
this._myRegistry.set(myExtensions);
1040+
1041+
if (isCI) {
1042+
this._logService.info(`$startExtensionHost: global extensions: ${printExtIds(this._globalRegistry)}`);
1043+
this._logService.info(`$startExtensionHost: local extensions: ${printExtIds(this._myRegistry)}`);
1044+
}
1045+
} catch (err) {
1046+
this._logService.error(`$startExtensionHost: error preparing extension registry; continuing with extension host startup`, err);
10261047
}
10271048

10281049
return this._startExtensionHost();
1050+
// --- End Positron ---
10291051
}
10301052

10311053
public $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void> {

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,8 +1002,15 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
10021002

10031003
// --- Start Positron ---
10041004
// When all the extension hosts have started, we can notify the main
1005-
// thread.
1006-
Promise.all(allExtensionHostStartups).then(() => {
1005+
// thread. Always open the barrier -- if a host's startup rejected, we
1006+
// log it but still release waiters so the workbench doesn't hang
1007+
// silently.
1008+
Promise.allSettled(allExtensionHostStartups).then((results) => {
1009+
for (const result of results) {
1010+
if (result.status === 'rejected') {
1011+
this._logService.error(`Extension host startup failed`, result.reason);
1012+
}
1013+
}
10071014
this._allExtensionHostsStarted.open();
10081015
});
10091016
// --- End Positron ---

0 commit comments

Comments
 (0)