Skip to content

Commit 0ec83f8

Browse files
committed
Check updates daily instead of every time
Also add a way to force a check.
1 parent db54f78 commit 0ec83f8

File tree

5 files changed

+103
-39
lines changed

5 files changed

+103
-39
lines changed

src/browser/pages/home.css

+12-5
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,24 @@
2222
}
2323

2424
.block-row > .item {
25-
color: #b6b6b6;
26-
display: flex;
25+
color: #c4c4c4;
2726
flex: 1;
28-
text-decoration: none;
2927
}
3028

31-
.block-row > .item.-link {
29+
.block-row > .item.-row {
30+
display: flex;
31+
}
32+
33+
.block-row > .item > .sub {
34+
color: #888;
35+
}
36+
37+
.block-row .-link {
3238
cursor: pointer;
39+
text-decoration: none;
3340
}
3441

35-
.block-row > .item.-link:hover {
42+
.block-row .-link:hover {
3643
color: #fafafa;
3744
}
3845

src/node/app/app.ts

+29-6
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export class MainHttpProvider extends HttpProvider {
115115

116116
private getAppRow(app: Application): string {
117117
return `<div class="block-row">
118-
<a class="item -link" href=".${app.path}">
118+
<a class="item -row -link" href=".${app.path}">
119119
${
120120
app.icon
121121
? `<img class="icon" src="data:image/png;base64,${app.icon}"></img>`
@@ -139,17 +139,40 @@ export class MainHttpProvider extends HttpProvider {
139139
return "Updates are disabled"
140140
}
141141

142+
const humanize = (time: number): string => {
143+
const d = new Date(time)
144+
const pad = (t: number): string => (t < 10 ? "0" : "") + t
145+
return (
146+
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
147+
` ${pad(d.getHours())}:${pad(d.getMinutes())}`
148+
)
149+
}
150+
142151
const update = await this.update.getUpdate()
143-
if (!update) {
152+
if (this.update.isLatestVersion(update)) {
144153
return `<div class="block-row">
145-
<span class="item">No updates available</span>
146-
<span class="current" >Current: ${this.update.currentVersion}</span>
154+
<div class="item">
155+
${update.version}
156+
<div class="sub">Up to date</div>
157+
</div>
158+
<div class="item">
159+
${humanize(update.checked)}
160+
<a class="sub -link" href="./update/check">Check now</a>
161+
</div>
162+
<div class="item" >Current: ${this.update.currentVersion}</div>
147163
</div>`
148164
}
149165

150166
return `<div class="block-row">
151-
<a class="item -link" href="./update">Update available: ${update.version}</a>
152-
<span class="current" >Current: ${this.update.currentVersion}</span>
167+
<a class="item -link" href="./update">
168+
${update.version}
169+
<div class="sub">Out of date</div>
170+
</a>
171+
<div class="item">
172+
${humanize(update.checked)}
173+
<a class="sub -link" href="./update/check">Check now</a>
174+
</div>
175+
<div class="item" >Current: ${this.update.currentVersion}</div>
153176
</div>`
154177
}
155178
}

src/node/app/update.ts

+36-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { field, logger } from "@coder/logger"
2+
import zip from "adm-zip"
23
import * as cp from "child_process"
34
import * as fs from "fs-extra"
45
import * as http from "http"
@@ -10,29 +11,35 @@ import { Readable, Writable } from "stream"
1011
import * as tar from "tar-fs"
1112
import * as url from "url"
1213
import * as util from "util"
13-
import zip from "adm-zip"
1414
import * as zlib from "zlib"
1515
import { HttpCode, HttpError } from "../../common/http"
1616
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
17+
import { settings } from "../settings"
1718
import { tmpdir } from "../util"
1819
import { ipcMain } from "../wrapper"
1920

2021
export interface Update {
22+
checked: number
2123
version: string
2224
}
2325

2426
/**
2527
* Update HTTP provider.
2628
*/
2729
export class UpdateHttpProvider extends HttpProvider {
28-
private update?: Promise<Update | undefined>
30+
private update?: Promise<Update>
31+
private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks.
2932

3033
public constructor(options: HttpProviderOptions, public readonly enabled: boolean) {
3134
super(options)
3235
}
3336

3437
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse | undefined> {
3538
switch (route.base) {
39+
case "/check":
40+
this.ensureMethod(request)
41+
this.getUpdate(true)
42+
return { redirect: "/login" }
3643
case "/": {
3744
this.ensureMethod(request, ["GET", "POST"])
3845
if (route.requestPath !== "/index.html") {
@@ -70,29 +77,38 @@ export class UpdateHttpProvider extends HttpProvider {
7077
/**
7178
* Query for and return the latest update.
7279
*/
73-
public async getUpdate(): Promise<Update | undefined> {
80+
public async getUpdate(force?: boolean): Promise<Update> {
7481
if (!this.enabled) {
7582
throw new Error("updates are not enabled")
7683
}
7784

7885
if (!this.update) {
79-
this.update = this._getUpdate()
86+
this.update = this._getUpdate(force)
87+
this.update.then(() => (this.update = undefined))
8088
}
8189

8290
return this.update
8391
}
8492

85-
private async _getUpdate(): Promise<Update | undefined> {
93+
private async _getUpdate(force?: boolean): Promise<Update> {
8694
const url = "https://api.github.com/repos/cdr/code-server/releases/latest"
95+
const now = Date.now()
8796
try {
88-
const buffer = await this.request(url)
89-
const data = JSON.parse(buffer.toString())
90-
const latest = { version: data.name }
91-
logger.debug("Got latest version", field("latest", latest.version))
92-
return this.isLatestVersion(latest) ? undefined : latest
97+
let { update } = !force ? await settings.read() : { update: undefined }
98+
if (!update || update.checked + this.updateInterval < now) {
99+
const buffer = await this.request(url)
100+
const data = JSON.parse(buffer.toString())
101+
update = { checked: now, version: data.name as string }
102+
settings.write({ update })
103+
}
104+
logger.debug("Got latest version", field("latest", update.version))
105+
return update
93106
} catch (error) {
94107
logger.error("Failed to get latest version", field("error", error.message))
95-
return undefined
108+
return {
109+
checked: now,
110+
version: "unknown",
111+
}
96112
}
97113
}
98114

@@ -103,10 +119,14 @@ export class UpdateHttpProvider extends HttpProvider {
103119
/**
104120
* Return true if the currently installed version is the latest.
105121
*/
106-
private isLatestVersion(latest: Update): boolean {
122+
public isLatestVersion(latest: Update): boolean {
107123
const version = this.currentVersion
108124
logger.debug("Comparing versions", field("current", version), field("latest", latest.version))
109-
return latest.version === version || semver.lt(latest.version, version)
125+
try {
126+
return latest.version === version || semver.lt(latest.version, version)
127+
} catch (error) {
128+
return true
129+
}
110130
}
111131

112132
private async getUpdateHtml(): Promise<string> {
@@ -115,8 +135,8 @@ export class UpdateHttpProvider extends HttpProvider {
115135
}
116136

117137
const update = await this.getUpdate()
118-
if (!update) {
119-
return "No updates available"
138+
if (this.isLatestVersion(update)) {
139+
throw new Error("No update available")
120140
}
121141

122142
return `<button type="submit" class="apply">
@@ -128,7 +148,7 @@ export class UpdateHttpProvider extends HttpProvider {
128148
public async tryUpdate(route: Route): Promise<HttpResponse> {
129149
try {
130150
const update = await this.getUpdate()
131-
if (!update) {
151+
if (this.isLatestVersion(update)) {
132152
throw new Error("no update available")
133153
}
134154
await this.downloadUpdate(update)

src/node/app/vscode.ts

+4-10
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,11 @@ import { HttpCode, HttpError } from "../../common/http"
1717
import { generateUuid } from "../../common/util"
1818
import { Args } from "../cli"
1919
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
20-
import { SettingsProvider } from "../settings"
21-
import { xdgLocalDir } from "../util"
22-
23-
export interface Settings {
24-
lastVisited: StartPath
25-
}
20+
import { settings } from "../settings"
2621

2722
export class VscodeHttpProvider extends HttpProvider {
2823
private readonly serverRootPath: string
2924
private readonly vsRootPath: string
30-
private readonly settings = new SettingsProvider<Settings>(path.join(xdgLocalDir, "coder.json"))
3125
private _vscode?: Promise<cp.ChildProcess>
3226
private workbenchOptions?: WorkbenchOptions
3327

@@ -178,12 +172,12 @@ export class VscodeHttpProvider extends HttpProvider {
178172

179173
private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
180174
const remoteAuthority = request.headers.host as string
181-
const settings = await this.settings.read()
175+
const { lastVisited } = await settings.read()
182176
const startPath = await this.getFirstValidPath(
183177
[
184178
{ url: route.query.workspace, workspace: true },
185179
{ url: route.query.folder, workspace: false },
186-
settings.lastVisited,
180+
lastVisited,
187181
this.args._ && this.args._.length > 0 ? { url: this.args._[0] } : undefined,
188182
],
189183
remoteAuthority
@@ -200,7 +194,7 @@ export class VscodeHttpProvider extends HttpProvider {
200194
this.workbenchOptions = options
201195

202196
if (startPath) {
203-
this.settings.write({
197+
settings.write({
204198
lastVisited: startPath,
205199
})
206200
}

src/node/settings.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from "fs-extra"
2+
import * as path from "path"
3+
import { extend, xdgLocalDir } from "./util"
24
import { logger } from "@coder/logger"
3-
import { extend } from "./util"
45

56
export type Settings = { [key: string]: Settings | string | boolean | number }
67

@@ -32,9 +33,28 @@ export class SettingsProvider<T> {
3233
*/
3334
public async write(settings: Partial<T>): Promise<void> {
3435
try {
35-
await fs.writeFile(this.settingsPath, JSON.stringify(extend(this.read(), settings)))
36+
await fs.writeFile(this.settingsPath, JSON.stringify(extend(await this.read(), settings), null, 2))
3637
} catch (error) {
3738
logger.warn(error.message)
3839
}
3940
}
4041
}
42+
43+
/**
44+
* Global code-server settings.
45+
*/
46+
export interface CoderSettings {
47+
lastVisited: {
48+
url: string
49+
workspace: boolean
50+
}
51+
update: {
52+
checked: number
53+
version: string
54+
}
55+
}
56+
57+
/**
58+
* Global code-server settings file.
59+
*/
60+
export const settings = new SettingsProvider<CoderSettings>(path.join(xdgLocalDir, "coder.json"))

0 commit comments

Comments
 (0)