terminal: Keep extension PATH contributions on their originating host (#341925)

* terminal: Keep extension PATH contributions on their originating host

Record producer authority, filter before merging, and prevent cached foreign PATH values from being re-tagged on extension-host startup. Preserve shared non-PATH values and historical remote recovery until relaunch.

Related to #286553. Live Windows/Linux acceptance remains pending.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* terminal: Handle Windows PATH casing in environment regression test

Prefer the exact PATH key, then use the existing case-insensitive lookup for inherited Windows Path. Assert the value is present before checking contributions.

Related to #286553.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* terminal: Match PATH locality to backend authority and platform

Normalize authority casing consistently with terminal backend routing. Preserve POSIX Path/path variables while matching all PATH spellings on Windows, using explicit target OS data for selection and cached extension-host initialization.

Related to #286553.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* terminal: Revert PATH documentation changes in vscode.d.ts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Don JayamanneandCopilot authored and GitHub committed 2026-10-19 21:07:21 +00:00
1 parent c5f8b150e4
commit e8314212e7
21 files changed
+870 -43

No files matched your search

+19
View File
@@ -112,6 +112,25 @@ usable after a failed refresh, but the extension does not advertise a folder
with surviving legacy wrappers that could shadow the native executable.
The terminal link provider and Copilot session data are unchanged.
### Terminal PATH locality and recovery
Authority matching follows terminal backend routing and is case-insensitive. Variable names follow the terminal's OS: Windows treats all PATH spellings alike; on macOS and Linux only `PATH` is subject to locality filtering, while `Path` and `path` remain distinct ordinary variables.
The extension's terminal PATH contribution is applied only to terminals on the
same computer as the producing extension host. A local contribution applies to
local terminals, including local terminals in remote windows; a remote
contribution applies only to terminals on that same remote authority. Other
environment variables retain their existing cross-host sharing behavior, and
the terminal's base PATH and independently installed commands are not changed.
Saved PATH contributions with an unknown origin, or an origin on another
computer, are absent from the extension's initial cached collection until the
extension refreshes them. Correctly tagged same-computer saved contributions
remain usable before activation. This cache migration is distinct from remote
process recovery: a recovered pre-fix remote process retains its complete
historical environment until normal relaunch, when the current filtered
contribution is applied.
### Ownership
VS Code owns only the shim, the launcher that gets Copilot CLI onto the device.
@@ -61,7 +61,7 @@ export class MainThreadTerminalService extends Disposable implements MainThreadT
private _os: OperatingSystem = OS;
constructor(
_extHostContext: IExtHostContext,
private readonly _extHostContext: IExtHostContext,
@ITerminalService private readonly _terminalService: ITerminalService,
@ITerminalLinkProviderService private readonly _terminalLinkProviderService: ITerminalLinkProviderService,
@ITerminalQuickFixService private readonly _terminalQuickFixService: ITerminalQuickFixService,
@@ -109,7 +109,11 @@ export class MainThreadTerminalService extends Disposable implements MainThreadT
this._proxy.$acceptActiveTerminalChanged(activeInstance.instanceId);
}
if (this._environmentVariableService.collections.size > 0) {
const collectionAsArray = [...this._environmentVariableService.collections.entries()];
const hostOS = _extHostContext.remoteAuthority === null ? OS : _extHostContext.remoteOS;
if (hostOS === undefined) {
throw new Error('Remote extension host operating system is required to restore terminal environment collections.');
}
const collectionAsArray = [...this._environmentVariableService.getMergedCollection(_extHostContext.remoteAuthority, hostOS).collections.entries()];
const serializedCollections: [string, ISerializableEnvironmentVariableCollection][] = collectionAsArray.map(e => {
return [e[0], serializeEnvironmentVariableCollection(e[1].map)];
});
@@ -467,7 +471,8 @@ export class MainThreadTerminalService extends Disposable implements MainThreadT
const translatedCollection = {
persistent,
map: deserializeEnvironmentVariableCollection(collection),
descriptionMap: deserializeEnvironmentDescriptionMap(descriptionMap)
descriptionMap: deserializeEnvironmentDescriptionMap(descriptionMap),
remoteAuthority: this._extHostContext.remoteAuthority?.toLowerCase() ?? null
};
this._environmentVariableService.set(extensionIdentifier, translatedCollection);
} else {
@@ -4,21 +4,29 @@
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { OperatingSystem } from '../../../../base/common/platform.js';
import { mainWindow } from '../../../../base/browser/window.js';
import { timeout } from '../../../../base/common/async.js';
import { Event } from '../../../../base/common/event.js';
import { mock } from '../../../../base/test/common/mock.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js';
import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { EnvironmentVariableMutatorType, IEnvironmentVariableMutator, ISerializableEnvironmentVariableCollection } from '../../../../platform/terminal/common/environmentVariable.js';
import { IViewDescriptorService } from '../../../common/views.js';
import { ITerminalGroupService, ITerminalInstanceService, ITerminalService } from '../../../contrib/terminal/browser/terminal.js';
import { TerminalGroupService } from '../../../contrib/terminal/browser/terminalGroupService.js';
import { TerminalInstanceService } from '../../../contrib/terminal/browser/terminalInstanceService.js';
import { TerminalService } from '../../../contrib/terminal/browser/terminalService.js';
import { IEnvironmentVariableCollectionWithPersistence, IEnvironmentVariableService } from '../../../contrib/terminal/common/environmentVariable.js';
import { ITerminalProfileService } from '../../../contrib/terminal/common/terminal.js';
import { TestViewDescriptorService } from '../../../contrib/terminal/test/browser/xterm/xtermTestUtils.js';
import { ITerminalLinkProviderService } from '../../../contrib/terminalContrib/links/browser/links.js';
import { ITerminalQuickFixService } from '../../../contrib/terminalContrib/quickFix/browser/quickFix.js';
import { ITerminalCompletionService } from '../../../contrib/terminalContrib/suggest/browser/terminalCompletionService.js';
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
import { IExtHostContext } from '../../../services/extensions/common/extHostCustomers.js';
import { ProxyIdentifier } from '../../../services/extensions/common/proxyIdentifier.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { TestTerminalProfileService, workbenchInstantiationService } from '../../../test/browser/workbenchTestServices.js';
import { MainThreadTerminalService } from '../../browser/mainThreadTerminalService.js';
@@ -31,11 +39,13 @@ suite('MainThreadTerminalService', () => {
let service: MainThreadTerminalService;
let terminals: TerminalService;
let groups: TerminalGroupService;
let environmentVariableService: IEnvironmentVariableService;
let instantiationService: TestInstantiationService;
let input: string[];
setup(async () => {
input = [];
const instantiationService = workbenchInstantiationService({
instantiationService = workbenchInstantiationService({
configurationService: () => new TestConfigurationService({
files: {},
terminal: {
@@ -66,6 +76,11 @@ suite('MainThreadTerminalService', () => {
terminals = store.add(instantiationService.createInstance(TerminalService));
terminals.registerProcessSupport(true);
instantiationService.set(ITerminalService, terminals);
// The window is remote, while the default extension host below is local.
instantiationService.stub(IWorkbenchEnvironmentService, new class extends mock<IWorkbenchEnvironmentService>() {
override readonly remoteAuthority = 'ssh-remote+linux';
});
environmentVariableService = instantiationService.get(IEnvironmentVariableService);
// These provider registries are not used by terminal creation or disposal.
instantiationService.stub(ITerminalLinkProviderService, {});
instantiationService.stub(ITerminalQuickFixService, {});
@@ -81,6 +96,133 @@ suite('MainThreadTerminalService', () => {
await timeout(0);
});
function createBridge(remoteAuthority: string | null, onInitialCollections: (collections: [string, ISerializableEnvironmentVariableCollection][]) => void, remoteOS = OperatingSystem.Linux): MainThreadTerminalService {
const rpc = AnyCallRPCProtocol<Pick<ExtHostTerminalServiceShape, '$initEnvironmentVariableCollections'>>({
$initEnvironmentVariableCollections: onInitialCollections
});
const extHostContext = new class extends mock<IExtHostContext>() {
override readonly remoteAuthority = remoteAuthority;
override readonly remoteOS = remoteOS;
override getProxy<T>(identifier: ProxyIdentifier<T>) {
return rpc.getProxy(identifier);
}
};
return store.add(instantiationService.createInstance(MainThreadTerminalService, extHostContext));
}
function collection(entries: ISerializableEnvironmentVariableCollection): IEnvironmentVariableCollectionWithPersistence {
return {
persistent: true,
map: new Map(entries),
descriptionMap: new Map()
};
}
test('records a local extension host origin in a remote window', () => {
const pathCollection: ISerializableEnvironmentVariableCollection = [[
'PATH',
{ variable: 'PATH', value: 'C:\\copilot', type: EnvironmentVariableMutatorType.Prepend }
]];
service.$setEnvironmentVariableCollection('publisher.local', true, pathCollection, []);
assert.deepStrictEqual(environmentVariableService.collections.get('publisher.local'), {
...collection(pathCollection),
remoteAuthority: null
});
});
test('does not bootstrap a foreign PATH into a remote extension host and retains non-PATH values', () => {
const foreignCollection: ISerializableEnvironmentVariableCollection = [
['PATH', { variable: 'PATH', value: 'C:\\copilot', type: EnvironmentVariableMutatorType.Prepend }],
['EDITOR', { variable: 'EDITOR', value: 'code', type: EnvironmentVariableMutatorType.Replace }]
];
const persistedCollection = {
...collection(foreignCollection),
remoteAuthority: 'ssh-remote+windows'
};
const initialCollections: [string, ISerializableEnvironmentVariableCollection][][] = [];
environmentVariableService.set('publisher.extension', persistedCollection);
const remoteBridge = createBridge('ssh-remote+linux', collections => initialCollections.push(collections));
assert.deepStrictEqual(initialCollections, [[['publisher.extension', [
['EDITOR', { variable: 'EDITOR', value: 'code', type: EnvironmentVariableMutatorType.Replace }]
]]]]);
const resyncedCollection = initialCollections[0][0][1].map<[string, IEnvironmentVariableMutator]>(([key, mutator]) => [
key,
mutator.variable === 'EDITOR' ? { ...mutator, value: 'code-insiders' } : mutator
]);
remoteBridge.$setEnvironmentVariableCollection('publisher.extension', true, resyncedCollection, []);
assert.deepStrictEqual(environmentVariableService.collections.get('publisher.extension'), {
...collection([['EDITOR', { variable: 'EDITOR', value: 'code-insiders', type: EnvironmentVariableMutatorType.Replace }]]),
remoteAuthority: 'ssh-remote+linux'
});
});
test('bootstraps same-host persisted PATH into its extension host', () => {
const pathCollection: ISerializableEnvironmentVariableCollection = [[
'PATH',
{ variable: 'PATH', value: ':/copilot', type: EnvironmentVariableMutatorType.Prepend }
]];
const persistedCollection: IEnvironmentVariableCollectionWithPersistence = {
...collection(pathCollection),
remoteAuthority: 'ssh-remote+Linux'
};
environmentVariableService.set('publisher.extension', persistedCollection);
const initialCollections: [string, ISerializableEnvironmentVariableCollection][][] = [];
createBridge('ssh-remote+linux', collections => initialCollections.push(collections));
assert.deepStrictEqual(initialCollections, [[['publisher.extension', pathCollection]]]);
});
test('normalizes the producing authority when an extension publishes its collection', () => {
const bridge = createBridge('wsl+Ubuntu', () => { });
bridge.$setEnvironmentVariableCollection('publisher.extension', true, [], []);
assert.deepStrictEqual(environmentVariableService.collections.get('publisher.extension'), {
...collection([]),
remoteAuthority: 'wsl+ubuntu'
});
});
for (const os of [OperatingSystem.Windows, OperatingSystem.Linux, OperatingSystem.Macintosh]) {
test(`uses extension-host OS ${os} synchronously for cached PATH spelling`, () => {
const entries: ISerializableEnvironmentVariableCollection = ['PATH', 'Path', 'path', 'EDITOR'].map(variable => [
variable, { variable, value: 'value', type: EnvironmentVariableMutatorType.Replace }
]);
environmentVariableService.set('publisher.legacy', collection(entries));
const initial: [string, ISerializableEnvironmentVariableCollection][][] = [];
createBridge('ssh-remote+target', collections => initial.push(collections), os);
assert.deepStrictEqual(initial, [[['publisher.legacy', entries.filter(([, mutator]) =>
os === OperatingSystem.Windows ? mutator.variable === 'EDITOR' : mutator.variable !== 'PATH'
)]]]);
});
}
test('initializes an empty filtered snapshot and permits empty sync and delete', () => {
environmentVariableService.set('publisher.legacy', collection([[
'PATH',
{ variable: 'PATH', value: 'C:\\copilot', type: EnvironmentVariableMutatorType.Prepend }
]]));
const initialCollections: [string, ISerializableEnvironmentVariableCollection][][] = [];
const remoteBridge = createBridge('ssh-remote+linux', collections => initialCollections.push(collections));
assert.deepStrictEqual(initialCollections, [[]]);
remoteBridge.$setEnvironmentVariableCollection('publisher.legacy', false, [], []);
assert.deepStrictEqual(environmentVariableService.collections.get('publisher.legacy'), {
...collection([]),
persistent: false,
remoteAuthority: 'ssh-remote+linux'
});
remoteBridge.$setEnvironmentVariableCollection('publisher.legacy', false, undefined, []);
assert.strictEqual(environmentVariableService.collections.has('publisher.legacy'), false);
});
test('releases the launch request after an API terminal closes', async function () {
if (typeof globalThis.gc !== 'function') {
this.skip(); // Run the Electron suite with --js-flags=--expose-gc.
@@ -205,7 +205,8 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack
shouldPersist,
cols,
rows,
unicodeVersion
unicodeVersion,
remoteEnv.os
);
const pty = this._instantiationService.createInstance(RemotePty, result.persistentTerminalId, shouldPersist, this._remoteTerminalChannel);
this._ptys.set(result.persistentTerminalId, pty);
@@ -2353,9 +2353,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
this._shellLaunchConfig.env = shellLaunchConfig.env;
}
private _onEnvironmentVariableInfoChanged(info: IEnvironmentVariableInfo): void {
if (info.requiresAction) {
private _onEnvironmentVariableInfoChanged(info: IEnvironmentVariableInfo | undefined): void {
if (info?.requiresAction) {
this.xterm?.raw.textarea?.setAttribute('aria-label', nls.localize('terminalStaleTextBoxAriaLabel', "Terminal {0} environment is stale, run the 'Show Environment Information' command for more information", this._instanceId));
} else {
this._setAriaLabel(this.xterm?.raw, this._instanceId, this._title);
}
this._refreshEnvironmentVariableInfoWidgetState(info);
}
@@ -121,7 +121,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
readonly onProcessReplayComplete = this._onProcessReplayComplete.event;
private readonly _onDidChangeProperty = this._register(new Emitter<IProcessProperty>());
readonly onDidChangeProperty = this._onDidChangeProperty.event;
private readonly _onEnvironmentVariableInfoChange = this._register(new Emitter<IEnvironmentVariableInfo>());
private readonly _onEnvironmentVariableInfoChange = this._register(new Emitter<IEnvironmentVariableInfo | undefined>());
readonly onEnvironmentVariableInfoChanged = this._onEnvironmentVariableInfoChange.event;
private readonly _onProcessExit = this._register(new Emitter<number | undefined>());
readonly onProcessExit = this._onProcessExit.event;
@@ -187,7 +187,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
if (environmentVariableCollections) {
this._extEnvironmentVariableCollection = new MergedEnvironmentVariableCollection(environmentVariableCollections);
this._environmentVariableCollectionListener.value = this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection));
this._environmentVariableCollectionListener.value = this._environmentVariableService.onDidChangeCollections(() => this._onEnvironmentVariableCollectionChange());
this.environmentVariableInfo = this._instantiationService.createInstance(EnvironmentVariableInfoChangesActive, this._extEnvironmentVariableCollection);
this._onEnvironmentVariableInfoChange.fire(this.environmentVariableInfo);
}
@@ -284,13 +284,14 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
this.userHome = remoteEnv.userHome.path;
this.os = remoteEnv.os;
// this is a copy of what the merged environment collection is on the remote side
const env = await this._resolveEnvironment(backend, variableResolver, shellLaunchConfig);
const shouldPersist = ((this._configurationService.getValue(TaskSettingId.Reconnection) && shellLaunchConfig.reconnectionProperties) || !shellLaunchConfig.isFeatureTerminal) && this._terminalConfigurationService.config.enablePersistentSessions && !shellLaunchConfig.isTransient;
if (shellLaunchConfig.attachPersistentProcess) {
const result = await backend.attachToProcess(shellLaunchConfig.attachPersistentProcess.id);
if (result) {
newProcess = result;
if (this._extEnvironmentVariableCollection) {
this._onEnvironmentVariableCollectionChange();
}
} else {
// Warn and just create a new terminal if attach failed for some reason
this._logService.warn(`Attach to process failed for terminal`, shellLaunchConfig.attachPersistentProcess);
@@ -298,6 +299,10 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
}
}
if (!newProcess) {
// This is a copy of what the merged environment collection is on the remote side.
// Do not resolve it until attachment has failed: an attached process retains its
// actual historical collection as the diff baseline until it is relaunched.
const env = await this._resolveEnvironment(backend, variableResolver, shellLaunchConfig);
await this._terminalProfileResolverService.resolveShellLaunchConfig(shellLaunchConfig, {
remoteAuthority: this.remoteAuthority,
os: this.os
@@ -309,7 +314,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
nonce: this.shellIntegrationNonce
},
windowsUseConptyDll: this._terminalConfigurationService.config.windowsUseConptyDll ?? false,
environmentVariableCollections: this._extEnvironmentVariableCollection?.collections ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined,
environmentVariableCollections: shouldUseEnvironmentVariableCollection(shellLaunchConfig) && this._extEnvironmentVariableCollection?.collections ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined,
workspaceFolder: this._cwdWorkspaceFolder,
isScreenReaderOptimized: this._accessibilityService.isScreenReaderOptimized()
};
@@ -341,6 +346,9 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
const result = findRevivedId ? await backend.attachToRevivedProcess(id, pid) : await backend.attachToProcess(id);
if (result) {
newProcess = result;
if (this._extEnvironmentVariableCollection) {
this._onEnvironmentVariableCollectionChange();
}
} else {
// Warn and just create a new terminal if attach failed for some reason
this._logService.warn(`Attach to process failed for terminal`, shellLaunchConfig.attachPersistentProcess);
@@ -475,9 +483,9 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
this._logService.debug(`Terminal environment created with ${Object.keys(env).length} variables: ${Object.keys(env).sort().join(', ')}`);
this._environmentVariableCollectionListener.clear();
if (!this._isDisposed && shouldUseEnvironmentVariableCollection(shellLaunchConfig)) {
this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection;
this._extEnvironmentVariableCollection = this._environmentVariableService.getMergedCollection(this.remoteAuthority, this.os ?? await this.getBackendOS());
this._environmentVariableCollectionListener.value = this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection));
this._environmentVariableCollectionListener.value = this._environmentVariableService.onDidChangeCollections(() => this._onEnvironmentVariableCollectionChange());
// For remote terminals, this is a copy of the mergedEnvironmentCollection created on
// the remote side. Since the environment collection is synced between the remote and
// local sides immediately this is a fairly safe way of enabling the env var diffing and
@@ -488,11 +496,23 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
if (this._extEnvironmentVariableCollection.getVariableMap({ workspaceFolder }).size) {
this.environmentVariableInfo = this._instantiationService.createInstance(EnvironmentVariableInfoChangesActive, this._extEnvironmentVariableCollection);
this._onEnvironmentVariableInfoChange.fire(this.environmentVariableInfo);
} else {
this._clearEnvironmentVariableInfo();
}
} else {
this._extEnvironmentVariableCollection = undefined;
this._clearEnvironmentVariableInfo();
}
return env;
}
private _clearEnvironmentVariableInfo(): void {
if (this.environmentVariableInfo !== undefined) {
this.environmentVariableInfo = undefined;
this._onEnvironmentVariableInfoChange.fire(undefined);
}
}
private async _launchLocalProcess(
backend: ITerminalBackend,
shellLaunchConfig: IShellLaunchConfig,
@@ -525,7 +545,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
nonce: this.shellIntegrationNonce
},
windowsUseConptyDll: this._terminalConfigurationService.config.windowsUseConptyDll ?? false,
environmentVariableCollections: this._extEnvironmentVariableCollection ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined,
environmentVariableCollections: shouldUseEnvironmentVariableCollection(shellLaunchConfig) && this._extEnvironmentVariableCollection ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined,
workspaceFolder: this._cwdWorkspaceFolder,
isScreenReaderOptimized: this._accessibilityService.isScreenReaderOptimized()
};
@@ -723,7 +743,11 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
this._onProcessStateChange.fire();
}
private _onEnvironmentVariableCollectionChange(newCollection: IMergedEnvironmentVariableCollection): void {
private _onEnvironmentVariableCollectionChange(): void {
if (this.os === undefined) {
return; // Process creation will compare collections once the backend OS is known.
}
const newCollection = this._environmentVariableService.getMergedCollection(this.remoteAuthority, this.os);
const diff = this._extEnvironmentVariableCollection!.diff(newCollection, { workspaceFolder: this._cwdWorkspaceFolder });
if (diff === undefined) {
// If there are no longer differences, remove the stale info indicator
@@ -5,6 +5,7 @@
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { Event } from '../../../../base/common/event.js';
import { OperatingSystem } from '../../../../base/common/platform.js';
import { EnvironmentVariableScope, IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } from '../../../../platform/terminal/common/environmentVariable.js';
import { ITerminalStatus } from './terminal.js';
@@ -28,6 +29,12 @@ export interface IEnvironmentVariableService {
*/
readonly mergedCollection: IMergedEnvironmentVariableCollection;
/**
* Gets a collection suitable for a terminal running on the provided authority. PATH
* contributions are restricted to the extension host that produced them.
*/
getMergedCollection(remoteAuthority: string | null | undefined, os: OperatingSystem): IMergedEnvironmentVariableCollection;
/**
* An event that is fired when an extension's environment variable collection changes, the event
* provides the new merged collection.
@@ -47,6 +54,11 @@ export interface IEnvironmentVariableService {
export interface IEnvironmentVariableCollectionWithPersistence extends IEnvironmentVariableCollection {
readonly persistent: boolean;
/**
* The extension host that produced this collection. An absent value is a legacy,
* unknown origin and must not be treated as local.
*/
readonly remoteAuthority?: string | null;
}
export interface IEnvironmentVariableInfo {
@@ -13,11 +13,14 @@ import { IEnvironmentVariableCollectionWithPersistence, IEnvironmentVariableServ
import { TerminalStorageKeys } from './terminalStorageKeys.js';
import { IMergedEnvironmentVariableCollection, ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariableCollection } from '../../../../platform/terminal/common/environmentVariable.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { OperatingSystem } from '../../../../base/common/platform.js';
interface ISerializableExtensionEnvironmentVariableCollection {
extensionIdentifier: string;
collection: ISerializableEnvironmentVariableCollection;
description?: ISerializableEnvironmentDescriptionMap;
remoteAuthority?: string | null;
}
/**
@@ -28,13 +31,15 @@ export class EnvironmentVariableService extends Disposable implements IEnvironme
collections: Map<string, IEnvironmentVariableCollectionWithPersistence> = new Map();
mergedCollection: IMergedEnvironmentVariableCollection;
private readonly _mergedCollectionsByAuthority = new Map<OperatingSystem, Map<string | null, IMergedEnvironmentVariableCollection>>();
private readonly _onDidChangeCollections = this._register(new Emitter<IMergedEnvironmentVariableCollection>());
get onDidChangeCollections(): Event<IMergedEnvironmentVariableCollection> { return this._onDidChangeCollections.event; }
constructor(
@IExtensionService private readonly _extensionService: IExtensionService,
@IStorageService private readonly _storageService: IStorageService
@IStorageService private readonly _storageService: IStorageService,
@ILogService private readonly _logService: ILogService
) {
super();
@@ -45,7 +50,8 @@ export class EnvironmentVariableService extends Disposable implements IEnvironme
collectionsJson.forEach(c => this.collections.set(c.extensionIdentifier, {
persistent: true,
map: deserializeEnvironmentVariableCollection(c.collection),
descriptionMap: deserializeEnvironmentDescriptionMap(c.description)
descriptionMap: deserializeEnvironmentDescriptionMap(c.description),
remoteAuthority: c.remoteAuthority
}));
// Asynchronously invalidate collections where extensions have been uninstalled, this is
@@ -69,8 +75,50 @@ export class EnvironmentVariableService extends Disposable implements IEnvironme
this._updateCollections();
}
getMergedCollection(remoteAuthority: string | null | undefined, os: OperatingSystem): IMergedEnvironmentVariableCollection {
const targetAuthority = remoteAuthority?.toLowerCase() ?? null;
let targetCollections = this._mergedCollectionsByAuthority.get(os);
if (!targetCollections) {
targetCollections = new Map();
this._mergedCollectionsByAuthority.set(os, targetCollections);
}
let mergedCollection = targetCollections.get(targetAuthority);
if (!mergedCollection) {
const collections = new Map<string, IEnvironmentVariableCollectionWithPersistence>();
this.collections.forEach((collection, extensionIdentifier) => {
const isKnownOrigin = collection.remoteAuthority === null || (typeof collection.remoteAuthority === 'string' && collection.remoteAuthority.length > 0);
if (!isKnownOrigin && collection.remoteAuthority !== undefined) {
this._logService.warn(`Ignoring terminal PATH contributions with an invalid origin for extension '${extensionIdentifier}'.`);
}
const originAuthority = typeof collection.remoteAuthority === 'string' ? collection.remoteAuthority.toLowerCase() : collection.remoteAuthority;
const map = new Map(collection.map);
let filteredPath = false;
for (const [key, mutator] of map) {
const isPath = os === OperatingSystem.Windows ? mutator.variable.toLowerCase() === 'path' : mutator.variable === 'PATH';
if (isPath && (!isKnownOrigin || originAuthority !== targetAuthority)) {
map.delete(key);
filteredPath = true;
}
}
if (filteredPath && map.size === 0) {
return;
}
if (filteredPath && collection.descriptionMap) {
// PATH-specific descriptions are not truthful after filtering.
collections.set(extensionIdentifier, { ...collection, map, descriptionMap: undefined });
} else {
collections.set(extensionIdentifier, { ...collection, map });
}
});
mergedCollection = new MergedEnvironmentVariableCollection(collections);
targetCollections.set(targetAuthority, mergedCollection);
}
return mergedCollection;
}
private _updateCollections(): void {
this._persistCollectionsEventually();
this._mergedCollectionsByAuthority.clear();
this.mergedCollection = this._resolveMergedCollection();
this._notifyCollectionUpdatesEventually();
}
@@ -87,7 +135,8 @@ export class EnvironmentVariableService extends Disposable implements IEnvironme
collectionsJson.push({
extensionIdentifier,
collection: serializeEnvironmentVariableCollection(this.collections.get(extensionIdentifier)!.map),
description: serializeEnvironmentDescriptionMap(collection.descriptionMap)
description: serializeEnvironmentDescriptionMap(collection.descriptionMap),
remoteAuthority: collection.remoteAuthority
});
}
});
@@ -127,7 +127,8 @@ export class RemoteTerminalChannelClient implements IPtyHostController {
shouldPersistTerminal: boolean,
cols: number,
rows: number,
unicodeVersion: '6' | '11'
unicodeVersion: '6' | '11',
os: OperatingSystem
): Promise<ICreateTerminalProcessResult> {
// Be sure to first wait for the remote configuration
await this._configurationService.whenRemoteConfigurationLoaded();
@@ -149,8 +150,10 @@ export class RemoteTerminalChannelClient implements IPtyHostController {
}
const envVariableCollections: ITerminalEnvironmentVariableCollections = [];
for (const [k, v] of this._environmentVariableService.collections.entries()) {
envVariableCollections.push([k, serializeEnvironmentVariableCollection(v.map), serializeEnvironmentDescriptionMap(v.descriptionMap)]);
if (options.environmentVariableCollections !== undefined) {
for (const [k, v] of this._environmentVariableService.getMergedCollection(this._remoteAuthority, os).collections.entries()) {
envVariableCollections.push([k, serializeEnvironmentVariableCollection(v.map), serializeEnvironmentDescriptionMap(v.descriptionMap)]);
}
}
const resolverResult = await this._remoteAuthorityResolverService.resolveAuthority(this._remoteAuthority);
@@ -294,7 +294,7 @@ export interface ITerminalProcessManager extends IDisposable, ITerminalProcessIn
readonly onBeforeProcessData: Event<IBeforeProcessDataEvent>;
readonly onProcessData: Event<IProcessDataEvent>;
readonly onProcessReplayComplete: Event<void>;
readonly onEnvironmentVariableInfoChanged: Event<IEnvironmentVariableInfo>;
readonly onEnvironmentVariableInfoChanged: Event<IEnvironmentVariableInfo | undefined>;
readonly onDidChangeProperty: Event<IProcessProperty>;
readonly onProcessExit: Event<number | undefined>;
readonly onRestoreCommands: Event<ISerializedCommandDetectionCapability>;
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Emitter } from '../../../../base/common/event.js';
import { IProcessEnvironment, isMacintosh, isWindows, OperatingSystem } from '../../../../base/common/platform.js';
import { IProcessEnvironment, isMacintosh, isWindows, OperatingSystem, OS } from '../../../../base/common/platform.js';
import { URI } from '../../../../base/common/uri.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
@@ -40,6 +40,7 @@ import { IRemoteAgentService } from '../../../services/remote/common/remoteAgent
import { INativeWorkbenchEnvironmentService } from '../../../services/environment/electron-browser/environmentService.js';
import { shouldUseEnvironmentVariableCollection } from '../../../../platform/terminal/common/terminalEnvironment.js';
import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { serializeEnvironmentVariableCollections } from '../../../../platform/terminal/common/environmentVariableShared.js';
export class LocalTerminalBackendContribution implements IWorkbenchContribution {
@@ -343,12 +344,11 @@ export class LocalTerminalBackend extends BaseTerminalBackend implements ITermin
// Re-resolve the environments and replace it on the state so local terminals use a fresh
// environment
mark('code/terminal/willGetReviveEnvironments');
await Promise.all(reviveBufferState.map(state => new Promise<void>(r => {
this._resolveEnvironmentForRevive(variableResolver, state.shellLaunchConfig).then(freshEnv => {
state.processLaunchConfig.env = freshEnv;
r();
});
})));
await Promise.all(reviveBufferState.map(async state => {
const { env, environmentVariableCollections } = await this._resolveEnvironmentForRevive(variableResolver, state.shellLaunchConfig);
state.processLaunchConfig.env = env;
state.processLaunchConfig.options.environmentVariableCollections = environmentVariableCollections;
}));
mark('code/terminal/didGetReviveEnvironments');
mark('code/terminal/willReviveTerminalProcesses');
@@ -372,16 +372,19 @@ export class LocalTerminalBackend extends BaseTerminalBackend implements ITermin
return this._proxy.getTerminalLayoutInfo(layoutArgs);
}
private async _resolveEnvironmentForRevive(variableResolver: terminalEnvironment.VariableResolver | undefined, shellLaunchConfig: IShellLaunchConfig): Promise<IProcessEnvironment> {
private async _resolveEnvironmentForRevive(variableResolver: terminalEnvironment.VariableResolver | undefined, shellLaunchConfig: IShellLaunchConfig): Promise<{ env: IProcessEnvironment; environmentVariableCollections: ReturnType<typeof serializeEnvironmentVariableCollections> | undefined }> {
const platformKey = isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux');
const envFromConfigValue = this._configurationService.getValue<ITerminalEnvironment | undefined>(`terminal.integrated.env.${platformKey}`);
const baseEnv = await (shellLaunchConfig.useShellEnvironment ? this.getShellEnvironment() : this.getEnvironment());
const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configurationService.getValue(TerminalSettingId.DetectLocale), baseEnv);
let environmentVariableCollections: ReturnType<typeof serializeEnvironmentVariableCollections> | undefined;
if (shouldUseEnvironmentVariableCollection(shellLaunchConfig)) {
const workspaceFolder = terminalEnvironment.getWorkspaceForTerminal(shellLaunchConfig.cwd, this._workspaceContextService, this._historyService);
await this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, { workspaceFolder }, variableResolver);
const collection = this._environmentVariableService.getMergedCollection(this.remoteAuthority, OS);
await collection.applyToProcessEnvironment(env, { workspaceFolder }, variableResolver);
environmentVariableCollections = serializeEnvironmentVariableCollections(collection.collections);
}
return env;
return { env, environmentVariableCollections };
}
private _getWorkspaceName(): string {
@@ -3,18 +3,23 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { deepStrictEqual, strictEqual } from 'assert';
import { deepStrictEqual, ok, strictEqual } from 'assert';
import { Emitter, Event } from '../../../../../base/common/event.js';
import { Schemas } from '../../../../../base/common/network.js';
import { OperatingSystem } from '../../../../../base/common/platform.js';
import { getCaseInsensitive } from '../../../../../base/common/objects.js';
import { OperatingSystem, type IProcessEnvironment } from '../../../../../base/common/platform.js';
import { URI } from '../../../../../base/common/uri.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { upcastPartial } from '../../../../../base/test/common/mock.js';
import { IConfigurationService, type IConfigurationChangeEvent } from '../../../../../platform/configuration/common/configuration.js';
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
import { ITerminalChildProcess, TitleEventSource, type IProcessReadyEvent, type ITerminalBackend } from '../../../../../platform/terminal/common/terminal.js';
import { EnvironmentVariableMutatorType, type IEnvironmentVariableCollection } from '../../../../../platform/terminal/common/environmentVariable.js';
import { ITerminalChildProcess, TitleEventSource, type IProcessReadyEvent, type ITerminalBackend, type ITerminalProcessOptions } from '../../../../../platform/terminal/common/terminal.js';
import { ITerminalInstanceService, ITerminalService } from '../../browser/terminal.js';
import { TerminalProcessManager } from '../../browser/terminalProcessManager.js';
import { IEnvironmentVariableService } from '../../common/environmentVariable.js';
import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js';
import { IRemoteAgentEnvironment } from '../../../../../platform/remote/common/remoteAgentEnvironment.js';
import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js';
function listenerCount(emitter: Emitter<unknown>): number {
@@ -64,6 +69,7 @@ class TestTerminalInstanceService implements Partial<ITerminalInstanceService> {
readonly ptyHostRestartEmitter = new Emitter<void>();
readonly revivedAttachArgs: [id: number, pid: number][] = [];
readonly createdProcesses: TestTerminalChildProcess[] = [];
readonly createProcessRequests: { env: IProcessEnvironment; options: ITerminalProcessOptions }[] = [];
refuseRevivedAttach = false;
async getBackend() {
return {
@@ -79,10 +85,11 @@ class TestTerminalInstanceService implements Partial<ITerminalInstanceService> {
cols: number,
rows: number,
unicodeVersion: '6' | '11',
env: any,
options: any,
env: IProcessEnvironment,
options: ITerminalProcessOptions,
shouldPersist: boolean
) => {
this.createProcessRequests.push({ env: { ...env }, options });
const childProcess = new TestTerminalChildProcess(shouldPersist);
childProcess.id = this.createdProcesses.push(childProcess);
return childProcess;
@@ -101,11 +108,12 @@ suite('Workbench - TerminalProcessManager', () => {
let manager: TerminalProcessManager;
let terminalInstanceService: TestTerminalInstanceService;
let environmentVariableService: IEnvironmentVariableService;
let instantiationService: ReturnType<typeof workbenchInstantiationService>;
const store = ensureNoDisposablesAreLeakedInTestSuite();
setup(async () => {
const instantiationService = workbenchInstantiationService(undefined, store);
instantiationService = workbenchInstantiationService(undefined, store);
const configurationService = instantiationService.get(IConfigurationService) as TestConfigurationService;
await configurationService.setUserConfiguration('editor', { fontFamily: 'foo' });
await configurationService.setUserConfiguration('terminal', {
@@ -129,6 +137,135 @@ suite('Workbench - TerminalProcessManager', () => {
manager = store.add(instantiationService.createInstance(TerminalProcessManager, 1, undefined, undefined, undefined));
});
interface IEnvironmentVariableCollectionWithProducer extends IEnvironmentVariableCollection {
readonly remoteAuthority?: string | null;
}
function setCollection(extensionIdentifier: string, collection: IEnvironmentVariableCollectionWithProducer): void {
environmentVariableService.set(extensionIdentifier, collection);
}
function serializedVariables(options: ITerminalProcessOptions): string[] {
return options.environmentVariableCollections?.flatMap(([, collection]) => collection.map(([, mutator]) => mutator.variable)) ?? [];
}
test('does not apply or serialize PATH from a different terminal host while retaining non-PATH variables', async () => {
setCollection('foreign.extension', {
remoteAuthority: 'ssh-remote+foreign-host',
map: new Map([
['foreign-path', { variable: 'PATH', value: ':/foreign/bin', type: EnvironmentVariableMutatorType.Append }],
['shared-value', { variable: 'SHARED_VALUE', value: 'available', type: EnvironmentVariableMutatorType.Replace }]
])
});
setCollection('local.extension', {
remoteAuthority: null,
map: new Map([
['local-path', { variable: 'PATH', value: ':/local/bin', type: EnvironmentVariableMutatorType.Append }]
])
});
await manager.createProcess({}, 80, 24, false);
const request = terminalInstanceService.createProcessRequests.at(-1)!;
const path = request.env.PATH ?? getCaseInsensitive(request.env, 'PATH');
ok(typeof path === 'string');
strictEqual(path.includes('/foreign/bin'), false);
strictEqual(path.includes('/local/bin'), true);
strictEqual(request.env.SHARED_VALUE, 'available');
deepStrictEqual(serializedVariables(request.options), ['SHARED_VALUE', 'PATH']);
});
test('does not carry a historical collection into a strict-environment launch', async () => {
const historicalManager = store.add(instantiationService.createInstance(TerminalProcessManager, 2, undefined, new Map([
['historical.extension', {
map: new Map([
['historical-path', { variable: 'PATH', value: ':/historical/bin', type: EnvironmentVariableMutatorType.Append }]
])
}]
]), undefined));
const environmentInfoChanges: (typeof historicalManager.environmentVariableInfo)[] = [];
store.add(historicalManager.onEnvironmentVariableInfoChanged(info => environmentInfoChanges.push(info)));
await historicalManager.createProcess({ strictEnv: true, env: { PATH: '/strict/bin' } }, 80, 24, false);
const request = terminalInstanceService.createProcessRequests.at(-1)!;
strictEqual(request.env.PATH, '/strict/bin');
strictEqual(request.options.environmentVariableCollections, undefined);
strictEqual(historicalManager.environmentVariableInfo, undefined);
deepStrictEqual(environmentInfoChanges, [undefined]);
});
test('clears historical PATH status when attachment falls back to a fresh launch with no eligible collection', async () => {
const historicalManager = store.add(instantiationService.createInstance(TerminalProcessManager, 3, undefined, new Map([
['historical.extension', {
map: new Map([
['historical-path', { variable: 'PATH', value: ':/historical/bin', type: EnvironmentVariableMutatorType.Append }]
])
}]
]), undefined));
terminalInstanceService.refuseRevivedAttach = true;
const environmentInfoChanges: (typeof historicalManager.environmentVariableInfo)[] = [];
store.add(historicalManager.onEnvironmentVariableInfoChanged(info => environmentInfoChanges.push(info)));
await historicalManager.createProcess({
attachPersistentProcess: { id: 21, pid: 72265, findRevivedId: true, title: 'zsh', titleSource: TitleEventSource.Process, cwd: '', shellIntegrationNonce: '' }
}, 80, 24, false);
strictEqual(historicalManager.environmentVariableInfo, undefined);
deepStrictEqual(environmentInfoChanges, [undefined]);
});
test('ignores collection changes that affect only another terminal host', async () => {
setCollection('foreign.extension', {
remoteAuthority: 'ssh-remote+foreign-host',
map: new Map([
['foreign-path', { variable: 'PATH', value: ':/foreign/bin', type: EnvironmentVariableMutatorType.Append }]
])
});
await manager.createProcess({}, 80, 24, false);
const changeCollectionsEmitter = (environmentVariableService as unknown as { _onDidChangeCollections: Emitter<unknown> })._onDidChangeCollections;
changeCollectionsEmitter.fire(environmentVariableService.mergedCollection);
strictEqual(manager.environmentVariableInfo, undefined);
});
for (const os of [OperatingSystem.Windows, OperatingSystem.Linux, OperatingSystem.Macintosh]) {
test(`uses remote OS ${os} and case-insensitive authority for process options and updates`, async () => {
instantiationService.stub(IRemoteAgentService, {
getEnvironment: async () => upcastPartial<IRemoteAgentEnvironment>({ os, userHome: URI.file('/home/test') })
});
setCollection('remote.extension', {
remoteAuthority: 'wsl+ubuntu',
map: new Map([['PATH', { variable: 'PATH', value: '/tools:', type: EnvironmentVariableMutatorType.Prepend }]])
});
setCollection('local.extension', {
remoteAuthority: null,
map: new Map(['Path', 'path'].map(variable => [variable, {
variable, value: 'value', type: EnvironmentVariableMutatorType.Replace
}]))
});
const remoteManager = store.add(instantiationService.createInstance(TerminalProcessManager, 3,
URI.from({ scheme: Schemas.vscodeRemote, authority: 'wsl+Ubuntu', path: '/workspace' }), undefined, undefined));
await remoteManager.createProcess({}, 80, 24, false);
const variables = serializedVariables(terminalInstanceService.createProcessRequests.at(-1)!.options);
const beforeUpdate = remoteManager.environmentVariableInfo?.requiresAction;
const changed = Event.toPromise(environmentVariableService.onDidChangeCollections);
setCollection('remote.extension', {
remoteAuthority: 'WSL+UBUNTU',
map: new Map([['PATH', { variable: 'PATH', value: '/updated:', type: EnvironmentVariableMutatorType.Prepend }]])
});
await changed;
deepStrictEqual({ variables, beforeUpdate, afterUpdate: remoteManager.environmentVariableInfo?.requiresAction }, {
variables: os === OperatingSystem.Windows ? ['PATH'] : ['PATH', 'Path', 'path'],
beforeUpdate: false,
afterUpdate: true
});
});
}
test('does not accumulate environment variable collection listeners when relaunching', async () => {
const changeCollectionsEmitter = (environmentVariableService as unknown as { _onDidChangeCollections: Emitter<unknown> })._onDidChangeCollections;
const initialListenerCount = listenerCount(changeCollectionsEmitter);
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { deepStrictEqual } from 'assert';
import { deepStrictEqual, notStrictEqual, strictEqual } from 'assert';
import { TestExtensionService, TestHistoryService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';
import { EnvironmentVariableService } from '../../common/environmentVariableService.js';
import { EnvironmentVariableMutatorType, IEnvironmentVariableMutator } from '../../../../../platform/terminal/common/environmentVariable.js';
@@ -11,10 +11,12 @@ import { IStorageService } from '../../../../../platform/storage/common/storage.
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
import { Emitter } from '../../../../../base/common/event.js';
import { IProcessEnvironment } from '../../../../../base/common/platform.js';
import { IProcessEnvironment, OperatingSystem } from '../../../../../base/common/platform.js';
import { IHistoryService } from '../../../../services/history/common/history.js';
import { URI } from '../../../../../base/common/uri.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IEnvironmentVariableCollectionWithPersistence } from '../../common/environmentVariable.js';
class TestEnvironmentVariableService extends EnvironmentVariableService {
persistCollections(): void { this._persistCollections(); }
@@ -27,13 +29,16 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => {
let instantiationService: TestInstantiationService;
let environmentVariableService: TestEnvironmentVariableService;
let changeExtensionsEvent: Emitter<void>;
let warnings: string[];
setup(() => {
changeExtensionsEvent = store.add(new Emitter<void>());
warnings = [];
instantiationService = store.add(new TestInstantiationService());
instantiationService.stub(IExtensionService, TestExtensionService);
instantiationService.stub(IStorageService, store.add(new TestStorageService()));
instantiationService.stub(ILogService, { warn(message: string) { warnings.push(message); } });
instantiationService.stub(IHistoryService, new TestHistoryService());
instantiationService.stub(IExtensionService, TestExtensionService);
instantiationService.stub(IExtensionService, 'onDidChangeExtensions', changeExtensionsEvent.event);
@@ -68,6 +73,23 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => {
]);
});
test('preserves a known local origin distinctly from a legacy unknown origin', () => {
environmentVariableService.set('local', {
persistent: true,
remoteAuthority: null,
map: new Map([['PATH', { value: ':/local', type: EnvironmentVariableMutatorType.Append, variable: 'PATH' }]])
});
environmentVariableService.set('legacy', {
persistent: true,
map: new Map([['PATH', { value: ':/legacy', type: EnvironmentVariableMutatorType.Append, variable: 'PATH' }]])
});
environmentVariableService.persistCollections();
const restored = store.add(instantiationService.createInstance(TestEnvironmentVariableService));
deepStrictEqual(restored.collections.get('local')!.remoteAuthority, null);
deepStrictEqual(restored.collections.get('legacy')!.remoteAuthority, undefined);
});
suite('mergedCollection', () => {
test('should overwrite any other variable with the first extension that replaces', () => {
const collection1 = new Map<string, IEnvironmentVariableMutator>();
@@ -143,5 +165,129 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => {
await environmentVariableService.mergedCollection.applyToProcessEnvironment(env, scope1);
deepStrictEqual(env, { A: 'a2:foo:a1' });
});
suite('getMergedCollection', () => {
test('shares a cached view across authority casing, including restored origins', () => {
environmentVariableService.set('ext1', {
persistent: true,
remoteAuthority: 'wsl+Ubuntu',
map: new Map([['PATH', { variable: 'PATH', value: '/tools:', type: EnvironmentVariableMutatorType.Prepend }]])
});
environmentVariableService.persistCollections();
const restored = store.add(instantiationService.createInstance(TestEnvironmentVariableService));
const lower = restored.getMergedCollection('wsl+ubuntu', OperatingSystem.Linux);
const mixed = restored.getMergedCollection('wsl+Ubuntu', OperatingSystem.Linux);
deepStrictEqual([...lower.getVariableMap(undefined).keys()], ['PATH']);
strictEqual(lower, mixed);
deepStrictEqual([...restored.getMergedCollection('wsl+other', OperatingSystem.Linux).getVariableMap(undefined).keys()], []);
});
test('preserves foreign POSIX Path and path variables independently of PATH', () => {
environmentVariableService.set('ext1', {
persistent: true,
remoteAuthority: null,
map: new Map(['PATH', 'Path', 'path'].map(variable => [variable, {
variable, value: 'value', type: EnvironmentVariableMutatorType.Replace
}]))
});
const collection = environmentVariableService.getMergedCollection('ssh-remote+linux', OperatingSystem.Linux);
deepStrictEqual([...collection.getVariableMap(undefined).keys()], ['Path', 'path']);
const mac = environmentVariableService.getMergedCollection('ssh-remote+linux', OperatingSystem.Macintosh);
deepStrictEqual([...mac.getVariableMap(undefined).keys()], ['Path', 'path']);
const windows = environmentVariableService.getMergedCollection('ssh-remote+linux', OperatingSystem.Windows);
deepStrictEqual([...windows.getVariableMap(undefined).keys()], []);
notStrictEqual(windows, collection);
strictEqual(environmentVariableService.getMergedCollection('SSH-REMOTE+LINUX', OperatingSystem.Linux), collection);
});
test('filters PATH by producer authority before merging and preserves non-PATH mutations', () => {
environmentVariableService.set('foreignReplace', {
persistent: true,
remoteAuthority: 'ssh-remote+windows',
map: new Map([
['opaque-path-key', { variable: 'Path', value: 'C:\\copilot;', type: EnvironmentVariableMutatorType.Replace }],
['editor-key', { variable: 'EDITOR', value: 'code', type: EnvironmentVariableMutatorType.Replace }]
]),
descriptionMap: new Map([['description', { description: 'Copilot command' }]])
});
environmentVariableService.set('localAppend', {
persistent: true,
remoteAuthority: null,
map: new Map([['scoped-path-key', {
variable: 'PATH',
value: ':/copilot',
type: EnvironmentVariableMutatorType.Append,
scope: { workspaceFolder: { uri: URI.file('workspace'), name: 'workspace', index: 0 } },
options: { applyAtProcessCreation: true, applyAtShellIntegration: true }
}]]),
descriptionMap: new Map()
});
const local = environmentVariableService.getMergedCollection(undefined, OperatingSystem.Windows);
deepStrictEqual([...local.getVariableMap(undefined).entries()], [
['EDITOR', [{ extensionIdentifier: 'foreignReplace', type: EnvironmentVariableMutatorType.Replace, value: 'code', variable: 'EDITOR', options: undefined }]]
]);
deepStrictEqual([...local.getVariableMap({ workspaceFolder: { uri: URI.file('workspace'), name: 'workspace', index: 0 } }).entries()], [
['EDITOR', [{ extensionIdentifier: 'foreignReplace', type: EnvironmentVariableMutatorType.Replace, value: 'code', variable: 'EDITOR', options: undefined }]],
['PATH', [{ extensionIdentifier: 'localAppend', type: EnvironmentVariableMutatorType.Append, value: ':/copilot', variable: 'PATH', scope: { workspaceFolder: { uri: URI.file('workspace'), name: 'workspace', index: 0 } }, options: { applyAtProcessCreation: true, applyAtShellIntegration: true } }]]
]);
deepStrictEqual([...local.getDescriptionMap(undefined).entries()], []);
deepStrictEqual([...environmentVariableService.getMergedCollection('ssh-remote+windows', OperatingSystem.Windows).getVariableMap(undefined).entries()], [
['Path', [{ extensionIdentifier: 'foreignReplace', type: EnvironmentVariableMutatorType.Replace, value: 'C:\\copilot;', variable: 'Path', options: undefined }]],
['EDITOR', [{ extensionIdentifier: 'foreignReplace', type: EnvironmentVariableMutatorType.Replace, value: 'code', variable: 'EDITOR', options: undefined }]]
]);
});
test('withholds unknown origins and caches target views until collections change', () => {
environmentVariableService.set('legacy', {
persistent: true,
map: new Map([['PATH', { variable: 'PATH', value: 'C:\\copilot;', type: EnvironmentVariableMutatorType.Prepend }]])
});
const local = environmentVariableService.getMergedCollection(null, OperatingSystem.Linux);
deepStrictEqual([...local.getVariableMap(undefined).entries()], []);
strictEqual(environmentVariableService.getMergedCollection(undefined, OperatingSystem.Linux), local);
environmentVariableService.set('local', {
persistent: true,
remoteAuthority: null,
map: new Map([['PATH', { variable: 'PATH', value: ':/copilot', type: EnvironmentVariableMutatorType.Append }]])
});
const updated = environmentVariableService.getMergedCollection(null, OperatingSystem.Linux);
notStrictEqual(updated, local);
deepStrictEqual([...updated.getVariableMap(undefined).keys()], ['PATH']);
});
test('filters a foreign PATH Replace before it can suppress a local PATH Append', async () => {
environmentVariableService.set('foreignReplace', {
persistent: true,
remoteAuthority: 'ssh-remote+windows',
map: new Map([['PATH', { variable: 'PATH', value: 'C:\\copilot;', type: EnvironmentVariableMutatorType.Replace }]])
});
environmentVariableService.set('localAppend', {
persistent: true,
remoteAuthority: null,
map: new Map([['PATH', { variable: 'PATH', value: ':/copilot', type: EnvironmentVariableMutatorType.Append }]])
});
const local = environmentVariableService.getMergedCollection(null, OperatingSystem.Linux);
deepStrictEqual([...local.getVariableMap(undefined).entries()], [
['PATH', [{ extensionIdentifier: 'localAppend', type: EnvironmentVariableMutatorType.Append, value: ':/copilot', variable: 'PATH', options: undefined }]]
]);
const env: IProcessEnvironment = { PATH: '/base' };
await local.applyToProcessEnvironment(env, undefined);
deepStrictEqual(env, { PATH: '/base:/copilot' });
});
test('withholds malformed origins and logs without exposing environment values', () => {
environmentVariableService.set('invalid', {
persistent: true,
remoteAuthority: '',
map: new Map([['PATH', { variable: 'PATH', value: '/secret/path', type: EnvironmentVariableMutatorType.Append }]])
} satisfies IEnvironmentVariableCollectionWithPersistence);
deepStrictEqual([...environmentVariableService.getMergedCollection(null, OperatingSystem.Linux).getVariableMap(undefined).entries()], []);
deepStrictEqual(warnings, ['Ignoring terminal PATH contributions with an invalid origin for extension \'invalid\'.']);
});
});
});
});
@@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { deepStrictEqual } from 'assert';
import { Event } from '../../../../../../base/common/event.js';
import { OperatingSystem } from '../../../../../../base/common/platform.js';
import { IChannel } from '../../../../../../base/parts/ipc/common/ipc.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
import { TestContextService, TestExtensionService, TestStorageService } from '../../../../../test/common/workbenchTestServices.js';
import { IExtensionService } from '../../../../../services/extensions/common/extensions.js';
import { EnvironmentVariableMutatorType, type IEnvironmentVariableCollection } from '../../../../../../platform/terminal/common/environmentVariable.js';
import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { IStorageService } from '../../../../../../platform/storage/common/storage.js';
import { IWorkbenchConfigurationService } from '../../../../../services/configuration/common/configuration.js';
import { IConfigurationResolverService } from '../../../../../services/configurationResolver/common/configurationResolver.js';
import { IEditorService } from '../../../../../services/editor/common/editorService.js';
import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js';
import { ILabelService } from '../../../../../../platform/label/common/label.js';
import { IRemoteAuthorityResolverService } from '../../../../../../platform/remote/common/remoteAuthorityResolver.js';
import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js';
import { IEnvironmentVariableService } from '../../../common/environmentVariable.js';
import { EnvironmentVariableService } from '../../../common/environmentVariableService.js';
import { ITerminalLogService, type ITerminalProcessOptions } from '../../../../../../platform/terminal/common/terminal.js';
import { RemoteTerminalChannelClient, type ICreateTerminalProcessArguments } from '../../../common/remote/remoteTerminalChannel.js';
import { RemoteTerminalChannelRequest } from '../../../common/remote/terminal.js';
import type { ICompleteTerminalConfiguration } from '../../../common/terminal.js';
class CapturingChannel implements IChannel {
createRequest: ICreateTerminalProcessArguments | undefined;
call<T>(command: string, arg?: unknown): Promise<T> {
if (command === RemoteTerminalChannelRequest.CreateProcess && isCreateTerminalProcessArguments(arg)) {
this.createRequest = arg;
}
return Promise.resolve(undefined!);
}
listen<T>(): Event<T> {
return Event.None;
}
}
function isCreateTerminalProcessArguments(value: unknown): value is ICreateTerminalProcessArguments {
return typeof value === 'object' && value !== null && 'envVariableCollections' in value;
}
suite('RemoteTerminalChannelClient', () => {
const store = ensureNoDisposablesAreLeakedInTestSuite();
test('serializes only target-eligible PATH mutations while retaining non-PATH mutations', async () => {
const instantiationService = store.add(new TestInstantiationService());
instantiationService.stub(IExtensionService, new TestExtensionService());
instantiationService.stub(IStorageService, store.add(new TestStorageService()));
instantiationService.stub(IWorkbenchConfigurationService, {
whenRemoteConfigurationLoaded: async () => { }
});
instantiationService.stub(IWorkspaceContextService, new TestContextService());
instantiationService.stub(IConfigurationResolverService, {
resolveAsync: async () => {
throw new Error('No configuration variables are resolved by this focused request test');
}
});
const environmentVariableService = store.add(instantiationService.createInstance(EnvironmentVariableService));
const foreignCollection: IEnvironmentVariableCollection & { readonly remoteAuthority: string; readonly persistent: boolean } = {
remoteAuthority: 'ssh-remote+other-host',
persistent: false,
map: new Map([
['foreign-path', { variable: 'PATH', value: ':/foreign/bin', type: EnvironmentVariableMutatorType.Append }],
['foreign-mixed', { variable: 'Path', value: 'mixed', type: EnvironmentVariableMutatorType.Replace }],
['foreign-lower', { variable: 'path', value: 'lower', type: EnvironmentVariableMutatorType.Replace }],
['shared-value', { variable: 'SHARED_VALUE', value: 'available', type: EnvironmentVariableMutatorType.Replace }]
])
};
environmentVariableService.set('foreign.extension', foreignCollection);
environmentVariableService.set('target.extension', {
remoteAuthority: 'SSH-REMOTE+TARGET-HOST',
persistent: false,
map: new Map([
['target-path', { variable: 'PATH', value: ':/target/bin', type: EnvironmentVariableMutatorType.Append }],
['target-value', { variable: 'TARGET_VALUE', value: 'available', type: EnvironmentVariableMutatorType.Replace }]
])
});
instantiationService.stub(IEnvironmentVariableService, environmentVariableService);
instantiationService.stub(IRemoteAuthorityResolverService, {
resolveAuthority: async () => ({
authority: {
authority: 'ssh-remote+target-host',
connectTo: { type: 0, host: 'target-host', port: 22 },
connectionToken: undefined
}
})
});
instantiationService.stub(ITerminalLogService, {
error: () => { }
});
instantiationService.stub(IEditorService, {
activeEditor: undefined
});
instantiationService.stub(ILabelService, {
getWorkspaceLabel: () => 'workspace'
});
instantiationService.stub(IWorkbenchEnvironmentService, {
debugExtensionHost: { port: null, break: false, env: {} }
});
const channel = new CapturingChannel();
const client = instantiationService.createInstance(RemoteTerminalChannelClient, 'ssh-remote+target-host', channel);
const configuration: ICompleteTerminalConfiguration = {
'terminal.integrated.env.windows': {},
'terminal.integrated.env.osx': {},
'terminal.integrated.env.linux': {},
'terminal.integrated.cwd': '',
'terminal.integrated.detectLocale': 'auto'
};
const options: ITerminalProcessOptions = {
shellIntegration: { enabled: false, suggestEnabled: false, nonce: '' },
windowsUseConptyDll: false,
environmentVariableCollections: [],
workspaceFolder: undefined,
isScreenReaderOptimized: false
};
await client.createProcess({}, configuration, undefined, options, false, 80, 24, '6', OperatingSystem.Linux);
deepStrictEqual(channel.createRequest?.envVariableCollections, [[
'foreign.extension',
[
['foreign-mixed', { variable: 'Path', value: 'mixed', type: EnvironmentVariableMutatorType.Replace }],
['foreign-lower', { variable: 'path', value: 'lower', type: EnvironmentVariableMutatorType.Replace }],
['shared-value', { variable: 'SHARED_VALUE', value: 'available', type: EnvironmentVariableMutatorType.Replace }]
],
[]
], [
'target.extension',
[
['target-path', { variable: 'PATH', value: ':/target/bin', type: EnvironmentVariableMutatorType.Append }],
['target-value', { variable: 'TARGET_VALUE', value: 'available', type: EnvironmentVariableMutatorType.Replace }]
],
[]
]]);
await client.createProcess({}, configuration, undefined, options, false, 80, 24, '6', OperatingSystem.Windows);
deepStrictEqual(channel.createRequest?.envVariableCollections[0][1].map(([, mutator]) => mutator.variable), ['SHARED_VALUE']);
await client.createProcess({}, configuration, undefined, { ...options, environmentVariableCollections: undefined }, false, 80, 24, '6', OperatingSystem.Linux);
deepStrictEqual(channel.createRequest?.envVariableCollections, []);
});
});
@@ -10,12 +10,33 @@ import { upcastPartial } from '../../../../../base/test/common/mock.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { NullLogService } from '../../../../../platform/log/common/log.js';
import { ILocalPtyService, ITerminalChildProcess, ITerminalLogService } from '../../../../../platform/terminal/common/terminal.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
import { ExtensionIdentifier, type IExtensionDescription } from '../../../../../platform/extensions/common/extensions.js';
import { IProductService } from '../../../../../platform/product/common/productService.js';
import { EnvironmentVariableMutatorType, type ISerializableEnvironmentVariableCollections } from '../../../../../platform/terminal/common/environmentVariable.js';
import { ILocalPtyService, ITerminalChildProcess, ITerminalLogService, type ISerializedTerminalState } from '../../../../../platform/terminal/common/terminal.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { TestContextService } from '../../../../test/common/workbenchTestServices.js';
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
import { IConfigurationResolverService } from '../../../../services/configurationResolver/common/configurationResolver.js';
import { IHistoryService } from '../../../../services/history/common/history.js';
import { IEnvironmentVariableService } from '../../common/environmentVariable.js';
import { EnvironmentVariableService } from '../../common/environmentVariableService.js';
import { ITerminalProfileResolverService } from '../../common/terminal.js';
import { TerminalStorageKeys } from '../../common/terminalStorageKeys.js';
import { TestContextService, TestHistoryService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';
import { LocalPty } from '../../electron-browser/localPty.js';
import { LocalTerminalBackend } from '../../electron-browser/localTerminalBackend.js';
class RecordingLogService extends NullLogService {
readonly warnings: { message: string; args: unknown[] }[] = [];
override warn(message: string, ...args: unknown[]): void {
this.warnings.push({ message, args });
}
}
suite('LocalTerminalBackend', () => {
const store = ensureNoDisposablesAreLeakedInTestSuite();
@@ -82,4 +103,106 @@ suite('LocalTerminalBackend', () => {
attachById: { attachedId: 3, requests: [[3, undefined]] },
});
});
test('revival refreshes both the environment and collection options before sending persisted state to the pty host', async () => {
let revivedState: ISerializedTerminalState[] | undefined;
const instantiationService = store.add(new TestInstantiationService());
const storageService = store.add(new TestStorageService());
const logService = new RecordingLogService();
const registeredExtensions: readonly IExtensionDescription[] = [
upcastPartial<IExtensionDescription>({
identifier: new ExtensionIdentifier('foreign.extension')
})
];
instantiationService.stub(IWorkspaceContextService, new TestContextService());
instantiationService.stub(ITerminalLogService, logService);
instantiationService.stub(IStorageService, storageService);
instantiationService.stub(IExtensionService, upcastPartial<IExtensionService>({
whenInstalledExtensionsRegistered: async () => true,
onDidChangeExtensions: Event.None,
extensions: registeredExtensions
}));
instantiationService.stub(IConfigurationService, new TestConfigurationService());
instantiationService.stub(IConfigurationResolverService, upcastPartial<IConfigurationResolverService>({
resolveWithEnvironment: async (_environment, _folder, value) => value
}));
instantiationService.stub(IHistoryService, new TestHistoryService());
instantiationService.stub(IProductService, upcastPartial<IProductService>({ version: 'test' }));
instantiationService.stub(ITerminalProfileResolverService, upcastPartial<ITerminalProfileResolverService>({
getEnvironment: async () => ({ PATH: '/base/bin' })
}));
const environmentVariableService = store.add(instantiationService.createInstance(EnvironmentVariableService));
const foreignCollection = {
remoteAuthority: 'ssh-remote+foreign-host',
map: new Map([
['foreign-path', { variable: 'PATH', value: '/foreign/bin:', type: EnvironmentVariableMutatorType.Prepend }],
['shared-value', { variable: 'SHARED_VALUE', value: 'available', type: EnvironmentVariableMutatorType.Replace }]
])
};
environmentVariableService.set('foreign.extension', { ...foreignCollection, persistent: false });
instantiationService.stub(IEnvironmentVariableService, environmentVariableService);
instantiationService.stub(ILocalPtyService, upcastPartial<ILocalPtyService>({
onPtyHostExit: Event.None,
onPtyHostStart: Event.None,
onPtyHostUnresponsive: Event.None,
onPtyHostResponsive: Event.None,
onPtyHostRequestResolveVariables: Event.None,
getEnvironment: async () => ({ PATH: '/base/bin' }),
reviveTerminalProcesses: async (_workspaceId, state) => {
revivedState = state;
},
getTerminalLayoutInfo: async () => undefined
}));
const staleOptions: ISerializableEnvironmentVariableCollections = [[
'foreign.extension',
[['foreign-path', { variable: 'PATH', value: '/foreign/bin:', type: EnvironmentVariableMutatorType.Prepend }]],
[]
]];
storageService.store(TerminalStorageKeys.TerminalBufferState, JSON.stringify({
version: 1,
state: [{
id: 1,
shellLaunchConfig: {},
processLaunchConfig: {
env: { PATH: '/stale/bin:/foreign/bin' },
executableEnv: {},
options: {
shellIntegration: { enabled: false, suggestEnabled: false, nonce: '' },
windowsUseConptyDll: false,
environmentVariableCollections: staleOptions,
isScreenReaderOptimized: false
}
}
}]
}), StorageScope.WORKSPACE, StorageTarget.MACHINE);
const backend = store.add(instantiationService.createInstance(LocalTerminalBackend));
await backend.getTerminalLayoutInfo();
deepStrictEqual({
revivedStateLength: revivedState?.length,
warnings: logService.warnings
}, {
revivedStateLength: 1,
warnings: []
});
if (!revivedState) {
throw new Error('Expected persisted terminal state to be revived');
}
deepStrictEqual({
env: {
PATH: revivedState[0].processLaunchConfig.env.PATH,
SHARED_VALUE: revivedState[0].processLaunchConfig.env.SHARED_VALUE
},
collections: revivedState[0].processLaunchConfig.options.environmentVariableCollections
}, {
env: { PATH: '/base/bin', SHARED_VALUE: 'available' },
collections: [[
'foreign.extension',
[['shared-value', { variable: 'SHARED_VALUE', value: 'available', type: EnvironmentVariableMutatorType.Replace }]],
[]
]]
});
});
});
@@ -298,6 +298,7 @@ class BrowserExtensionHostFactory implements IExtensionHostFactory {
return {
connectionData: this._remoteAuthorityResolverService.getConnectionData(remoteAuthority),
pid: remoteEnv.pid,
os: remoteEnv.os,
appRoot: remoteEnv.appRoot,
extensionHostLogsPath: remoteEnv.extensionHostLogsPath,
globalStorageHome: remoteEnv.globalStorageHome,
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from '../../../../base/common/lifecycle.js';
import { OperatingSystem } from '../../../../base/common/platform.js';
import { BrandedService, IConstructorSignature } from '../../../../platform/instantiation/common/instantiation.js';
import { ExtensionHostKind } from './extensionHostKind.js';
import { IExtensionHostProxy } from './extensionHostProxy.js';
@@ -12,6 +13,7 @@ import { IRPCProtocol, ProxyIdentifier } from './proxyIdentifier.js';
export interface IExtHostContext extends IRPCProtocol {
readonly remoteAuthority: string | null;
readonly remoteOS?: OperatingSystem;
readonly extensionHostKind: ExtensionHostKind;
}
@@ -261,6 +261,7 @@ export class ExtensionHostManager extends Disposable implements IExtensionHostMa
let mainProxyIdentifiers: ProxyIdentifier<any>[] = [];
const extHostContext: IInternalExtHostContext = {
remoteAuthority: this._extensionHost.remoteAuthority,
remoteOS: this._extensionHost.remoteOS,
extensionHostKind: this.kind,
getProxy: <T>(identifier: ProxyIdentifier<T>): Proxied<T> => this._rpcProtocol!.getProxy(identifier),
set: <T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R => this._rpcProtocol!.set(identifier, instance),
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Event } from '../../../../base/common/event.js';
import { OperatingSystem } from '../../../../base/common/platform.js';
import Severity from '../../../../base/common/severity.js';
import { URI } from '../../../../base/common/uri.js';
import { IMessagePassingProtocol } from '../../../../base/parts/ipc/common/ipc.js';
@@ -123,6 +124,8 @@ export interface IExtensionHost {
readonly pid: number | null;
readonly runningLocation: ExtensionRunningLocation;
readonly remoteAuthority: string | null;
/** Available after start resolves for a remote host. */
readonly remoteOS?: OperatingSystem;
readonly startup: ExtensionHostStartup;
/**
* A collection of extensions which includes information about which
@@ -30,6 +30,7 @@ import { RemoteRunningLocation } from './extensionRunningLocation.js';
import { ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost } from './extensions.js';
export interface IRemoteExtensionHostInitData {
readonly os: platform.OperatingSystem;
readonly connectionData: IRemoteConnectionData | null;
readonly pid: number;
readonly appRoot: URI;
@@ -48,6 +49,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost {
public readonly pid = null;
public readonly remoteAuthority: string;
public remoteOS: platform.OperatingSystem | undefined;
public readonly startup = ExtensionHostStartup.EagerAutoStart;
public extensions: ExtensionHostExtensions | null = null;
@@ -205,6 +207,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost {
private async _createExtHostInitData(isExtensionDevelopmentDebug: boolean): Promise<IExtensionHostInitData> {
const remoteInitData = await this._initDataProvider.getInitData();
this.remoteOS = remoteInitData.os;
this.extensions = remoteInitData.extensions;
const workspace = this._contextService.getWorkspace();
return {
@@ -653,6 +653,7 @@ class NativeExtensionHostFactory implements IExtensionHostFactory {
return {
connectionData: this._remoteAuthorityResolverService.getConnectionData(remoteAuthority),
pid: remoteEnv.pid,
os: remoteEnv.os,
appRoot: remoteEnv.appRoot,
extensionHostLogsPath: remoteEnv.extensionHostLogsPath,
globalStorageHome: remoteEnv.globalStorageHome,