• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import * as ts from "./_namespaces/ts";
2import * as server from "./_namespaces/ts.server";
3import {
4    ActionInvalidate, ActionPackageInstalled, ActionSet, Arguments, BaseLogger, BeginInstallTypes,
5    createInstallTypingsRequest, EndInstallTypes, EventBeginInstallTypes, EventEndInstallTypes,
6    EventInitializationFailed, EventTypesRegistry, findArgument, formatMessage, getLogLevel, hasArgument, indent,
7    InitializationFailedResponse, InstallPackageOptionsWithProject, InstallPackageRequest, InvalidateCachedTypings,
8    ITypingsInstaller, Logger, LogLevel, ModuleImportResult, Msg, nullCancellationToken, nullTypingsInstaller,
9    PackageInstalledResponse, Project, ProjectService, protocol, ServerCancellationToken, ServerHost, Session,
10    SetTypings, StartInput, StartSessionOptions, stringifyIndented, toEvent, TypesRegistryResponse,
11    TypingInstallerRequestUnion,
12} from "./_namespaces/ts.server";
13import {
14    ApplyCodeActionCommandResult, assertType, CharacterCodes, combinePaths, createQueue, Debug, directorySeparator,
15    DirectoryWatcherCallback, ESMap, FileWatcher, getDirectoryPath, getEntries, getNodeMajorVersion, getRootLength,
16    JsTyping, LanguageServiceMode, Map, MapLike, noop, noopFileWatcher, normalizePath, normalizeSlashes, resolveJSModule,
17    SortedReadonlyArray, startTracing, stripQuotes, sys, toFileNameLowerCase, tracing, TypeAcquisition,
18    validateLocaleAndSetLanguage, versionMajorMinor, WatchOptions,
19} from "./_namespaces/ts";
20
21interface LogOptions {
22    file?: string;
23    detailLevel?: LogLevel;
24    traceToConsole?: boolean;
25    logToFile?: boolean;
26}
27
28interface NodeChildProcess {
29    send(message: any, sendHandle?: any): void;
30    on(message: "message" | "exit", f: (m: any) => void): void;
31    kill(): void;
32    pid: number;
33}
34
35interface ReadLineOptions {
36    input: NodeJS.ReadableStream;
37    output?: NodeJS.WritableStream;
38    terminal?: boolean;
39    historySize?: number;
40}
41
42interface NodeSocket {
43    write(data: string, encoding: string): boolean;
44}
45
46function parseLoggingEnvironmentString(logEnvStr: string | undefined): LogOptions {
47    if (!logEnvStr) {
48        return {};
49    }
50    const logEnv: LogOptions = { logToFile: true };
51    const args = logEnvStr.split(" ");
52    const len = args.length - 1;
53    for (let i = 0; i < len; i += 2) {
54        const option = args[i];
55        const { value, extraPartCounter } = getEntireValue(i + 1);
56        i += extraPartCounter;
57        if (option && value) {
58            switch (option) {
59                case "-file":
60                    logEnv.file = value;
61                    break;
62                case "-level":
63                    const level = getLogLevel(value);
64                    logEnv.detailLevel = level !== undefined ? level : LogLevel.normal;
65                    break;
66                case "-traceToConsole":
67                    logEnv.traceToConsole = value.toLowerCase() === "true";
68                    break;
69                case "-logToFile":
70                    logEnv.logToFile = value.toLowerCase() === "true";
71                    break;
72            }
73        }
74    }
75    return logEnv;
76
77    function getEntireValue(initialIndex: number) {
78        let pathStart = args[initialIndex];
79        let extraPartCounter = 0;
80        if (pathStart.charCodeAt(0) === CharacterCodes.doubleQuote &&
81            pathStart.charCodeAt(pathStart.length - 1) !== CharacterCodes.doubleQuote) {
82            for (let i = initialIndex + 1; i < args.length; i++) {
83                pathStart += " ";
84                pathStart += args[i];
85                extraPartCounter++;
86                if (pathStart.charCodeAt(pathStart.length - 1) === CharacterCodes.doubleQuote) break;
87            }
88        }
89        return { value: stripQuotes(pathStart), extraPartCounter };
90    }
91}
92
93function parseServerMode(): LanguageServiceMode | string | undefined {
94    const mode = findArgument("--serverMode");
95    if (!mode) return undefined;
96
97    switch (mode.toLowerCase()) {
98        case "semantic":
99            return LanguageServiceMode.Semantic;
100        case "partialsemantic":
101            return LanguageServiceMode.PartialSemantic;
102        case "syntactic":
103            return LanguageServiceMode.Syntactic;
104        default:
105            return mode;
106    }
107}
108
109/** @internal */
110export function initializeNodeSystem(): StartInput {
111    const sys = Debug.checkDefined(ts.sys) as ServerHost;
112    const childProcess: {
113        execFileSync(file: string, args: string[], options: { stdio: "ignore", env: MapLike<string> }): string | Buffer;
114    } = require("child_process");
115
116    interface Stats {
117        isFile(): boolean;
118        isDirectory(): boolean;
119        isBlockDevice(): boolean;
120        isCharacterDevice(): boolean;
121        isSymbolicLink(): boolean;
122        isFIFO(): boolean;
123        isSocket(): boolean;
124        dev: number;
125        ino: number;
126        mode: number;
127        nlink: number;
128        uid: number;
129        gid: number;
130        rdev: number;
131        size: number;
132        blksize: number;
133        blocks: number;
134        atime: Date;
135        mtime: Date;
136        ctime: Date;
137        birthtime: Date;
138    }
139
140    const fs: {
141        openSync(path: string, options: string): number;
142        close(fd: number, callback: (err: NodeJS.ErrnoException) => void): void;
143        writeSync(fd: number, buffer: Buffer, offset: number, length: number, position?: number): number;
144        statSync(path: string): Stats;
145        stat(path: string, callback?: (err: NodeJS.ErrnoException, stats: Stats) => any): void;
146    } = require("fs");
147
148    class Logger extends BaseLogger {
149        private fd = -1;
150        constructor(
151            private readonly logFilename: string,
152            private readonly traceToConsole: boolean,
153            level: LogLevel
154        ) {
155            super(level);
156            if (this.logFilename) {
157                try {
158                    this.fd = fs.openSync(this.logFilename, "w");
159                }
160                catch (_) {
161                    // swallow the error and keep logging disabled if file cannot be opened
162                }
163            }
164        }
165
166        close() {
167            if (this.fd >= 0) {
168                fs.close(this.fd, noop);
169            }
170        }
171
172        getLogFileName() {
173            return this.logFilename;
174        }
175
176        loggingEnabled() {
177            return !!this.logFilename || this.traceToConsole;
178        }
179
180        protected canWrite() {
181            return this.fd >= 0 || this.traceToConsole;
182        }
183
184        protected write(s: string, _type: Msg) {
185            if (this.fd >= 0) {
186                const buf = sys.bufferFrom!(s);
187                // eslint-disable-next-line no-null/no-null
188                fs.writeSync(this.fd, buf as globalThis.Buffer, 0, buf.length, /*position*/ null!); // TODO: GH#18217
189            }
190            if (this.traceToConsole) {
191                console.warn(s);
192            }
193        }
194    }
195    const libDirectory = getDirectoryPath(normalizePath(sys.getExecutingFilePath()));
196
197    const nodeVersion = getNodeMajorVersion();
198    // use watchGuard process on Windows when node version is 4 or later
199    const useWatchGuard = process.platform === "win32" && nodeVersion! >= 4;
200    const originalWatchDirectory: ServerHost["watchDirectory"] = sys.watchDirectory.bind(sys);
201    const logger = createLogger();
202
203    // enable deprecation logging
204    Debug.loggingHost = {
205        log(level, s) {
206            switch (level) {
207                case ts.LogLevel.Error:
208                case ts.LogLevel.Warning:
209                    return logger.msg(s, Msg.Err);
210                case ts.LogLevel.Info:
211                case ts.LogLevel.Verbose:
212                    return logger.msg(s, Msg.Info);
213            }
214        }
215    };
216
217    const pending = createQueue<Buffer>();
218    let canWrite = true;
219
220    if (useWatchGuard) {
221        const currentDrive = extractWatchDirectoryCacheKey(sys.resolvePath(sys.getCurrentDirectory()), /*currentDriveKey*/ undefined);
222        const statusCache = new Map<string, boolean>();
223        sys.watchDirectory = (path, callback, recursive, options) => {
224            const cacheKey = extractWatchDirectoryCacheKey(path, currentDrive);
225            let status = cacheKey && statusCache.get(cacheKey);
226            if (status === undefined) {
227                if (logger.hasLevel(LogLevel.verbose)) {
228                    logger.info(`${cacheKey} for path ${path} not found in cache...`);
229                }
230                try {
231                    const args = [combinePaths(libDirectory, "watchGuard.js"), path];
232                    if (logger.hasLevel(LogLevel.verbose)) {
233                        logger.info(`Starting ${process.execPath} with args:${stringifyIndented(args)}`);
234                    }
235                    childProcess.execFileSync(process.execPath, args, { stdio: "ignore", env: { ELECTRON_RUN_AS_NODE: "1" } });
236                    status = true;
237                    if (logger.hasLevel(LogLevel.verbose)) {
238                        logger.info(`WatchGuard for path ${path} returned: OK`);
239                    }
240                }
241                catch (e) {
242                    status = false;
243                    if (logger.hasLevel(LogLevel.verbose)) {
244                        logger.info(`WatchGuard for path ${path} returned: ${e.message}`);
245                    }
246                }
247                if (cacheKey) {
248                    statusCache.set(cacheKey, status);
249                }
250            }
251            else if (logger.hasLevel(LogLevel.verbose)) {
252                logger.info(`watchDirectory for ${path} uses cached drive information.`);
253            }
254            if (status) {
255                // this drive is safe to use - call real 'watchDirectory'
256                return watchDirectorySwallowingException(path, callback, recursive, options);
257            }
258            else {
259                // this drive is unsafe - return no-op watcher
260                return noopFileWatcher;
261            }
262        };
263    }
264    else {
265        sys.watchDirectory = watchDirectorySwallowingException;
266    }
267
268    // Override sys.write because fs.writeSync is not reliable on Node 4
269    sys.write = (s: string) => writeMessage(sys.bufferFrom!(s, "utf8") as globalThis.Buffer);
270
271    /* eslint-disable no-restricted-globals */
272    sys.setTimeout = setTimeout;
273    sys.clearTimeout = clearTimeout;
274    sys.setImmediate = setImmediate;
275    sys.clearImmediate = clearImmediate;
276    /* eslint-enable no-restricted-globals */
277
278    if (typeof global !== "undefined" && global.gc) {
279        sys.gc = () => global.gc?.();
280    }
281
282    sys.require = (initialDir: string, moduleName: string): ModuleImportResult => {
283        try {
284            return { module: require(resolveJSModule(moduleName, initialDir, sys)), error: undefined };
285        }
286        catch (error) {
287            return { module: undefined, error };
288        }
289    };
290
291    let cancellationToken: ServerCancellationToken;
292    try {
293        const factory = require("./cancellationToken");
294        cancellationToken = factory(sys.args);
295    }
296    catch (e) {
297        cancellationToken = nullCancellationToken;
298    }
299
300    const localeStr = findArgument("--locale");
301    if (localeStr) {
302        validateLocaleAndSetLanguage(localeStr, sys);
303    }
304
305    const modeOrUnknown = parseServerMode();
306    let serverMode: LanguageServiceMode | undefined;
307    let unknownServerMode: string | undefined;
308    if (modeOrUnknown !== undefined) {
309        if (typeof modeOrUnknown === "number") serverMode = modeOrUnknown;
310        else unknownServerMode = modeOrUnknown;
311    }
312    return {
313        args: process.argv,
314        logger,
315        cancellationToken,
316        serverMode,
317        unknownServerMode,
318        startSession: startNodeSession
319    };
320
321    // TSS_LOG "{ level: "normal | verbose | terse", file?: string}"
322    function createLogger() {
323        const cmdLineLogFileName = findArgument("--logFile");
324        const cmdLineVerbosity = getLogLevel(findArgument("--logVerbosity"));
325        const envLogOptions = parseLoggingEnvironmentString(process.env.TSS_LOG);
326
327        const unsubstitutedLogFileName = cmdLineLogFileName
328            ? stripQuotes(cmdLineLogFileName)
329            : envLogOptions.logToFile
330                ? envLogOptions.file || (libDirectory + "/.log" + process.pid.toString())
331                : undefined;
332
333        const substitutedLogFileName = unsubstitutedLogFileName
334            ? unsubstitutedLogFileName.replace("PID", process.pid.toString())
335            : undefined;
336
337        const logVerbosity = cmdLineVerbosity || envLogOptions.detailLevel;
338        return new Logger(substitutedLogFileName!, envLogOptions.traceToConsole!, logVerbosity!); // TODO: GH#18217
339    }
340
341    function writeMessage(buf: Buffer) {
342        if (!canWrite) {
343            pending.enqueue(buf);
344        }
345        else {
346            canWrite = false;
347            process.stdout.write(buf, setCanWriteFlagAndWriteMessageIfNecessary);
348        }
349    }
350
351    function setCanWriteFlagAndWriteMessageIfNecessary() {
352        canWrite = true;
353        if (!pending.isEmpty()) {
354            writeMessage(pending.dequeue());
355        }
356    }
357
358    function extractWatchDirectoryCacheKey(path: string, currentDriveKey: string | undefined) {
359        path = normalizeSlashes(path);
360        if (isUNCPath(path)) {
361            // UNC path: extract server name
362            // //server/location
363            //         ^ <- from 0 to this position
364            const firstSlash = path.indexOf(directorySeparator, 2);
365            return firstSlash !== -1 ? toFileNameLowerCase(path.substring(0, firstSlash)) : path;
366        }
367        const rootLength = getRootLength(path);
368        if (rootLength === 0) {
369            // relative path - assume file is on the current drive
370            return currentDriveKey;
371        }
372        if (path.charCodeAt(1) === CharacterCodes.colon && path.charCodeAt(2) === CharacterCodes.slash) {
373            // rooted path that starts with c:/... - extract drive letter
374            return toFileNameLowerCase(path.charAt(0));
375        }
376        if (path.charCodeAt(0) === CharacterCodes.slash && path.charCodeAt(1) !== CharacterCodes.slash) {
377            // rooted path that starts with slash - /somename - use key for current drive
378            return currentDriveKey;
379        }
380        // do not cache any other cases
381        return undefined;
382    }
383
384    function isUNCPath(s: string): boolean {
385        return s.length > 2 && s.charCodeAt(0) === CharacterCodes.slash && s.charCodeAt(1) === CharacterCodes.slash;
386    }
387
388    // This is the function that catches the exceptions when watching directory, and yet lets project service continue to function
389    // Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point
390    function watchDirectorySwallowingException(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher {
391        try {
392            return originalWatchDirectory(path, callback, recursive, options);
393        }
394        catch (e) {
395            logger.info(`Exception when creating directory watcher: ${e.message}`);
396            return noopFileWatcher;
397        }
398    }
399}
400
401function parseEventPort(eventPortStr: string | undefined) {
402    const eventPort = eventPortStr === undefined ? undefined : parseInt(eventPortStr);
403    return eventPort !== undefined && !isNaN(eventPort) ? eventPort : undefined;
404}
405
406function startNodeSession(options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken) {
407    const childProcess: {
408        fork(modulePath: string, args: string[], options?: { execArgv: string[], env?: MapLike<string> }): NodeChildProcess;
409    } = require("child_process");
410
411    const os: {
412        homedir?(): string;
413        tmpdir(): string;
414    } = require("os");
415
416    const net: {
417        connect(options: { port: number }, onConnect?: () => void): NodeSocket
418    } = require("net");
419
420    const readline: {
421        createInterface(options: ReadLineOptions): NodeJS.EventEmitter;
422    } = require("readline");
423
424    const rl = readline.createInterface({
425        input: process.stdin,
426        output: process.stdout,
427        terminal: false,
428    });
429
430    interface QueuedOperation {
431        operationId: string;
432        operation: () => void;
433    }
434
435    class NodeTypingsInstaller implements ITypingsInstaller {
436        private installer!: NodeChildProcess;
437        private projectService!: ProjectService;
438        private activeRequestCount = 0;
439        private requestQueue = createQueue<QueuedOperation>();
440        private requestMap = new Map<string, QueuedOperation>(); // Maps operation ID to newest requestQueue entry with that ID
441        /** We will lazily request the types registry on the first call to `isKnownTypesPackageName` and store it in `typesRegistryCache`. */
442        private requestedRegistry = false;
443        private typesRegistryCache: ESMap<string, MapLike<string>> | undefined;
444
445        // This number is essentially arbitrary.  Processing more than one typings request
446        // at a time makes sense, but having too many in the pipe results in a hang
447        // (see https://github.com/nodejs/node/issues/7657).
448        // It would be preferable to base our limit on the amount of space left in the
449        // buffer, but we have yet to find a way to retrieve that value.
450        private static readonly maxActiveRequestCount = 10;
451        private static readonly requestDelayMillis = 100;
452        private packageInstalledPromise: { resolve(value: ApplyCodeActionCommandResult): void, reject(reason: unknown): void } | undefined;
453
454        constructor(
455            private readonly telemetryEnabled: boolean,
456            private readonly logger: Logger,
457            private readonly host: ServerHost,
458            readonly globalTypingsCacheLocation: string,
459            readonly typingSafeListLocation: string,
460            readonly typesMapLocation: string,
461            private readonly npmLocation: string | undefined,
462            private readonly validateDefaultNpmLocation: boolean,
463            private event: server.Event) {
464        }
465
466        isKnownTypesPackageName(name: string): boolean {
467            // We want to avoid looking this up in the registry as that is expensive. So first check that it's actually an NPM package.
468            const validationResult = JsTyping.validatePackageName(name);
469            if (validationResult !== JsTyping.NameValidationResult.Ok) {
470                return false;
471            }
472
473            if (this.requestedRegistry) {
474                return !!this.typesRegistryCache && this.typesRegistryCache.has(name);
475            }
476
477            this.requestedRegistry = true;
478            this.send({ kind: "typesRegistry" });
479            return false;
480        }
481
482        installPackage(options: InstallPackageOptionsWithProject): Promise<ApplyCodeActionCommandResult> {
483            this.send<InstallPackageRequest>({ kind: "installPackage", ...options });
484            Debug.assert(this.packageInstalledPromise === undefined);
485            return new Promise<ApplyCodeActionCommandResult>((resolve, reject) => {
486                this.packageInstalledPromise = { resolve, reject };
487            });
488        }
489
490        attach(projectService: ProjectService) {
491            this.projectService = projectService;
492            if (this.logger.hasLevel(LogLevel.requestTime)) {
493                this.logger.info("Binding...");
494            }
495
496            const args: string[] = [Arguments.GlobalCacheLocation, this.globalTypingsCacheLocation];
497            if (this.telemetryEnabled) {
498                args.push(Arguments.EnableTelemetry);
499            }
500            if (this.logger.loggingEnabled() && this.logger.getLogFileName()) {
501                args.push(Arguments.LogFile, combinePaths(getDirectoryPath(normalizeSlashes(this.logger.getLogFileName()!)), `ti-${process.pid}.log`));
502            }
503            if (this.typingSafeListLocation) {
504                args.push(Arguments.TypingSafeListLocation, this.typingSafeListLocation);
505            }
506            if (this.typesMapLocation) {
507                args.push(Arguments.TypesMapLocation, this.typesMapLocation);
508            }
509            if (this.npmLocation) {
510                args.push(Arguments.NpmLocation, this.npmLocation);
511            }
512            if (this.validateDefaultNpmLocation) {
513                args.push(Arguments.ValidateDefaultNpmLocation);
514            }
515
516            const execArgv: string[] = [];
517            for (const arg of process.execArgv) {
518                const match = /^--((?:debug|inspect)(?:-brk)?)(?:=(\d+))?$/.exec(arg);
519                if (match) {
520                    // if port is specified - use port + 1
521                    // otherwise pick a default port depending on if 'debug' or 'inspect' and use its value + 1
522                    const currentPort = match[2] !== undefined
523                        ? +match[2]
524                        : match[1].charAt(0) === "d" ? 5858 : 9229;
525                    execArgv.push(`--${match[1]}=${currentPort + 1}`);
526                    break;
527                }
528            }
529
530            const typingsInstaller = combinePaths(getDirectoryPath(sys.getExecutingFilePath()), "typingsInstaller.js");
531            this.installer = childProcess.fork(typingsInstaller, args, { execArgv });
532            this.installer.on("message", m => this.handleMessage(m));
533
534            // We have to schedule this event to the next tick
535            // cause this fn will be called during
536            // new IOSession => super(which is Session) => new ProjectService => NodeTypingsInstaller.attach
537            // and if "event" is referencing "this" before super class is initialized, it will be a ReferenceError in ES6 class.
538            this.host.setImmediate(() => this.event({ pid: this.installer.pid }, "typingsInstallerPid"));
539
540            process.on("exit", () => {
541                this.installer.kill();
542            });
543        }
544
545        onProjectClosed(p: Project): void {
546            this.send({ projectName: p.getProjectName(), kind: "closeProject" });
547        }
548
549        private send<T extends TypingInstallerRequestUnion>(rq: T): void {
550            this.installer.send(rq);
551        }
552
553        enqueueInstallTypingsRequest(project: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>): void {
554            const request = createInstallTypingsRequest(project, typeAcquisition, unresolvedImports);
555            if (this.logger.hasLevel(LogLevel.verbose)) {
556                if (this.logger.hasLevel(LogLevel.verbose)) {
557                    this.logger.info(`Scheduling throttled operation:${stringifyIndented(request)}`);
558                }
559            }
560
561            const operationId = project.getProjectName();
562            const operation = () => {
563                if (this.logger.hasLevel(LogLevel.verbose)) {
564                    this.logger.info(`Sending request:${stringifyIndented(request)}`);
565                }
566                this.send(request);
567            };
568            const queuedRequest: QueuedOperation = { operationId, operation };
569
570            if (this.activeRequestCount < NodeTypingsInstaller.maxActiveRequestCount) {
571                this.scheduleRequest(queuedRequest);
572            }
573            else {
574                if (this.logger.hasLevel(LogLevel.verbose)) {
575                    this.logger.info(`Deferring request for: ${operationId}`);
576                }
577                this.requestQueue.enqueue(queuedRequest);
578                this.requestMap.set(operationId, queuedRequest);
579            }
580        }
581
582        private handleMessage(response: TypesRegistryResponse | PackageInstalledResponse | SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) {
583            if (this.logger.hasLevel(LogLevel.verbose)) {
584                this.logger.info(`Received response:${stringifyIndented(response)}`);
585            }
586
587            switch (response.kind) {
588                case EventTypesRegistry:
589                    this.typesRegistryCache = new Map(getEntries(response.typesRegistry));
590                    break;
591                case ActionPackageInstalled: {
592                    const { success, message } = response;
593                    if (success) {
594                        this.packageInstalledPromise!.resolve({ successMessage: message });
595                    }
596                    else {
597                        this.packageInstalledPromise!.reject(message);
598                    }
599                    this.packageInstalledPromise = undefined;
600
601                    this.projectService.updateTypingsForProject(response);
602
603                    // The behavior is the same as for setTypings, so send the same event.
604                    this.event(response, "setTypings");
605                    break;
606                }
607                case EventInitializationFailed: {
608                    const body: protocol.TypesInstallerInitializationFailedEventBody = {
609                        message: response.message
610                    };
611                    const eventName: protocol.TypesInstallerInitializationFailedEventName = "typesInstallerInitializationFailed";
612                    this.event(body, eventName);
613                    break;
614                }
615                case EventBeginInstallTypes: {
616                    const body: protocol.BeginInstallTypesEventBody = {
617                        eventId: response.eventId,
618                        packages: response.packagesToInstall,
619                    };
620                    const eventName: protocol.BeginInstallTypesEventName = "beginInstallTypes";
621                    this.event(body, eventName);
622                    break;
623                }
624                case EventEndInstallTypes: {
625                    if (this.telemetryEnabled) {
626                        const body: protocol.TypingsInstalledTelemetryEventBody = {
627                            telemetryEventName: "typingsInstalled",
628                            payload: {
629                                installedPackages: response.packagesToInstall.join(","),
630                                installSuccess: response.installSuccess,
631                                typingsInstallerVersion: response.typingsInstallerVersion
632                            }
633                        };
634                        const eventName: protocol.TelemetryEventName = "telemetry";
635                        this.event(body, eventName);
636                    }
637
638                    const body: protocol.EndInstallTypesEventBody = {
639                        eventId: response.eventId,
640                        packages: response.packagesToInstall,
641                        success: response.installSuccess,
642                    };
643                    const eventName: protocol.EndInstallTypesEventName = "endInstallTypes";
644                    this.event(body, eventName);
645                    break;
646                }
647                case ActionInvalidate: {
648                    this.projectService.updateTypingsForProject(response);
649                    break;
650                }
651                case ActionSet: {
652                    if (this.activeRequestCount > 0) {
653                        this.activeRequestCount--;
654                    }
655                    else {
656                        Debug.fail("Received too many responses");
657                    }
658
659                    while (!this.requestQueue.isEmpty()) {
660                        const queuedRequest = this.requestQueue.dequeue();
661                        if (this.requestMap.get(queuedRequest.operationId) === queuedRequest) {
662                            this.requestMap.delete(queuedRequest.operationId);
663                            this.scheduleRequest(queuedRequest);
664                            break;
665                        }
666
667                        if (this.logger.hasLevel(LogLevel.verbose)) {
668                            this.logger.info(`Skipping defunct request for: ${queuedRequest.operationId}`);
669                        }
670                    }
671
672                    this.projectService.updateTypingsForProject(response);
673
674                    this.event(response, "setTypings");
675
676                    break;
677                }
678                default:
679                    assertType<never>(response);
680            }
681        }
682
683        private scheduleRequest(request: QueuedOperation) {
684            if (this.logger.hasLevel(LogLevel.verbose)) {
685                this.logger.info(`Scheduling request for: ${request.operationId}`);
686            }
687            this.activeRequestCount++;
688            this.host.setTimeout(request.operation, NodeTypingsInstaller.requestDelayMillis);
689        }
690    }
691
692    class IOSession extends Session {
693        private eventPort: number | undefined;
694        private eventSocket: NodeSocket | undefined;
695        private socketEventQueue: { body: any, eventName: string }[] | undefined;
696        /** No longer needed if syntax target is es6 or above. Any access to "this" before initialized will be a runtime error. */
697        private constructed: boolean | undefined;
698
699        constructor() {
700            const event = (body: object, eventName: string) => {
701                this.event(body, eventName);
702            };
703
704            const host = sys as ServerHost;
705
706            const typingsInstaller = disableAutomaticTypingAcquisition
707                ? undefined
708                : new NodeTypingsInstaller(telemetryEnabled, logger, host, getGlobalTypingsCacheLocation(), typingSafeListLocation, typesMapLocation, npmLocation, validateDefaultNpmLocation, event);
709
710            super({
711                host,
712                cancellationToken,
713                ...options,
714                typingsInstaller: typingsInstaller || nullTypingsInstaller,
715                byteLength: Buffer.byteLength,
716                hrtime: process.hrtime,
717                logger,
718                canUseEvents: true,
719                typesMapLocation,
720            });
721
722            this.eventPort = eventPort;
723            if (this.canUseEvents && this.eventPort) {
724                const s = net.connect({ port: this.eventPort }, () => {
725                    this.eventSocket = s;
726                    if (this.socketEventQueue) {
727                        // flush queue.
728                        for (const event of this.socketEventQueue) {
729                            this.writeToEventSocket(event.body, event.eventName);
730                        }
731                        this.socketEventQueue = undefined;
732                    }
733                });
734            }
735
736            this.constructed = true;
737        }
738
739        event<T extends object>(body: T, eventName: string): void {
740            Debug.assert(!!this.constructed, "Should only call `IOSession.prototype.event` on an initialized IOSession");
741
742            if (this.canUseEvents && this.eventPort) {
743                if (!this.eventSocket) {
744                    if (this.logger.hasLevel(LogLevel.verbose)) {
745                        this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`);
746                    }
747                    (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName });
748                    return;
749                }
750                else {
751                    Debug.assert(this.socketEventQueue === undefined);
752                    this.writeToEventSocket(body, eventName);
753                }
754            }
755            else {
756                super.event(body, eventName);
757            }
758        }
759
760        private writeToEventSocket(body: object, eventName: string): void {
761            this.eventSocket!.write(formatMessage(toEvent(eventName, body), this.logger, this.byteLength, this.host.newLine), "utf8");
762        }
763
764        exit() {
765            this.logger.info("Exiting...");
766            this.projectService.closeLog();
767            tracing?.stopTracing();
768            process.exit(0);
769        }
770
771        listen() {
772            rl.on("line", (input: string) => {
773                const message = input.trim();
774                this.onMessage(message);
775            });
776
777            rl.on("close", () => {
778                this.exit();
779            });
780        }
781    }
782
783    class IpcIOSession extends IOSession {
784
785        protected writeMessage(msg: protocol.Message): void {
786            const verboseLogging = logger.hasLevel(LogLevel.verbose);
787            if (verboseLogging) {
788                const json = JSON.stringify(msg);
789                logger.info(`${msg.type}:${indent(json)}`);
790            }
791
792            process.send!(msg);
793        }
794
795        protected parseMessage(message: any): protocol.Request {
796            return message as protocol.Request;
797        }
798
799        protected toStringMessage(message: any) {
800            return JSON.stringify(message, undefined, 2);
801        }
802
803        public listen() {
804            process.on("message", (e: any) => {
805                this.onMessage(e);
806            });
807
808            process.on("disconnect", () => {
809                this.exit();
810            });
811        }
812    }
813
814    const eventPort: number | undefined = parseEventPort(findArgument("--eventPort"));
815    const typingSafeListLocation = findArgument(Arguments.TypingSafeListLocation)!; // TODO: GH#18217
816    const typesMapLocation = findArgument(Arguments.TypesMapLocation) || combinePaths(getDirectoryPath(sys.getExecutingFilePath()), "typesMap.json");
817    const npmLocation = findArgument(Arguments.NpmLocation);
818    const validateDefaultNpmLocation = hasArgument(Arguments.ValidateDefaultNpmLocation);
819    const disableAutomaticTypingAcquisition = hasArgument("--disableAutomaticTypingAcquisition");
820    const useNodeIpc = hasArgument("--useNodeIpc");
821    const telemetryEnabled = hasArgument(Arguments.EnableTelemetry);
822    const commandLineTraceDir = findArgument("--traceDirectory");
823    const traceDir = commandLineTraceDir
824        ? stripQuotes(commandLineTraceDir)
825        : process.env.TSS_TRACE;
826    if (traceDir) {
827        startTracing("server", traceDir);
828    }
829
830    const ioSession = useNodeIpc ? new IpcIOSession() : new IOSession();
831    process.on("uncaughtException", err => {
832        ioSession.logError(err, "unknown");
833    });
834    // See https://github.com/Microsoft/TypeScript/issues/11348
835    (process as any).noAsar = true;
836    // Start listening
837    ioSession.listen();
838
839    function getGlobalTypingsCacheLocation() {
840        switch (process.platform) {
841            case "win32": {
842                const basePath = process.env.LOCALAPPDATA ||
843                    process.env.APPDATA ||
844                    (os.homedir && os.homedir()) ||
845                    process.env.USERPROFILE ||
846                    (process.env.HOMEDRIVE && process.env.HOMEPATH && normalizeSlashes(process.env.HOMEDRIVE + process.env.HOMEPATH)) ||
847                    os.tmpdir();
848                return combinePaths(combinePaths(normalizeSlashes(basePath), "Microsoft/TypeScript"), versionMajorMinor);
849            }
850            case "openbsd":
851            case "freebsd":
852            case "netbsd":
853            case "darwin":
854            case "linux":
855            case "android": {
856                const cacheLocation = getNonWindowsCacheLocation(process.platform === "darwin");
857                return combinePaths(combinePaths(cacheLocation, "typescript"), versionMajorMinor);
858            }
859            default:
860                return Debug.fail(`unsupported platform '${process.platform}'`);
861        }
862    }
863
864    function getNonWindowsCacheLocation(platformIsDarwin: boolean) {
865        if (process.env.XDG_CACHE_HOME) {
866            return process.env.XDG_CACHE_HOME;
867        }
868        const usersDir = platformIsDarwin ? "Users" : "home";
869        const homePath = (os.homedir && os.homedir()) ||
870            process.env.HOME ||
871            ((process.env.LOGNAME || process.env.USER) && `/${usersDir}/${process.env.LOGNAME || process.env.USER}`) ||
872            os.tmpdir();
873        const cacheFolder = platformIsDarwin
874            ? "Library/Caches"
875            : ".cache";
876        return combinePaths(normalizeSlashes(homePath), cacheFolder);
877    }
878}
879