From aa19df565fd19957c7ef1b844bfae729adb6c1af Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Sat, 24 Jan 2026 04:22:53 -0800 Subject: [PATCH] Portable mode improvements and bug fixes (#287063) Disabled protocol handlers and registry updates on Windows in portable mode. Added API proposal to detect if VS Code is running in portable mode from extensions. Skipped protocol redirect in GitHub authentication in portable mode. --- .../github-authentication/media/index.html | 7 +++- extensions/github-authentication/package.json | 3 +- extensions/github-authentication/src/flows.ts | 2 +- .../src/node/authServer.ts | 8 +++-- .../src/test/node/authServer.test.ts | 34 ++++++++++++++++++- .../github-authentication/tsconfig.json | 3 +- .../microsoft-authentication/package.json | 3 +- .../src/node/authProvider.ts | 6 ++-- .../src/node/flows.ts | 12 +++++-- .../src/node/test/flows.test.ts | 31 +++++++++++++---- .../microsoft-authentication/tsconfig.json | 3 +- extensions/vscode-api-tests/package.json | 1 + .../electron-main/environmentMainService.ts | 4 +++ .../common/extensionsApiProposals.ts | 3 ++ .../url/electron-main/electronUrlListener.ts | 4 ++- src/vs/platform/window/common/window.ts | 1 + .../electron-main/windowsMainService.ts | 1 + .../workspacesHistoryMainService.ts | 24 ++++++++++--- .../workbench/api/common/extHost.api.impl.ts | 4 +++ .../electron-browser/environmentService.ts | 4 +++ .../browser/webWorkerExtensionHost.ts | 1 + .../common/extensionHostProtocol.ts | 1 + .../localProcessExtensionHost.ts | 1 + .../workingCopyBackupService.test.ts | 1 + .../vscode.proposed.envIsAppPortable.d.ts | 20 +++++++++++ 25 files changed, 156 insertions(+), 26 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.envIsAppPortable.d.ts diff --git a/extensions/github-authentication/media/index.html b/extensions/github-authentication/media/index.html index 385aa8991f1..3292e2a08fc 100644 --- a/extensions/github-authentication/media/index.html +++ b/extensions/github-authentication/media/index.html @@ -46,7 +46,12 @@ if (error) { document.querySelector('.error-message > .detail').textContent = error; document.querySelector('body').classList.add('error'); - } else if (redirectUri) { + } else if (!redirectUri) { + // Portable mode: authentication succeeded, no redirect needed + document.querySelector('.title').textContent = appName; + document.querySelector('.success-message > .subtitle').textContent = 'You have successfully signed in.'; + document.querySelector('.success-message > .detail').textContent = 'You can now close this window.'; + } else { // Wrap the redirect URI so that the browser remembers who triggered the redirect const wrappedRedirectUri = `https://vscode.dev/redirect?url=${encodeURIComponent(redirectUri)}`; // Set up the fallback link diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 91f2d1b57db..b9beaccfe2e 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -19,7 +19,8 @@ ], "enabledApiProposals": [ "authIssuers", - "authProviderSpecific" + "authProviderSpecific", + "envIsAppPortable" ], "activationEvents": [], "capabilities": { diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 76da600118a..dc9813436ac 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -354,7 +354,7 @@ class LocalServerFlow implements IFlow { path: '/login/oauth/authorize', query: searchParams.toString() }); - const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl.toString(true), callbackUri.toString(true)); + const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl.toString(true), callbackUri.toString(true), env.isAppPortable); const port = await server.start(); let codeToExchange; diff --git a/extensions/github-authentication/src/node/authServer.ts b/extensions/github-authentication/src/node/authServer.ts index 0bc2768826d..45dca93e2d0 100644 --- a/extensions/github-authentication/src/node/authServer.ts +++ b/extensions/github-authentication/src/node/authServer.ts @@ -87,7 +87,7 @@ export class LoopbackAuthServer implements ILoopbackServer { return this._startingRedirect.searchParams.get('state') ?? undefined; } - constructor(serveRoot: string, startingRedirect: string, callbackUri: string) { + constructor(serveRoot: string, startingRedirect: string, callbackUri: string, isPortable: boolean) { if (!serveRoot) { throw new Error('serveRoot must be defined'); } @@ -132,7 +132,11 @@ export class LoopbackAuthServer implements ILoopbackServer { throw new Error('Nonce does not match.'); } deferred.resolve({ code, state }); - res.writeHead(302, { location: `/?redirect_uri=${encodeURIComponent(callbackUri)}${appNameQueryParam}` }); + if (isPortable) { + res.writeHead(302, { location: `/?app_name=${encodeURIComponent(env.appName)}` }); + } else { + res.writeHead(302, { location: `/?redirect_uri=${encodeURIComponent(callbackUri)}${appNameQueryParam}` }); + } res.end(); break; } diff --git a/extensions/github-authentication/src/test/node/authServer.test.ts b/extensions/github-authentication/src/test/node/authServer.test.ts index e7fdf6139bb..0ea8d0353bd 100644 --- a/extensions/github-authentication/src/test/node/authServer.test.ts +++ b/extensions/github-authentication/src/test/node/authServer.test.ts @@ -12,7 +12,7 @@ suite('LoopbackAuthServer', () => { let port: number; setup(async () => { - server = new LoopbackAuthServer(__dirname, 'http://localhost:8080', 'https://code.visualstudio.com'); + server = new LoopbackAuthServer(__dirname, 'http://localhost:8080', 'https://code.visualstudio.com', false); port = await server.start(); }); @@ -64,3 +64,35 @@ suite('LoopbackAuthServer', () => { ]); }); }); + +suite('LoopbackAuthServer (portable mode)', () => { + let server: LoopbackAuthServer; + let port: number; + + setup(async () => { + server = new LoopbackAuthServer(__dirname, 'http://localhost:8080', 'https://code.visualstudio.com', true); + port = await server.start(); + }); + + teardown(async () => { + await server.stop(); + }); + + test('should redirect to success page without redirect_uri on /callback', async () => { + server.state = 'valid-state'; + const response = await fetch( + `http://localhost:${port}/callback?code=valid-code&state=${server.state}&nonce=${server.nonce}`, + { redirect: 'manual' } + ); + assert.strictEqual(response.status, 302); + // In portable mode, should redirect to success page without redirect_uri + assert.strictEqual(response.headers.get('location'), `/?app_name=${encodeURIComponent(env.appName)}`); + await Promise.race([ + server.waitForOAuthResponse().then(result => { + assert.strictEqual(result.code, 'valid-code'); + assert.strictEqual(result.state, server.state); + }), + new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000)) + ]); + }); +}); diff --git a/extensions/github-authentication/tsconfig.json b/extensions/github-authentication/tsconfig.json index 1a65fe81095..7d13f905d2d 100644 --- a/extensions/github-authentication/tsconfig.json +++ b/extensions/github-authentication/tsconfig.json @@ -13,6 +13,7 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.authIssuers.d.ts", - "../../src/vscode-dts/vscode.proposed.authProviderSpecific.d.ts" + "../../src/vscode-dts/vscode.proposed.authProviderSpecific.d.ts", + "../../src/vscode-dts/vscode.proposed.envIsAppPortable.d.ts" ] } diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index e30ddcd319c..6e80b8927be 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -16,7 +16,8 @@ "enabledApiProposals": [ "nativeWindowHandle", "authIssuers", - "authenticationChallenges" + "authenticationChallenges", + "envIsAppPortable" ], "capabilities": { "virtualWorkspaces": true, diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index 6b980dc7641..95921b9e3a3 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -228,7 +228,8 @@ export class MsalAuthProvider implements AuthenticationProvider { const flows = getMsalFlows({ extensionHost: this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote, supportedClient: isSupportedClient(callbackUri), - isBrokerSupported: cachedPca.isBrokerAvailable + isBrokerSupported: cachedPca.isBrokerAvailable, + isPortableMode: env.isAppPortable }); const authority = new URL(scopeData.tenant, this._env.activeDirectoryEndpointUrl).toString(); @@ -364,7 +365,8 @@ export class MsalAuthProvider implements AuthenticationProvider { const flows = getMsalFlows({ extensionHost: this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote, isBrokerSupported: cachedPca.isBrokerAvailable, - supportedClient: isSupportedClient(callbackUri) + supportedClient: isSupportedClient(callbackUri), + isPortableMode: env.isAppPortable }); const authority = new URL(scopeData.tenant, this._env.activeDirectoryEndpointUrl).toString(); diff --git a/extensions/microsoft-authentication/src/node/flows.ts b/extensions/microsoft-authentication/src/node/flows.ts index 22782330bd6..e5105fc58c5 100644 --- a/extensions/microsoft-authentication/src/node/flows.ts +++ b/extensions/microsoft-authentication/src/node/flows.ts @@ -22,6 +22,7 @@ interface IMsalFlowOptions { supportsRemoteExtensionHost: boolean; supportsUnsupportedClient: boolean; supportsBroker: boolean; + supportsPortableMode: boolean; } interface IMsalFlowTriggerOptions { @@ -47,7 +48,8 @@ class DefaultLoopbackFlow implements IMsalFlow { options: IMsalFlowOptions = { supportsRemoteExtensionHost: false, supportsUnsupportedClient: true, - supportsBroker: true + supportsBroker: true, + supportsPortableMode: true }; async trigger({ cachedPca, authority, scopes, claims, loginHint, windowHandle, logger }: IMsalFlowTriggerOptions): Promise { @@ -76,7 +78,8 @@ class UrlHandlerFlow implements IMsalFlow { options: IMsalFlowOptions = { supportsRemoteExtensionHost: true, supportsUnsupportedClient: false, - supportsBroker: false + supportsBroker: false, + supportsPortableMode: false }; async trigger({ cachedPca, authority, scopes, claims, loginHint, windowHandle, logger, uriHandler, callbackUri }: IMsalFlowTriggerOptions): Promise { @@ -105,7 +108,8 @@ class DeviceCodeFlow implements IMsalFlow { options: IMsalFlowOptions = { supportsRemoteExtensionHost: true, supportsUnsupportedClient: true, - supportsBroker: false + supportsBroker: false, + supportsPortableMode: true }; async trigger({ cachedPca, authority, scopes, claims, logger }: IMsalFlowTriggerOptions): Promise { @@ -128,6 +132,7 @@ export interface IMsalFlowQuery { extensionHost: ExtensionHost; supportedClient: boolean; isBrokerSupported: boolean; + isPortableMode: boolean; } export function getMsalFlows(query: IMsalFlowQuery): IMsalFlow[] { @@ -139,6 +144,7 @@ export function getMsalFlows(query: IMsalFlowQuery): IMsalFlow[] { } useFlow &&= flow.options.supportsBroker || !query.isBrokerSupported; useFlow &&= flow.options.supportsUnsupportedClient || query.supportedClient; + useFlow &&= flow.options.supportsPortableMode || !query.isPortableMode; if (useFlow) { flows.push(flow); } diff --git a/extensions/microsoft-authentication/src/node/test/flows.test.ts b/extensions/microsoft-authentication/src/node/test/flows.test.ts index b2685e783cc..9be191e8feb 100644 --- a/extensions/microsoft-authentication/src/node/test/flows.test.ts +++ b/extensions/microsoft-authentication/src/node/test/flows.test.ts @@ -11,7 +11,8 @@ suite('getMsalFlows', () => { const query: IMsalFlowQuery = { extensionHost: ExtensionHost.Local, supportedClient: true, - isBrokerSupported: false + isBrokerSupported: false, + isPortableMode: false }; const flows = getMsalFlows(query); assert.strictEqual(flows.length, 3); @@ -24,7 +25,8 @@ suite('getMsalFlows', () => { const query: IMsalFlowQuery = { extensionHost: ExtensionHost.Local, supportedClient: true, - isBrokerSupported: true + isBrokerSupported: true, + isPortableMode: false }; const flows = getMsalFlows(query); assert.strictEqual(flows.length, 1); @@ -35,7 +37,8 @@ suite('getMsalFlows', () => { const query: IMsalFlowQuery = { extensionHost: ExtensionHost.Remote, supportedClient: true, - isBrokerSupported: false + isBrokerSupported: false, + isPortableMode: false }; const flows = getMsalFlows(query); assert.strictEqual(flows.length, 2); @@ -47,7 +50,8 @@ suite('getMsalFlows', () => { const query: IMsalFlowQuery = { extensionHost: ExtensionHost.Local, supportedClient: false, - isBrokerSupported: false + isBrokerSupported: false, + isPortableMode: false }; const flows = getMsalFlows(query); assert.strictEqual(flows.length, 2); @@ -59,7 +63,8 @@ suite('getMsalFlows', () => { const query: IMsalFlowQuery = { extensionHost: ExtensionHost.Remote, supportedClient: false, - isBrokerSupported: false + isBrokerSupported: false, + isPortableMode: false }; const flows = getMsalFlows(query); assert.strictEqual(flows.length, 1); @@ -70,10 +75,24 @@ suite('getMsalFlows', () => { const query: IMsalFlowQuery = { extensionHost: ExtensionHost.Local, supportedClient: false, - isBrokerSupported: true + isBrokerSupported: true, + isPortableMode: false }; const flows = getMsalFlows(query); assert.strictEqual(flows.length, 1); assert.strictEqual(flows[0].label, 'default'); }); + + test('should exclude protocol handler flow in portable mode', () => { + const query: IMsalFlowQuery = { + extensionHost: ExtensionHost.Local, + supportedClient: true, + isBrokerSupported: false, + isPortableMode: true + }; + const flows = getMsalFlows(query); + assert.strictEqual(flows.length, 2); + assert.strictEqual(flows[0].label, 'default'); + assert.strictEqual(flows[1].label, 'device code'); + }); }); diff --git a/extensions/microsoft-authentication/tsconfig.json b/extensions/microsoft-authentication/tsconfig.json index e9a3ade3ed5..a2bc9b325bd 100644 --- a/extensions/microsoft-authentication/tsconfig.json +++ b/extensions/microsoft-authentication/tsconfig.json @@ -11,6 +11,7 @@ "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts", "../../src/vscode-dts/vscode.proposed.authIssuers.d.ts", - "../../src/vscode-dts/vscode.proposed.authenticationChallenges.d.ts" + "../../src/vscode-dts/vscode.proposed.authenticationChallenges.d.ts", + "../../src/vscode-dts/vscode.proposed.envIsAppPortable.d.ts" ] } diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 525f6195420..ecdc5446dd3 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -17,6 +17,7 @@ "documentFiltersExclusive", "editorInsets", "embeddings", + "envIsAppPortable", "extensionRuntime", "extensionsAny", "externalUriOpener", diff --git a/src/vs/platform/environment/electron-main/environmentMainService.ts b/src/vs/platform/environment/electron-main/environmentMainService.ts index 5b2b7858df5..f8486e8ab74 100644 --- a/src/vs/platform/environment/electron-main/environmentMainService.ts +++ b/src/vs/platform/environment/electron-main/environmentMainService.ts @@ -32,6 +32,7 @@ export interface IEnvironmentMainService extends INativeEnvironmentService { // --- config readonly disableUpdates: boolean; + readonly isPortable: boolean; // TODO@deepak1556 temporary until a real fix lands upstream readonly enableRDPDisplayTracking: boolean; @@ -56,6 +57,9 @@ export class EnvironmentMainService extends NativeEnvironmentService implements @memoize get disableUpdates(): boolean { return !!this.args['disable-updates']; } + @memoize + get isPortable(): boolean { return !!process.env['VSCODE_PORTABLE']; } + @memoize get crossOriginIsolated(): boolean { return !!this.args['enable-coi']; } diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 0a0ca8d185d..32b28f51a86 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -225,6 +225,9 @@ const _allApiProposals = { embeddings: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.embeddings.d.ts', }, + envIsAppPortable: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.envIsAppPortable.d.ts', + }, extensionAffinity: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionAffinity.d.ts', }, diff --git a/src/vs/platform/url/electron-main/electronUrlListener.ts b/src/vs/platform/url/electron-main/electronUrlListener.ts index ece5401ddf7..49c508e8450 100644 --- a/src/vs/platform/url/electron-main/electronUrlListener.ts +++ b/src/vs/platform/url/electron-main/electronUrlListener.ts @@ -49,7 +49,9 @@ export class ElectronURLListener extends Disposable { } // Windows: install as protocol handler - if (isWindows) { + // Skip in portable mode: the registered command wouldn't preserve + // portable mode settings, causing issues with OAuth flows + if (isWindows && !environmentMainService.isPortable) { const windowsParameters = environmentMainService.isBuilt ? [] : [`"${environmentMainService.appRoot}"`]; windowsParameters.push('--open-url', '--'); app.setAsDefaultProtocolClient(productService.urlProtocol, process.execPath, windowsParameters); diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index fa297d1bae2..256c55eba50 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -428,6 +428,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native machineId: string; sqmId: string; devDeviceId: string; + isPortable: boolean; execPath: string; backupPath?: string; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 117dfd2277f..e4f9be6fdb9 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -1483,6 +1483,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic machineId: this.machineId, sqmId: this.sqmId, devDeviceId: this.devDeviceId, + isPortable: this.environmentMainService.isPortable, windowId: -1, // Will be filled in by the window once loaded later diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index 0ee2bec4960..104f114ab89 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -25,6 +25,7 @@ import { IWorkspaceIdentifier, WORKSPACE_EXTENSION } from '../../workspace/commo import { IWorkspacesManagementMainService } from './workspacesManagementMainService.js'; import { ResourceMap } from '../../../base/common/map.js'; import { IDialogMainService } from '../../dialogs/electron-main/dialogMainService.js'; +import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; export const IWorkspacesHistoryMainService = createDecorator('workspacesHistoryMainService'); @@ -56,7 +57,8 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IApplicationStorageMainService private readonly applicationStorageMainService: IApplicationStorageMainService, - @IDialogMainService private readonly dialogMainService: IDialogMainService + @IDialogMainService private readonly dialogMainService: IDialogMainService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService ) { super(); @@ -104,7 +106,8 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa files.push(recent); // Add to recent documents (Windows only, macOS later) - if (isWindows && recent.fileUri.scheme === Schemas.file) { + // Skip in portable mode to avoid leaving traces on the machine + if (isWindows && recent.fileUri.scheme === Schemas.file && !this.environmentMainService.isPortable) { app.addRecentDocument(recent.fileUri.fsPath); } } @@ -127,7 +130,8 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa this._onDidChangeRecentlyOpened.fire(); // Schedule update to recent documents on macOS dock - if (isMacintosh) { + // Skip in portable mode to avoid leaving traces on the machine + if (isMacintosh && !this.environmentMainService.isPortable) { this.macOSRecentDocumentsUpdater.trigger(() => this.updateMacOSRecentDocuments()); } } @@ -153,7 +157,8 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa this._onDidChangeRecentlyOpened.fire(); // Schedule update to recent documents on macOS dock - if (isMacintosh) { + // Skip in portable mode to avoid leaving traces on the machine + if (isMacintosh && !this.environmentMainService.isPortable) { this.macOSRecentDocumentsUpdater.trigger(() => this.updateMacOSRecentDocuments()); } } @@ -178,7 +183,11 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } await this.saveRecentlyOpened({ workspaces: [], files: [] }); - app.clearRecentDocuments(); + + // Skip in portable mode to avoid leaving traces on the machine + if (!this.environmentMainService.isPortable) { + app.clearRecentDocuments(); + } // Event this._onDidChangeRecentlyOpened.fire(); @@ -311,6 +320,11 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa return; // only on windows } + // Skip in portable mode to avoid leaving traces on the machine + if (this.environmentMainService.isPortable) { + return; + } + await this.updateWindowsJumpList(); this._register(this.onDidChangeRecentlyOpened(() => this.updateWindowsJumpList())); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 452c9a9fe11..779c6c822ea 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -391,6 +391,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'devDeviceId'); return initData.telemetryInfo.devDeviceId ?? initData.telemetryInfo.machineId; }, + get isAppPortable() { + checkProposedApiEnabled(extension, 'envIsAppPortable'); + return initData.environment.isPortable ?? false; + }, get sessionId() { return initData.telemetryInfo.sessionId; }, get language() { return initData.environment.appLanguage; }, get appName() { return initData.environment.appName; }, diff --git a/src/vs/workbench/services/environment/electron-browser/environmentService.ts b/src/vs/workbench/services/environment/electron-browser/environmentService.ts index 2489fff63b5..b271289d59c 100644 --- a/src/vs/workbench/services/environment/electron-browser/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-browser/environmentService.ts @@ -43,6 +43,7 @@ export interface INativeWorkbenchEnvironmentService extends IBrowserWorkbenchEnv readonly machineId: string; readonly sqmId: string; readonly devDeviceId: string; + readonly isPortable: boolean; // --- Paths readonly execPath: string; @@ -70,6 +71,9 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment @memoize get devDeviceId() { return this.configuration.devDeviceId; } + @memoize + get isPortable() { return this.configuration.isPortable; } + @memoize get remoteAuthority() { return this.configuration.remoteAuthority; } diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 7e99fe33335..f8f208b8d61 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -313,6 +313,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost appUriScheme: this._productService.urlProtocol, appLanguage: platform.language, isExtensionTelemetryLoggingOnly: isLoggingOnly(this._productService, this._environmentService), + isPortable: false, extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, globalStorageHome: this._userDataProfilesService.defaultProfile.globalStorageHome, diff --git a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts index 2e33e2b1bf6..2ccaf82a8f8 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts @@ -67,6 +67,7 @@ export interface IEnvironment { appLanguage: string; isExtensionTelemetryLoggingOnly: boolean; appUriScheme: string; + isPortable?: boolean; extensionDevelopmentLocationURI?: URI[]; extensionTestsLocationURI?: URI; globalStorageHome: URI; diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 89db2c0c237..1a8a31ac4f3 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -482,6 +482,7 @@ export class NativeLocalProcessExtensionHost extends Disposable implements IExte appHost: this._productService.embedderIdentifier || 'desktop', appUriScheme: this._productService.urlProtocol, isExtensionTelemetryLoggingOnly: isLoggingOnly(this._productService, this._environmentService), + isPortable: this._environmentService.isPortable, appLanguage: platform.language, extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts index 6b7307e5863..710844b0653 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts @@ -59,6 +59,7 @@ const TestNativeWindowConfiguration: INativeWindowConfiguration = { machineId: 'testMachineId', sqmId: 'testSqmId', devDeviceId: 'testdevDeviceId', + isPortable: false, logLevel: LogLevel.Error, loggers: [], mainPid: 0, diff --git a/src/vscode-dts/vscode.proposed.envIsAppPortable.d.ts b/src/vscode-dts/vscode.proposed.envIsAppPortable.d.ts new file mode 100644 index 00000000000..aa3bc5bb525 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.envIsAppPortable.d.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export namespace env { + + /** + * Indicates whether the application is running in portable mode. + * + * Portable mode is enabled when the application is run from a folder that contains + * a `data` directory, allowing for self-contained installations. + * + * Learn more about [Portable Mode](https://code.visualstudio.com/docs/editor/portable). + */ + export const isAppPortable: boolean; + } +}