From fb6e655de8a44c65275ad45a27bf5bd684670ba0 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 17 Mar 2020 12:44:31 +0100 Subject: Rewrite auto-update Everything now happens in main.ts, in the bootstrap family of functions. The current flow is: * check everything only on extension installation. * if the user is on nightly channel, try to download the nightly extension and reload. * when we install nightly extension, we persist its release id, so that we can check if the current release is different. * if server binary was not downloaded by the current version of the extension, redownload it (we persist the version of ext that downloaded the server). --- editors/code/src/net.ts | 131 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 editors/code/src/net.ts (limited to 'editors/code/src/net.ts') diff --git a/editors/code/src/net.ts b/editors/code/src/net.ts new file mode 100644 index 000000000..492213937 --- /dev/null +++ b/editors/code/src/net.ts @@ -0,0 +1,131 @@ +import fetch from "node-fetch"; +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as stream from "stream"; +import * as util from "util"; +import { log, assert } from "./util"; + +const pipeline = util.promisify(stream.pipeline); + +const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; +const OWNER = "rust-analyzer"; +const REPO = "rust-analyzer"; + +export async function fetchRelease( + releaseTag: string +): Promise { + + const apiEndpointPath = `/repos/${OWNER}/${REPO}/releases/tags/${releaseTag}`; + + const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; + + log.debug("Issuing request for released artifacts metadata to", requestUrl); + + const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } }); + + if (!response.ok) { + log.error("Error fetching artifact release info", { + requestUrl, + releaseTag, + response: { + headers: response.headers, + status: response.status, + body: await response.text(), + } + }); + + throw new Error( + `Got response ${response.status} when trying to fetch ` + + `release info for ${releaseTag} release` + ); + } + + // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) + const release: GithubRelease = await response.json(); + return release; +} + +// We omit declaration of tremendous amount of fields that we are not using here +export interface GithubRelease { + name: string; + id: number; + // eslint-disable-next-line camelcase + published_at: string; + assets: Array<{ + name: string; + // eslint-disable-next-line camelcase + browser_download_url: string; + }>; +} + + +export async function download( + downloadUrl: string, + destinationPath: string, + progressTitle: string, + { mode }: { mode?: number } = {}, +) { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, + title: progressTitle + }, + async (progress, _cancellationToken) => { + let lastPercentage = 0; + await downloadFile(downloadUrl, destinationPath, mode, (readBytes, totalBytes) => { + const newPercentage = (readBytes / totalBytes) * 100; + progress.report({ + message: newPercentage.toFixed(0) + "%", + increment: newPercentage - lastPercentage + }); + + lastPercentage = newPercentage; + }); + } + ); +} + +/** + * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`. + * `onProgress` callback is called on recieveing each chunk of bytes + * to track the progress of downloading, it gets the already read and total + * amount of bytes to read as its parameters. + */ +async function downloadFile( + url: string, + destFilePath: fs.PathLike, + mode: number | undefined, + onProgress: (readBytes: number, totalBytes: number) => void +): Promise { + const res = await fetch(url); + + if (!res.ok) { + log.error("Error", res.status, "while downloading file from", url); + log.error({ body: await res.text(), headers: res.headers }); + + throw new Error(`Got response ${res.status} when trying to download a file.`); + } + + const totalBytes = Number(res.headers.get('content-length')); + assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol"); + + log.debug("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath); + + let readBytes = 0; + res.body.on("data", (chunk: Buffer) => { + readBytes += chunk.length; + onProgress(readBytes, totalBytes); + }); + + const destFileStream = fs.createWriteStream(destFilePath, { mode }); + + await pipeline(res.body, destFileStream); + return new Promise(resolve => { + destFileStream.on("close", resolve); + destFileStream.destroy(); + + // Details on workaround: https://github.com/rust-analyzer/rust-analyzer/pull/3092#discussion_r378191131 + // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 + }); +} -- cgit v1.2.3