Check updates daily instead of every time

Also add a way to force a check.
This commit is contained in:
Asher 2020-02-14 16:41:42 -06:00
parent db54f78e8e
commit 0ec83f8736
No known key found for this signature in database
GPG Key ID: D63C1EF81242354A
5 changed files with 105 additions and 41 deletions

View File

@ -22,17 +22,24 @@
} }
.block-row > .item { .block-row > .item {
color: #b6b6b6; color: #c4c4c4;
display: flex;
flex: 1; flex: 1;
}
.block-row > .item.-row {
display: flex;
}
.block-row > .item > .sub {
color: #888;
}
.block-row .-link {
cursor: pointer;
text-decoration: none; text-decoration: none;
} }
.block-row > .item.-link { .block-row .-link:hover {
cursor: pointer;
}
.block-row > .item.-link:hover {
color: #fafafa; color: #fafafa;
} }

View File

@ -115,7 +115,7 @@ export class MainHttpProvider extends HttpProvider {
private getAppRow(app: Application): string { private getAppRow(app: Application): string {
return `<div class="block-row"> return `<div class="block-row">
<a class="item -link" href=".${app.path}"> <a class="item -row -link" href=".${app.path}">
${ ${
app.icon app.icon
? `<img class="icon" src="data:image/png;base64,${app.icon}"></img>` ? `<img class="icon" src="data:image/png;base64,${app.icon}"></img>`
@ -139,17 +139,40 @@ export class MainHttpProvider extends HttpProvider {
return "Updates are disabled" 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() const update = await this.update.getUpdate()
if (!update) { if (this.update.isLatestVersion(update)) {
return `<div class="block-row"> return `<div class="block-row">
<span class="item">No updates available</span> <div class="item">
<span class="current" >Current: ${this.update.currentVersion}</span> ${update.version}
<div class="sub">Up to date</div>
</div>
<div class="item">
${humanize(update.checked)}
<a class="sub -link" href="./update/check">Check now</a>
</div>
<div class="item" >Current: ${this.update.currentVersion}</div>
</div>` </div>`
} }
return `<div class="block-row"> return `<div class="block-row">
<a class="item -link" href="./update">Update available: ${update.version}</a> <a class="item -link" href="./update">
<span class="current" >Current: ${this.update.currentVersion}</span> ${update.version}
<div class="sub">Out of date</div>
</a>
<div class="item">
${humanize(update.checked)}
<a class="sub -link" href="./update/check">Check now</a>
</div>
<div class="item" >Current: ${this.update.currentVersion}</div>
</div>` </div>`
} }
} }

View File

@ -1,4 +1,5 @@
import { field, logger } from "@coder/logger" import { field, logger } from "@coder/logger"
import zip from "adm-zip"
import * as cp from "child_process" import * as cp from "child_process"
import * as fs from "fs-extra" import * as fs from "fs-extra"
import * as http from "http" import * as http from "http"
@ -10,14 +11,15 @@ import { Readable, Writable } from "stream"
import * as tar from "tar-fs" import * as tar from "tar-fs"
import * as url from "url" import * as url from "url"
import * as util from "util" import * as util from "util"
import zip from "adm-zip"
import * as zlib from "zlib" import * as zlib from "zlib"
import { HttpCode, HttpError } from "../../common/http" import { HttpCode, HttpError } from "../../common/http"
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
import { settings } from "../settings"
import { tmpdir } from "../util" import { tmpdir } from "../util"
import { ipcMain } from "../wrapper" import { ipcMain } from "../wrapper"
export interface Update { export interface Update {
checked: number
version: string version: string
} }
@ -25,7 +27,8 @@ export interface Update {
* Update HTTP provider. * Update HTTP provider.
*/ */
export class UpdateHttpProvider extends HttpProvider { export class UpdateHttpProvider extends HttpProvider {
private update?: Promise<Update | undefined> private update?: Promise<Update>
private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks.
public constructor(options: HttpProviderOptions, public readonly enabled: boolean) { public constructor(options: HttpProviderOptions, public readonly enabled: boolean) {
super(options) super(options)
@ -33,6 +36,10 @@ export class UpdateHttpProvider extends HttpProvider {
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse | undefined> { public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse | undefined> {
switch (route.base) { switch (route.base) {
case "/check":
this.ensureMethod(request)
this.getUpdate(true)
return { redirect: "/login" }
case "/": { case "/": {
this.ensureMethod(request, ["GET", "POST"]) this.ensureMethod(request, ["GET", "POST"])
if (route.requestPath !== "/index.html") { if (route.requestPath !== "/index.html") {
@ -70,29 +77,38 @@ export class UpdateHttpProvider extends HttpProvider {
/** /**
* Query for and return the latest update. * Query for and return the latest update.
*/ */
public async getUpdate(): Promise<Update | undefined> { public async getUpdate(force?: boolean): Promise<Update> {
if (!this.enabled) { if (!this.enabled) {
throw new Error("updates are not enabled") throw new Error("updates are not enabled")
} }
if (!this.update) { if (!this.update) {
this.update = this._getUpdate() this.update = this._getUpdate(force)
this.update.then(() => (this.update = undefined))
} }
return this.update return this.update
} }
private async _getUpdate(): Promise<Update | undefined> { private async _getUpdate(force?: boolean): Promise<Update> {
const url = "https://api.github.com/repos/cdr/code-server/releases/latest" const url = "https://api.github.com/repos/cdr/code-server/releases/latest"
const now = Date.now()
try { try {
let { update } = !force ? await settings.read() : { update: undefined }
if (!update || update.checked + this.updateInterval < now) {
const buffer = await this.request(url) const buffer = await this.request(url)
const data = JSON.parse(buffer.toString()) const data = JSON.parse(buffer.toString())
const latest = { version: data.name } update = { checked: now, version: data.name as string }
logger.debug("Got latest version", field("latest", latest.version)) settings.write({ update })
return this.isLatestVersion(latest) ? undefined : latest }
logger.debug("Got latest version", field("latest", update.version))
return update
} catch (error) { } catch (error) {
logger.error("Failed to get latest version", field("error", error.message)) 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. * Return true if the currently installed version is the latest.
*/ */
private isLatestVersion(latest: Update): boolean { public isLatestVersion(latest: Update): boolean {
const version = this.currentVersion const version = this.currentVersion
logger.debug("Comparing versions", field("current", version), field("latest", latest.version)) logger.debug("Comparing versions", field("current", version), field("latest", latest.version))
try {
return latest.version === version || semver.lt(latest.version, version) return latest.version === version || semver.lt(latest.version, version)
} catch (error) {
return true
}
} }
private async getUpdateHtml(): Promise<string> { private async getUpdateHtml(): Promise<string> {
@ -115,8 +135,8 @@ export class UpdateHttpProvider extends HttpProvider {
} }
const update = await this.getUpdate() const update = await this.getUpdate()
if (!update) { if (this.isLatestVersion(update)) {
return "No updates available" throw new Error("No update available")
} }
return `<button type="submit" class="apply"> return `<button type="submit" class="apply">
@ -128,7 +148,7 @@ export class UpdateHttpProvider extends HttpProvider {
public async tryUpdate(route: Route): Promise<HttpResponse> { public async tryUpdate(route: Route): Promise<HttpResponse> {
try { try {
const update = await this.getUpdate() const update = await this.getUpdate()
if (!update) { if (this.isLatestVersion(update)) {
throw new Error("no update available") throw new Error("no update available")
} }
await this.downloadUpdate(update) await this.downloadUpdate(update)

View File

@ -17,17 +17,11 @@ import { HttpCode, HttpError } from "../../common/http"
import { generateUuid } from "../../common/util" import { generateUuid } from "../../common/util"
import { Args } from "../cli" import { Args } from "../cli"
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
import { SettingsProvider } from "../settings" import { settings } from "../settings"
import { xdgLocalDir } from "../util"
export interface Settings {
lastVisited: StartPath
}
export class VscodeHttpProvider extends HttpProvider { export class VscodeHttpProvider extends HttpProvider {
private readonly serverRootPath: string private readonly serverRootPath: string
private readonly vsRootPath: string private readonly vsRootPath: string
private readonly settings = new SettingsProvider<Settings>(path.join(xdgLocalDir, "coder.json"))
private _vscode?: Promise<cp.ChildProcess> private _vscode?: Promise<cp.ChildProcess>
private workbenchOptions?: WorkbenchOptions private workbenchOptions?: WorkbenchOptions
@ -178,12 +172,12 @@ export class VscodeHttpProvider extends HttpProvider {
private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> { private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
const remoteAuthority = request.headers.host as string const remoteAuthority = request.headers.host as string
const settings = await this.settings.read() const { lastVisited } = await settings.read()
const startPath = await this.getFirstValidPath( const startPath = await this.getFirstValidPath(
[ [
{ url: route.query.workspace, workspace: true }, { url: route.query.workspace, workspace: true },
{ url: route.query.folder, workspace: false }, { url: route.query.folder, workspace: false },
settings.lastVisited, lastVisited,
this.args._ && this.args._.length > 0 ? { url: this.args._[0] } : undefined, this.args._ && this.args._.length > 0 ? { url: this.args._[0] } : undefined,
], ],
remoteAuthority remoteAuthority
@ -200,7 +194,7 @@ export class VscodeHttpProvider extends HttpProvider {
this.workbenchOptions = options this.workbenchOptions = options
if (startPath) { if (startPath) {
this.settings.write({ settings.write({
lastVisited: startPath, lastVisited: startPath,
}) })
} }

View File

@ -1,6 +1,7 @@
import * as fs from "fs-extra" import * as fs from "fs-extra"
import * as path from "path"
import { extend, xdgLocalDir } from "./util"
import { logger } from "@coder/logger" import { logger } from "@coder/logger"
import { extend } from "./util"
export type Settings = { [key: string]: Settings | string | boolean | number } export type Settings = { [key: string]: Settings | string | boolean | number }
@ -32,9 +33,28 @@ export class SettingsProvider<T> {
*/ */
public async write(settings: Partial<T>): Promise<void> { public async write(settings: Partial<T>): Promise<void> {
try { try {
await fs.writeFile(this.settingsPath, JSON.stringify(extend(this.read(), settings))) await fs.writeFile(this.settingsPath, JSON.stringify(extend(await this.read(), settings), null, 2))
} catch (error) { } catch (error) {
logger.warn(error.message) logger.warn(error.message)
} }
} }
} }
/**
* Global code-server settings.
*/
export interface CoderSettings {
lastVisited: {
url: string
workspace: boolean
}
update: {
checked: number
version: string
}
}
/**
* Global code-server settings file.
*/
export const settings = new SettingsProvider<CoderSettings>(path.join(xdgLocalDir, "coder.json"))