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