diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index a0a6ac7f01e04..368d2a660d4e5 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -51,9 +51,8 @@ namespace ts.projectSystem { throttleLimit: number, installTypingHost: server.ServerHost, readonly typesRegistry = createMap(), - telemetryEnabled?: boolean, log?: TI.Log) { - super(installTypingHost, globalTypingsCacheLocation, safeList.path, throttleLimit, telemetryEnabled, log); + super(installTypingHost, globalTypingsCacheLocation, safeList.path, throttleLimit, log); } safeFileList = safeList.path; diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index aea9f4eb9ed1c..62d2f7ac80146 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -20,13 +20,12 @@ namespace ts.projectSystem { } class Installer extends TestTypingsInstaller { - constructor(host: server.ServerHost, p?: InstallerParams, telemetryEnabled?: boolean, log?: TI.Log) { + constructor(host: server.ServerHost, p?: InstallerParams, log?: TI.Log) { super( (p && p.globalTypingsCacheLocation) || "/a/data", (p && p.throttleLimit) || 5, host, (p && p.typesRegistry), - telemetryEnabled, log); } @@ -36,7 +35,7 @@ namespace ts.projectSystem { } } - function executeCommand(self: Installer, host: TestServerHost, installedTypings: string[], typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void { + function executeCommand(self: Installer, host: TestServerHost, installedTypings: string[] | string, typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void { self.addPostExecAction(installedTypings, success => { for (const file of typingFiles) { host.createFileOrFolder(file, /*createParentDirectory*/ true); @@ -907,7 +906,7 @@ namespace ts.projectSystem { const host = createServerHost([f1, packageJson]); const installer = new (class extends Installer { constructor() { - super(host, { globalTypingsCacheLocation: "/tmp" }, /*telemetryEnabled*/ false, { isEnabled: () => true, writeLine: msg => messages.push(msg) }); + super(host, { globalTypingsCacheLocation: "/tmp" }, { isEnabled: () => true, writeLine: msg => messages.push(msg) }); } installWorker(_requestId: number, _args: string[], _cwd: string, _cb: server.typingsInstaller.RequestCompletedAction) { assert(false, "runCommand should not be invoked"); @@ -971,15 +970,18 @@ namespace ts.projectSystem { let seenTelemetryEvent = false; const installer = new (class extends Installer { constructor() { - super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }, /*telemetryEnabled*/ true); + super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }); } installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { const installedTypings = ["@types/commander"]; const typingFiles = [commander]; executeCommand(this, host, installedTypings, typingFiles, cb); } - sendResponse(response: server.SetTypings | server.InvalidateCachedTypings | server.TypingsInstallEvent) { - if (response.kind === server.EventInstall) { + sendResponse(response: server.SetTypings | server.InvalidateCachedTypings | server.BeginInstallTypes | server.EndInstallTypes) { + if (response.kind === server.EventBeginInstallTypes) { + return; + } + if (response.kind === server.EventEndInstallTypes) { assert.deepEqual(response.packagesToInstall, ["@types/commander"]); seenTelemetryEvent = true; return; @@ -997,4 +999,102 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.inferredProjects[0], [f1.path, commander.path]); }); }); + + describe("progress notifications", () => { + it ("should be sent for success", () => { + const f1 = { + path: "/a/app.js", + content: "" + }; + const package = { + path: "/a/package.json", + content: JSON.stringify({ dependencies: { "commander": "1.0.0" } }) + }; + const cachePath = "/a/cache/"; + const commander = { + path: cachePath + "node_modules/@types/commander/index.d.ts", + content: "export let x: number" + }; + const host = createServerHost([f1, package]); + let beginEvent: server.BeginInstallTypes; + let endEvent: server.EndInstallTypes; + const installer = new (class extends Installer { + constructor() { + super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }); + } + installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + const installedTypings = ["@types/commander"]; + const typingFiles = [commander]; + executeCommand(this, host, installedTypings, typingFiles, cb); + } + sendResponse(response: server.SetTypings | server.InvalidateCachedTypings | server.BeginInstallTypes | server.EndInstallTypes) { + if (response.kind === server.EventBeginInstallTypes) { + beginEvent = response; + return; + } + if (response.kind === server.EventEndInstallTypes) { + endEvent = response; + return; + } + super.sendResponse(response); + } + })(); + const projectService = createProjectService(host, { typingsInstaller: installer }); + projectService.openClientFile(f1.path); + + installer.installAll(/*expectedCount*/ 1); + + assert.isTrue(!!beginEvent); + assert.isTrue(!!endEvent); + assert.isTrue(beginEvent.eventId === endEvent.eventId); + assert.isTrue(endEvent.installSuccess); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [f1.path, commander.path]); + }); + + it ("should be sent for error", () => { + const f1 = { + path: "/a/app.js", + content: "" + }; + const package = { + path: "/a/package.json", + content: JSON.stringify({ dependencies: { "commander": "1.0.0" } }) + }; + const cachePath = "/a/cache/"; + const host = createServerHost([f1, package]); + let beginEvent: server.BeginInstallTypes; + let endEvent: server.EndInstallTypes; + const installer = new (class extends Installer { + constructor() { + super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }); + } + installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) { + executeCommand(this, host, "", [], cb); + } + sendResponse(response: server.SetTypings | server.InvalidateCachedTypings | server.BeginInstallTypes | server.EndInstallTypes) { + if (response.kind === server.EventBeginInstallTypes) { + beginEvent = response; + return; + } + if (response.kind === server.EventEndInstallTypes) { + endEvent = response; + return; + } + super.sendResponse(response); + } + })(); + const projectService = createProjectService(host, { typingsInstaller: installer }); + projectService.openClientFile(f1.path); + + installer.installAll(/*expectedCount*/ 1); + + assert.isTrue(!!beginEvent); + assert.isTrue(!!endEvent); + assert.isTrue(beginEvent.eventId === endEvent.eventId); + assert.isFalse(endEvent.installSuccess); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [f1.path]); + }); + }); } \ No newline at end of file diff --git a/src/server/protocol.ts b/src/server/protocol.ts index e1735f768e42b..1e29b0291040e 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2117,6 +2117,40 @@ namespace ts.server.protocol { typingsInstallerVersion: string; } + export type BeginInstallTypesEventName = "beginInstallTypes"; + export type EndInstallTypesEventName = "endInstallTypes"; + + export interface BeginInstallTypesEvent extends Event { + event: BeginInstallTypesEventName; + body: BeginInstallTypesEventBody; + } + + export interface EndInstallTypesEvent extends Event { + event: EndInstallTypesEventName; + body: EndInstallTypesEventBody; + } + + export interface InstallTypesEventBody { + /** + * correlation id to match begin and end events + */ + eventId: number; + /** + * list of packages to install + */ + packages: ReadonlyArray; + } + + export interface BeginInstallTypesEventBody extends InstallTypesEventBody { + } + + export interface EndInstallTypesEventBody extends InstallTypesEventBody { + /** + * true if installation succeeded, otherwise false + */ + success: boolean; + } + export interface NavBarResponse extends Response { body?: NavigationBarItem[]; } diff --git a/src/server/server.ts b/src/server/server.ts index d5ccea4cd3251..7367741b9ea21 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -198,7 +198,7 @@ namespace ts.server { private socket: NodeSocket; private projectService: ProjectService; private throttledOperations: ThrottledOperations; - private telemetrySender: EventSender; + private eventSender: EventSender; constructor( private readonly telemetryEnabled: boolean, @@ -231,7 +231,7 @@ namespace ts.server { } setTelemetrySender(telemetrySender: EventSender) { - this.telemetrySender = telemetrySender; + this.eventSender = telemetrySender; } attach(projectService: ProjectService) { @@ -291,12 +291,30 @@ namespace ts.server { }); } - private handleMessage(response: SetTypings | InvalidateCachedTypings | TypingsInstallEvent) { + private handleMessage(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes) { if (this.logger.hasLevel(LogLevel.verbose)) { this.logger.info(`Received response: ${JSON.stringify(response)}`); } - if (response.kind === EventInstall) { - if (this.telemetrySender) { + + if (response.kind === EventBeginInstallTypes) { + if (!this.eventSender) { + return; + } + const body: protocol.BeginInstallTypesEventBody = { + eventId: response.eventId, + packages: response.packagesToInstall, + }; + const eventName: protocol.BeginInstallTypesEventName = "beginInstallTypes"; + this.eventSender.event(body, eventName); + + return; + } + + if (response.kind === EventEndInstallTypes) { + if (!this.eventSender) { + return; + } + if (this.telemetryEnabled) { const body: protocol.TypingsInstalledTelemetryEventBody = { telemetryEventName: "typingsInstalled", payload: { @@ -306,10 +324,19 @@ namespace ts.server { } }; const eventName: protocol.TelemetryEventName = "telemetry"; - this.telemetrySender.event(body, eventName); + this.eventSender.event(body, eventName); } + + const body: protocol.EndInstallTypesEventBody = { + eventId: response.eventId, + packages: response.packagesToInstall, + success: response.installSuccess, + }; + const eventName: protocol.EndInstallTypesEventName = "endInstallTypes"; + this.eventSender.event(body, eventName); return; } + this.projectService.updateTypingsForProject(response); if (response.kind == ActionSet && this.socket) { this.sendEvent(0, "setTypings", response); diff --git a/src/server/shared.ts b/src/server/shared.ts index 81a1f7fb55bc2..c56d4098e750e 100644 --- a/src/server/shared.ts +++ b/src/server/shared.ts @@ -3,7 +3,8 @@ namespace ts.server { export const ActionSet: ActionSet = "action::set"; export const ActionInvalidate: ActionInvalidate = "action::invalidate"; - export const EventInstall: EventInstall = "event::install"; + export const EventBeginInstallTypes: EventBeginInstallTypes = "event::beginInstallTypes"; + export const EventEndInstallTypes: EventEndInstallTypes = "event::endInstallTypes"; export namespace Arguments { export const GlobalCacheLocation = "--globalTypingsCacheLocation"; diff --git a/src/server/types.d.ts b/src/server/types.d.ts index 77e9e762b5996..9f53fa8def1a4 100644 --- a/src/server/types.d.ts +++ b/src/server/types.d.ts @@ -43,10 +43,11 @@ declare namespace ts.server { export type ActionSet = "action::set"; export type ActionInvalidate = "action::invalidate"; - export type EventInstall = "event::install"; + export type EventBeginInstallTypes = "event::beginInstallTypes"; + export type EventEndInstallTypes = "event::endInstallTypes"; export interface TypingInstallerResponse { - readonly kind: ActionSet | ActionInvalidate | EventInstall; + readonly kind: ActionSet | ActionInvalidate | EventBeginInstallTypes | EventEndInstallTypes; } export interface ProjectResponse extends TypingInstallerResponse { @@ -65,11 +66,20 @@ declare namespace ts.server { readonly kind: ActionInvalidate; } - export interface TypingsInstallEvent extends TypingInstallerResponse { + export interface InstallTypes extends ProjectResponse { + readonly kind: EventBeginInstallTypes | EventEndInstallTypes; + readonly eventId: number; + readonly typingsInstallerVersion: string; readonly packagesToInstall: ReadonlyArray; - readonly kind: EventInstall; + } + + export interface BeginInstallTypes extends InstallTypes { + readonly kind: EventBeginInstallTypes; + } + + export interface EndInstallTypes extends InstallTypes { + readonly kind: EventEndInstallTypes; readonly installSuccess: boolean; - readonly typingsInstallerVersion: string; } export interface InstallTypingHost extends JsTyping.TypingResolutionHost { diff --git a/src/server/typingsInstaller/nodeTypingsInstaller.ts b/src/server/typingsInstaller/nodeTypingsInstaller.ts index 235c2f16dad73..19f794ad57ca2 100644 --- a/src/server/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/server/typingsInstaller/nodeTypingsInstaller.ts @@ -70,13 +70,12 @@ namespace ts.server.typingsInstaller { private readonly npmPath: string; readonly typesRegistry: Map; - constructor(globalTypingsCacheLocation: string, throttleLimit: number, telemetryEnabled: boolean, log: Log) { + constructor(globalTypingsCacheLocation: string, throttleLimit: number, log: Log) { super( sys, globalTypingsCacheLocation, toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)), throttleLimit, - telemetryEnabled, log); if (this.log.isEnabled()) { this.log.writeLine(`Process id: ${process.pid}`); @@ -113,7 +112,7 @@ namespace ts.server.typingsInstaller { }); } - protected sendResponse(response: SetTypings | InvalidateCachedTypings) { + protected sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes) { if (this.log.isEnabled()) { this.log.writeLine(`Sending response: ${JSON.stringify(response)}`); } @@ -149,7 +148,6 @@ namespace ts.server.typingsInstaller { const logFilePath = findArgument(server.Arguments.LogFile); const globalTypingsCacheLocation = findArgument(server.Arguments.GlobalCacheLocation); - const telemetryEnabled = hasArgument(server.Arguments.EnableTelemetry); const log = new FileLog(logFilePath); if (log.isEnabled()) { @@ -163,6 +161,6 @@ namespace ts.server.typingsInstaller { } process.exit(0); }); - const installer = new NodeTypingsInstaller(globalTypingsCacheLocation, /*throttleLimit*/5, telemetryEnabled, log); + const installer = new NodeTypingsInstaller(globalTypingsCacheLocation, /*throttleLimit*/5, log); installer.listen(); } \ No newline at end of file diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts index 25d53e14e755d..7a09c1f6c216d 100644 --- a/src/server/typingsInstaller/typingsInstaller.ts +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -97,7 +97,6 @@ namespace ts.server.typingsInstaller { readonly globalCachePath: string, readonly safeListPath: Path, readonly throttleLimit: number, - readonly telemetryEnabled: boolean, protected readonly log = nullLog) { if (this.log.isEnabled()) { this.log.writeLine(`Global cache location '${globalCachePath}', safe file path '${safeListPath}'`); @@ -309,47 +308,58 @@ namespace ts.server.typingsInstaller { const requestId = this.installRunCount; this.installRunCount++; + // send progress event + this.sendResponse({ + kind: EventBeginInstallTypes, + eventId: requestId, + typingsInstallerVersion: ts.version, // qualified explicitly to prevent occasional shadowing + projectName: req.projectName + }); + this.installTypingsAsync(requestId, scopedTypings, cachePath, ok => { - if (this.telemetryEnabled) { - this.sendResponse({ - kind: EventInstall, - packagesToInstall: scopedTypings, - installSuccess: ok, - typingsInstallerVersion: ts.version // qualified explicitly to prevent occasional shadowing - }); - } + try { + if (!ok) { + if (this.log.isEnabled()) { + this.log.writeLine(`install request failed, marking packages as missing to prevent repeated requests: ${JSON.stringify(filteredTypings)}`); + } + for (const typing of filteredTypings) { + this.missingTypingsSet[typing] = true; + } + return; + } - if (!ok) { + // TODO: watch project directory if (this.log.isEnabled()) { - this.log.writeLine(`install request failed, marking packages as missing to prevent repeated requests: ${JSON.stringify(filteredTypings)}`); - } - for (const typing of filteredTypings) { - this.missingTypingsSet[typing] = true; + this.log.writeLine(`Installed typings ${JSON.stringify(scopedTypings)}`); } - return; - } - - // TODO: watch project directory - if (this.log.isEnabled()) { - this.log.writeLine(`Installed typings ${JSON.stringify(scopedTypings)}`); - } - const installedTypingFiles: string[] = []; - for (const packageName of filteredTypings) { - const typingFile = typingToFileName(cachePath, packageName, this.installTypingHost, this.log); - if (!typingFile) { - this.missingTypingsSet[packageName] = true; - continue; + const installedTypingFiles: string[] = []; + for (const packageName of filteredTypings) { + const typingFile = typingToFileName(cachePath, packageName, this.installTypingHost, this.log); + if (!typingFile) { + this.missingTypingsSet[packageName] = true; + continue; + } + if (!this.packageNameToTypingLocation[packageName]) { + this.packageNameToTypingLocation[packageName] = typingFile; + } + installedTypingFiles.push(typingFile); } - if (!this.packageNameToTypingLocation[packageName]) { - this.packageNameToTypingLocation[packageName] = typingFile; + if (this.log.isEnabled()) { + this.log.writeLine(`Installed typing files ${JSON.stringify(installedTypingFiles)}`); } - installedTypingFiles.push(typingFile); + + this.sendResponse(this.createSetTypings(req, currentlyCachedTypings.concat(installedTypingFiles))); } - if (this.log.isEnabled()) { - this.log.writeLine(`Installed typing files ${JSON.stringify(installedTypingFiles)}`); + finally { + this.sendResponse({ + kind: EventEndInstallTypes, + eventId: requestId, + projectName: req.projectName, + packagesToInstall: scopedTypings, + installSuccess: ok, + typingsInstallerVersion: ts.version // qualified explicitly to prevent occasional shadowing + }); } - - this.sendResponse(this.createSetTypings(req, currentlyCachedTypings.concat(installedTypingFiles))); }); } @@ -417,6 +427,6 @@ namespace ts.server.typingsInstaller { } protected abstract installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void; - protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings | TypingsInstallEvent): void; + protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes): void; } } \ No newline at end of file