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