From ccd01c49b9ea1af6aafd25ff05c03768aeb85b1a Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 2 Mar 2020 14:39:12 -0600 Subject: [PATCH] Integrate update notifications into VS Code --- ci/vscode.patch | 72 ++++++++++++++++++++++++++++++++--- src/browser/pages/app.html | 2 +- src/browser/pages/error.html | 4 +- src/browser/pages/home.html | 2 +- src/browser/pages/login.html | 2 +- src/browser/pages/update.css | 14 +++++++ src/browser/pages/update.html | 8 ++-- src/node/app/api.ts | 3 ++ src/node/app/app.ts | 9 +++-- src/node/app/dashboard.ts | 16 ++++---- src/node/app/login.ts | 11 +----- src/node/app/static.ts | 5 --- src/node/app/update.ts | 67 +++++++++++++++++++------------- src/node/http.ts | 17 ++++++--- test/update.test.ts | 4 +- 15 files changed, 161 insertions(+), 75 deletions(-) diff --git a/ci/vscode.patch b/ci/vscode.patch index 54a3a759..d0192456 100644 --- a/ci/vscode.patch +++ b/ci/vscode.patch @@ -444,10 +444,10 @@ index d0f6e6b18a..1966fd297d 100644 - diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts new file mode 100644 -index 0000000000..3a62205b38 +index 0000000000..1e6bca3b52 --- /dev/null +++ b/src/vs/server/browser/client.ts -@@ -0,0 +1,162 @@ +@@ -0,0 +1,224 @@ +import { Emitter } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; @@ -455,6 +455,7 @@ index 0000000000..3a62205b38 +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; ++import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; @@ -575,6 +576,67 @@ index 0000000000..3a62205b38 + } + }); + } ++ ++ const applyUpdate = async (): Promise => { ++ (services.get(ILogService) as ILogService).debug("Applying update..."); ++ ++ const response = await fetch("./update/apply", { ++ headers: { "content-type": "application/json" }, ++ }); ++ if (response.status !== 200) { ++ throw new Error("Unexpected response"); ++ } ++ ++ const json = await response.json(); ++ if (!json.isLatest) { ++ throw new Error("Update failed"); ++ } ++ ++ (services.get(INotificationService) as INotificationService).info(`Updated to ${json.version}`); ++ }; ++ ++ const getUpdate = async (): Promise => { ++ (services.get(ILogService) as ILogService).debug("Checking for update..."); ++ ++ const response = await fetch("./update", { ++ headers: { "content-type": "application/json" }, ++ }); ++ if (response.status !== 200) { ++ throw new Error("unexpected response"); ++ } ++ ++ const json = await response.json(); ++ if (json.isLatest) { ++ return; ++ } ++ ++ (services.get(INotificationService) as INotificationService).notify({ ++ severity: Severity.Info, ++ message: `code-server has an update: ${json.version}`, ++ actions: { ++ primary: [{ ++ id: 'update', ++ label: 'Apply Update', ++ tooltip: '', ++ class: undefined, ++ enabled: true, ++ checked: true, ++ dispose: () => undefined, ++ run: applyUpdate, ++ }], ++ } ++ }); ++ }; ++ ++ const updateLoop = (): void => { ++ getUpdate().catch((error) => { ++ (services.get(ILogService) as ILogService).warn(error); ++ }).finally(() => { ++ setTimeout(updateLoop, 300000); ++ }); ++ }; ++ ++ updateLoop(); +}; + +export interface Query { @@ -2968,7 +3030,7 @@ index bbb72e9511..0785d3391d 100644 -registerSingleton(IExtensionStoragePaths, class extends NotImplementedProxy(IExtensionStoragePaths) { whenReady = Promise.resolve(); }); +registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths); diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts -index 79455414c0..a407593b4d 100644 +index 79455414c0..8931c1355a 100644 --- a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts @@ -14,7 +14,11 @@ @@ -2978,8 +3040,8 @@ index 79455414c0..a407593b4d 100644 - catchError: true + catchError: true, + paths: { -+ '@coder/node-browser': `../node_modules/@coder/node-browser/out/client/client.js`, -+ '@coder/requirefs': `../node_modules/@coder/requirefs/out/requirefs.js`, ++ '@coder/node-browser': `{{BASE}}/static/{{COMMIT}}/lib/vscode/node_modules/@coder/node-browser/out/client/client.js`, ++ '@coder/requirefs': `{{BASE}}/static/{{COMMIT}}/lib/vscode/node_modules/@coder/requirefs/out/requirefs.js`, + } }); diff --git a/src/browser/pages/app.html b/src/browser/pages/app.html index 616e303c..bfd58737 100644 --- a/src/browser/pages/app.html +++ b/src/browser/pages/app.html @@ -15,7 +15,7 @@ crossorigin="use-credentials" /> - + diff --git a/src/browser/pages/error.html b/src/browser/pages/error.html index 83dfcbaa..0e389221 100644 --- a/src/browser/pages/error.html +++ b/src/browser/pages/error.html @@ -15,7 +15,7 @@ crossorigin="use-credentials" /> - + @@ -26,7 +26,7 @@ {{ERROR_BODY}} diff --git a/src/browser/pages/home.html b/src/browser/pages/home.html index 3a06ec25..ef2eff14 100644 --- a/src/browser/pages/home.html +++ b/src/browser/pages/home.html @@ -15,7 +15,7 @@ crossorigin="use-credentials" /> - + diff --git a/src/browser/pages/login.html b/src/browser/pages/login.html index 9a98d012..68a339db 100644 --- a/src/browser/pages/login.html +++ b/src/browser/pages/login.html @@ -18,7 +18,7 @@ crossorigin="use-credentials" /> - + diff --git a/src/browser/pages/update.css b/src/browser/pages/update.css index 9a7c8632..eaa8feec 100644 --- a/src/browser/pages/update.css +++ b/src/browser/pages/update.css @@ -10,3 +10,17 @@ color: red; margin-top: 16px; } + +.update-form > .links { + margin-top: 20px; +} + +.update-form > .links > .link { + color: rgb(87, 114, 245); + text-align: center; + text-decoration: none; +} + +.update-form > .links > .link:hover { + text-decoration: underline; +} diff --git a/src/browser/pages/update.html b/src/browser/pages/update.html index b55e4791..bcac2caa 100644 --- a/src/browser/pages/update.html +++ b/src/browser/pages/update.html @@ -15,7 +15,7 @@ crossorigin="use-credentials" /> - + @@ -26,9 +26,11 @@
Update code-server.
-
+ {{UPDATE_STATUS}} {{ERROR}} - Cancel +
diff --git a/src/node/app/api.ts b/src/node/app/api.ts index ab6da9ac..6615df1e 100644 --- a/src/node/app/api.ts +++ b/src/node/app/api.ts @@ -59,6 +59,9 @@ export class ApiHttpProvider extends HttpProvider { public async handleRequest(route: Route, request: http.IncomingMessage): Promise { this.ensureAuthenticated(request) + if (route.requestPath !== "/index.html") { + throw new HttpError("Not found", HttpCode.NotFound) + } switch (route.base) { case ApiEndpoint.applications: diff --git a/src/node/app/app.ts b/src/node/app/app.ts index c115f7e8..9de8ade2 100644 --- a/src/node/app/app.ts +++ b/src/node/app/app.ts @@ -16,6 +16,11 @@ export class AppHttpProvider extends HttpProvider { return { redirect: "/login", query: { to: route.fullPath } } } + this.ensureMethod(request) + if (route.requestPath !== "/index.html") { + throw new HttpError("Not found", HttpCode.NotFound) + } + // Run an existing app, but if it doesn't exist go ahead and start it. let app = this.api.getRunningApplication(route.base) let sessionId = app && app.sessionId @@ -38,8 +43,4 @@ export class AppHttpProvider extends HttpProvider { response.content = response.content.replace(/{{APP_NAME}}/, name) return this.replaceTemplates(route, response, sessionId) } - - public async handleWebSocket(): Promise { - throw new HttpError("Not found", HttpCode.NotFound) - } } diff --git a/src/node/app/dashboard.ts b/src/node/app/dashboard.ts index 106834a0..f8b4df40 100644 --- a/src/node/app/dashboard.ts +++ b/src/node/app/dashboard.ts @@ -20,10 +20,14 @@ export class DashboardHttpProvider extends HttpProvider { } public async handleRequest(route: Route, request: http.IncomingMessage): Promise { + if (route.requestPath !== "/index.html") { + throw new HttpError("Not found", HttpCode.NotFound) + } + switch (route.base) { case "/delete": { - this.ensureMethod(request, "POST") this.ensureAuthenticated(request) + this.ensureMethod(request, "POST") const data = await this.getData(request) const p = data ? querystring.parse(data) : {} this.api.deleteSession(p.sessionId as string) @@ -32,9 +36,7 @@ export class DashboardHttpProvider extends HttpProvider { case "/": { this.ensureMethod(request) - if (route.requestPath !== "/index.html") { - throw new HttpError("Not found", HttpCode.NotFound) - } else if (!this.authenticated(request)) { + if (!this.authenticated(request)) { return { redirect: "/login", query: { to: this.options.base } } } return this.getRoot(route) @@ -69,10 +71,6 @@ export class DashboardHttpProvider extends HttpProvider { return this.replaceTemplates(route, response) } - public async handleWebSocket(): Promise { - throw new HttpError("Not found", HttpCode.NotFound) - } - private getRecentProjectRows(base: string, recents: RecentResponse): string { return recents.paths.length > 0 || recents.workspaces.length > 0 ? recents.paths.map((recent) => this.getRecentProjectRow(base, recent)).join("\n") + @@ -151,7 +149,7 @@ export class DashboardHttpProvider extends HttpProvider {
${humanize(update.checked)} - Update now + Update now
Current: ${this.update.currentVersion}
` diff --git a/src/node/app/login.ts b/src/node/app/login.ts index 14bc4d7c..598b13ab 100644 --- a/src/node/app/login.ts +++ b/src/node/app/login.ts @@ -18,17 +18,14 @@ interface LoginPayload { */ export class LoginHttpProvider extends HttpProvider { public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - if (this.options.auth !== AuthType.Password) { + if (this.options.auth !== AuthType.Password || route.requestPath !== "/index.html") { throw new HttpError("Not found", HttpCode.NotFound) } switch (route.base) { case "/": - if (route.requestPath !== "/index.html") { - throw new HttpError("Not found", HttpCode.NotFound) - } - switch (request.method) { case "POST": + this.ensureMethod(request, ["GET", "POST"]) return this.tryLogin(route, request) default: this.ensureMethod(request) @@ -110,8 +107,4 @@ export class LoginHttpProvider extends HttpProvider { throw new Error("Missing password") } - - public async handleWebSocket(): Promise { - return undefined - } } diff --git a/src/node/app/static.ts b/src/node/app/static.ts index fc2cacd2..57e1ece3 100644 --- a/src/node/app/static.ts +++ b/src/node/app/static.ts @@ -1,5 +1,4 @@ import * as http from "http" -import { HttpCode, HttpError } from "../../common/http" import { HttpProvider, HttpResponse, Route } from "../http" /** @@ -32,8 +31,4 @@ export class StaticHttpProvider extends HttpProvider { } return this.getResource(this.rootPath, ...split) } - - public async handleWebSocket(): Promise { - throw new HttpError("Not found", HttpCode.NotFound) - } } diff --git a/src/node/app/update.ts b/src/node/app/update.ts index 19f8b240..41e1bf5c 100644 --- a/src/node/app/update.ts +++ b/src/node/app/update.ts @@ -59,10 +59,14 @@ export class UpdateHttpProvider extends HttpProvider { public async handleRequest(route: Route, request: http.IncomingMessage): Promise { this.ensureAuthenticated(request) + this.ensureMethod(request) + + if (route.requestPath !== "/index.html") { + throw new HttpError("Not found", HttpCode.NotFound) + } switch (route.base) { case "/check": - this.ensureMethod(request) this.getUpdate(true) if (route.query && route.query.to) { return { @@ -70,37 +74,45 @@ export class UpdateHttpProvider extends HttpProvider { query: { to: undefined }, } } - return this.getRoot(route) - case "/": { - this.ensureMethod(request, ["GET", "POST"]) - if (route.requestPath !== "/index.html") { - throw new HttpError("Not found", HttpCode.NotFound) - } - - switch (request.method) { - case "GET": - return this.getRoot(route) - case "POST": - return this.tryUpdate(route) - } - } + return this.getRoot(route, request) + case "/apply": + return this.tryUpdate(route, request) + case "/": + return this.getRoot(route, request) } throw new HttpError("Not found", HttpCode.NotFound) } - public async getRoot(route: Route, error?: Error): Promise { + public async getRoot( + route: Route, + request: http.IncomingMessage, + appliedUpdate?: string, + error?: Error, + ): Promise { + if (request.headers["content-type"] === "application/json") { + if (!this.enabled) { + return { + content: { + isLatest: true, + }, + } + } + const update = await this.getUpdate() + return { + content: { + ...update, + isLatest: this.isLatestVersion(update), + }, + } + } const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/update.html") response.content = response.content - .replace(/{{UPDATE_STATUS}}/, await this.getUpdateHtml()) + .replace(/{{UPDATE_STATUS}}/, appliedUpdate ? `Updated to ${appliedUpdate}` : await this.getUpdateHtml()) .replace(/{{ERROR}}/, error ? `
${error.message}
` : "") return this.replaceTemplates(route, response) } - public async handleWebSocket(): Promise { - throw new HttpError("Not found", HttpCode.NotFound) - } - /** * Query for and return the latest update. */ @@ -163,25 +175,26 @@ export class UpdateHttpProvider extends HttpProvider { const update = await this.getUpdate() if (this.isLatestVersion(update)) { - throw new Error("No update available") + return "No update available" } return `` } - public async tryUpdate(route: Route): Promise { + public async tryUpdate(route: Route, request: http.IncomingMessage): Promise { try { const update = await this.getUpdate() if (!this.isLatestVersion(update)) { - await this.downloadUpdate(update) + await this.downloadAndApplyUpdate(update) + return this.getRoot(route, request, update.version) } - return this.getRoot(route) + return this.getRoot(route, request) } catch (error) { - return this.getRoot(route, error) + return this.getRoot(route, request, undefined, error) } } - public async downloadUpdate(update: Update, targetPath?: string, target?: string): Promise { + public async downloadAndApplyUpdate(update: Update, targetPath?: string, target?: string): Promise { const releaseName = await this.getReleaseName(update, target) const url = this.downloadUrl.replace("{{VERSION}}", update.version).replace("{{RELEASE_NAME}}", releaseName) diff --git a/src/node/http.ts b/src/node/http.ts index 29442724..cea4aae7 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -140,12 +140,16 @@ export abstract class HttpProvider { /** * Handle web sockets on the registered endpoint. */ - public abstract handleWebSocket( - route: Route, - request: http.IncomingMessage, - socket: net.Socket, - head: Buffer, - ): Promise + public handleWebSocket( + /* eslint-disable @typescript-eslint/no-unused-vars */ + _route: Route, + _request: http.IncomingMessage, + _socket: net.Socket, + _head: Buffer, + /* eslint-enable @typescript-eslint/no-unused-vars */ + ): Promise { + throw new HttpError("Not found", HttpCode.NotFound) + } /** * Handle requests to the registered endpoint. @@ -194,6 +198,7 @@ export abstract class HttpProvider { } response.content = response.content .replace(/{{COMMIT}}/g, this.options.commit) + .replace(/{{TO}}/g, Array.isArray(route.query.to) ? route.query.to[0] : route.query.to || "/dashboard") .replace(/{{BASE}}/g, this.base(route)) .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`) return response diff --git a/test/update.test.ts b/test/update.test.ts index 46bb3c29..903c9df7 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -205,12 +205,12 @@ describe("update", () => { assert.equal(`console.log("OLD")`, await fs.readFile(entry, "utf8")) // Updating should replace the existing version. - await p.downloadUpdate(update, destination, "linux") + await p.downloadAndApplyUpdate(update, destination, "linux") assert.equal(`console.log("UPDATED")`, await fs.readFile(entry, "utf8")) // Should still work if there is no existing version somehow. await fs.remove(destination) - await p.downloadUpdate(update, destination, "linux") + await p.downloadAndApplyUpdate(update, destination, "linux") assert.equal(`console.log("UPDATED")`, await fs.readFile(entry, "utf8")) assert.deepEqual(spy, [