diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 5274dda31ab4d..96e26071500b8 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -409,9 +409,15 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d const productSubJsonStream = embedded ? gulp.src(['product.json'], { base: '.' }) .pipe(jsonEditor((json: Record) => { + // Preserve the host's mutex name before overlaying embedded properties, + // so the embedded app can poll for the correct InnoSetup -ready mutex. + const hostMutexName = json['win32MutexName']; Object.keys(embedded).forEach(key => { json[key] = embedded[key as keyof EmbeddedProductInfo]; }); + if (hostMutexName) { + json['win32SetupMutexName'] = hostMutexName; + } return json; })) .pipe(rename('product.sub.json')) diff --git a/build/gulpfile.vscode.win32.ts b/build/gulpfile.vscode.win32.ts index e09dc366aa467..a634eb4c68bbd 100644 --- a/build/gulpfile.vscode.win32.ts +++ b/build/gulpfile.vscode.win32.ts @@ -127,6 +127,7 @@ function buildWin32Setup(arch: string, target: string): task.CallbackTask { definitions['ProxyAppUserId'] = embedded.win32AppUserModelId; definitions['ProxyNameLong'] = embedded.nameLong; definitions['ProxyExeUrlProtocol'] = embedded.urlProtocol; + definitions['ProxyMutex'] = embedded.win32MutexName; } if (quality === 'stable' || quality === 'insider') { diff --git a/build/win32/code.iss b/build/win32/code.iss index e7bf55036b7c6..c1f954d290fb9 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -125,6 +125,9 @@ Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#ProxyNameLong}"; [Run] Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: ShouldRunAfterUpdate Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Flags: nowait postinstall; Check: WizardNotSilent +#ifdef ProxyExeBasename +Filename: "{app}\{#ProxyExeBasename}.exe"; Description: "{cm:LaunchProgram,{#ProxyNameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: ShouldRunProxyAfterUpdate +#endif [Registry] #if "user" == InstallTarget @@ -1414,6 +1417,10 @@ end; var ShouldRestartTunnelService: Boolean; +#ifdef ProxyMutex + ProxyWasRunning: Boolean; + AppWasRunning: Boolean; +#endif function StopTunnelOtherProcesses(): Boolean; var @@ -1509,11 +1516,27 @@ end; function ShouldRunAfterUpdate(): Boolean; begin if IsBackgroundUpdate() then +#ifdef ProxyMutex + Result := (not LockFileExists()) and AppWasRunning +#else Result := not LockFileExists() +#endif else Result := True; end; +#ifdef ProxyMutex +function ShouldRunProxyAfterUpdate(): Boolean; +begin + // Relaunch the proxy app after a background update if it was + // running when the update started (detected via its mutex). + if IsBackgroundUpdate() then + Result := (not LockFileExists()) and ProxyWasRunning + else + Result := False; +end; +#endif + function IsWindows11OrLater(): Boolean; begin Result := (GetWindowsVersion >= $0A0055F0); @@ -1603,7 +1626,11 @@ begin if IsBackgroundUpdate() then Result := '' else +#ifdef ProxyMutex + Result := '{#AppMutex},{#ProxyMutex}'; +#else Result := '{#AppMutex}'; +#endif end; function GetSetupMutex(Value: string): string; @@ -1612,7 +1639,11 @@ begin // During background updates, also create a -updating mutex that VS Code checks // to avoid launching while an update is in progress. if IsBackgroundUpdate() then +#ifdef ProxyMutex + Result := '{#AppMutex}setup,{#AppMutex}-updating,{#ProxyMutex}-updating' +#else Result := '{#AppMutex}setup,{#AppMutex}-updating' +#endif else Result := '{#AppMutex}setup'; end; @@ -1788,12 +1819,24 @@ begin if IsBackgroundUpdate() then begin +#ifdef ProxyMutex + // Snapshot whether each app is running before we wait for them to exit + ProxyWasRunning := CheckForMutexes('{#ProxyMutex}'); + AppWasRunning := CheckForMutexes('{#AppMutex}'); + Log('App was running: ' + BoolToStr(AppWasRunning)); + Log('Proxy app was running: ' + BoolToStr(ProxyWasRunning)); +#endif + SaveStringToFile(ExpandConstant('{app}\updating_version'), '{#Commit}', False); CreateMutex('{#AppMutex}-ready'); DeleteFile(GetUpdateProgressFilePath()); Log('Checking whether application is still running...'); +#ifdef ProxyMutex + while (CheckForMutexes('{#AppMutex},{#ProxyMutex}')) do +#else while (CheckForMutexes('{#AppMutex}')) do +#endif begin if CancelFileExists() then begin diff --git a/src/typings/electron-cross-app-ipc.d.ts b/src/typings/electron-cross-app-ipc.d.ts new file mode 100644 index 0000000000000..4a184909159bb --- /dev/null +++ b/src/typings/electron-cross-app-ipc.d.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Type definitions for Electron's crossAppIPC module (custom build). + * + * This module provides secure IPC between an Electron host app and an + * embedded Electron app (MiniApp) within nested bundles. Communication + * is authenticated via code-signature verification (macOS: Mach ports, + * Windows: named pipes). + */ + +declare namespace Electron { + + interface CrossAppIPCMessageEvent { + /** The deserialized message data sent by the peer app. */ + data: any; + /** Array of transferred MessagePortMain objects (if any). */ + ports: Electron.MessagePortMain[]; + } + + type CrossAppIPCDisconnectReason = + | 'peer-disconnected' + | 'handshake-failed' + | 'connection-failed' + | 'connection-timeout'; + + interface CrossAppIPC extends NodeJS.EventEmitter { + on(event: 'connected', listener: () => void): this; + once(event: 'connected', listener: () => void): this; + removeListener(event: 'connected', listener: () => void): this; + + on(event: 'message', listener: (messageEvent: CrossAppIPCMessageEvent) => void): this; + once(event: 'message', listener: (messageEvent: CrossAppIPCMessageEvent) => void): this; + removeListener(event: 'message', listener: (messageEvent: CrossAppIPCMessageEvent) => void): this; + + on(event: 'disconnected', listener: (reason: CrossAppIPCDisconnectReason) => void): this; + once(event: 'disconnected', listener: (reason: CrossAppIPCDisconnectReason) => void): this; + removeListener(event: 'disconnected', listener: (reason: CrossAppIPCDisconnectReason) => void): this; + + connect(): void; + close(): void; + postMessage(message: any, transferables?: Electron.MessagePortMain[]): void; + readonly connected: boolean; + readonly isServer: boolean; + } + + interface CrossAppIPCModule { + createCrossAppIPC(): CrossAppIPC; + } + + namespace Main { + const crossAppIPC: CrossAppIPCModule | undefined; + } + + namespace CrossProcessExports { + const crossAppIPC: CrossAppIPCModule | undefined; + } +} diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index ebfcf24d90ef3..c8d4aa33e2dbe 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -75,6 +75,7 @@ export interface IProductConfiguration { readonly win32AppUserModelId?: string; readonly win32MutexName?: string; + readonly win32SetupMutexName?: string; readonly win32RegValueName?: string; readonly win32NameVersion?: string; readonly win32VersionedUpdate?: boolean; diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 188b0b7543134..d69e9d0e4fb39 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -84,6 +84,8 @@ import { ITelemetryServiceConfig, TelemetryService } from '../../platform/teleme import { getPiiPathsFromEnvironment, getTelemetryLevel, isInternalTelemetry, NullTelemetryService, supportsTelemetry } from '../../platform/telemetry/common/telemetryUtils.js'; import { IUpdateService } from '../../platform/update/common/update.js'; import { UpdateChannel } from '../../platform/update/common/updateIpc.js'; +import { AbstractUpdateService } from '../../platform/update/electron-main/abstractUpdateService.js'; +import { CrossAppUpdateCoordinator } from '../../platform/update/electron-main/crossAppUpdateIpc.js'; import { DarwinUpdateService } from '../../platform/update/electron-main/updateService.darwin.js'; import { LinuxUpdateService } from '../../platform/update/electron-main/updateService.linux.js'; import { SnapUpdateService } from '../../platform/update/electron-main/updateService.snap.js'; @@ -1235,8 +1237,20 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('userDataProfiles', userDataProfilesService); sharedProcessClient.then(client => client.registerChannel('userDataProfiles', userDataProfilesService)); - // Update - const updateChannel = new UpdateChannel(accessor.get(IUpdateService)); + // Update (with cross-app coordination on macOS/Windows where crossAppIPC is available) + const localUpdateService = accessor.get(IUpdateService); + let effectiveUpdateService: IUpdateService = localUpdateService; + const isInsiderOrExploration = this.productService.quality === 'insider' || this.productService.quality === 'exploration'; + if (isWindows && isInsiderOrExploration) { + const updateCoordinator = this._register(new CrossAppUpdateCoordinator( + localUpdateService as AbstractUpdateService, + this.logService, + this.lifecycleMainService, + )); + updateCoordinator.initialize(); + effectiveUpdateService = updateCoordinator; + } + const updateChannel = new UpdateChannel(effectiveUpdateService); mainProcessElectronServer.registerChannel('update', updateChannel); // Metered Connection diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 371478fbe9a40..e13d1ba5fc79c 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -86,6 +86,7 @@ export abstract class AbstractUpdateService implements IUpdateService { private _hasCheckedForOverwriteOnQuit: boolean = false; private readonly overwriteUpdatesCheckInterval = new IntervalTimer(); private _internalOrg: string | undefined = undefined; + private _suspended = false; private readonly _onStateChange = new Emitter(); readonly onStateChange: Event = this._onStateChange.event; @@ -95,7 +96,11 @@ export abstract class AbstractUpdateService implements IUpdateService { } protected setState(state: State): void { - this.logService.info('update#setState', state.type); + if (state.type === StateType.Updating) { + this.logService.trace('update#setState', state.type); + } else { + this.logService.info('update#setState', state.type); + } this._state = state; this._onStateChange.fire(state); @@ -284,6 +289,11 @@ export abstract class AbstractUpdateService implements IUpdateService { async checkForUpdates(explicit: boolean): Promise { this.logService.trace('update#checkForUpdates, state = ', this.state.type); + if (this._suspended) { + this.logService.trace('update#checkForUpdates - suspended, skipping'); + return; + } + if (this.state.type !== StateType.Idle) { return; } @@ -291,6 +301,19 @@ export abstract class AbstractUpdateService implements IUpdateService { this.doCheckForUpdates(explicit); } + /** + * Prevents all update checks (automatic and manual) from running. + * Used by the cross-app update coordinator when another app owns + * the update client. + */ + suspend(): void { + this._suspended = true; + } + + resume(): void { + this._suspended = false; + } + async downloadUpdate(explicit: boolean): Promise { this.logService.trace('update#downloadUpdate, state = ', this.state.type); diff --git a/src/vs/platform/update/electron-main/crossAppUpdateIpc.ts b/src/vs/platform/update/electron-main/crossAppUpdateIpc.ts new file mode 100644 index 0000000000000..0b8e64bc7b77d --- /dev/null +++ b/src/vs/platform/update/electron-main/crossAppUpdateIpc.ts @@ -0,0 +1,335 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as electron from 'electron'; +import { Emitter, Event } from '../../../base/common/event.js'; +import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; +import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.js'; +import { ILogService } from '../../log/common/log.js'; +import { IUpdateService, State } from '../common/update.js'; +import { AbstractUpdateService } from './abstractUpdateService.js'; + +/** + * Message types exchanged between apps over crossAppIPC. + */ +const enum CrossAppUpdateMessageType { + /** Server → Client: Update state changed */ + StateChange = 'update/stateChange', + /** Client → Server: Request to check for updates */ + CheckForUpdates = 'update/checkForUpdates', + /** Client → Server: Request to download an available update */ + DownloadUpdate = 'update/downloadUpdate', + /** Client → Server: Request to apply a downloaded update */ + ApplyUpdate = 'update/applyUpdate', + /** Client → Server: Request to quit and install */ + QuitAndInstall = 'update/quitAndInstall', + /** Server → Client: Initial state sync after connection */ + InitialState = 'update/initialState', + /** Client → Server: Request initial state */ + RequestInitialState = 'update/requestInitialState', + /** Server → Client: Ask client to quit for an upcoming update */ + PrepareForQuit = 'update/prepareForQuit', + /** Client → Server: Client confirms it will quit */ + QuitConfirmed = 'update/quitConfirmed', + /** Client → Server: Client's quit was vetoed by the user */ + QuitVetoed = 'update/quitVetoed', +} + +interface CrossAppUpdateMessage { + type: CrossAppUpdateMessageType; + data?: State | boolean; +} + +/** + * Coordinates update ownership between host and embedded Electron apps + * using crossAppIPC. Whichever app starts first becomes the IPC server + * and owns the update client. The second app becomes the client and + * proxies update operations to the server. + * + * When only one app is running, it uses its local update service directly. + * When both apps are running, the IPC server owns the update client and + * the IPC client's local service is suspended to prevent duplicate + * checks and downloads. + * + * This class implements {@link IUpdateService} so it can be used directly + * as the update channel source for renderer processes while transparently + * handling the coordination. + */ +export class CrossAppUpdateCoordinator extends Disposable implements IUpdateService { + + declare readonly _serviceBrand: undefined; + + private ipc: Electron.CrossAppIPC | undefined; + private mode: 'standalone' | 'server' | 'client' = 'standalone'; + + private _state: State; + + private readonly _onStateChange = this._register(new Emitter()); + readonly onStateChange: Event = this._onStateChange.event; + + /** Disposed when entering client mode, re-registered on disconnect. */ + private localStateListener: IDisposable | undefined; + + /** True when the server has sent PrepareForQuit and is waiting for a response. */ + private pendingQuitAndInstall = false; + + get state(): State { return this._state; } + + constructor( + private readonly localUpdateService: AbstractUpdateService, + private readonly logService: ILogService, + private readonly lifecycleMainService: ILifecycleMainService, + ) { + super(); + + // Start with the local service's current state + this._state = this.localUpdateService.state; + + // Track local service state changes (used in standalone/server mode) + this.registerLocalStateListener(); + } + + private registerLocalStateListener(): void { + this.localStateListener = this.localUpdateService.onStateChange(state => { + this.updateState(state); + this.broadcastState(state); + }); + } + + initialize(): void { + const crossAppIPC: Electron.CrossAppIPCModule | undefined = electron.crossAppIPC; + + if (!crossAppIPC) { + this.logService.info('CrossAppUpdateCoordinator: crossAppIPC not available, running in standalone mode'); + return; + } + + const ipc = crossAppIPC.createCrossAppIPC(); + this.ipc = ipc; + + ipc.on('connected', () => { + this.logService.info(`CrossAppUpdateCoordinator: connected (isServer=${ipc.isServer})`); + + if (ipc.isServer) { + this.mode = 'server'; + // Broadcast current state to the newly connected client + this.broadcastState(this.localUpdateService.state); + } else { + this.mode = 'client'; + // Suspend the local update service and stop listening to its state + // changes. All update operations are proxied to the server, so + // neither automatic nor manual checks go through the local service. + this.localUpdateService.suspend(); + this.localStateListener?.dispose(); + this.localStateListener = undefined; + // Request current state from the server + this.sendMessage({ type: CrossAppUpdateMessageType.RequestInitialState }); + } + }); + + ipc.on('message', (messageEvent) => { + this.handleMessage(messageEvent.data as CrossAppUpdateMessage); + }); + + ipc.on('disconnected', (reason) => { + this.logService.info(`CrossAppUpdateCoordinator: disconnected (${reason}), was ${this.mode}`); + + if (this.mode === 'client') { + // Resume the local update service — we're now the only app + this.localUpdateService.resume(); + this.registerLocalStateListener(); + // Sync coordinator state with the local service + this.updateState(this.localUpdateService.state); + } + + // If the server was waiting for a quit confirmation and the client + // disconnected, treat it as an implicit confirmation — the client + // quit successfully but the IPC pipe was torn down before the + // QuitConfirmed message could be delivered. + if (this.mode === 'server' && this.pendingQuitAndInstall) { + this.logService.info('CrossAppUpdateCoordinator: client disconnected during pending quit, treating as confirmed'); + this.pendingQuitAndInstall = false; + this.mode = 'standalone'; + this.localUpdateService.quitAndInstall(); + return; + } + + this.mode = 'standalone'; + + // Reconnect to wait for the peer's next launch. + // Delay briefly to allow the old Mach bootstrap service to be + // deregistered before re-creating the server endpoint (macOS). + if (reason === 'peer-disconnected') { + setTimeout(() => ipc.connect(), 1000); + } + }); + + ipc.connect(); + this.logService.info('CrossAppUpdateCoordinator: connecting to peer'); + } + + private handleMessage(msg: CrossAppUpdateMessage): void { + this.logService.trace(`CrossAppUpdateCoordinator: received ${msg.type} (mode=${this.mode})`); + + switch (msg.type) { + // --- Messages handled by the client --- + case CrossAppUpdateMessageType.StateChange: + case CrossAppUpdateMessageType.InitialState: + if (this.mode === 'client') { + this.updateState(msg.data as State); + } + break; + + case CrossAppUpdateMessageType.PrepareForQuit: + if (this.mode === 'client') { + this.logService.info('CrossAppUpdateCoordinator: server requested quit for update'); + this.lifecycleMainService.quit().then(veto => { + if (veto) { + this.logService.info('CrossAppUpdateCoordinator: client quit was vetoed'); + this.sendMessage({ type: CrossAppUpdateMessageType.QuitVetoed }); + } else { + this.sendMessage({ type: CrossAppUpdateMessageType.QuitConfirmed }); + } + }); + } + break; + + // --- Messages handled by the server --- + case CrossAppUpdateMessageType.RequestInitialState: + if (this.mode === 'server') { + this.sendMessage({ type: CrossAppUpdateMessageType.InitialState, data: this.localUpdateService.state }); + } + break; + + case CrossAppUpdateMessageType.CheckForUpdates: + if (this.mode === 'server') { + this.localUpdateService.checkForUpdates(typeof msg.data === 'boolean' ? msg.data : true); + } + break; + + case CrossAppUpdateMessageType.DownloadUpdate: + if (this.mode === 'server') { + this.localUpdateService.downloadUpdate(typeof msg.data === 'boolean' ? msg.data : true); + } + break; + + case CrossAppUpdateMessageType.ApplyUpdate: + if (this.mode === 'server') { + this.localUpdateService.applyUpdate(); + } + break; + + case CrossAppUpdateMessageType.QuitAndInstall: + if (this.mode === 'server') { + this.doCoordinatedQuitAndInstall(); + } + break; + + case CrossAppUpdateMessageType.QuitConfirmed: + if (this.mode === 'server') { + this.logService.info('CrossAppUpdateCoordinator: client confirmed quit, proceeding with quitAndInstall'); + this.pendingQuitAndInstall = false; + this.localUpdateService.quitAndInstall(); + } + break; + + case CrossAppUpdateMessageType.QuitVetoed: + if (this.mode === 'server') { + this.logService.info('CrossAppUpdateCoordinator: client vetoed quit, aborting quitAndInstall'); + this.pendingQuitAndInstall = false; + } + break; + } + } + + private updateState(state: State): void { + this._state = state; + this._onStateChange.fire(state); + } + + private broadcastState(state: State): void { + if (this.mode === 'server') { + this.sendMessage({ type: CrossAppUpdateMessageType.StateChange, data: state }); + } + } + + private sendMessage(msg: CrossAppUpdateMessage): void { + if (this.ipc?.connected) { + this.ipc.postMessage(msg); + } + } + + // --- IUpdateService implementation --- + + async checkForUpdates(explicit: boolean): Promise { + if (this.mode === 'client') { + this.sendMessage({ type: CrossAppUpdateMessageType.CheckForUpdates, data: explicit }); + } else { + await this.localUpdateService.checkForUpdates(explicit); + } + } + + async downloadUpdate(explicit: boolean): Promise { + if (this.mode === 'client') { + this.sendMessage({ type: CrossAppUpdateMessageType.DownloadUpdate, data: explicit }); + } else { + await this.localUpdateService.downloadUpdate(explicit); + } + } + + async applyUpdate(): Promise { + if (this.mode === 'client') { + this.sendMessage({ type: CrossAppUpdateMessageType.ApplyUpdate }); + } else { + await this.localUpdateService.applyUpdate(); + } + } + + /** + * Coordinates quit-and-install when a peer is connected. + * Asks the client to quit first; only proceeds with the server's + * quitAndInstall if the client confirms. If the client's quit is + * vetoed (e.g. unsaved editors), the whole operation is aborted. + * + * If no peer is connected (standalone), proceeds directly. + */ + private doCoordinatedQuitAndInstall(): void { + if (this.ipc?.connected) { + // Ask the client to quit; it will respond with QuitConfirmed/QuitVetoed, + // or disconnect (treated as implicit confirmation). + this.pendingQuitAndInstall = true; + this.sendMessage({ type: CrossAppUpdateMessageType.PrepareForQuit }); + } else { + this.localUpdateService.quitAndInstall(); + } + } + + async quitAndInstall(): Promise { + if (this.mode === 'client') { + // Ask the server to start the coordinated quit flow + this.sendMessage({ type: CrossAppUpdateMessageType.QuitAndInstall }); + } else { + this.doCoordinatedQuitAndInstall(); + } + } + + async isLatestVersion(): Promise { + return this.localUpdateService.isLatestVersion(); + } + + async _applySpecificUpdate(packagePath: string): Promise { + return this.localUpdateService._applySpecificUpdate(packagePath); + } + + async setInternalOrg(internalOrg: string | undefined): Promise { + return this.localUpdateService.setInternalOrg(internalOrg); + } + + override dispose(): void { + this.localStateListener?.dispose(); + this.ipc?.close(); + super.dispose(); + } +} diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index cbf603459c5f1..3ddb310e2e2d4 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -114,16 +114,6 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau return url; } - override async checkForUpdates(explicit: boolean): Promise { - this.logService.trace('update#checkForUpdates, state = ', this.state.type); - - if (this.state.type !== StateType.Idle) { - return; - } - - this.doCheckForUpdates(explicit); - } - protected doCheckForUpdates(explicit: boolean, pendingCommit?: string): void { if (!this.quality) { return; diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 7712a7c35dc43..222db559f1240 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -15,6 +15,7 @@ import { memoize } from '../../../base/common/decorators.js'; import { hash } from '../../../base/common/hash.js'; import * as path from '../../../base/common/path.js'; import { basename } from '../../../base/common/path.js'; +import { INodeProcess } from '../../../base/common/platform.js'; import { transform } from '../../../base/common/stream.js'; import { URI } from '../../../base/common/uri.js'; import { checksum } from '../../../base/node/crypto.js'; @@ -34,7 +35,6 @@ import { IApplicationStorageMainService } from '../../storage/electron-main/stor import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js'; import { AbstractUpdateService, createUpdateURL, getUpdateRequestHeaders, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js'; -import { INodeProcess } from '../../../base/common/platform.js'; interface IAvailableUpdate { packagePath: string; @@ -101,14 +101,6 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } protected override async initialize(): Promise { - // In the embedded app, skip win32-specific setup (cache paths, telemetry) - // but still run the base initialization to detect available updates. - if ((process as INodeProcess).isEmbeddedApp) { - this.logService.info('update#ctor - embedded app: checking for updates without auto-download'); - await super.initialize(); - return; - } - if (this.productService.win32VersionedUpdate) { const cachePath = await this.cachePath; app.setPath('appUpdate', cachePath); @@ -233,13 +225,6 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun return Promise.resolve(null); } - // In the embedded app, signal that an update exists but can't be installed here. - if ((process as INodeProcess).isEmbeddedApp) { - this.logService.info('update#doCheckForUpdates - embedded app: update available, skipping download'); - this.setState(State.AvailableForDownload(update, /* canInstall */ false)); - return Promise.resolve(null); - } - // When connection is metered and this is not an explicit check, // show update is available but don't start downloading if (!explicit && this.meteredConnectionService.isConnectionMetered) { @@ -397,7 +382,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun this.setState(State.Idle(getUpdateType())); }); - const readyMutexName = `${this.productService.win32MutexName}-ready`; + // The InnoSetup installer creates the -ready mutex using the host app's + // mutex name ({#AppMutex}). When running as the embedded app, use + // win32SetupMutexName (the host's mutex) to find the correct signal. + const setupMutexName = (process as INodeProcess).isEmbeddedApp + ? this.productService.win32SetupMutexName + : this.productService.win32MutexName; + const readyMutexName = `${setupMutexName}-ready`; const mutex = await import('@vscode/windows-mutex'); this.updateCancellationTokenSource?.dispose(true); diff --git a/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts b/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts index 8f94c7c160923..4f7889b36813f 100644 --- a/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts +++ b/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts @@ -29,10 +29,11 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { IUpdateService, State, StateType } from '../../../../platform/update/common/update.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IHostService } from '../../../../workbench/services/host/browser/host.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { URI } from '../../../../base/common/uri.js'; +import { isWindows } from '../../../../base/common/platform.js'; import { UpdateHoverWidget } from './updateHoverWidget.js'; import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService } from '../../../../workbench/services/chat/common/chatEntitlementService.js'; import { ChatStatusDashboard } from '../../../../workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.js'; @@ -122,25 +123,27 @@ async function runSessionsUpdateAction( dialogService: IDialogService, hostService: IHostService, ): Promise { - if (state.type === StateType.AvailableForDownload && state.canInstall === false) { - const { confirmed } = await dialogService.confirm({ - message: localize('sessionsUpdateFromVSCode.title', "Update from VS Code"), - detail: localize('sessionsUpdateFromVSCode.detail', "This will close the Agents app and open VS Code so you can install the update.\n\nLaunch Agents again after the update is complete."), - primaryButton: localize('sessionsUpdateFromVSCode.open', "Close and Open VS Code"), - }); + if (state.type === StateType.AvailableForDownload) { + const isInsiderOrExploration = productService.quality === 'insider' || productService.quality === 'exploration'; + const hasCrossAppCoordinator = isWindows && isInsiderOrExploration; + if (!hasCrossAppCoordinator) { + const { confirmed } = await dialogService.confirm({ + message: localize('sessionsUpdateFromVSCode.title', "Update from VS Code"), + detail: localize('sessionsUpdateFromVSCode.detail', "This will close the Agents app and open VS Code so you can install the update.\n\nLaunch Agents again after the update is complete."), + primaryButton: localize('sessionsUpdateFromVSCode.open', "Close and Open VS Code"), + }); + + if (confirmed) { + await openerService.open(URI.from({ + scheme: productService.urlProtocol, + query: 'windowId=_blank', + }), { openExternal: true }); + await hostService.shutdown(); + } - if (confirmed) { - await openerService.open(URI.from({ - scheme: productService.urlProtocol, - query: 'windowId=_blank', - }), { openExternal: true }); - await hostService.shutdown(); + return; } - return; - } - - if (state.type === StateType.AvailableForDownload) { await updateService.downloadUpdate(true); return; }