mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
289 lines
12 KiB
TypeScript
289 lines
12 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { Emitter, Event } from 'vs/base/common/event';
|
|
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
|
|
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
|
import { ILabelService } from 'vs/platform/label/common/label';
|
|
import { ILogService } from 'vs/platform/log/common/log';
|
|
import { connectRemoteAgentExtensionHost, IRemoteExtensionHostStartParams, IConnectionOptions, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
|
|
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
|
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
|
import { IInitData, UIKind } from 'vs/workbench/api/common/extHost.protocol';
|
|
import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
|
|
import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions';
|
|
import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions';
|
|
import { IRemoteAuthorityResolverService, IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
|
import * as platform from 'vs/base/common/platform';
|
|
import { Schemas } from 'vs/base/common/network';
|
|
import { Disposable } from 'vs/base/common/lifecycle';
|
|
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
|
import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net';
|
|
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
|
import { VSBuffer } from 'vs/base/common/buffer';
|
|
import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug';
|
|
import { IProductService } from 'vs/platform/product/common/productService';
|
|
import { ISignService } from 'vs/platform/sign/common/sign';
|
|
import { joinPath } from 'vs/base/common/resources';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import { Registry } from 'vs/platform/registry/common/platform';
|
|
import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output';
|
|
import { localize } from 'vs/nls';
|
|
|
|
export interface IRemoteExtensionHostInitData {
|
|
readonly connectionData: IRemoteConnectionData | null;
|
|
readonly pid: number;
|
|
readonly appRoot: URI;
|
|
readonly extensionHostLogsPath: URI;
|
|
readonly globalStorageHome: URI;
|
|
readonly workspaceStorageHome: URI;
|
|
readonly extensions: IExtensionDescription[];
|
|
readonly allExtensions: IExtensionDescription[];
|
|
}
|
|
|
|
export interface IRemoteExtensionHostDataProvider {
|
|
readonly remoteAuthority: string;
|
|
getInitData(): Promise<IRemoteExtensionHostInitData>;
|
|
}
|
|
|
|
export class RemoteExtensionHost extends Disposable implements IExtensionHost {
|
|
|
|
public readonly kind = ExtensionHostKind.Remote;
|
|
public readonly remoteAuthority: string;
|
|
|
|
private _onExit: Emitter<[number, string | null]> = this._register(new Emitter<[number, string | null]>());
|
|
public readonly onExit: Event<[number, string | null]> = this._onExit.event;
|
|
|
|
private _protocol: PersistentProtocol | null;
|
|
private _hasLostConnection: boolean;
|
|
private _terminating: boolean;
|
|
private readonly _isExtensionDevHost: boolean;
|
|
|
|
constructor(
|
|
private readonly _initDataProvider: IRemoteExtensionHostDataProvider,
|
|
private readonly _socketFactory: ISocketFactory,
|
|
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
|
|
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
|
|
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
|
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
|
|
@ILogService private readonly _logService: ILogService,
|
|
@ILabelService private readonly _labelService: ILabelService,
|
|
@IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService,
|
|
@IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService,
|
|
@IProductService private readonly _productService: IProductService,
|
|
@ISignService private readonly _signService: ISignService
|
|
) {
|
|
super();
|
|
this.remoteAuthority = this._initDataProvider.remoteAuthority;
|
|
this._protocol = null;
|
|
this._hasLostConnection = false;
|
|
this._terminating = false;
|
|
|
|
this._register(this._lifecycleService.onShutdown(reason => this.dispose()));
|
|
|
|
const devOpts = parseExtensionDevOptions(this._environmentService);
|
|
this._isExtensionDevHost = devOpts.isExtensionDevHost;
|
|
}
|
|
|
|
public start(): Promise<IMessagePassingProtocol> {
|
|
const options: IConnectionOptions = {
|
|
commit: this._productService.commit,
|
|
socketFactory: this._socketFactory,
|
|
addressProvider: {
|
|
getAddress: async () => {
|
|
const { authority } = await this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority);
|
|
return { host: authority.host, port: authority.port, connectionToken: authority.connectionToken };
|
|
}
|
|
},
|
|
signService: this._signService,
|
|
logService: this._logService,
|
|
ipcLogger: null
|
|
};
|
|
return this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority).then((resolverResult) => {
|
|
|
|
const startParams: IRemoteExtensionHostStartParams = {
|
|
language: platform.language,
|
|
debugId: this._environmentService.debugExtensionHost.debugId,
|
|
break: this._environmentService.debugExtensionHost.break,
|
|
port: this._environmentService.debugExtensionHost.port,
|
|
env: resolverResult.options && resolverResult.options.extensionHostEnv
|
|
};
|
|
|
|
const extDevLocs = this._environmentService.extensionDevelopmentLocationURI;
|
|
|
|
let debugOk = true;
|
|
if (extDevLocs && extDevLocs.length > 0) {
|
|
// TODO@AW: handles only first path in array
|
|
if (extDevLocs[0].scheme === Schemas.file) {
|
|
debugOk = false;
|
|
}
|
|
}
|
|
|
|
if (!debugOk) {
|
|
startParams.break = false;
|
|
}
|
|
|
|
return connectRemoteAgentExtensionHost(options, startParams).then(result => {
|
|
let { protocol, debugPort } = result;
|
|
const isExtensionDevelopmentDebug = typeof debugPort === 'number';
|
|
if (debugOk && this._environmentService.isExtensionDevelopment && this._environmentService.debugExtensionHost.debugId && debugPort) {
|
|
this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, debugPort, this._initDataProvider.remoteAuthority);
|
|
}
|
|
|
|
protocol.onDidDispose(() => {
|
|
this._onExtHostConnectionLost();
|
|
});
|
|
|
|
protocol.onSocketClose(() => {
|
|
if (this._isExtensionDevHost) {
|
|
this._onExtHostConnectionLost();
|
|
}
|
|
});
|
|
|
|
// 1) wait for the incoming `ready` event and send the initialization data.
|
|
// 2) wait for the incoming `initialized` event.
|
|
return new Promise<IMessagePassingProtocol>((resolve, reject) => {
|
|
|
|
let handle = setTimeout(() => {
|
|
reject('timeout');
|
|
}, 60 * 1000);
|
|
|
|
let logFile: URI;
|
|
|
|
const disposable = protocol.onMessage(msg => {
|
|
|
|
if (isMessageOfType(msg, MessageType.Ready)) {
|
|
// 1) Extension Host is ready to receive messages, initialize it
|
|
this._createExtHostInitData(isExtensionDevelopmentDebug).then(data => {
|
|
logFile = data.logFile;
|
|
protocol.send(VSBuffer.fromString(JSON.stringify(data)));
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (isMessageOfType(msg, MessageType.Initialized)) {
|
|
// 2) Extension Host is initialized
|
|
|
|
clearTimeout(handle);
|
|
|
|
// stop listening for messages here
|
|
disposable.dispose();
|
|
|
|
// Register log channel for remote exthost log
|
|
Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({ id: 'remoteExtHostLog', label: localize('remote extension host Log', "Remote Extension Host"), file: logFile, log: true });
|
|
|
|
// release this promise
|
|
this._protocol = protocol;
|
|
resolve(protocol);
|
|
|
|
return;
|
|
}
|
|
|
|
console.error(`received unexpected message during handshake phase from the extension host: `, msg);
|
|
});
|
|
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
private _onExtHostConnectionLost(): void {
|
|
if (this._hasLostConnection) {
|
|
// avoid re-entering this method
|
|
return;
|
|
}
|
|
this._hasLostConnection = true;
|
|
|
|
if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId) {
|
|
this._extensionHostDebugService.close(this._environmentService.debugExtensionHost.debugId);
|
|
}
|
|
|
|
if (this._terminating) {
|
|
// Expected termination path (we asked the process to terminate)
|
|
return;
|
|
}
|
|
|
|
this._onExit.fire([0, null]);
|
|
}
|
|
|
|
private async _createExtHostInitData(isExtensionDevelopmentDebug: boolean): Promise<IInitData> {
|
|
const [telemetryInfo, remoteInitData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]);
|
|
|
|
// Collect all identifiers for extension ids which can be considered "resolved"
|
|
const remoteExtensions = new Set<string>();
|
|
remoteInitData.extensions.forEach((extension) => remoteExtensions.add(ExtensionIdentifier.toKey(extension.identifier.value)));
|
|
|
|
const resolvedExtensions = remoteInitData.allExtensions.filter(extension => !extension.main && !extension.browser).map(extension => extension.identifier);
|
|
const hostExtensions = (
|
|
remoteInitData.allExtensions
|
|
.filter(extension => !remoteExtensions.has(ExtensionIdentifier.toKey(extension.identifier.value)))
|
|
.filter(extension => (extension.main || extension.browser) && extension.api === 'none').map(extension => extension.identifier)
|
|
);
|
|
const workspace = this._contextService.getWorkspace();
|
|
return {
|
|
commit: this._productService.commit,
|
|
version: this._productService.version,
|
|
parentPid: remoteInitData.pid,
|
|
environment: {
|
|
isExtensionDevelopmentDebug,
|
|
appRoot: remoteInitData.appRoot,
|
|
appName: this._productService.nameLong,
|
|
appUriScheme: this._productService.urlProtocol,
|
|
appLanguage: platform.language,
|
|
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
|
|
extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,
|
|
globalStorageHome: remoteInitData.globalStorageHome,
|
|
workspaceStorageHome: remoteInitData.workspaceStorageHome,
|
|
webviewResourceRoot: this._environmentService.webviewResourceRoot,
|
|
webviewCspSource: this._environmentService.webviewCspSource,
|
|
},
|
|
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : {
|
|
configuration: workspace.configuration,
|
|
id: workspace.id,
|
|
name: this._labelService.getWorkspaceLabel(workspace)
|
|
},
|
|
remote: {
|
|
isRemote: true,
|
|
authority: this._initDataProvider.remoteAuthority,
|
|
connectionData: remoteInitData.connectionData
|
|
},
|
|
resolvedExtensions: resolvedExtensions,
|
|
hostExtensions: hostExtensions,
|
|
extensions: remoteInitData.extensions,
|
|
telemetryInfo,
|
|
logLevel: this._logService.getLevel(),
|
|
logsLocation: remoteInitData.extensionHostLogsPath,
|
|
logFile: joinPath(remoteInitData.extensionHostLogsPath, `${ExtensionHostLogFileName}.log`),
|
|
autoStart: true,
|
|
uiKind: platform.isWeb ? UIKind.Web : UIKind.Desktop
|
|
};
|
|
}
|
|
|
|
getInspectPort(): number | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
enableInspectPort(): Promise<boolean> {
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
dispose(): void {
|
|
super.dispose();
|
|
|
|
this._terminating = true;
|
|
|
|
if (this._protocol) {
|
|
// Send the extension host a request to terminate itself
|
|
// (graceful termination)
|
|
const socket = this._protocol.getSocket();
|
|
this._protocol.send(createMessageOfType(MessageType.Terminate));
|
|
this._protocol.sendDisconnect();
|
|
this._protocol.dispose();
|
|
socket.end();
|
|
this._protocol = null;
|
|
}
|
|
}
|
|
}
|