2020-10-21 01:05:58 +02:00
|
|
|
import { field, logger } from "@coder/logger"
|
|
|
|
import * as http from "http"
|
|
|
|
import * as https from "https"
|
|
|
|
import * as semver from "semver"
|
|
|
|
import * as url from "url"
|
|
|
|
import { version } from "./constants"
|
|
|
|
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "./settings"
|
|
|
|
|
|
|
|
export interface Update {
|
|
|
|
checked: number
|
|
|
|
version: string
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface LatestResponse {
|
|
|
|
name: string
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Provide update information.
|
|
|
|
*/
|
|
|
|
export class UpdateProvider {
|
|
|
|
private update?: Promise<Update>
|
|
|
|
private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks.
|
|
|
|
|
|
|
|
public constructor(
|
|
|
|
/**
|
|
|
|
* The URL for getting the latest version of code-server. Should return JSON
|
|
|
|
* that fulfills `LatestResponse`.
|
|
|
|
*/
|
|
|
|
private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest",
|
|
|
|
/**
|
|
|
|
* Update information will be stored here. If not provided, the global
|
|
|
|
* settings will be used.
|
|
|
|
*/
|
|
|
|
private readonly settings: SettingsProvider<UpdateSettings> = globalSettings,
|
|
|
|
) {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Query for and return the latest update.
|
|
|
|
*/
|
|
|
|
public async getUpdate(force?: boolean): Promise<Update> {
|
|
|
|
// Don't run multiple requests at a time.
|
|
|
|
if (!this.update) {
|
|
|
|
this.update = this._getUpdate(force)
|
|
|
|
this.update.then(() => (this.update = undefined))
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.update
|
|
|
|
}
|
|
|
|
|
|
|
|
private async _getUpdate(force?: boolean): Promise<Update> {
|
|
|
|
const now = Date.now()
|
|
|
|
try {
|
|
|
|
let { update } = !force ? await this.settings.read() : { update: undefined }
|
|
|
|
if (!update || update.checked + this.updateInterval < now) {
|
|
|
|
const buffer = await this.request(this.latestUrl)
|
|
|
|
const data = JSON.parse(buffer.toString()) as LatestResponse
|
|
|
|
update = { checked: now, version: data.name.replace(/^v/, "") }
|
|
|
|
await this.settings.write({ update })
|
|
|
|
}
|
|
|
|
logger.debug("got latest version", field("latest", update.version))
|
|
|
|
return update
|
2021-09-30 05:14:56 +02:00
|
|
|
} catch (error: any) {
|
2020-10-21 01:05:58 +02:00
|
|
|
logger.error("Failed to get latest version", field("error", error.message))
|
|
|
|
return {
|
|
|
|
checked: now,
|
|
|
|
version: "unknown",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return true if the currently installed version is the latest.
|
|
|
|
*/
|
|
|
|
public isLatestVersion(latest: Update): boolean {
|
|
|
|
logger.debug("comparing versions", field("current", version), field("latest", latest.version))
|
|
|
|
try {
|
2020-11-24 03:09:27 +01:00
|
|
|
return semver.lte(latest.version, version)
|
2020-10-21 01:05:58 +02:00
|
|
|
} catch (error) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async request(uri: string): Promise<Buffer> {
|
|
|
|
const response = await this.requestResponse(uri)
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const chunks: Buffer[] = []
|
|
|
|
let bufferLength = 0
|
|
|
|
response.on("data", (chunk) => {
|
|
|
|
bufferLength += chunk.length
|
|
|
|
chunks.push(chunk)
|
|
|
|
})
|
|
|
|
response.on("error", reject)
|
|
|
|
response.on("end", () => {
|
|
|
|
resolve(Buffer.concat(chunks, bufferLength))
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
private async requestResponse(uri: string): Promise<http.IncomingMessage> {
|
|
|
|
let redirects = 0
|
|
|
|
const maxRedirects = 10
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const request = (uri: string): void => {
|
|
|
|
logger.debug("Making request", field("uri", uri))
|
|
|
|
const httpx = uri.startsWith("https") ? https : http
|
|
|
|
const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => {
|
2020-11-03 21:31:32 +01:00
|
|
|
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) {
|
2020-12-04 06:02:00 +01:00
|
|
|
response.destroy()
|
2020-11-03 21:31:32 +01:00
|
|
|
return reject(new Error(`${uri}: ${response.statusCode || "500"}`))
|
|
|
|
}
|
|
|
|
|
|
|
|
if (response.statusCode >= 300) {
|
2020-11-05 21:34:57 +01:00
|
|
|
response.destroy()
|
2020-12-04 06:02:00 +01:00
|
|
|
++redirects
|
2020-10-21 01:05:58 +02:00
|
|
|
if (redirects > maxRedirects) {
|
|
|
|
return reject(new Error("reached max redirects"))
|
|
|
|
}
|
2020-11-03 21:31:32 +01:00
|
|
|
if (!response.headers.location) {
|
|
|
|
return reject(new Error("received redirect with no location header"))
|
|
|
|
}
|
2020-10-21 01:05:58 +02:00
|
|
|
return request(url.resolve(uri, response.headers.location))
|
|
|
|
}
|
|
|
|
|
|
|
|
resolve(response)
|
|
|
|
})
|
|
|
|
client.on("error", reject)
|
|
|
|
}
|
|
|
|
request(uri)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|