From c2a42605f7858dd775613df5bb303067ebdca273 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 6 May 2026 15:42:18 -0700 Subject: [PATCH] Terminate COM surrogate process before update/gc (#313791) --------- Co-authored-by: Copilot --- build/gulpfile.vscode.win32.ts | 4 ++ build/win32/code.iss | 29 ++++++++++++++ src/vs/base/common/product.ts | 1 + src/vs/code/electron-main/main.ts | 32 ++++++++++++--- .../electron-main/updateService.win32.ts | 39 +++++++++++++++++++ 5 files changed, 99 insertions(+), 6 deletions(-) diff --git a/build/gulpfile.vscode.win32.ts b/build/gulpfile.vscode.win32.ts index 7a711276be5..01070c9503c 100644 --- a/build/gulpfile.vscode.win32.ts +++ b/build/gulpfile.vscode.win32.ts @@ -135,6 +135,10 @@ function buildWin32Setup(arch: string, target: string): task.CallbackTask { definitions['AppxPackage'] = `${quality === 'stable' ? 'code' : 'code_insider'}_${arch}.appx`; definitions['AppxPackageDll'] = `${quality === 'stable' ? 'code' : 'code_insider'}_explorer_command_${arch}.dll`; definitions['AppxPackageName'] = `${product.win32AppUserModelId}`; + const ctxMenu = (product as { win32ContextMenu?: Record }).win32ContextMenu; + if (ctxMenu && ctxMenu[arch]) { + definitions['FileExplorerContextMenuCLSID'] = ctxMenu[arch].clsid; + } } fs.writeFileSync(productJsonPath, JSON.stringify(productJson, undefined, '\t')); diff --git a/build/win32/code.iss b/build/win32/code.iss index 7f23530dcb1..594453ed7b9 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -1718,6 +1718,32 @@ begin Result := False; end; +// Unblock inno_updater --gc when our context-menu COM surrogate keeps a +// handle on the orphan commit folder. See https://github.com/microsoft/vscode/issues/294546. +// No-op when FileExplorerContextMenuCLSID is not defined (e.g. OSS builds). +procedure KillContextMenuComSurrogate(); +var + KillErrorCode: Integer; + Command: String; +begin +#ifdef FileExplorerContextMenuCLSID + Log('KillContextMenuComSurrogate: stopping COM surrogate(s) hosting context-menu DLL ({#FileExplorerContextMenuCLSID})'); + + Command := + '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command "' + + 'Get-CimInstance Win32_Process -Filter ""Name = ''dllhost.exe''"" | ' + + 'Where-Object { $_.CommandLine -like ''*/Processid:{#FileExplorerContextMenuCLSID}*'' } | ' + + 'ForEach-Object { try { Stop-Process -Id $_.ProcessId -Force -ErrorAction Stop } catch {} }"'; + + if not ShellExec('', 'powershell.exe', Command, '', SW_HIDE, ewWaitUntilTerminated, KillErrorCode) then + Log('KillContextMenuComSurrogate: ShellExec failed with error code ' + IntToStr(KillErrorCode)) + else if KillErrorCode <> 0 then + Log('KillContextMenuComSurrogate: PowerShell exited with non-zero code ' + IntToStr(KillErrorCode)) + else + Log('KillContextMenuComSurrogate: complete'); +#endif +end; + #ifdef AppxPackageName var AppxPackageFullname: String; @@ -1776,6 +1802,7 @@ procedure RemoveAppxPackage(); var RemoveAppxPackageResultCode: Integer; begin + KillContextMenuComSurrogate(); // Remove the old context menu package // Following condition can be removed in v1.111. if QualityIsInsiders() and not SessionEndFileExists() and AppxPackageInstalled('Microsoft.VSCodeInsiders', RemoveAppxPackageResultCode) then begin @@ -1860,6 +1887,7 @@ begin Log('inno_updater completed successfully'); #if "system" == InstallTarget if IsVersionedUpdate() then begin + KillContextMenuComSurrogate(); Log('Invoking inno_updater to remove previous installation folder'); Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}" "{#ExeBasename}.exe"' {#ifdef ProxyExeBasename} + ' "{#ProxyExeBasename}.exe"' {#endif}), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); Log('inno_updater completed gc successfully'); @@ -1870,6 +1898,7 @@ begin end; end else begin if IsVersionedUpdate() then begin + KillContextMenuComSurrogate(); Log('Invoking inno_updater to remove previous installation folder'); Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}" "{#ExeBasename}.exe"' {#ifdef ProxyExeBasename} + ' "{#ProxyExeBasename}.exe"' {#endif}), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); Log('inno_updater completed gc successfully'); diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index bc7c8f68eeb..f594d9451ae 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -80,6 +80,7 @@ export interface IProductConfiguration { readonly win32NameVersion?: string; readonly win32VersionedUpdate?: boolean; readonly win32SiblingExeBasename?: string; + readonly win32ContextMenu?: { readonly [arch: string]: { readonly clsid: string } }; readonly applicationName: string; readonly embedderIdentifier?: string; readonly telemetryAppName?: string; diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 6ad77c91cac..dbc801366dd 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -9,7 +9,7 @@ import { app, dialog } from 'electron'; import { unlinkSync, promises } from 'fs'; import { URI } from '../../base/common/uri.js'; import { coalesce, distinct } from '../../base/common/arrays.js'; -import { Promises } from '../../base/common/async.js'; +import { Promises, retry } from '../../base/common/async.js'; import { toErrorMessage } from '../../base/common/errorMessage.js'; import { ExpectedError, setUnexpectedErrorHandler } from '../../base/common/errors.js'; import { IPathWithLineAndColumn, isValidBasename, parseLineAndColumnAware, sanitizeFilePath } from '../../base/common/extpath.js'; @@ -144,8 +144,8 @@ class CodeMain { evt.join('instanceLockfile', promises.unlink(environmentMainService.mainLockfile).catch(() => { /* ignored */ })); }); - // Check if Inno Setup is running - const innoSetupActive = await this.checkInnoSetupMutex(productService); + // Check if Inno Setup is running. Briefly wait for the updating mutex to be released before refusing to launch. + const innoSetupActive = await this.checkInnoSetupMutex(productService, logService); if (innoSetupActive) { const message = `${productService.nameShort} is currently being updated. Please wait for the update to complete before launching.`; instantiationService.invokeFunction(this.quit, new Error(message)); @@ -501,7 +501,7 @@ class CodeMain { lifecycleMainService.kill(exitCode); } - private async checkInnoSetupMutex(productService: IProductService): Promise { + private async checkInnoSetupMutex(productService: IProductService, logService: ILogService): Promise { if (!(isWindows && productService.win32MutexName && productService.win32VersionedUpdate)) { return false; } @@ -509,9 +509,29 @@ class CodeMain { try { const updatingMutexName = `${productService.win32MutexName}-updating`; const mutex = await import('@vscode/windows-mutex'); - return mutex.isActive(updatingMutexName); + + if (!mutex.isActive(updatingMutexName)) { + return false; + } + + // Wait briefly for setup teardown to release the mutex; Inno's `nowait postinstall` runcode can race the setup process exit. + const pollIntervalMs = 250, retries = 120; // 30s total + logService.info(`checkInnoSetupMutex: ${updatingMutexName} is held, waiting up to ${(pollIntervalMs * retries) / 1000}s for setup to finish...`); + const start = Date.now(); + try { + await retry(async () => { + if (mutex.isActive(updatingMutexName)) { + throw new Error('mutex still held'); + } + }, pollIntervalMs, retries); + logService.info(`checkInnoSetupMutex: ${updatingMutexName} released after ${Date.now() - start}ms`); + return false; + } catch { + logService.warn(`checkInnoSetupMutex: ${updatingMutexName} still held after ${Date.now() - start}ms, giving up`); + return true; + } } catch (error) { - console.error('Failed to check Inno Setup mutex:', error); + logService.error('Failed to check Inno Setup mutex:', error); return false; } } diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 63327397766..6b97e209fb7 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -164,6 +164,9 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun const innoUpdater = path.join(exeDir, versionedResourcesFolder, 'tools', 'inno_updater.exe'); const exeName = basename(exePath); const siblingExeName = this.productService.win32SiblingExeBasename ? `${this.productService.win32SiblingExeBasename}.exe` : ''; + // Unblock inno_updater --gc when our context-menu COM surrogate keeps a + // handle on the orphan commit folder. See https://github.com/microsoft/vscode/issues/294546. + await this.killContextMenuComSurrogate(); await new Promise(resolve => { const child = spawn(innoUpdater, ['--gc', exePath, versionedResourcesFolder, exeName, siblingExeName], { stdio: ['ignore', 'ignore', 'ignore'], @@ -176,6 +179,42 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } } + private async killContextMenuComSurrogate(): Promise { + const clsid = this.productService.win32ContextMenu?.[process.arch]?.clsid; + if (!clsid) { + return; + } + + const command = + `Get-CimInstance Win32_Process -Filter "Name = 'dllhost.exe'" | ` + + `Where-Object { $_.CommandLine -like '*/Processid:${clsid}*' } | ` + + `ForEach-Object { try { Stop-Process -Id $_.ProcessId -Force -ErrorAction Stop } catch {} }`; + + await new Promise(resolve => { + try { + spawn('powershell.exe', [ + '-NoLogo', '-NoProfile', '-NonInteractive', + '-WindowStyle', 'Hidden', + '-ExecutionPolicy', 'Bypass', + '-Command', command + ], { + stdio: ['ignore', 'ignore', 'ignore'], + windowsHide: true, + timeout: 5 * 1000 + }).once('exit', code => { + this.logService.info(`update#killContextMenuComSurrogate: powershell exited with code ${code}`); + resolve(); + }).once('error', err => { + this.logService.warn(`update#killContextMenuComSurrogate: failed to spawn powershell: ${err}`); + resolve(); + }); + } catch (err) { + this.logService.warn(`update#killContextMenuComSurrogate: spawn threw: ${err}`); + resolve(); + } + }); + } + protected buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string | undefined { let platform = `win32-${process.arch}`;