- Notifications
You must be signed in to change notification settings - Fork24
fix: keep binary in same place, update if needed#33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
82717c2
197385a
400ba7a
369a823
923225b
0a0b041
eafac66
6f6adb7
7a31663
5528dc8
db53aaa
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -85,6 +85,7 @@ | ||
"build": "webpack", | ||
"watch": "webpack --watch", | ||
"package": "webpack --mode production --devtool hidden-source-map", | ||
"package:prerelease": "npx vsce package --pre-release", | ||
Kira-Pilot marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
"lint": "eslint . --ext ts,md", | ||
"tsc:compile": "tsc", | ||
"tsc:watch": "tsc -w", | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
import axios from "axios" | ||
import { execFile } from "child_process" | ||
import { getBuildInfo } from "coder/site/src/api/api" | ||
import * as crypto from "crypto" | ||
import { createWriteStream, createReadStream } from "fs" | ||
import { ensureDir } from "fs-extra" | ||
import fs from "fs/promises" | ||
import { IncomingMessage } from "http" | ||
@@ -73,39 +74,16 @@ export class Storage { | ||
// fetchBinary returns the path to a Coder binary. | ||
// The binary will be cached if a matching server version already exists. | ||
public async fetchBinary(): Promise<string | undefined> { | ||
await this.cleanUpOldBinaries() | ||
const baseURL = this.getURL() | ||
if (!baseURL) { | ||
throw new Error("Must be logged in!") | ||
} | ||
const baseURI = vscode.Uri.parse(baseURL) | ||
const buildInfo = await getBuildInfo() | ||
const binPath = this.binaryPath() | ||
const exists = await this.checkBinaryExists(binPath) | ||
jsjoeio marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page.
Kira-Pilot marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
const os = goos() | ||
const arch = goarch() | ||
let binName = `coder-${os}-${arch}` | ||
@@ -114,106 +92,153 @@ export class Storage { | ||
binName += ".exe" | ||
} | ||
const controller = new AbortController() | ||
if (exists) { | ||
this.output.appendLine(`Found existing binary: ${binPath}`) | ||
const valid = await this.checkBinaryValid(binPath) | ||
if (!valid) { | ||
const removed = await this.rmBinary(binPath) | ||
if (!removed) { | ||
vscode.window.showErrorMessage("Failed to remove existing binary!") | ||
return undefined | ||
} | ||
} | ||
} | ||
const etag = await this.getBinaryETag() | ||
this.output.appendLine(`Using binName: ${binName}`) | ||
this.output.appendLine(`Using binPath: ${binPath}`) | ||
jsjoeio marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
this.output.appendLine(`Using ETag: ${etag}`) | ||
const resp = await axios.get("/bin/" + binName, { | ||
signal: controller.signal, | ||
baseURL: baseURL, | ||
responseType: "stream", | ||
headers: { | ||
"Accept-Encoding": "gzip", | ||
"If-None-Match": `"${etag}"`, | ||
}, | ||
decompress: true, | ||
// Ignore all errors so we can catch a 404! | ||
validateStatus: () => true, | ||
}) | ||
jsjoeio marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
this.output.appendLine("Response status code: " + resp.status) | ||
switch (resp.status) { | ||
case 200: { | ||
const contentLength = Number.parseInt(resp.headers["content-length"]) | ||
// Ensure the binary directory exists! | ||
await fs.mkdir(path.dirname(binPath), { recursive: true }) | ||
const tempFile = binPath + ".temp-" + Math.random().toString(36).substring(8) | ||
const completed = await vscode.window.withProgress<boolean>( | ||
{ | ||
location: vscode.ProgressLocation.Notification, | ||
title: `Downloading the latest binary (${buildInfo.version} from ${baseURI.authority})`, | ||
cancellable: true, | ||
}, | ||
async (progress, token) => { | ||
const readStream = resp.data as IncomingMessage | ||
let cancelled = false | ||
token.onCancellationRequested(() => { | ||
controller.abort() | ||
readStream.destroy() | ||
cancelled = true | ||
}) | ||
let contentLengthPretty = "" | ||
// Reverse proxies might not always send a content length! | ||
if (!Number.isNaN(contentLength)) { | ||
contentLengthPretty = " / " + prettyBytes(contentLength) | ||
} | ||
const writeStream = createWriteStream(tempFile, { | ||
autoClose: true, | ||
mode: 0o755, | ||
}) | ||
let written = 0 | ||
readStream.on("data", (buffer: Buffer) => { | ||
writeStream.write(buffer, () => { | ||
written += buffer.byteLength | ||
progress.report({ | ||
message: `${prettyBytes(written)}${contentLengthPretty}`, | ||
increment: (buffer.byteLength / contentLength) * 100, | ||
}) | ||
}) | ||
}) | ||
try { | ||
await new Promise<void>((resolve, reject) => { | ||
readStream.on("error", (err) => { | ||
reject(err) | ||
}) | ||
readStream.on("close", () => { | ||
if (cancelled) { | ||
return reject() | ||
} | ||
writeStream.close() | ||
resolve() | ||
}) | ||
}) | ||
return true | ||
} catch (ex) { | ||
return false | ||
} | ||
}, | ||
) | ||
if (!completed) { | ||
return | ||
} | ||
this.output.appendLine(`Downloaded binary: ${binPath}`) | ||
const oldBinPath = binPath + ".old-" + Math.random().toString(36).substring(8) | ||
await fs.rename(binPath, oldBinPath).catch(() => { | ||
this.output.appendLine(`Warning: failed to rename ${binPath} to ${oldBinPath}`) | ||
}) | ||
await fs.rename(tempFile, binPath) | ||
await fs.rm(oldBinPath, { force: true }).catch((error) => { | ||
this.output.appendLine(`Warning: failed to remove old binary: ${error}`) | ||
}) | ||
return binPath | ||
} | ||
case 304: { | ||
this.output.appendLine(`Using cached binary: ${binPath}`) | ||
return binPath | ||
} | ||
case 404: { | ||
vscode.window | ||
.showErrorMessage( | ||
"Coder isn't supported for your platform. Please open an issue, we'd love to support it!", | ||
"Open an Issue", | ||
) | ||
.then((value) => { | ||
if (!value) { | ||
return | ||
} | ||
const params = new URLSearchParams({ | ||
title: `Support the \`${os}-${arch}\` platform`, | ||
body: `I'd like to use the \`${os}-${arch}\` architecture with the VS Code extension.`, | ||
}) | ||
const uri = vscode.Uri.parse(`https://github.com/coder/vscode-coder/issues/new?` + params.toString()) | ||
vscode.env.openExternal(uri) | ||
}) | ||
return undefined | ||
} | ||
default: { | ||
vscode.window | ||
.showErrorMessage("Failed to download binary. Please open an issue.", "Open an Issue") | ||
.then((value) => { | ||
if (!value) { | ||
return | ||
} | ||
const params = new URLSearchParams({ | ||
title: `Failed to download binary on \`${os}-${arch}\``, | ||
body: `Received status code \`${resp.status}\` when downloading the binary.`, | ||
}) | ||
const uri = vscode.Uri.parse(`https://github.com/coder/vscode-coder/issues/new?` + params.toString()) | ||
vscode.env.openExternal(uri) | ||
}) | ||
return undefined | ||
} | ||
} | ||
} | ||
// getBinaryCachePath returns the path where binaries are cached. | ||
@@ -240,6 +265,23 @@ export class Storage { | ||
return path.join(this.globalStorageUri.fsPath, "url") | ||
} | ||
public getBinaryETag(): Promise<string> { | ||
const hash = crypto.createHash("sha1") | ||
const stream = createReadStream(this.binaryPath()) | ||
return new Promise((resolve, reject) => { | ||
stream.on("end", () => { | ||
hash.end() | ||
resolve(hash.digest("hex")) | ||
}) | ||
stream.on("error", (err) => { | ||
reject(err) | ||
}) | ||
stream.on("data", (chunk) => { | ||
hash.update(chunk) | ||
}) | ||
}) | ||
} | ||
private appDataDir(): string { | ||
switch (process.platform) { | ||
case "darwin": | ||
@@ -264,16 +306,62 @@ export class Storage { | ||
} | ||
} | ||
private async cleanUpOldBinaries(): Promise<void> { | ||
const binPath = this.binaryPath() | ||
const binDir = path.dirname(binPath) | ||
const files = await fs.readdir(binDir) | ||
for (const file of files) { | ||
const fileName = path.basename(file) | ||
if (fileName.includes(".old-")) { | ||
try { | ||
await fs.rm(path.join(binDir, file), { force: true }) | ||
} catch (error) { | ||
this.output.appendLine(`Warning: failed to remove ${fileName}. Error: ${error}`) | ||
} | ||
} | ||
} | ||
} | ||
private binaryPath(): string { | ||
const os = goos() | ||
const arch = goarch() | ||
let binPath = path.join(this.getBinaryCachePath(), `coder-${os}-${arch}`) | ||
jsjoeio marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
if (os === "windows") { | ||
binPath += ".exe" | ||
} | ||
return binPath | ||
} | ||
private async checkBinaryExists(binPath: string): Promise<boolean> { | ||
return await fs | ||
.stat(binPath) | ||
.then(() => true) | ||
.catch(() => false) | ||
} | ||
private async rmBinary(binPath: string): Promise<boolean> { | ||
return await fs | ||
.rm(binPath, { force: true }) | ||
.then(() => true) | ||
.catch(() => false) | ||
} | ||
private async checkBinaryValid(binPath: string): Promise<boolean> { | ||
return await new Promise<boolean>((resolve) => { | ||
try { | ||
execFile(binPath, ["version"], (err) => { | ||
if (err) { | ||
this.output.appendLine("Check for binary corruption: " + err) | ||
} | ||
resolve(err === null) | ||
}) | ||
} catch (ex) { | ||
this.output.appendLine("The cached binary cannot be executed: " + ex) | ||
resolve(false) | ||
} | ||
}) | ||
} | ||
private async updateSessionToken() { | ||
const token = await this.getSessionToken() | ||
if (token) { | ||