From 27060cdf98b23ee0cbf218c90cf2ccda745f1589 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 12 Oct 2016 17:16:47 -0700 Subject: [PATCH] use query to npm registry instead of 'npm view' --- .../unittests/tsserverProjectSystem.ts | 16 ++-- src/harness/unittests/typingsInstaller.ts | 28 +++---- .../typingsInstaller/nodeTypingsInstaller.ts | 80 +++++++++++++++---- .../typingsInstaller/typingsInstaller.ts | 29 +++---- 4 files changed, 99 insertions(+), 54 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 2a11e885f64..b12a8675613 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -18,10 +18,8 @@ namespace ts.projectSystem { export interface PostExecAction { readonly requestKind: TI.RequestKind; - readonly error: Error; - readonly stdout: string; - readonly stderr: string; - readonly callback: (err: Error, stdout: string, stderr: string) => void; + readonly success: boolean; + readonly callback: TI.RequestCompletedAction; } export function notImplemented(): any { @@ -53,7 +51,7 @@ namespace ts.projectSystem { export class TestTypingsInstaller extends TI.TypingsInstaller implements server.ITypingsInstaller { protected projectService: server.ProjectService; constructor(readonly globalTypingsCacheLocation: string, throttleLimit: number, readonly installTypingHost: server.ServerHost, log?: TI.Log) { - super(globalTypingsCacheLocation, "npm", safeList.path, throttleLimit, log); + super(globalTypingsCacheLocation, safeList.path, throttleLimit, log); this.init(); } @@ -64,7 +62,7 @@ namespace ts.projectSystem { const actionsToRun = this.postExecActions; this.postExecActions = []; for (const action of actionsToRun) { - action.callback(action.error, action.stdout, action.stderr); + action.callback(action.success); } } @@ -84,7 +82,7 @@ namespace ts.projectSystem { return this.installTypingHost; } - runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: (err: Error, stdout: string, stderr: string) => void): void { + executeRequest(requestKind: TI.RequestKind, requestId: number, args: string[], cwd: string, cb: TI.RequestCompletedAction): void { switch (requestKind) { case TI.NpmViewRequest: case TI.NpmInstallRequest: @@ -107,9 +105,7 @@ namespace ts.projectSystem { addPostExecAction(requestKind: TI.RequestKind, stdout: string | string[], cb: TI.RequestCompletedAction) { const out = typeof stdout === "string" ? stdout : createNpmPackageJsonString(stdout); const action: PostExecAction = { - error: undefined, - stdout: out, - stderr: "", + success: !!out, callback: cb, requestKind }; diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index d79a1a7723c..ebd0f0ef792 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -31,11 +31,11 @@ namespace ts.projectSystem { function executeCommand(self: Installer, host: TestServerHost, installedTypings: string[], typingFiles: FileOrFolder[], requestKind: TI.RequestKind, cb: TI.RequestCompletedAction): void { switch (requestKind) { case TI.NpmInstallRequest: - self.addPostExecAction(requestKind, installedTypings, (err, stdout, stderr) => { + self.addPostExecAction(requestKind, installedTypings, success => { for (const file of typingFiles) { host.createFileOrFolder(file, /*createParentDirectory*/ true); } - cb(err, stdout, stderr); + cb(success); }); break; case TI.NpmViewRequest: @@ -81,7 +81,7 @@ namespace ts.projectSystem { constructor() { super(host); } - runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + executeRequest(requestKind: TI.RequestKind, requestId: number, args: string[], cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { const installedTypings = ["@types/jquery"]; const typingFiles = [jquery]; executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); @@ -125,7 +125,7 @@ namespace ts.projectSystem { constructor() { super(host); } - runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + executeRequest(requestKind: TI.RequestKind, requestId: number, args: string[], cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { const installedTypings = ["@types/jquery"]; const typingFiles = [jquery]; executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); @@ -221,7 +221,7 @@ namespace ts.projectSystem { enqueueIsCalled = true; super.enqueueInstallTypingsRequest(project, typingOptions); } - runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: TI.RequestCompletedAction): void { + executeRequest(requestKind: TI.RequestKind, requestId: number, args: string[], cwd: string, cb: TI.RequestCompletedAction): void { const installedTypings = ["@types/jquery"]; const typingFiles = [jquery]; executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); @@ -275,7 +275,7 @@ namespace ts.projectSystem { constructor() { super(host); } - runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: TI.RequestCompletedAction): void { + executeRequest(requestKind: TI.RequestKind, requestId: number, args: string[], cwd: string, cb: TI.RequestCompletedAction): void { const installedTypings = ["@types/lodash", "@types/react"]; const typingFiles = [lodash, react]; executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); @@ -323,7 +323,7 @@ namespace ts.projectSystem { enqueueIsCalled = true; super.enqueueInstallTypingsRequest(project, typingOptions); } - runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: TI.RequestCompletedAction): void { + executeRequest(requestKind: TI.RequestKind, requestId: number, args: string[], cwd: string, cb: TI.RequestCompletedAction): void { const installedTypings: string[] = []; const typingFiles: FileOrFolder[] = []; executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); @@ -398,7 +398,7 @@ namespace ts.projectSystem { constructor() { super(host); } - runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: TI.RequestCompletedAction): void { + executeRequest(requestKind: TI.RequestKind, requestId: number, args: string[], cwd: string, cb: TI.RequestCompletedAction): void { const installedTypings = ["@types/commander", "@types/express", "@types/jquery", "@types/moment"]; const typingFiles = [commander, express, jquery, moment]; executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); @@ -477,7 +477,7 @@ namespace ts.projectSystem { constructor() { super(host, { throttleLimit: 3 }); } - runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: TI.RequestCompletedAction): void { + executeRequest(requestKind: TI.RequestKind, requestId: number, args: string[], cwd: string, cb: TI.RequestCompletedAction): void { const installedTypings = ["@types/commander", "@types/express", "@types/jquery", "@types/moment", "@types/lodash"]; executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); } @@ -567,10 +567,10 @@ namespace ts.projectSystem { constructor() { super(host, { throttleLimit: 3 }); } - runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: TI.RequestCompletedAction): void { + executeRequest(requestKind: TI.RequestKind, requestId: number, args: string[], cwd: string, cb: TI.RequestCompletedAction): void { if (requestKind === TI.NpmInstallRequest) { let typingFiles: (FileOrFolder & { typings: string}) [] = []; - if (command.indexOf("commander") >= 0) { + if (args.indexOf("@types/commander") >= 0) { typingFiles = [commander, jquery, lodash, cordova]; } else { @@ -655,7 +655,7 @@ namespace ts.projectSystem { constructor() { super(host, { globalTypingsCacheLocation: "/tmp" }); } - runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + executeRequest(requestKind: TI.RequestKind, requestId: number, args: string[], cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { const installedTypings = ["@types/jquery"]; const typingFiles = [jqueryDTS]; executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); @@ -701,7 +701,7 @@ namespace ts.projectSystem { constructor() { super(host, { globalTypingsCacheLocation: "/tmp" }); } - runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + executeRequest(requestKind: TI.RequestKind, requestId: number, args: string[], cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { const installedTypings = ["@types/jquery"]; const typingFiles = [jqueryDTS]; executeCommand(this, host, installedTypings, typingFiles, requestKind, cb); @@ -766,7 +766,7 @@ namespace ts.projectSystem { constructor() { super(host, { globalTypingsCacheLocation: "/tmp" }, { isEnabled: () => true, writeLine: msg => messages.push(msg) }); } - runCommand(requestKind: TI.RequestKind, requestId: number, command: string, cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + executeRequest(requestKind: TI.RequestKind, requestId: number, args: string[], cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { assert(false, "runCommand should not be invoked"); } })(); diff --git a/src/server/typingsInstaller/nodeTypingsInstaller.ts b/src/server/typingsInstaller/nodeTypingsInstaller.ts index b73a8e4e79d..044935670f2 100644 --- a/src/server/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/server/typingsInstaller/nodeTypingsInstaller.ts @@ -33,23 +33,38 @@ namespace ts.server.typingsInstaller { } } - export class NodeTypingsInstaller extends TypingsInstaller { - private readonly exec: { (command: string, options: { cwd: string }, callback?: (error: Error, stdout: string, stderr: string) => void): any }; + type HttpGet = { + (url: string, callback: (response: HttpResponse) => void): NodeJS.EventEmitter; + } + interface HttpResponse extends NodeJS.ReadableStream { + statusCode: number; + statusMessage: string; + destroy(): void; + } + + type Exec = { + (command: string, options: { cwd: string }, callback?: (error: Error, stdout: string, stderr: string) => void): any + } + + export class NodeTypingsInstaller extends TypingsInstaller { + private readonly exec: Exec; + private readonly httpGet: HttpGet; + private readonly npmPath: string; readonly installTypingHost: InstallTypingHost = sys; constructor(globalTypingsCacheLocation: string, throttleLimit: number, log: Log) { super( globalTypingsCacheLocation, - /*npmPath*/ getNPMLocation(process.argv[0]), toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), throttleLimit, log); if (this.log.isEnabled()) { this.log.writeLine(`Process id: ${process.pid}`); } - const { exec } = require("child_process"); - this.exec = exec; + this.npmPath = getNPMLocation(process.argv[0]); + this.exec = require("child_process").exec; + this.httpGet = require("http").get; } init() { @@ -75,17 +90,54 @@ namespace ts.server.typingsInstaller { } } - protected runCommand(requestKind: RequestKind, requestId: number, command: string, cwd: string, onRequestCompleted: RequestCompletedAction): void { + protected executeRequest(requestKind: RequestKind, requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { if (this.log.isEnabled()) { - this.log.writeLine(`#${requestId} running command '${command}'.`); + this.log.writeLine(`#${requestId} executing ${requestKind}, arguments'${JSON.stringify(args)}'.`); + } + switch (requestKind) { + case NpmViewRequest: { + // const command = `${self.npmPath} view @types/${typing} --silent name`; + // use http request to global npm registry instead of running npm view + Debug.assert(args.length === 1); + const url = `http://registry.npmjs.org/@types%2f${args[0]}`; + const start = Date.now(); + this.httpGet(url, response => { + let ok = false; + if (this.log.isEnabled()) { + this.log.writeLine(`${requestKind} #${requestId} request to ${url}:: status code ${response.statusCode}, status message '${response.statusMessage}', took ${Date.now() - start} ms`); + } + switch (response.statusCode) { + case 200: // OK + case 301: // redirect - Moved - treat package as present + case 302: // redirect - Found - treat package as present + ok = true; + break; + } + response.destroy(); + onRequestCompleted(ok); + }).on("error", (err: Error) => { + if (this.log.isEnabled()) { + this.log.writeLine(`${requestKind} #${requestId} query to npm registry failed with error ${err.message}, stack ${err.stack}`); + } + onRequestCompleted(/*success*/ false); + }); + } + break; + case NpmInstallRequest: { + const command = `${this.npmPath} install ${args.join(" ")} --save-dev`; + const start = Date.now(); + this.exec(command, { cwd }, (err, stdout, stderr) => { + if (this.log.isEnabled()) { + this.log.writeLine(`${requestKind} #${requestId} took: ${Date.now() - start} ms${sys.newLine}stdout: ${stdout}${sys.newLine}stderr: ${stderr}`); + } + // treat any output on stdout as success + onRequestCompleted(!!stdout); + }); + } + break; + default: + Debug.assert(false, `Unknown request kind ${requestKind}`); } - this.exec(command, { cwd }, (err, stdout, stderr) => { - if (this.log.isEnabled()) { - this.log.writeLine(`${requestKind} #${requestId} stdout: ${stdout}`); - this.log.writeLine(`${requestKind} #${requestId} stderr: ${stderr}`); - } - onRequestCompleted(err, stdout, stderr); - }); } } diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts index 0ed2ccce99d..f62f5fc4abe 100644 --- a/src/server/typingsInstaller/typingsInstaller.ts +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -65,11 +65,11 @@ namespace ts.server.typingsInstaller { export type RequestKind = typeof NpmViewRequest | typeof NpmInstallRequest; - export type RequestCompletedAction = (err: Error, stdout: string, stderr: string) => void; + export type RequestCompletedAction = (success: boolean) => void; type PendingRequest = { requestKind: RequestKind; requestId: number; - command: string; + args: string[]; cwd: string; onRequestCompleted: RequestCompletedAction }; @@ -88,7 +88,6 @@ namespace ts.server.typingsInstaller { constructor( readonly globalCachePath: string, - readonly npmPath: string, readonly safeListPath: Path, readonly throttleLimit: number, protected readonly log = nullLog) { @@ -331,13 +330,12 @@ namespace ts.server.typingsInstaller { let execInstallCmdCount = 0; const filteredTypings: string[] = []; for (const typing of typingsToInstall) { - execNpmViewTyping(this, typing); + filterExistingTypings(this, typing); } - function execNpmViewTyping(self: TypingsInstaller, typing: string) { - const command = `${self.npmPath} view @types/${typing} --silent name`; - self.execAsync(NpmViewRequest, requestId, command, cachePath, (err, stdout, stderr) => { - if (stdout) { + function filterExistingTypings(self: TypingsInstaller, typing: string) { + self.execAsync(NpmViewRequest, requestId, [typing], cachePath, ok => { + if (ok) { filteredTypings.push(typing); } execInstallCmdCount++; @@ -353,9 +351,8 @@ namespace ts.server.typingsInstaller { return; } const scopedTypings = filteredTypings.map(t => "@types/" + t); - const command = `${self.npmPath} install ${scopedTypings.join(" ")} --save-dev`; - self.execAsync(NpmInstallRequest, requestId, command, cachePath, (err, stdout, stderr) => { - postInstallAction(stdout ? scopedTypings : []); + self.execAsync(NpmInstallRequest, requestId, scopedTypings, cachePath, ok => { + postInstallAction(ok ? scopedTypings : []); }); } } @@ -403,8 +400,8 @@ namespace ts.server.typingsInstaller { }; } - private execAsync(requestKind: RequestKind, requestId: number, command: string, cwd: string, onRequestCompleted: RequestCompletedAction): void { - this.pendingRunRequests.unshift({ requestKind, requestId, command, cwd, onRequestCompleted }); + private execAsync(requestKind: RequestKind, requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { + this.pendingRunRequests.unshift({ requestKind, requestId, args, cwd, onRequestCompleted }); this.executeWithThrottling(); } @@ -412,15 +409,15 @@ namespace ts.server.typingsInstaller { while (this.inFlightRequestCount < this.throttleLimit && this.pendingRunRequests.length) { this.inFlightRequestCount++; const request = this.pendingRunRequests.pop(); - this.runCommand(request.requestKind, request.requestId, request.command, request.cwd, (err, stdout, stderr) => { + this.executeRequest(request.requestKind, request.requestId, request.args, request.cwd, ok => { this.inFlightRequestCount--; - request.onRequestCompleted(err, stdout, stderr); + request.onRequestCompleted(ok); this.executeWithThrottling(); }); } } - protected abstract runCommand(requestKind: RequestKind, requestId: number, command: string, cwd: string, onRequestCompleted: RequestCompletedAction): void; + protected abstract executeRequest(requestKind: RequestKind, requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void; protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings): void; } } \ No newline at end of file