From 23da1cf822c6965fdb34ae3df8a72ca174ea3137 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 8 Nov 2017 18:28:50 -0800 Subject: [PATCH] send all events through common stream --- src/server/editorServices.ts | 4 +- src/server/server.ts | 66 ++++++++--------------- src/server/session.ts | 100 ++++++++++++++++++++++++++--------- src/server/typingsCache.ts | 2 +- 4 files changed, 99 insertions(+), 73 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 2205f65cbe4..d7ee0af7ef9 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -16,6 +16,7 @@ namespace ts.server { export const ProjectInfoTelemetryEvent = "projectInfo"; // tslint:enable variable-name + // TODO: make these inherit from protocol.Event? export interface ProjectsUpdatedInBackgroundEvent { eventName: typeof ProjectsUpdatedInBackgroundEvent; data: { openFiles: string[]; }; @@ -320,6 +321,7 @@ namespace ts.server { pluginProbeLocations?: ReadonlyArray; allowLocalPluginLoads?: boolean; typesMapLocation?: string; + eventSender?: EventSender; } type WatchFile = (host: ServerHost, file: string, cb: FileWatcherCallback, watchType: WatchType, project?: Project) => FileWatcher; @@ -436,7 +438,7 @@ namespace ts.server { this.loadTypesMap(); } - this.typingsInstaller.attach(this); + this.typingsInstaller.attach(this, opts.eventSender); this.typingsCache = new TypingsCache(this.typingsInstaller); diff --git a/src/server/server.ts b/src/server/server.ts index f4faa0d3c77..97eba991884 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,4 +1,3 @@ -/// /// /// @@ -7,7 +6,7 @@ namespace ts.server { host: ServerHost; cancellationToken: ServerCancellationToken; canUseEvents: boolean; - installerEventPort: number; + eventPort: number; useSingleInferredProject: boolean; useInferredProjectPerProjectRoot: boolean; disableAutomaticTypingAcquisition: boolean; @@ -22,10 +21,6 @@ namespace ts.server { allowLocalPluginLoads: boolean; } - const net: { - connect(options: { port: number }, onConnect?: () => void): NodeSocket - } = require("net"); - const childProcess: { fork(modulePath: string, args: string[], options?: { execArgv: string[], env?: MapLike }): NodeChildProcess; execFileSync(file: string, args: string[], options: { stdio: "ignore", env: MapLike }): string | Buffer; @@ -83,10 +78,6 @@ namespace ts.server { pid: number; } - interface NodeSocket { - write(data: string, encoding: string): boolean; - } - interface ReadLineOptions { input: NodeJS.ReadableStream; output?: NodeJS.WritableStream; @@ -244,9 +235,8 @@ namespace ts.server { class NodeTypingsInstaller implements ITypingsInstaller { private installer: NodeChildProcess; private installerPidReported = false; - private socket: NodeSocket; private projectService: ProjectService; - private eventSender: EventSender; + private eventSender: EventSender | undefined; private activeRequestCount = 0; private requestQueue: QueuedOperation[] = []; private requestMap = createMap(); // Maps operation ID to newest requestQueue entry with that ID @@ -267,18 +257,10 @@ namespace ts.server { private readonly telemetryEnabled: boolean, private readonly logger: server.Logger, private readonly host: ServerHost, - eventPort: number, readonly globalTypingsCacheLocation: string, readonly typingSafeListLocation: string, readonly typesMapLocation: string, - private readonly npmLocation: string | undefined, - private newLine: string) { - if (eventPort) { - const s = net.connect({ port: eventPort }, () => { - this.socket = s; - this.reportInstallerProcessId(); - }); - } + private readonly npmLocation: string | undefined) { } isKnownTypesPackageName(name: string): boolean { @@ -310,26 +292,23 @@ namespace ts.server { if (this.installerPidReported) { return; } - if (this.socket && this.installer) { - this.sendEvent(0, "typingsInstallerPid", { pid: this.installer.pid }); + if (this.installer && this.eventSender) { + this.eventSender.event({ pid: this.installer.pid }, "typingsInstallerPid"); this.installerPidReported = true; } } - private sendEvent(seq: number, event: string, body: any): void { - this.socket.write(formatMessage({ seq, type: "event", event, body }, this.logger, Buffer.byteLength, this.newLine), "utf8"); - } - setTelemetrySender(telemetrySender: EventSender) { - this.eventSender = telemetrySender; - } - - attach(projectService: ProjectService) { + attach(projectService: ProjectService, eventSender?: EventSender) { this.projectService = projectService; if (this.logger.hasLevel(LogLevel.requestTime)) { this.logger.info("Binding..."); } + if (eventSender) { + this.eventSender = eventSender; + } + const args: string[] = [Arguments.GlobalCacheLocation, this.globalTypingsCacheLocation]; if (this.telemetryEnabled) { args.push(Arguments.EnableTelemetry); @@ -353,10 +332,10 @@ namespace ts.server { if (match) { // if port is specified - use port + 1 // otherwise pick a default port depending on if 'debug' or 'inspect' and use its value + 1 - const currentPort = match[2] !== undefined - ? +match[2] - : match[1].charAt(0) === "d" ? 5858 : 9229; - execArgv.push(`--${match[1]}=${currentPort + 1}`); + // const currentPort = match[2] !== undefined + // ? +match[2] + // : match[1].charAt(0) === "d" ? 5858 : 9229; + // execArgv.push(`--${match[1]}=${currentPort + 1}`); break; } } @@ -508,8 +487,8 @@ namespace ts.server { this.projectService.updateTypingsForProject(response); - if (this.socket) { - this.sendEvent(0, "setTypings", response); + if (this.eventSender) { + this.eventSender.event(response, "setTypings"); } break; @@ -530,10 +509,10 @@ namespace ts.server { class IOSession extends Session { constructor(options: IoSessionOptions) { - const { host, installerEventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; + const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; const typingsInstaller = disableAutomaticTypingAcquisition ? undefined - : new NodeTypingsInstaller(telemetryEnabled, logger, host, installerEventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, host.newLine); + : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation); super({ host, @@ -545,13 +524,10 @@ namespace ts.server { hrtime: process.hrtime, logger, canUseEvents, + eventPort, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, allowLocalPluginLoads: options.allowLocalPluginLoads }); - - if (telemetryEnabled && typingsInstaller) { - typingsInstaller.setTelemetrySender(this); - } } exit() { @@ -936,8 +912,8 @@ namespace ts.server { const options: IoSessionOptions = { host: sys, cancellationToken, - installerEventPort: eventPort, - canUseEvents: eventPort === undefined, + eventPort, + canUseEvents: true, useSingleInferredProject, useInferredProjectPerProjectRoot, disableAutomaticTypingAcquisition, diff --git a/src/server/session.ts b/src/server/session.ts index 6c97c3c8bd4..b728ea6d552 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1,9 +1,19 @@ +/// /// /// /// /// namespace ts.server { + + interface NodeSocket { + write(data: string, encoding: string): boolean; + } + + const net: { + connect(options: { port: number }, onConnect?: () => void): NodeSocket + } = require("net"); + interface StackTraceError extends Error { stack?: string; } @@ -253,6 +263,10 @@ namespace ts.server { hrtime: (start?: number[]) => number[]; logger: Logger; canUseEvents: boolean; + /** + * If defined, the Session will send events through `eventPort` instead of stdout. + */ + eventPort?: number; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -269,15 +283,19 @@ namespace ts.server { private currentRequestId: number; private errorCheck: MultistepOperation; - private eventHandler: ProjectServiceEventHandler; - private host: ServerHost; private readonly cancellationToken: ServerCancellationToken; protected readonly typingsInstaller: ITypingsInstaller; private byteLength: (buf: string, encoding?: string) => number; private hrtime: (start?: number[]) => number[]; protected logger: Logger; + private canUseEvents: boolean; + private eventPort: number | undefined; + private eventSocket: NodeSocket; + private eventHandler: ProjectServiceEventHandler; + public readonly event: EventSender["event"]; + private socketEventQueue: { info: any, eventName: string}[] | undefined; constructor(opts: SessionOptions) { this.host = opts.host; @@ -286,14 +304,49 @@ namespace ts.server { this.byteLength = opts.byteLength; this.hrtime = opts.hrtime; this.logger = opts.logger; + this.eventPort = opts.eventPort; this.canUseEvents = opts.canUseEvents; const { throttleWaitMilliseconds } = opts; + if (!this.canUseEvents) { + this.event = noop; + } + else if (this.eventPort) { + const s = net.connect({ port: this.eventPort }, () => { + this.eventSocket = s; + this.clearSocketEventQueue(); + }); + + this.event = function (info: T, eventName: string) { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ info, eventName }); + return; + } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(info, eventName); + } + }; + } + else { + this.event = function (info: T, eventName: string) { + const ev: protocol.Event = { + seq: 0, + type: "event", + event: eventName, + body: info + }; + this.send(ev); + }; + } + this.eventHandler = this.canUseEvents ? opts.eventHandler || (event => this.defaultEventHandler(event)) : undefined; - const multistepOperationHost: MultistepOperationHost = { executeWithRequestId: (requestId, action) => this.executeWithRequestId(requestId, action), getCurrentRequestId: () => this.currentRequestId, @@ -314,20 +367,26 @@ namespace ts.server { eventHandler: this.eventHandler, globalPlugins: opts.globalPlugins, pluginProbeLocations: opts.pluginProbeLocations, - allowLocalPluginLoads: opts.allowLocalPluginLoads + allowLocalPluginLoads: opts.allowLocalPluginLoads, + eventSender: this }; this.projectService = new ProjectService(settings); this.gcTimer = new GcTimer(this.host, /*delay*/ 7000, this.logger); } + private clearSocketEventQueue() { + for (const event of this.socketEventQueue) { + this.writeToEventSocket(event.info, event.eventName); + } + this.socketEventQueue = undefined; + } + + private writeToEventSocket(info: any, eventName: string): void { + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body: info }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); + } + private sendRequestCompletedEvent(requestId: number): void { - const event: protocol.RequestCompletedEvent = { - seq: 0, - type: "event", - event: "requestCompleted", - body: { request_seq: requestId } - }; - this.send(event); + this.event({ request_seq: requestId }, "requestCompleted"); } private defaultEventHandler(event: ProjectServiceEvent) { @@ -392,26 +451,15 @@ namespace ts.server { } public send(msg: protocol.Message) { - if (msg.type === "event" && !this.canUseEvents) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); - } - return; + if (msg.type === "event") { + Debug.assert(this.canUseEvents); + Debug.assert(!this.eventPort); } this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine)); } - public event(info: T, eventName: string) { - const ev: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body: info - }; - this.send(ev); - } - // For backwards-compatibility only. + /** @deprecated */ public output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void { this.doOutput(info, cmdName, reqSeq, /*success*/ !errorMsg, errorMsg); } diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts index cde303bfd39..6fbf0939431 100644 --- a/src/server/typingsCache.ts +++ b/src/server/typingsCache.ts @@ -10,7 +10,7 @@ namespace ts.server { isKnownTypesPackageName(name: string): boolean; installPackage(options: InstallPackageOptionsWithProjectRootPath): Promise; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; - attach(projectService: ProjectService): void; + attach(projectService: ProjectService, eventSender?: EventSender): void; onProjectClosed(p: Project): void; readonly globalTypingsCacheLocation: string; }