Path: blob/main/src/vs/workbench/services/extensions/common/extensionHostManager.ts
3296 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;73private _hasStarted = false;7475public get pid(): number | null {76return this._extensionHost.pid;77}7879public get kind(): ExtensionHostKind {80return this._extensionHost.runningLocation.kind;81}8283public get startup(): ExtensionHostStartup {84return this._extensionHost.startup;85}8687public get friendyName(): string {88return friendlyExtHostName(this.kind, this.pid);89}9091constructor(92extensionHost: IExtensionHost,93initialActivationEvents: string[],94private readonly _internalExtensionService: IInternalExtensionService,95@IInstantiationService private readonly _instantiationService: IInstantiationService,96@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,97@ITelemetryService private readonly _telemetryService: ITelemetryService,98@ILogService private readonly _logService: ILogService,99) {100super();101this._cachedActivationEvents = new Map<string, Promise<void>>();102this._resolvedActivationEvents = new Set<string>();103this._rpcProtocol = null;104this._customers = [];105106this._extensionHost = extensionHost;107this.onDidExit = this._extensionHost.onExit;108109const startingTelemetryEvent: ExtensionHostStartupEvent = {110time: Date.now(),111action: 'starting',112kind: extensionHostKindToString(this.kind)113};114this._telemetryService.publicLog2<ExtensionHostStartupEvent, ExtensionHostStartupClassification>('extensionHostStartup', startingTelemetryEvent);115116this._proxy = this._extensionHost.start().then(117(protocol) => {118this._hasStarted = true;119120// Track healthy extension host startup121const successTelemetryEvent: ExtensionHostStartupEvent = {122time: Date.now(),123action: 'success',124kind: extensionHostKindToString(this.kind)125};126this._telemetryService.publicLog2<ExtensionHostStartupEvent, ExtensionHostStartupClassification>('extensionHostStartup', successTelemetryEvent);127128return this._createExtensionHostCustomers(this.kind, protocol);129},130(err) => {131this._logService.error(`Error received from starting extension host (kind: ${extensionHostKindToString(this.kind)})`);132this._logService.error(err);133134// Track errors during extension host startup135const failureTelemetryEvent: ExtensionHostStartupEvent = {136time: Date.now(),137action: 'error',138kind: extensionHostKindToString(this.kind)139};140141if (err && err.name) {142failureTelemetryEvent.errorName = err.name;143}144if (err && err.message) {145failureTelemetryEvent.errorMessage = err.message;146}147if (err && err.stack) {148failureTelemetryEvent.errorStack = err.stack;149}150this._telemetryService.publicLog2<ExtensionHostStartupEvent, ExtensionHostStartupClassification>('extensionHostStartup', failureTelemetryEvent);151152return null;153}154);155this._proxy.then(() => {156initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent, ActivationKind.Normal));157this._register(registerLatencyTestProvider({158measure: () => this.measure()159}));160});161}162163public async disconnect(): Promise<void> {164await this._extensionHost?.disconnect?.();165}166167public override dispose(): void {168this._extensionHost?.dispose();169this._rpcProtocol?.dispose();170171for (let i = 0, len = this._customers.length; i < len; i++) {172const customer = this._customers[i];173try {174customer.dispose();175} catch (err) {176errors.onUnexpectedError(err);177}178}179this._proxy = null;180181super.dispose();182}183184private async measure(): Promise<ExtHostLatencyResult | null> {185const proxy = await this._proxy;186if (!proxy) {187return null;188}189const latency = await this._measureLatency(proxy);190const down = await this._measureDown(proxy);191const up = await this._measureUp(proxy);192return {193remoteAuthority: this._extensionHost.remoteAuthority,194latency,195down,196up197};198}199200public async ready(): Promise<void> {201await this._proxy;202}203204private async _measureLatency(proxy: IExtensionHostProxy): Promise<number> {205const COUNT = 10;206207let sum = 0;208for (let i = 0; i < COUNT; i++) {209const sw = StopWatch.create();210await proxy.test_latency(i);211sw.stop();212sum += sw.elapsed();213}214return (sum / COUNT);215}216217private static _convert(byteCount: number, elapsedMillis: number): number {218return (byteCount * 1000 * 8) / elapsedMillis;219}220221private async _measureUp(proxy: IExtensionHostProxy): Promise<number> {222const SIZE = 10 * 1024 * 1024; // 10MB223224const buff = VSBuffer.alloc(SIZE);225const value = Math.ceil(Math.random() * 256);226for (let i = 0; i < buff.byteLength; i++) {227buff.writeUInt8(i, value);228}229const sw = StopWatch.create();230await proxy.test_up(buff);231sw.stop();232return ExtensionHostManager._convert(SIZE, sw.elapsed());233}234235private async _measureDown(proxy: IExtensionHostProxy): Promise<number> {236const SIZE = 10 * 1024 * 1024; // 10MB237238const sw = StopWatch.create();239await proxy.test_down(SIZE);240sw.stop();241return ExtensionHostManager._convert(SIZE, sw.elapsed());242}243244private _createExtensionHostCustomers(kind: ExtensionHostKind, protocol: IMessagePassingProtocol): IExtensionHostProxy {245246let logger: IRPCProtocolLogger | null = null;247if (LOG_EXTENSION_HOST_COMMUNICATION || this._environmentService.logExtensionHostCommunication) {248logger = new RPCLogger(kind);249} else if (TelemetryRPCLogger.isEnabled()) {250logger = new TelemetryRPCLogger(this._telemetryService);251}252253this._rpcProtocol = new RPCProtocol(protocol, logger);254this._register(this._rpcProtocol.onDidChangeResponsiveState((responsiveState: ResponsiveState) => this._onDidChangeResponsiveState.fire(responsiveState)));255let extensionHostProxy: IExtensionHostProxy | null = null as IExtensionHostProxy | null;256let mainProxyIdentifiers: ProxyIdentifier<any>[] = [];257const extHostContext: IInternalExtHostContext = {258remoteAuthority: this._extensionHost.remoteAuthority,259extensionHostKind: this.kind,260getProxy: <T>(identifier: ProxyIdentifier<T>): Proxied<T> => this._rpcProtocol!.getProxy(identifier),261set: <T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R => this._rpcProtocol!.set(identifier, instance),262dispose: (): void => this._rpcProtocol!.dispose(),263assertRegistered: (identifiers: ProxyIdentifier<any>[]): void => this._rpcProtocol!.assertRegistered(identifiers),264drain: (): Promise<void> => this._rpcProtocol!.drain(),265266//#region internal267internalExtensionService: this._internalExtensionService,268_setExtensionHostProxy: (value: IExtensionHostProxy): void => {269extensionHostProxy = value;270},271_setAllMainProxyIdentifiers: (value: ProxyIdentifier<any>[]): void => {272mainProxyIdentifiers = value;273},274//#endregion275};276277// Named customers278const namedCustomers = ExtHostCustomersRegistry.getNamedCustomers();279for (let i = 0, len = namedCustomers.length; i < len; i++) {280const [id, ctor] = namedCustomers[i];281try {282const instance = this._instantiationService.createInstance(ctor, extHostContext);283this._customers.push(instance);284this._rpcProtocol.set(id, instance);285} catch (err) {286this._logService.error(`Cannot instantiate named customer: '${id.sid}'`);287this._logService.error(err);288errors.onUnexpectedError(err);289}290}291292// Customers293const customers = ExtHostCustomersRegistry.getCustomers();294for (const ctor of customers) {295try {296const instance = this._instantiationService.createInstance(ctor, extHostContext);297this._customers.push(instance);298} catch (err) {299this._logService.error(err);300errors.onUnexpectedError(err);301}302}303304if (!extensionHostProxy) {305throw new Error(`Missing IExtensionHostProxy!`);306}307308// Check that no named customers are missing309this._rpcProtocol.assertRegistered(mainProxyIdentifiers);310311return extensionHostProxy;312}313314public async activate(extension: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<boolean> {315const proxy = await this._proxy;316if (!proxy) {317return false;318}319return proxy.activate(extension, reason);320}321322public activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void> {323if (activationKind === ActivationKind.Immediate && !this._hasStarted) {324return Promise.resolve();325}326327if (!this._cachedActivationEvents.has(activationEvent)) {328this._cachedActivationEvents.set(activationEvent, this._activateByEvent(activationEvent, activationKind));329}330return this._cachedActivationEvents.get(activationEvent)!;331}332333public activationEventIsDone(activationEvent: string): boolean {334return this._resolvedActivationEvents.has(activationEvent);335}336337private async _activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void> {338if (!this._proxy) {339return;340}341const proxy = await this._proxy;342if (!proxy) {343// this case is already covered above and logged.344// i.e. the extension host could not be started345return;346}347348if (!this._extensionHost.extensions!.containsActivationEvent(activationEvent)) {349this._resolvedActivationEvents.add(activationEvent);350return;351}352353await proxy.activateByEvent(activationEvent, activationKind);354this._resolvedActivationEvents.add(activationEvent);355}356357public async getInspectPort(tryEnableInspector: boolean): Promise<IExtensionInspectInfo | undefined> {358if (this._extensionHost) {359if (tryEnableInspector) {360await this._extensionHost.enableInspectPort();361}362const port = this._extensionHost.getInspectPort();363if (port) {364return port;365}366}367368return undefined;369}370371public async resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise<IResolveAuthorityResult> {372const sw = StopWatch.create(false);373const prefix = () => `[${extensionHostKindToString(this._extensionHost.runningLocation.kind)}${this._extensionHost.runningLocation.affinity}][resolveAuthority(${getRemoteAuthorityPrefix(remoteAuthority)},${resolveAttempt})][${sw.elapsed()}ms] `;374const logInfo = (msg: string) => this._logService.info(`${prefix()}${msg}`);375const logError = (msg: string, err: any = undefined) => this._logService.error(`${prefix()}${msg}`, err);376377logInfo(`obtaining proxy...`);378const proxy = await this._proxy;379if (!proxy) {380logError(`no proxy`);381return {382type: 'error',383error: {384message: `Cannot resolve authority`,385code: RemoteAuthorityResolverErrorCode.Unknown,386detail: undefined387}388};389}390logInfo(`invoking...`);391const intervalLogger = new IntervalTimer();392try {393intervalLogger.cancelAndSet(() => logInfo('waiting...'), 1000);394const resolverResult = await proxy.resolveAuthority(remoteAuthority, resolveAttempt);395intervalLogger.dispose();396if (resolverResult.type === 'ok') {397logInfo(`returned ${resolverResult.value.authority.connectTo}`);398} else {399logError(`returned an error`, resolverResult.error);400}401return resolverResult;402} catch (err) {403intervalLogger.dispose();404logError(`returned an error`, err);405return {406type: 'error',407error: {408message: err.message,409code: RemoteAuthorityResolverErrorCode.Unknown,410detail: err411}412};413}414}415416public async getCanonicalURI(remoteAuthority: string, uri: URI): Promise<URI | null> {417const proxy = await this._proxy;418if (!proxy) {419throw new Error(`Cannot resolve canonical URI`);420}421return proxy.getCanonicalURI(remoteAuthority, uri);422}423424public async start(extensionRegistryVersionId: number, allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise<void> {425const proxy = await this._proxy;426if (!proxy) {427return;428}429const deltaExtensions = this._extensionHost.extensions!.set(extensionRegistryVersionId, allExtensions, myExtensions);430return proxy.startExtensionHost(deltaExtensions);431}432433public async extensionTestsExecute(): Promise<number> {434const proxy = await this._proxy;435if (!proxy) {436throw new Error('Could not obtain Extension Host Proxy');437}438return proxy.extensionTestsExecute();439}440441public representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean {442return this._extensionHost.runningLocation.equals(runningLocation);443}444445public async deltaExtensions(incomingExtensionsDelta: IExtensionDescriptionDelta): Promise<void> {446const proxy = await this._proxy;447if (!proxy) {448return;449}450const outgoingExtensionsDelta = this._extensionHost.extensions!.delta(incomingExtensionsDelta);451if (!outgoingExtensionsDelta) {452// The extension host already has this version of the extensions.453return;454}455return proxy.deltaExtensions(outgoingExtensionsDelta);456}457458public containsExtension(extensionId: ExtensionIdentifier): boolean {459return this._extensionHost.extensions?.containsExtension(extensionId) ?? false;460}461462public async setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void> {463const proxy = await this._proxy;464if (!proxy) {465return;466}467468return proxy.setRemoteEnvironment(env);469}470}471472export function friendlyExtHostName(kind: ExtensionHostKind, pid: number | null) {473if (pid) {474return `${extensionHostKindToString(kind)} pid: ${pid}`;475}476return `${extensionHostKindToString(kind)}`;477}478479const colorTables = [480['#2977B1', '#FC802D', '#34A13A', '#D3282F', '#9366BA'],481['#8B564C', '#E177C0', '#7F7F7F', '#BBBE3D', '#2EBECD']482];483484function prettyWithoutArrays(data: any): any {485if (Array.isArray(data)) {486return data;487}488if (data && typeof data === 'object' && typeof data.toString === 'function') {489const result = data.toString();490if (result !== '[object Object]') {491return result;492}493}494return data;495}496497function pretty(data: any): any {498if (Array.isArray(data)) {499return data.map(prettyWithoutArrays);500}501return prettyWithoutArrays(data);502}503504class RPCLogger implements IRPCProtocolLogger {505506private _totalIncoming = 0;507private _totalOutgoing = 0;508509constructor(510private readonly _kind: ExtensionHostKind511) { }512513private _log(direction: string, totalLength: number, msgLength: number, req: number, initiator: RequestInitiator, str: string, data: any): void {514data = pretty(data);515516const colorTable = colorTables[initiator];517const color = LOG_USE_COLORS ? colorTable[req % colorTable.length] : '#000000';518let 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}`];519if (/\($/.test(str)) {520args = args.concat(data);521args.push(')');522} else {523args.push(data);524}525console.log.apply(console, args as [string, ...string[]]);526}527528logIncoming(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void {529this._totalIncoming += msgLength;530this._log('Ext \u2192 Win', this._totalIncoming, msgLength, req, initiator, str, data);531}532533logOutgoing(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void {534this._totalOutgoing += msgLength;535this._log('Win \u2192 Ext', this._totalOutgoing, msgLength, req, initiator, str, data);536}537}538539interface RPCTelemetryData {540type: string;541length: number;542}543544type RPCTelemetryDataClassification = {545owner: 'jrieken';546comment: 'Insights about RPC message sizes';547type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The type of the RPC message' };548length: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The byte-length of the RPC message' };549};550551class TelemetryRPCLogger implements IRPCProtocolLogger {552553static isEnabled(): boolean {554return Math.random() < 0.0001; // 0.01% of users555}556557private readonly _pendingRequests = new Map<number, string>();558559constructor(@ITelemetryService private readonly _telemetryService: ITelemetryService) { }560561logIncoming(msgLength: number, req: number, initiator: RequestInitiator, str: string): void {562563if (initiator === RequestInitiator.LocalSide && /^receiveReply(Err)?:/.test(str)) {564// log the size of reply messages565const requestStr = this._pendingRequests.get(req) ?? 'unknown_reply';566this._pendingRequests.delete(req);567this._telemetryService.publicLog2<RPCTelemetryData, RPCTelemetryDataClassification>('extensionhost.incoming', {568type: `${str} ${requestStr}`,569length: msgLength570});571}572573if (initiator === RequestInitiator.OtherSide && /^receiveRequest /.test(str)) {574// incoming request575this._telemetryService.publicLog2<RPCTelemetryData, RPCTelemetryDataClassification>('extensionhost.incoming', {576type: `${str}`,577length: msgLength578});579}580}581582logOutgoing(msgLength: number, req: number, initiator: RequestInitiator, str: string): void {583584if (initiator === RequestInitiator.LocalSide && str.startsWith('request: ')) {585this._pendingRequests.set(req, str);586this._telemetryService.publicLog2<RPCTelemetryData, RPCTelemetryDataClassification>('extensionhost.outgoing', {587type: str,588length: msgLength589});590}591}592}593594interface ExtHostLatencyResult {595remoteAuthority: string | null;596up: number;597down: number;598latency: number;599}600601interface ExtHostLatencyProvider {602measure(): Promise<ExtHostLatencyResult | null>;603}604605const providers: ExtHostLatencyProvider[] = [];606function registerLatencyTestProvider(provider: ExtHostLatencyProvider): IDisposable {607providers.push(provider);608return {609dispose: () => {610for (let i = 0; i < providers.length; i++) {611if (providers[i] === provider) {612providers.splice(i, 1);613return;614}615}616}617};618}619620function getLatencyTestProviders(): ExtHostLatencyProvider[] {621return providers.slice(0);622}623624registerAction2(class MeasureExtHostLatencyAction extends Action2 {625626constructor() {627super({628id: 'editor.action.measureExtHostLatency',629title: nls.localize2('measureExtHostLatency', "Measure Extension Host Latency"),630category: Categories.Developer,631f1: true632});633}634635async run(accessor: ServicesAccessor) {636637const editorService = accessor.get(IEditorService);638639const measurements = await Promise.all(getLatencyTestProviders().map(provider => provider.measure()));640editorService.openEditor({ resource: undefined, contents: measurements.map(MeasureExtHostLatencyAction._print).join('\n\n'), options: { pinned: true } });641}642643private static _print(m: ExtHostLatencyResult | null): string {644if (!m) {645return '';646}647return `${m.remoteAuthority ? `Authority: ${m.remoteAuthority}\n` : ``}Roundtrip latency: ${m.latency.toFixed(3)}ms\nUp: ${MeasureExtHostLatencyAction._printSpeed(m.up)}\nDown: ${MeasureExtHostLatencyAction._printSpeed(m.down)}\n`;648}649650private static _printSpeed(n: number): string {651if (n <= 1024) {652return `${n} bps`;653}654if (n < 1024 * 1024) {655return `${(n / 1024).toFixed(1)} kbps`;656}657return `${(n / 1024 / 1024).toFixed(1)} Mbps`;658}659});660661662