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