mirror of https://github.com/coder/code-server.git
Check updates daily instead of every time
Also add a way to force a check.
This commit is contained in:
parent
db54f78e8e
commit
0ec83f8736
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
Loading…
Reference in New Issue