From 213f59e9621e1a3f71ff85d67b56766c60591f3d Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Thu, 19 Feb 2026 20:42:02 -0800 Subject: [PATCH] Gemini implementation --- report2.md | 68 +++++++++++++++ .../electron-main/updateService.win32.ts | 87 +++++++++++++------ 2 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 report2.md diff --git a/report2.md b/report2.md new file mode 100644 index 0000000000000..20c80841ff6c6 --- /dev/null +++ b/report2.md @@ -0,0 +1,68 @@ +# Analysis of Issue #62476: "Setup has detected that setup is currently running" + +## 1. Thorough Analysis +The issue describes a situation where users see an Inno Setup popup stating "Setup has detected that setup is currently running" while using VS Code. This popup is generated by the Inno Setup installer when it detects that another instance of the installer is already running (by checking for a specific mutex). + +VS Code uses Inno Setup for background updates on Windows. The update process works as follows: +1. VS Code checks for updates and downloads the update package to a shared cache directory (`%TMP%\vscode---`). +2. Once downloaded, `Win32UpdateService.doApplyUpdate()` spawns the Inno Setup installer in the background (`/verysilent`). +3. The installer creates a setup mutex (`setup`) to prevent multiple instances from running. +4. The installer extracts the files, creates a ready mutex (`-ready`), and waits for VS Code to close. +5. VS Code polls for the ready mutex and transitions to the "Ready" state (showing the "Restart to Update" badge). + +The problem occurs because `doApplyUpdate()` does not check if the setup mutex is already active before spawning the installer. If multiple instances of VS Code attempt to apply the update, or if a previous background update is still running, VS Code will spawn a second instance of the installer. The second instance detects the first one and displays the error popup. + +## 2. Repro Scenarios + +### Scenario 1: Multiple VS Code Instances +1. The user opens a VS Code window (Instance A). +2. Instance A checks for updates, downloads the package, and spawns the background installer. +3. While the background installer is running, the user opens a second VS Code window (Instance B). +4. Instance B checks for updates, sees that the package is already downloaded in the shared cache directory, and immediately calls `doApplyUpdate()`. +5. Instance B spawns a second background installer. +6. The second installer detects the first one (via the setup mutex) and shows the popup. + +### Scenario 2: Interrupted Background Update +1. A background update is running, and the installer creates the `updating_version` file in the VS Code installation directory. +2. The user closes VS Code before the update finishes, or the update gets stuck. +3. When the user opens VS Code again, `Win32UpdateService.postInitialize()` detects the `updating_version` file and calls `_applySpecificUpdate()`, which spawns the installer. +4. If the previous installer is still running (e.g., it's stuck waiting for a process to close), the new installer detects it and shows the popup. + +### Scenario 3: Manual Update During Background Update +1. The user manually downloads the VS Code installer and runs it while VS Code is open. +2. At the same time, VS Code checks for updates in the background, downloads the package, and spawns the background installer. +3. The background installer detects the manual installer (which holds the setup mutex) and shows the popup. + +## 3. Implementation Plan + +To fix this issue, we need to prevent VS Code from spawning the background installer if it is already running. We can achieve this by checking the setup mutex before spawning the process. + +**File to modify:** `src/vs/platform/update/electron-main/updateService.win32.ts` + +**Changes:** +1. In `doApplyUpdate()`, before spawning the installer, use `@vscode/windows-mutex` to check if the setup mutex (`${this.productService.win32MutexName}setup`) is active. +2. If the setup mutex is active, log a message and **skip** spawning the installer. +3. Proceed to the polling loop. The polling loop will wait for the existing installer to create the ready mutex (`${this.productService.win32MutexName}-ready`). +4. Update the polling loop to handle the case where we didn't spawn the installer. If the setup mutex becomes inactive and the ready mutex is not active, it means the existing installer exited prematurely (e.g., it failed or was cancelled). In this case, transition to the `Idle` state with an error message. + +**Code Snippet:** +```typescript +const mutex = await import('@vscode/windows-mutex'); +const setupMutexName = `${this.productService.win32MutexName}setup`; +const readyMutexName = `${this.productService.win32MutexName}-ready`; + +if (mutex.isActive(setupMutexName)) { + this.logService.info('update#doApplyUpdate: setup is already running'); + // Skip spawning the installer +} else { + // Spawn the installer as usual + // ... +} + +// In the polling loop: +if (!this.availableUpdate?.updateProcess && !mutex.isActive(setupMutexName)) { + this.logService.warn('update#doApplyUpdate: setup is no longer running and ready mutex is not active'); + this.setState(State.Idle(getUpdateType(), 'Update failed or was cancelled')); + return; +} +``` diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index da4c875845a5d..cc40b6b64772d 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -347,35 +347,42 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun this.availableUpdate.updateFilePath = path.join(cachePath, `CodeSetup-${this.productService.quality}-${update.version}.flag`); this.availableUpdate.cancelFilePath = cancelFilePath; - await pfs.Promises.writeFile(this.availableUpdate.updateFilePath, 'flag'); - const child = spawn(this.availableUpdate.packagePath, - [ - '/verysilent', - '/log', - `/update="${this.availableUpdate.updateFilePath}"`, - `/progress="${progressFilePath}"`, - `/sessionend="${sessionEndFlagPath}"`, - `/cancel="${cancelFilePath}"`, - '/nocloseapplications', - '/mergetasks=runcode,!desktopicon,!quicklaunchicon' - ], - { - detached: true, - stdio: ['ignore', 'ignore', 'ignore'], - windowsVerbatimArguments: true - } - ); + const readyMutexName = `${this.productService.win32MutexName}-ready`; + const mutex = await import('@vscode/windows-mutex'); - // Track the process so we can cancel it if needed - this.availableUpdate.updateProcess = child; + const setupMutexName = `${this.productService.win32MutexName}setup`; + const isSetupRunning = mutex.isActive(setupMutexName); - child.once('exit', () => { - this.availableUpdate = undefined; - this.setState(State.Idle(getUpdateType())); - }); + if (isSetupRunning) { + this.logService.info('update#doApplyUpdate: setup is already running'); + } else { + await pfs.Promises.writeFile(this.availableUpdate.updateFilePath, 'flag'); + const child = spawn(this.availableUpdate.packagePath, + [ + '/verysilent', + '/log', + `/update="${this.availableUpdate.updateFilePath}"`, + `/progress="${progressFilePath}"`, + `/sessionend="${sessionEndFlagPath}"`, + `/cancel="${cancelFilePath}"`, + '/nocloseapplications', + '/mergetasks=runcode,!desktopicon,!quicklaunchicon' + ], + { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'], + windowsVerbatimArguments: true + } + ); - const readyMutexName = `${this.productService.win32MutexName}-ready`; - const mutex = await import('@vscode/windows-mutex'); + // Track the process so we can cancel it if needed + this.availableUpdate.updateProcess = child; + + child.once('exit', () => { + this.availableUpdate = undefined; + this.setState(State.Idle(getUpdateType())); + }); + } // Poll for progress and ready mutex (timeout after 30 minutes) const pollTimeoutMs = 30 * 60 * 1000; @@ -392,6 +399,12 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun return; } + if (!this.availableUpdate?.updateProcess && !mutex.isActive(setupMutexName)) { + this.logService.warn('update#doApplyUpdate: setup is no longer running and ready mutex is not active'); + this.setState(State.Idle(getUpdateType(), 'Update failed or was cancelled')); + return; + } + if (Date.now() - pollStartTime > pollTimeoutMs) { this.logService.warn('update#doApplyUpdate: polling timed out waiting for update to be ready'); this.setState(State.Idle(getUpdateType(), 'Update did not complete within expected time')); @@ -459,6 +472,22 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun this.logService.trace('update#cancelPendingUpdate: process did not exit gracefully, killing process tree'); await killTree(pid, true); } + } else if (!updateProcess) { + if (cancelFilePath) { + try { + await pfs.Promises.writeFile(cancelFilePath, 'cancel'); + } catch (err) { + this.logService.warn('update#cancelPendingUpdate: failed to write cancel file', err); + } + } + + const mutex = await import('@vscode/windows-mutex'); + const setupMutexName = `${this.productService.win32MutexName}setup`; + let retries = 30; + while (mutex.isActive(setupMutexName) && retries > 0) { + await timeout(1000); + retries--; + } } // Clean up the flag file @@ -490,7 +519,11 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun this.logService.trace('update#quitAndInstall(): running raw#quitAndInstall()'); if (this.availableUpdate.updateFilePath) { - unlinkSync(this.availableUpdate.updateFilePath); + try { + unlinkSync(this.availableUpdate.updateFilePath); + } catch (e) { + // ignore + } } else { spawn(this.availableUpdate.packagePath, ['/silent', '/log', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { detached: true,