fb6e655de8
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).
132 lines
4.2 KiB
TypeScript
132 lines
4.2 KiB
TypeScript
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<GithubRelease> {
|
|
|
|
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<void> {
|
|
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<void>(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
|
|
});
|
|
}
|