mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
Fix double update issue on Windows (#292746)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user