Fix double update issue on Windows (#292746)

This commit is contained in:
Dmitriy Vasyura
2026-02-10 04:32:55 -08:00
committed by GitHub
parent a8f61833a3
commit e60eb8c3b1
3 changed files with 136 additions and 23 deletions

View File

@@ -1453,6 +1453,12 @@ begin
Result := not (IsBackgroundUpdate() and FileExists(Path));
end;
// Check if VS Code created a cancel file to signal that the update should be aborted
function CancelFileExists(): Boolean;
begin
Result := FileExists(ExpandConstant('{param:cancel}'))
end;
function ShouldRunAfterUpdate(): Boolean;
begin
if IsBackgroundUpdate() then
@@ -1639,11 +1645,17 @@ begin
Log('Checking whether application is still running...');
while (CheckForMutexes('{#AppMutex}')) do
begin
if CancelFileExists() then
begin
Log('Cancel file detected, aborting background update.');
DeleteFile(ExpandConstant('{app}\updating_version'));
Abort;
end;
Sleep(1000)
end;
Log('Application appears not to be running.');
if not SessionEndFileExists() then begin
if not SessionEndFileExists() and not CancelFileExists() then begin
StopTunnelServiceIfNeeded();
Log('Invoking inno_updater for background update');
Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"{app}\{#ExeBasename}.exe" ' + BoolToStr(LockFileExists()) + ' "{cm:UpdatingVisualStudioCode}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode);
@@ -1657,7 +1669,7 @@ begin
end;
#endif
end else begin
Log('Skipping inno_updater.exe call because OS session is ending');
Log('Skipping inno_updater.exe call because OS session is ending or cancel was requested');
end;
end else begin
if IsVersionedUpdate() then begin

View File

@@ -253,7 +253,15 @@ export abstract class AbstractUpdateService implements IUpdateService {
if (isLatest === false && this._state.type === StateType.Ready) {
this.logService.info('update#readyStateCheck: newer update available, restarting update machinery');
await this.cancelPendingUpdate();
try {
await this.cancelPendingUpdate();
} catch (error) {
this.logService.error('update#checkForOverwriteUpdates(): failed to cancel pending update, aborting overwrite');
this.logService.error(error);
return false;
}
this._overwrite = true;
this.setState(State.Overwriting(this._state.update, explicit));
this.doCheckForUpdates(explicit, pendingUpdateCommit);

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { spawn } from 'child_process';
import { ChildProcess, spawn } from 'child_process';
import { existsSync, unlinkSync } from 'fs';
import { mkdir, readFile, unlink } from 'fs/promises';
import { tmpdir } from 'os';
@@ -18,6 +18,7 @@ import { transform } from '../../../base/common/stream.js';
import { URI } from '../../../base/common/uri.js';
import { checksum } from '../../../base/node/crypto.js';
import * as pfs from '../../../base/node/pfs.js';
import { killTree } from '../../../base/node/processes.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
import { IFileService } from '../../files/common/files.js';
@@ -40,6 +41,10 @@ async function pollUntil(fn: () => boolean, millis = 1000): Promise<void> {
interface IAvailableUpdate {
packagePath: string;
updateFilePath?: string;
/** File path used to signal the Inno Setup installer to cancel */
cancelFilePath?: string;
/** The Inno Setup process that is applying the update in the background */
updateProcess?: ChildProcess;
}
let _updateType: UpdateType | undefined = undefined;
@@ -75,7 +80,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
@IProductService productService: IProductService,
@IMeteredConnectionService meteredConnectionService: IMeteredConnectionService,
) {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, meteredConnectionService, false);
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, meteredConnectionService, true);
lifecycleMainService.setRelaunchHandler(this);
}
@@ -168,14 +173,18 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
return createUpdateURL(this.productService.updateUrl!, platform, quality, commit, options);
}
protected doCheckForUpdates(explicit: boolean): void {
protected doCheckForUpdates(explicit: boolean, pendingCommit?: string): void {
if (!this.quality) {
return;
}
const background = !explicit && !this.shouldDisableProgressiveReleases();
const url = this.buildUpdateFeedUrl(this.quality, this.productService.commit!, { background });
this.setState(State.CheckingForUpdates(explicit));
const url = this.buildUpdateFeedUrl(this.quality, pendingCommit ?? this.productService.commit!, { background });
// Only set CheckingForUpdates if we're not already in Overwriting state
if (this.state.type !== StateType.Overwriting) {
this.setState(State.CheckingForUpdates(explicit));
}
this.requestService.request({ url }, CancellationToken.None)
.then<IUpdate | null>(asJson)
@@ -183,7 +192,14 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
const updateType = getUpdateType();
if (!update || !update.url || !update.version || !update.productVersion) {
this.setState(State.Idle(updateType));
// If we were checking for an overwrite update and found nothing newer,
// restore the Ready state with the pending update
if (this.state.type === StateType.Overwriting) {
this._overwrite = false;
this.setState(State.Ready(this.state.update, this.state.explicit, false));
} else {
this.setState(State.Idle(updateType));
}
return Promise.resolve(null);
}
@@ -249,10 +265,8 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
this.setState(State.Downloaded(update, explicit, this._overwrite));
const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates');
if (fastUpdatesEnabled) {
if (this.productService.target === 'user') {
this.doApplyUpdate();
}
if (fastUpdatesEnabled && this.productService.target === 'user') {
this.doApplyUpdate();
} else {
this.setState(State.Ready(update, explicit, this._overwrite));
}
@@ -265,7 +279,15 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
// only show message when explicitly checking for updates
const message: string | undefined = explicit ? (err.message || err) : undefined;
this.setState(State.Idle(getUpdateType(), message));
// If we were checking for an overwrite update and it failed,
// restore the Ready state with the pending update
if (this.state.type === StateType.Overwriting) {
this._overwrite = false;
this.setState(State.Ready(this.state.update, this.state.explicit, false));
} else {
this.setState(State.Idle(getUpdateType(), message));
}
});
}
@@ -313,15 +335,36 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
const cachePath = await this.cachePath;
const sessionEndFlagPath = path.join(cachePath, 'session-ending.flag');
const cancelFilePath = path.join(cachePath, `cancel.flag`);
try {
await unlink(cancelFilePath);
} catch {
// ignore
}
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}"`, `/sessionend="${sessionEndFlagPath}"`, '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], {
detached: true,
stdio: ['ignore', 'ignore', 'ignore'],
windowsVerbatimArguments: true
});
const child = spawn(this.availableUpdate.packagePath,
[
'/verysilent',
'/log',
`/update="${this.availableUpdate.updateFilePath}"`,
`/sessionend="${sessionEndFlagPath}"`,
`/cancel="${cancelFilePath}"`,
'/nocloseapplications',
'/mergetasks=runcode,!desktopicon,!quicklaunchicon'
],
{
detached: true,
stdio: ['ignore', 'ignore', 'ignore'],
windowsVerbatimArguments: true
}
);
// Track the process so we can cancel it if needed
this.availableUpdate.updateProcess = child;
child.once('exit', () => {
this.availableUpdate = undefined;
@@ -336,6 +379,58 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
.then(() => this.setState(State.Ready(update, explicit, this._overwrite)));
}
protected override async cancelPendingUpdate(): Promise<void> {
if (!this.availableUpdate) {
return;
}
this.logService.trace('update#cancelPendingUpdate: cancelling pending update');
const { updateProcess, updateFilePath, cancelFilePath } = this.availableUpdate;
if (updateProcess && updateProcess.exitCode === null) {
// Remove all listeners to prevent the exit handler from changing state
updateProcess.removeAllListeners();
const exitPromise = new Promise<boolean>(resolve => updateProcess.once('exit', () => resolve(true)));
// Write the cancel file to signal Inno Setup to exit gracefully
if (cancelFilePath) {
try {
await pfs.Promises.writeFile(cancelFilePath, 'cancel');
} catch (err) {
this.logService.warn('update#cancelPendingUpdate: failed to write cancel file', err);
}
}
// Wait for the process to exit gracefully, then force-kill if needed
const pid = updateProcess.pid;
const exited = await Promise.race([exitPromise, timeout(30 * 1000).then(() => false)]);
if (pid && !exited) {
this.logService.trace('update#cancelPendingUpdate: process did not exit gracefully, killing process tree');
await killTree(pid, true);
}
}
// Clean up the flag file
if (updateFilePath) {
try {
await unlink(updateFilePath);
} catch (err) {
// ignore
}
}
// Clean up the cancel file
if (cancelFilePath) {
try {
await unlink(cancelFilePath);
} catch (err) {
// ignore
}
}
this.availableUpdate = undefined;
}
protected override doQuitAndInstall(): void {
if (this.state.type !== StateType.Ready || !this.availableUpdate) {
return;
@@ -393,10 +488,8 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
this.availableUpdate = { packagePath };
this.setState(State.Downloaded(update, true, false));
if (fastUpdatesEnabled) {
if (this.productService.target === 'user') {
this.doApplyUpdate();
}
if (fastUpdatesEnabled && this.productService.target === 'user') {
this.doApplyUpdate();
} else {
this.setState(State.Ready(update, true, false));
}