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