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