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; + } +}