diff --git a/src/browser/pages/home.css b/src/browser/pages/home.css index c14a539a..018623ba 100644 --- a/src/browser/pages/home.css +++ b/src/browser/pages/home.css @@ -22,17 +22,24 @@ } .block-row > .item { - color: #b6b6b6; - display: flex; + color: #c4c4c4; flex: 1; +} + +.block-row > .item.-row { + display: flex; +} + +.block-row > .item > .sub { + color: #888; +} + +.block-row .-link { + cursor: pointer; text-decoration: none; } -.block-row > .item.-link { - cursor: pointer; -} - -.block-row > .item.-link:hover { +.block-row .-link:hover { color: #fafafa; } diff --git a/src/node/app/app.ts b/src/node/app/app.ts index 62672e45..136c4cc6 100644 --- a/src/node/app/app.ts +++ b/src/node/app/app.ts @@ -115,7 +115,7 @@ export class MainHttpProvider extends HttpProvider { private getAppRow(app: Application): string { return `
- + ${ app.icon ? `` @@ -139,17 +139,40 @@ export class MainHttpProvider extends HttpProvider { return "Updates are disabled" } + const humanize = (time: number): string => { + const d = new Date(time) + const pad = (t: number): string => (t < 10 ? "0" : "") + t + return ( + `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` + + ` ${pad(d.getHours())}:${pad(d.getMinutes())}` + ) + } + const update = await this.update.getUpdate() - if (!update) { + if (this.update.isLatestVersion(update)) { return `
- No updates available - Current: ${this.update.currentVersion} +
+ ${update.version} +
Up to date
+
+
+
Current: ${this.update.currentVersion}
` } return `
- Update available: ${update.version} - Current: ${this.update.currentVersion} + + ${update.version} +
Out of date
+
+
+ ${humanize(update.checked)} + Check now +
+
Current: ${this.update.currentVersion}
` } } diff --git a/src/node/app/update.ts b/src/node/app/update.ts index 34df2cfc..55bb9dec 100644 --- a/src/node/app/update.ts +++ b/src/node/app/update.ts @@ -1,4 +1,5 @@ import { field, logger } from "@coder/logger" +import zip from "adm-zip" import * as cp from "child_process" import * as fs from "fs-extra" import * as http from "http" @@ -10,14 +11,15 @@ import { Readable, Writable } from "stream" import * as tar from "tar-fs" import * as url from "url" import * as util from "util" -import zip from "adm-zip" import * as zlib from "zlib" import { HttpCode, HttpError } from "../../common/http" import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" +import { settings } from "../settings" import { tmpdir } from "../util" import { ipcMain } from "../wrapper" export interface Update { + checked: number version: string } @@ -25,7 +27,8 @@ export interface Update { * Update HTTP provider. */ export class UpdateHttpProvider extends HttpProvider { - private update?: Promise + private update?: Promise + private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks. public constructor(options: HttpProviderOptions, public readonly enabled: boolean) { super(options) @@ -33,6 +36,10 @@ export class UpdateHttpProvider extends HttpProvider { public async handleRequest(route: Route, request: http.IncomingMessage): Promise { switch (route.base) { + case "/check": + this.ensureMethod(request) + this.getUpdate(true) + return { redirect: "/login" } case "/": { this.ensureMethod(request, ["GET", "POST"]) if (route.requestPath !== "/index.html") { @@ -70,29 +77,38 @@ export class UpdateHttpProvider extends HttpProvider { /** * Query for and return the latest update. */ - public async getUpdate(): Promise { + public async getUpdate(force?: boolean): Promise { if (!this.enabled) { throw new Error("updates are not enabled") } if (!this.update) { - this.update = this._getUpdate() + this.update = this._getUpdate(force) + this.update.then(() => (this.update = undefined)) } return this.update } - private async _getUpdate(): Promise { + private async _getUpdate(force?: boolean): Promise { const url = "https://api.github.com/repos/cdr/code-server/releases/latest" + const now = Date.now() try { - const buffer = await this.request(url) - const data = JSON.parse(buffer.toString()) - const latest = { version: data.name } - logger.debug("Got latest version", field("latest", latest.version)) - return this.isLatestVersion(latest) ? undefined : latest + let { update } = !force ? await settings.read() : { update: undefined } + if (!update || update.checked + this.updateInterval < now) { + const buffer = await this.request(url) + const data = JSON.parse(buffer.toString()) + update = { checked: now, version: data.name as string } + settings.write({ update }) + } + logger.debug("Got latest version", field("latest", update.version)) + return update } catch (error) { logger.error("Failed to get latest version", field("error", error.message)) - return undefined + return { + checked: now, + version: "unknown", + } } } @@ -103,10 +119,14 @@ export class UpdateHttpProvider extends HttpProvider { /** * Return true if the currently installed version is the latest. */ - private isLatestVersion(latest: Update): boolean { + public isLatestVersion(latest: Update): boolean { const version = this.currentVersion logger.debug("Comparing versions", field("current", version), field("latest", latest.version)) - return latest.version === version || semver.lt(latest.version, version) + try { + return latest.version === version || semver.lt(latest.version, version) + } catch (error) { + return true + } } private async getUpdateHtml(): Promise { @@ -115,8 +135,8 @@ export class UpdateHttpProvider extends HttpProvider { } const update = await this.getUpdate() - if (!update) { - return "No updates available" + if (this.isLatestVersion(update)) { + throw new Error("No update available") } return `