Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions build/gulpfile.vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) => {
// 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'))
Expand Down
1 change: 1 addition & 0 deletions build/gulpfile.vscode.win32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
43 changes: 43 additions & 0 deletions build/win32/code.iss
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1414,6 +1417,10 @@ end;

var
ShouldRestartTunnelService: Boolean;
#ifdef ProxyMutex
ProxyWasRunning: Boolean;
AppWasRunning: Boolean;
#endif

function StopTunnelOtherProcesses(): Boolean;
var
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions src/typings/electron-cross-app-ipc.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions src/vs/base/common/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 16 additions & 2 deletions src/vs/code/electron-main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion src/vs/platform/update/electron-main/abstractUpdateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<State>();
readonly onStateChange: Event<State> = this._onStateChange.event;
Expand All @@ -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);

Expand Down Expand Up @@ -284,13 +289,31 @@ export abstract class AbstractUpdateService implements IUpdateService {
async checkForUpdates(explicit: boolean): Promise<void> {
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;
}

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<void> {
this.logService.trace('update#downloadUpdate, state = ', this.state.type);

Expand Down
Loading
Loading