Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 78 additions & 35 deletions src/vs/platform/update/electron-main/updateService.win32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,21 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
private availableUpdate: IAvailableUpdate | undefined;
private updateCancellationTokenSource: CancellationTokenSource | undefined;

private readonly readyMutexName: string;
private readonly updatingMutexName: string;
private readonly setupMutexName: string;

@memoize
get cachePath(): Promise<string> {
const result = path.join(tmpdir(), `vscode-${this.productService.quality}-${this.productService.target}-${process.arch}`);
return mkdir(result, { recursive: true }).then(() => result);
}

@memoize
private get mutex(): Promise<typeof import('@vscode/windows-mutex')> {
return import('@vscode/windows-mutex');
}

constructor(
@ILifecycleMainService lifecycleMainService: ILifecycleMainService,
@IConfigurationService configurationService: IConfigurationService,
Expand All @@ -81,6 +90,10 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
) {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, telemetryService, applicationStorageMainService, meteredConnectionService, true);

this.readyMutexName = `${productService.win32MutexName}-ready`;
this.updatingMutexName = `${productService.win32MutexName}-updating`;
this.setupMutexName = `${productService.win32MutexName}setup`;

lifecycleMainService.setRelaunchHandler(this);
}

Expand Down Expand Up @@ -344,56 +357,75 @@ 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`);
await this.unlink(cancelFilePath);

const progressFilePath = path.join(cachePath, `update-progress`);
await this.unlink(progressFilePath);

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,
env: { ...process.env, __COMPAT_LAYER: 'RunAsInvoker' }
}
);
const mutex = await this.mutex;
const skippedSpawn = this.isInstallerActive(mutex);

// Track the process so we can cancel it if needed
this.availableUpdate.updateProcess = child;
// Skip the spawn if another Inno Setup is already running for this product (background update or a manual installer);
// otherwise Inno's "Setup is already running" modal pops up. The `-ready` mutex poll below still advances our state when it finishes.
if (skippedSpawn) {
this.logService.info('update#doApplyUpdate: another instance is already running setup, waiting for it to finish');
} else {
Comment thread
dmitrivMS marked this conversation as resolved.
await this.unlink(cancelFilePath);
await this.unlink(progressFilePath);
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,
env: { ...process.env, __COMPAT_LAYER: 'RunAsInvoker' }
}
);

child.once('exit', () => {
this.availableUpdate = undefined;
this.setState(State.Idle(getUpdateType()));
});
// Track the process so we can cancel it if needed
this.availableUpdate.updateProcess = child;

const readyMutexName = `${this.productService.win32MutexName}-ready`;
const mutex = await import('@vscode/windows-mutex');
child.once('exit', () => {
this.availableUpdate = undefined;
this.setState(State.Idle(getUpdateType()));
});
}

this.updateCancellationTokenSource?.dispose(true);
const cts = this.updateCancellationTokenSource = new CancellationTokenSource();
const token = cts.token;

const poll = async () => {
// If we skipped the spawn, the foreign installer was active when we started; treat that as having seen it run
// so a quick exit (cancel/fail) before the first poll iteration still drops us to Idle.
let seenRunning = skippedSpawn;
while (this.state.type === StateType.Updating && !token.isCancellationRequested) {
if (mutex.isActive(readyMutexName)) {
if (mutex.isActive(this.readyMutexName)) {
this.setState(State.Ready(update, explicit, this._overwrite));
return;
}

// Inno gone without `-ready` => install cancelled/failed; drop to Idle.
if (this.isInstallerActive(mutex)) {
seenRunning = true;
} else if (seenRunning) {
if (!this.availableUpdate?.updateProcess) {
this.availableUpdate = undefined;
this.setState(State.Idle(getUpdateType()));
Comment thread
dmitrivMS marked this conversation as resolved.
}
return;
}

try {
const progressContent = await readFile(progressFilePath, 'utf8');
if (!token.isCancellationRequested) {
Expand Down Expand Up @@ -435,14 +467,21 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
return;
}

const { updateProcess, updateFilePath, cancelFilePath } = this.availableUpdate;

// Another instance owns the installer: abort if it's still running so we don't start a new
// update cycle on top of it; keep `availableUpdate` so quit-and-install can still complete.
if (!updateProcess && this.isInstallerActive(await this.mutex)) {
throw new Error('Cannot cancel pending update: another instance is still running setup');
}

// Cancel the polling loop
this.updateCancellationTokenSource?.dispose(true);
this.updateCancellationTokenSource = undefined;

this.logService.trace('update#cancelPendingUpdate: cancelling pending update');
const { updateProcess, updateFilePath, cancelFilePath } = this.availableUpdate;

if (updateProcess && updateProcess.exitCode === null) {
this.logService.trace('update#cancelPendingUpdate: cancelling pending update');

// Remove all listeners to prevent the exit handler from changing state
updateProcess.removeAllListeners();
const exitPromise = new Promise<boolean>(resolve => updateProcess.once('exit', () => resolve(true)));
Expand Down Expand Up @@ -543,6 +582,10 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
}
}

private isInstallerActive(mutex: typeof import('@vscode/windows-mutex')): boolean {
return mutex.isActive(this.updatingMutexName) || mutex.isActive(this.setupMutexName);
}

private async unlink(path: string | undefined): Promise<void> {
if (path) {
try {
Expand Down
Loading