Path: blob/main/src/vs/workbench/services/extensions/common/extensionHostManager.ts
5251 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IntervalTimer } from '../../../../base/common/async.js';6import { VSBuffer } from '../../../../base/common/buffer.js';7import * as errors from '../../../../base/common/errors.js';8import { Emitter, Event } from '../../../../base/common/event.js';9import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';10import { StopWatch } from '../../../../base/common/stopwatch.js';11import { URI } from '../../../../base/common/uri.js';12import { IMessagePassingProtocol } from '../../../../base/parts/ipc/common/ipc.js';13import * as nls from '../../../../nls.js';14import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';15import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';16import { ExtensionIdentifier, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';17import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';18import { ILogService } from '../../../../platform/log/common/log.js';19import { RemoteAuthorityResolverErrorCode, getRemoteAuthorityPrefix } from '../../../../platform/remote/common/remoteAuthorityResolver.js';20import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';21import { IEditorService } from '../../editor/common/editorService.js';22import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';23import { ExtHostCustomersRegistry, IInternalExtHostContext } from './extHostCustomers.js';24import { ExtensionHostKind, extensionHostKindToString } from './extensionHostKind.js';25import { IExtensionHostManager } from './extensionHostManagers.js';26import { IExtensionDescriptionDelta } from './extensionHostProtocol.js';27import { IExtensionHostProxy, IResolveAuthorityResult } from './extensionHostProxy.js';28import { ExtensionRunningLocation } from './extensionRunningLocation.js';29import { ActivationKind, ExtensionActivationReason, ExtensionHostStartup, IExtensionHost, IExtensionInspectInfo, IInternalExtensionService } from './extensions.js';30import { Proxied, ProxyIdentifier } from './proxyIdentifier.js';31import { IRPCProtocolLogger, RPCProtocol, RequestInitiator, ResponsiveState } from './rpcProtocol.js';3233// Enable to see detailed message communication between window and extension host34const LOG_EXTENSION_HOST_COMMUNICATION = false;35const LOG_USE_COLORS = true;3637type ExtensionHostStartupClassification = {38owner: 'alexdima';39comment: 'The startup state of the extension host';40time: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The time reported by Date.now().' };41action: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The action: starting, success or error.' };42kind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The extension host kind: LocalProcess, LocalWebWorker or Remote.' };43errorName?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error name.' };44errorMessage?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error message.' };45errorStack?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error stack.' };46};4748type ExtensionHostStartupEvent = {49time: number;50action: 'starting' | 'success' | 'error';51kind: string;52errorName?: string;53errorMessage?: string;54errorStack?: string;55};5657export class ExtensionHostManager extends Disposable implements IExtensionHostManager {5859public readonly onDidExit: Event<[number, string | null]>;6061private readonly _onDidChangeResponsiveState: Emitter<ResponsiveState> = this._register(new Emitter<ResponsiveState>());62public readonly onDidChangeResponsiveState: Event<ResponsiveState> = this._onDidChangeResponsiveState.event;6364/**65* A map of already requested activation events to speed things up if the same activation event is triggered multiple times.66*/67private readonly _cachedActivationEvents: Map<string, Promise<void>>;68private readonly _resolvedActivationEvents: Set<string>;69private _rpcProtocol: RPCProtocol | null;70private readonly _customers: IDisposable[];71private readonly _extensionHost: IExtensionHost;72private _proxy: Promise<IExtensionHostProxy | null> | null;7374public get pid(): number | null {75return this._extensionHost.pid;76}7778public get kind(): ExtensionHostKind {79return this._extensionHost.runningLocation.kind;80}8182public get startup(): ExtensionHostStartup {83return this._extensionHost.startup;84}8586public get friendyName(): string {87return friendlyExtHostName(this.kind, this.pid);88}8990constructor(91extensionHost: IExtensionHost,92initialActivationEvents: string[],93private readonly _internalExtensionService: IInternalExtensionService,94@IInstantiationService private readonly _instantiationService: IInstantiationService,95@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,96@ITelemetryService private readonly _telemetryService: ITelemetryService,97@ILogService private readonly _logService: ILogService,98) {99super();100this._cachedActivationEvents = new Map<string, Promise<void>>();101this._resolvedActivationEvents = new Set<string>();102this._rpcProtocol = null;103this._customers = [];104105this._extensionHost = extensionHost;106this.onDidExit = this._extensionHost.onExit;107108const startingTelemetryEvent: ExtensionHostStartupEvent = {109time: Date.now(),110action: 'starting',111kind: extensionHostKindToString(this.kind)112};113this._telemetryService.publicLog2<ExtensionHostStartupEvent, ExtensionHostStartupClassification>('extensionHostStartup', startingTelemetryEvent);114115this._proxy = this._extensionHost.start().then(116(protocol) => {117118// Track healthy extension host startup119const successTelemetryEvent: ExtensionHostStartupEvent = {120time: Date.now(),121action: 'success',122kind: extensionHostKindToString(this.kind)123};124this._telemetryService.publicLog2<ExtensionHostStartupEvent, ExtensionHostStartupClassification>('extensionHostStartup', successTelemetryEvent);125126return this._createExtensionHostCustomers(this.kind, protocol);127},128(err) => {129this._logService.error(`Error received from starting extension host (kind: ${extensionHostKindToString(this.kind)})`);130this._logService.error(err);131132// Track errors during extension host startup133const failureTelemetryEvent: ExtensionHostStartupEvent = {134time: Date.now(),135action: 'error',136kind: extensionHostKindToString(this.kind)137};138139if (err && err.name) {140failureTelemetryEvent.errorName = err.name;141}142if (err && err.message) {143failureTelemetryEvent.errorMessage = err.message;144}145if (err && err.stack) {146failureTelemetryEvent.errorStack = err.stack;147}148this._telemetryService.publicLog2<ExtensionHostStartupEvent, ExtensionHostStartupClassification>('extensionHostStartup', failureTelemetryEvent);149150return null;151}152);153this._proxy.then(() => {154initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent, ActivationKind.Normal));155this._register(registerLatencyTestProvider({156measure: () => this.measure()157}));158});159}160161public async disconnect(): Promise<void> {162await this._extensionHost?.disconnect?.();163}164165public override dispose(): void {166this._extensionHost?.dispose();167this._rpcProtocol?.dispose();168169for (let i = 0, len = this._customers.length; i < len; i++) {170const customer = this._customers[i];171try {172customer.dispose();173} catch (err) {174errors.onUnexpectedError(err);175}176}177this._proxy = null;178179super.dispose();180}181182private async measure(): Promise<ExtHostLatencyResult | null> {183const proxy = await this._proxy;184if (!proxy) {185return null;186}187const latency = await this._measureLatency(proxy);188const down = await this._measureDown(proxy);189const up = await this._measureUp(proxy);190return {191remoteAuthority: this._extensionHost.remoteAuthority,192latency,193down,194up195};196}197198public async ready(): Promise<void> {199await this._proxy;200}201202private async _measureLatency(proxy: IExtensionHostProxy): Promise<number> {203const COUNT = 10;204205let sum = 0;206for (let i = 0; i < COUNT; i++) {207const sw = StopWatch.create();208await proxy.test_latency(i);209sw.stop();210sum += sw.elapsed();211}212return (sum / COUNT);213}214215private static _convert(byteCount: number, elapsedMillis: number): number {216return (byteCount * 1000 * 8) / elapsedMillis;217}218219private async _measureUp(proxy: IExtensionHostProxy): Promise<number> {220const SIZE = 10 * 1024 * 1024; // 10MB221222const buff = VSBuffer.alloc(SIZE);223const value = Math.ceil(Math.random() * 256);224for (let i = 0; i < buff.byteLength; i++) {225buff.writeUInt8(i, value);226}227const sw = StopWatch.create();228await proxy.test_up(buff);229sw.stop();230return ExtensionHostManager._convert(SIZE, sw.elapsed());231}232233private async _measureDown(proxy: IExtensionHostProxy): Promise<number> {234const SIZE = 10 * 1024 * 1024; // 10MB235236const sw = StopWatch.create();237await proxy.test_down(SIZE);238sw.stop();239return ExtensionHostManager._convert(SIZE, sw.elapsed());240}241242private _createExtensionHostCustomers(kind: ExtensionHostKind, protocol: IMessagePassingProtocol): IExtensionHostProxy {243244let logger: IRPCProtocolLogger | null = null;245if (LOG_EXTENSION_HOST_COMMUNICATION || this._environmentService.logExtensionHostCommunication) {246logger = new RPCLogger(kind);247} else if (TelemetryRPCLogger.isEnabled()) {248logger = new TelemetryRPCLogger(this._telemetryService);249}250251this._rpcProtocol = new RPCProtocol(protocol, logger);252this._register(this._rpcProtocol.onDidChangeResponsiveState((responsiveState: ResponsiveState) => this._onDidChangeResponsiveState.fire(responsiveState)));253let extensionHostProxy: IExtensionHostProxy | null = null as IExtensionHostProxy | null;254let mainProxyIdentifiers: ProxyIdentifier<any>[] = [];255const extHostContext: IInternalExtHostContext = {256remoteAuthority: this._extensionHost.remoteAuthority,257extensionHostKind: this.kind,258getProxy: <T>(identifier: ProxyIdentifier<T>): Proxied<T> => this._rpcProtocol!.getProxy(identifier),259set: <T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R => this._rpcProtocol!.set(identifier, instance),260dispose: (): void => this._rpcProtocol!.dispose(),261assertRegistered: (identifiers: ProxyIdentifier<any>[]): void => this._rpcProtocol!.assertRegistered(identifiers),262drain: (): Promise<void> => this._rpcProtocol!.drain(),263264//#region internal265internalExtensionService: this._internalExtensionService,266_setExtensionHostProxy: (value: IExtensionHostProxy): void => {267extensionHostProxy = value;268},269_setAllMainProxyIdentifiers: (value: ProxyIdentifier<any>[]): void => {270mainProxyIdentifiers = value;271},272//#endregion273};274275// Named customers276const namedCustomers = ExtHostCustomersRegistry.getNamedCustomers();277for (let i = 0, len = namedCustomers.length; i < len; i++) {278const [id, ctor] = namedCustomers[i];279try {280const instance = this._instantiationService.createInstance(ctor, extHostContext);281this._customers.push(instance);282this._rpcProtocol.set(id, instance);283} catch (err) {284this._logService.error(`Cannot instantiate named customer: '${id.sid}'`);285this._logService.error(err);286errors.onUnexpectedError(err);287}288}289290// Customers291const customers = ExtHostCustomersRegistry.getCustomers();292for (const ctor of customers) {293try {294const instance = this._instantiationService.createInstance(ctor, extHostContext);295this._customers.push(instance);296} catch (err) {297this._logService.error(err);298errors.onUnexpectedError(err);299}300}301302if (!extensionHostProxy) {303throw new Error(`Missing IExtensionHostProxy!`);304}305306// Check that no named customers are missing307this._rpcProtocol.assertRegistered(mainProxyIdentifiers);308309return extensionHostProxy;310}311312public async activate(extension: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<boolean> {313const proxy = await this._proxy;314if (!proxy) {315return false;316}317return proxy.activate(extension, reason);318}319320public activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void> {321if (!this._cachedActivationEvents.has(activationEvent)) {322this._cachedActivationEvents.set(activationEvent, this._activateByEvent(activationEvent, activationKind));323}324return this._cachedActivationEvents.get(activationEvent)!;325}326327public activationEventIsDone(activationEvent: string): boolean {328return this._resolvedActivationEvents.has(activationEvent);329}330331private async _activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void> {332if (!this._proxy) {333return;334}335const proxy = await this._proxy;336if (!proxy) {337// this case is already covered above and logged.338// i.e. the extension host could not be started339return;340}341342if (!this._extensionHost.extensions!.containsActivationEvent(activationEvent)) {343this._resolvedActivationEvents.add(activationEvent);344return;345}346347await proxy.activateByEvent(activationEvent, activationKind);348this._resolvedActivationEvents.add(activationEvent);349}350351public async getInspectPort(tryEnableInspector: boolean): Promise<IExtensionInspectInfo | undefined> {352if (this._extensionHost) {353if (tryEnableInspector) {354await this._extensionHost.enableInspectPort();355}356const port = this._extensionHost.getInspectPort();357if (port) {358return port;359}360}361362return undefined;363}364365public async resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise<IResolveAuthorityResult> {366const sw = StopWatch.create(false);367const prefix = () => `[${extensionHostKindToString(this._extensionHost.runningLocation.kind)}${this._extensionHost.runningLocation.affinity}][resolveAuthority(${getRemoteAuthorityPrefix(remoteAuthority)},${resolveAttempt})][${sw.elapsed()}ms] `;368const logInfo = (msg: string) => this._logService.info(`${prefix()}${msg}`);369const logError = (msg: string, err: any = undefined) => this._logService.error(`${prefix()}${msg}`, err);370371logInfo(`obtaining proxy...`);372const proxy = await this._proxy;373if (!proxy) {374logError(`no proxy`);375return {376type: 'error',377error: {378message: `Cannot resolve authority`,379code: RemoteAuthorityResolverErrorCode.Unknown,380detail: undefined381}382};383}384logInfo(`invoking...`);385const intervalLogger = new IntervalTimer();386try {387intervalLogger.cancelAndSet(() => logInfo('waiting...'), 1000);388const resolverResult = await proxy.resolveAuthority(remoteAuthority, resolveAttempt);389intervalLogger.dispose();390if (resolverResult.type === 'ok') {391logInfo(`returned ${resolverResult.value.authority.connectTo}`);392} else {393logError(`returned an error`, resolverResult.error);394}395return resolverResult;396} catch (err) {397intervalLogger.dispose();398logError(`returned an error`, err);399return {400type: 'error',401error: {402message: err.message,403code: RemoteAuthorityResolverErrorCode.Unknown,404detail: err405}406};407}408}409410public async getCanonicalURI(remoteAuthority: string, uri: URI): Promise<URI | null> {411const proxy = await this._proxy;412if (!proxy) {413throw new Error(`Cannot resolve canonical URI`);414}415return proxy.getCanonicalURI(remoteAuthority, uri);416}417418public async start(extensionRegistryVersionId: number, allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise<void> {419const proxy = await this._proxy;420if (!proxy) {421return;422}423const deltaExtensions = this._extensionHost.extensions!.set(extensionRegistryVersionId, allExtensions, myExtensions);424return proxy.startExtensionHost(deltaExtensions);425}426427public async extensionTestsExecute(): Promise<number> {428const proxy = await this._proxy;429if (!proxy) {430throw new Error('Could not obtain Extension Host Proxy');431}432return proxy.extensionTestsExecute();433}434435public representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean {436return this._extensionHost.runningLocation.equals(runningLocation);437}438439public async deltaExtensions(incomingExtensionsDelta: IExtensionDescriptionDelta): Promise<void> {440const proxy = await this._proxy;441if (!proxy) {442return;443}444const outgoingExtensionsDelta = this._extensionHost.extensions!.delta(incomingExtensionsDelta);445if (!outgoingExtensionsDelta) {446// The extension host already has this version of the extensions.447return;448}449return proxy.deltaExtensions(outgoingExtensionsDelta);450}451452public containsExtension(extensionId: ExtensionIdentifier): boolean {453return this._extensionHost.extensions?.containsExtension(extensionId) ?? false;454}455456public async setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void> {457const proxy = await this._proxy;458if (!proxy) {459return;460}461462return proxy.setRemoteEnvironment(env);463}464}465466export function friendlyExtHostName(kind: ExtensionHostKind, pid: number | null) {467if (pid) {468return `${extensionHostKindToString(kind)} pid: ${pid}`;469}470return `${extensionHostKindToString(kind)}`;471}472473const colorTables = [474['#2977B1', '#FC802D', '#34A13A', '#D3282F', '#9366BA'],475['#8B564C', '#E177C0', '#7F7F7F', '#BBBE3D', '#2EBECD']476];477478function prettyWithoutArrays(data: any): any {479if (Array.isArray(data)) {480return data;481}482if (data && typeof data === 'object' && typeof data.toString === 'function') {483const result = data.toString();484if (result !== '[object Object]') {485return result;486}487}488return data;489}490491function pretty(data: any): any {492if (Array.isArray(data)) {493return data.map(prettyWithoutArrays);494}495return prettyWithoutArrays(data);496}497498class RPCLogger implements IRPCProtocolLogger {499500private _totalIncoming = 0;501private _totalOutgoing = 0;502503constructor(504private readonly _kind: ExtensionHostKind505) { }506507private _log(direction: string, totalLength: number, msgLength: number, req: number, initiator: RequestInitiator, str: string, data: any): void {508data = pretty(data);509510const colorTable = colorTables[initiator];511const color = LOG_USE_COLORS ? colorTable[req % colorTable.length] : '#000000';512let args = [`%c[${extensionHostKindToString(this._kind)}][${direction}]%c[${String(totalLength).padStart(7)}]%c[len: ${String(msgLength).padStart(5)}]%c${String(req).padStart(5)} - ${str}`, 'color: darkgreen', 'color: grey', 'color: grey', `color: ${color}`];513if (/\($/.test(str)) {514args = args.concat(data);515args.push(')');516} else {517args.push(data);518}519console.log.apply(console, args as [string, ...string[]]);520}521522logIncoming(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void {523this._totalIncoming += msgLength;524this._log('Ext \u2192 Win', this._totalIncoming, msgLength, req, initiator, str, data);525}526527logOutgoing(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void {528this._totalOutgoing += msgLength;529this._log('Win \u2192 Ext', this._totalOutgoing, msgLength, req, initiator, str, data);530}531}532533interface RPCTelemetryData {534type: string;535length: number;536}537538type RPCTelemetryDataClassification = {539owner: 'jrieken';540comment: 'Insights about RPC message sizes';541type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The type of the RPC message' };542length: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The byte-length of the RPC message' };543};544545class TelemetryRPCLogger implements IRPCProtocolLogger {546547static isEnabled(): boolean {548return Math.random() < 0.0001; // 0.01% of users549}550551private readonly _pendingRequests = new Map<number, string>();552553constructor(@ITelemetryService private readonly _telemetryService: ITelemetryService) { }554555logIncoming(msgLength: number, req: number, initiator: RequestInitiator, str: string): void {556557if (initiator === RequestInitiator.LocalSide && /^receiveReply(Err)?:/.test(str)) {558// log the size of reply messages559const requestStr = this._pendingRequests.get(req) ?? 'unknown_reply';560this._pendingRequests.delete(req);561this._telemetryService.publicLog2<RPCTelemetryData, RPCTelemetryDataClassification>('extensionhost.incoming', {562type: `${str} ${requestStr}`,563length: msgLength564});565}566567if (initiator === RequestInitiator.OtherSide && /^receiveRequest /.test(str)) {568// incoming request569this._telemetryService.publicLog2<RPCTelemetryData, RPCTelemetryDataClassification>('extensionhost.incoming', {570type: `${str}`,571length: msgLength572});573}574}575576logOutgoing(msgLength: number, req: number, initiator: RequestInitiator, str: string): void {577578if (initiator === RequestInitiator.LocalSide && str.startsWith('request: ')) {579this._pendingRequests.set(req, str);580this._telemetryService.publicLog2<RPCTelemetryData, RPCTelemetryDataClassification>('extensionhost.outgoing', {581type: str,582length: msgLength583});584}585}586}587588interface ExtHostLatencyResult {589remoteAuthority: string | null;590up: number;591down: number;592latency: number;593}594595interface ExtHostLatencyProvider {596measure(): Promise<ExtHostLatencyResult | null>;597}598599const providers: ExtHostLatencyProvider[] = [];600function registerLatencyTestProvider(provider: ExtHostLatencyProvider): IDisposable {601providers.push(provider);602return {603dispose: () => {604for (let i = 0; i < providers.length; i++) {605if (providers[i] === provider) {606providers.splice(i, 1);607return;608}609}610}611};612}613614function getLatencyTestProviders(): ExtHostLatencyProvider[] {615return providers.slice(0);616}617618registerAction2(class MeasureExtHostLatencyAction extends Action2 {619620constructor() {621super({622id: 'editor.action.measureExtHostLatency',623title: nls.localize2('measureExtHostLatency', "Measure Extension Host Latency"),624category: Categories.Developer,625f1: true626});627}628629async run(accessor: ServicesAccessor) {630631const editorService = accessor.get(IEditorService);632633const measurements = await Promise.all(getLatencyTestProviders().map(provider => provider.measure()));634editorService.openEditor({ resource: undefined, contents: measurements.map(MeasureExtHostLatencyAction._print).join('\n\n'), options: { pinned: true } });635}636637private static _print(m: ExtHostLatencyResult | null): string {638if (!m) {639return '';640}641return `${m.remoteAuthority ? `Authority: ${m.remoteAuthority}\n` : ``}Roundtrip latency: ${m.latency.toFixed(3)}ms\nUp: ${MeasureExtHostLatencyAction._printSpeed(m.up)}\nDown: ${MeasureExtHostLatencyAction._printSpeed(m.down)}\n`;642}643644private static _printSpeed(n: number): string {645if (n <= 1024) {646return `${n} bps`;647}648if (n < 1024 * 1024) {649return `${(n / 1024).toFixed(1)} kbps`;650}651return `${(n / 1024 / 1024).toFixed(1)} Mbps`;652}653});654655656