11106: internal: Remove network access from Code extension r=lnicola a=lnicola



Co-authored-by: Laurențiu Nicola <lnicola@dend.ro>
This commit is contained in:
bors[bot] 2021-12-23 12:43:54 +00:00 committed by GitHub
commit cb0e270c02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 54 additions and 667 deletions

View file

@ -1 +1 @@
See the [Privacy](https://rust-analyzer.github.io/manual.html#security) section of the user manual.
See the [Privacy](https://rust-analyzer.github.io/manual.html#privacy) section of the user manual.

View file

@ -653,10 +653,12 @@ Here is a **non-exhaustive** list of ways to make rust-analyzer execute arbitrar
The LSP server performs no network access in itself, but runs `cargo metadata` which will update or download the crate registry and the source code of the project dependencies.
If enabled (the default), build scripts and procedural macros can do anything.
The Code extension automatically connects to GitHub to download updated LSP binaries and, if the nightly channel is selected, to perform update checks using the GitHub API. For `rust-analyzer` developers, using `cargo xtask release` uses the same API to put together the release notes.
The Code extension does not access the network.
Any other editor plugins are not under the control of the `rust-analyzer` developers. For any privacy concerns, you should check with their respective developers.
For `rust-analyzer` developers, `cargo xtask release` uses the GitHub API to put together the release notes.
== Features
include::./generated_features.adoc[]

View file

@ -11,8 +11,6 @@
"dependencies": {
"d3": "^7.2.0",
"d3-graphviz": "^4.0.0",
"https-proxy-agent": "^5.0.0",
"node-fetch": "^3.0.3",
"vscode-languageclient": "8.0.0-next.2"
},
"devDependencies": {
@ -345,6 +343,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"dependencies": {
"debug": "4"
},
@ -1274,18 +1273,11 @@
"node": ">=12"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
"integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==",
"engines": {
"node": ">= 12"
}
},
"node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
@ -2059,27 +2051,6 @@
"pend": "~1.2.0"
}
},
"node_modules/fetch-blob": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.3.tgz",
"integrity": "sha512-ax1Y5I9w+9+JiM+wdHkhBoxew+zG4AJ2SvAD1v1szpddUIiPERVGBxrMcB2ZqW0Y3PP8bOWYv2zqQq1Jp2kqUQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"dependencies": {
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -2123,17 +2094,6 @@
"integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==",
"dev": true
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@ -2403,6 +2363,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"dev": true,
"dependencies": {
"agent-base": "6",
"debug": "4"
@ -2770,7 +2731,8 @@
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/mute-stream": {
"version": "0.0.8",
@ -2814,23 +2776,6 @@
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==",
"dev": true
},
"node_modules/node-fetch": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.1.0.tgz",
"integrity": "sha512-QU0WbIfMUjd5+MUzQOYhenAazakV7Irh1SGkWCsRzBwvm4fAhzEUaHMJ6QLP7gWT6WO9/oH2zhKMMGMuIrDyKw==",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.2",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
@ -3866,14 +3811,6 @@
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.3.tgz",
"integrity": "sha512-VQcXnhKYxUW6OiRMhG++SzmZYMJwusXknJGd+FfdOnS1yHAo734OHyR0e2eEHDlv0/oWc8RZPgx/VKSKyondVg=="
},
"node_modules/web-streams-polyfill": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz",
"integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==",
"engines": {
"node": ">= 8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -4170,6 +4107,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"requires": {
"debug": "4"
}
@ -4874,15 +4812,11 @@
"d3-transition": "2 - 3"
}
},
"data-uri-to-buffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
"integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA=="
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
@ -5434,14 +5368,6 @@
"pend": "~1.2.0"
}
},
"fetch-blob": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.3.tgz",
"integrity": "sha512-ax1Y5I9w+9+JiM+wdHkhBoxew+zG4AJ2SvAD1v1szpddUIiPERVGBxrMcB2ZqW0Y3PP8bOWYv2zqQq1Jp2kqUQ==",
"requires": {
"web-streams-polyfill": "^3.0.3"
}
},
"file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -5476,14 +5402,6 @@
"integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==",
"dev": true
},
"formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"requires": {
"fetch-blob": "^3.1.2"
}
},
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@ -5696,6 +5614,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"dev": true,
"requires": {
"agent-base": "6",
"debug": "4"
@ -5978,7 +5897,8 @@
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"mute-stream": {
"version": "0.0.8",
@ -6021,16 +5941,6 @@
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==",
"dev": true
},
"node-fetch": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.1.0.tgz",
"integrity": "sha512-QU0WbIfMUjd5+MUzQOYhenAazakV7Irh1SGkWCsRzBwvm4fAhzEUaHMJ6QLP7gWT6WO9/oH2zhKMMGMuIrDyKw==",
"requires": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.2",
"formdata-polyfill": "^4.0.10"
}
},
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
@ -6827,11 +6737,6 @@
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.3.tgz",
"integrity": "sha512-VQcXnhKYxUW6OiRMhG++SzmZYMJwusXknJGd+FfdOnS1yHAo734OHyR0e2eEHDlv0/oWc8RZPgx/VKSKyondVg=="
},
"web-streams-polyfill": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz",
"integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA=="
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -36,8 +36,6 @@
"test": "node ./out/tests/runTests.js"
},
"dependencies": {
"https-proxy-agent": "^5.0.0",
"node-fetch": "^3.0.3",
"vscode-languageclient": "8.0.0-next.2",
"d3": "^7.2.0",
"d3-graphviz": "^4.0.0"
@ -305,24 +303,6 @@
"default": true,
"description": "Whether inlay hints font size should be smaller than editor's font size."
},
"rust-analyzer.updates.channel": {
"type": "string",
"enum": [
"stable",
"nightly"
],
"default": "stable",
"markdownEnumDescriptions": [
"`stable` updates are shipped weekly, they don't contain cutting-edge features from VSCode proposed APIs but have less bugs in general.",
"`nightly` updates are shipped daily (extension updates automatically by downloading artifacts directly from GitHub), they contain cutting-edge features and latest bug fixes. These releases help us get your feedback very quickly and speed up rust-analyzer development **drastically**."
],
"markdownDescription": "Choose `nightly` updates to get the latest features and bug fixes every day. While `stable` releases occur weekly and don't contain cutting-edge features from VSCode proposed APIs."
},
"rust-analyzer.updates.askBeforeDownload": {
"type": "boolean",
"default": false,
"description": "Whether to ask for permission before downloading any files from the Internet."
},
"rust-analyzer.server.path": {
"type": [
"null",

View file

@ -8,11 +8,6 @@ const NIGHTLY_TAG = "nightly";
export type RunnableEnvCfg = undefined | Record<string, string> | { mask?: string; env: Record<string, string> }[];
export class ProxySettings {
proxy?: string = undefined;
strictSSL: boolean = true;
}
export class Config {
readonly extensionId = "matklad.rust-analyzer";
@ -24,7 +19,6 @@ export class Config {
"procMacro",
"files",
"highlighting",
"updates.channel",
"lens", // works as lens.*
]
.map(opt => `${this.rootSection}.${opt}`);
@ -36,11 +30,9 @@ export class Config {
} = vscode.extensions.getExtension(this.extensionId)!.packageJSON;
readonly globalStorageUri: vscode.Uri;
readonly installUri: vscode.Uri;
constructor(ctx: vscode.ExtensionContext) {
this.globalStorageUri = ctx.globalStorageUri;
this.installUri = ctx.extensionUri;
vscode.workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, ctx.subscriptions);
this.refreshLogging();
}
@ -103,21 +95,7 @@ export class Config {
return this.get<null | string>("server.path") ?? this.get<null | string>("serverPath");
}
get serverExtraEnv() { return this.get<Env | null>("server.extraEnv") ?? {}; }
get channel() { return this.get<UpdatesChannel>("updates.channel"); }
get askBeforeDownload() { return this.get<boolean>("updates.askBeforeDownload"); }
get traceExtension() { return this.get<boolean>("trace.extension"); }
get proxySettings(): ProxySettings {
const proxy = vscode
.workspace
.getConfiguration('http')
.get<null | string>("proxy")! || process.env["https_proxy"] || process.env["HTTPS_PROXY"];
const strictSSL = vscode.workspace.getConfiguration("http").get<boolean>("proxyStrictSSL") ?? true;
return {
proxy: proxy,
strictSSL: strictSSL,
};
}
get inlayHints() {
return {

View file

@ -5,12 +5,11 @@ import * as commands from './commands';
import { activateInlayHints } from './inlay_hints';
import { Ctx } from './ctx';
import { Config } from './config';
import { log, assert, isValidExecutable, isRustDocument } from './util';
import { log, isValidExecutable, isRustDocument } from './util';
import { PersistentState } from './persistent_state';
import { fetchRelease, download } from './net';
import { activateTaskProvider } from './tasks';
import { setContextValue } from './util';
import { exec, spawnSync } from 'child_process';
import { exec } from 'child_process';
let ctx: Ctx | undefined;
@ -28,14 +27,9 @@ export async function activate(context: vscode.ExtensionContext) {
async function tryActivate(context: vscode.ExtensionContext) {
const config = new Config(context);
const state = new PersistentState(context.globalState);
const serverPath = await bootstrap(config, state).catch(err => {
const serverPath = await bootstrap(context, config, state).catch(err => {
let message = "bootstrap error. ";
if (err.code === "EBUSY" || err.code === "ETXTBSY" || err.code === "EPERM") {
message += "Other vscode windows might be using rust-analyzer, ";
message += "you should close them and reload this window to retry. ";
}
message += 'See the logs in "OUTPUT > Rust Analyzer Client" (should open automatically). ';
message += 'To enable verbose logs use { "rust-analyzer.trace.extension": true }';
@ -111,10 +105,6 @@ async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) {
await activate(context).catch(log.error);
});
ctx.registerCommand('updateGithubToken', ctx => async () => {
await queryForGithubToken(new PersistentState(ctx.globalState));
});
ctx.registerCommand('analyzerStatus', commands.analyzerStatus);
ctx.registerCommand('memoryUsage', commands.memoryUsage);
ctx.registerCommand('shuffleCrateGraph', commands.shuffleCrateGraph);
@ -161,99 +151,8 @@ export async function deactivate() {
ctx = undefined;
}
async function bootstrap(config: Config, state: PersistentState): Promise<string> {
await vscode.workspace.fs.createDirectory(config.globalStorageUri).then();
if (!config.currentExtensionIsNightly) {
await state.updateNightlyReleaseId(undefined);
}
await bootstrapExtension(config, state);
const path = await bootstrapServer(config, state);
return path;
}
async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
if (config.package.releaseTag === null) return;
if (config.channel === "stable") {
if (config.currentExtensionIsNightly) {
void vscode.window.showWarningMessage(
`You are running a nightly version of rust-analyzer extension. ` +
`To switch to stable, uninstall the extension and re-install it from the marketplace`
);
}
return;
};
if (serverPath(config)) return;
const now = Date.now();
const isInitialNightlyDownload = state.nightlyReleaseId === undefined;
if (config.currentExtensionIsNightly) {
// Check if we should poll github api for the new nightly version
// if we haven't done it during the past hour
const lastCheck = state.lastCheck;
const anHour = 60 * 60 * 1000;
const shouldCheckForNewNightly = isInitialNightlyDownload || (now - (lastCheck ?? 0)) > anHour;
if (!shouldCheckForNewNightly) return;
}
const latestNightlyRelease = await downloadWithRetryDialog(state, async () => {
return await fetchRelease("nightly", state.githubToken, config.proxySettings);
}).catch(async (e) => {
log.error(e);
if (isInitialNightlyDownload) {
await vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly: ${e}`);
}
return;
});
if (latestNightlyRelease === undefined) {
if (isInitialNightlyDownload) {
await vscode.window.showErrorMessage("Failed to download rust-analyzer nightly: empty release contents returned");
}
return;
}
if (config.currentExtensionIsNightly && latestNightlyRelease.id === state.nightlyReleaseId) return;
const userResponse = await vscode.window.showInformationMessage(
"New version of rust-analyzer (nightly) is available (requires reload).",
"Update"
);
if (userResponse !== "Update") return;
let arch = process.arch;
if (arch === "ia32") {
arch = "x64";
}
let platform = process.platform as string;
if (platform === "linux" && isMusl()) {
platform = "alpine";
}
const artifactName = `rust-analyzer-${platform}-${arch}.vsix`;
const artifact = latestNightlyRelease.assets.find(artifact => artifact.name === artifactName);
assert(!!artifact, `Bad release: ${JSON.stringify(latestNightlyRelease)}`);
const dest = vscode.Uri.joinPath(config.globalStorageUri, "rust-analyzer.vsix");
await downloadWithRetryDialog(state, async () => {
await download({
url: artifact.browser_download_url,
dest,
progressTitle: "Downloading rust-analyzer extension",
proxySettings: config.proxySettings,
});
});
await vscode.commands.executeCommand("workbench.extensions.installExtension", dest);
await vscode.workspace.fs.delete(dest);
await state.updateNightlyReleaseId(latestNightlyRelease.id);
await state.updateLastCheck(now);
await vscode.commands.executeCommand("workbench.action.reloadWindow");
}
async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
const path = await getServer(config, state);
async function bootstrap(context: vscode.ExtensionContext, config: Config, state: PersistentState): Promise<string> {
const path = await getServer(context, config, state);
if (!path) {
throw new Error(
"Rust Analyzer Language Server is not available. " +
@ -318,7 +217,7 @@ async function patchelf(dest: vscode.Uri): Promise<void> {
);
}
async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
async function getServer(context: vscode.ExtensionContext, config: Config, state: PersistentState): Promise<string | undefined> {
const explicitPath = serverPath(config);
if (explicitPath) {
if (explicitPath.startsWith("~/")) {
@ -328,85 +227,39 @@ async function getServer(config: Config, state: PersistentState): Promise<string
};
if (config.package.releaseTag === null) return "rust-analyzer";
const platforms: { [key: string]: string } = {
"ia32 win32": "x86_64-pc-windows-msvc",
"x64 win32": "x86_64-pc-windows-msvc",
"x64 linux": "x86_64-unknown-linux-gnu",
"x64 darwin": "x86_64-apple-darwin",
"arm64 win32": "aarch64-pc-windows-msvc",
"arm64 linux": "aarch64-unknown-linux-gnu",
"arm64 darwin": "aarch64-apple-darwin",
};
let platform = platforms[`${process.arch} ${process.platform}`];
if (platform === undefined) {
await vscode.window.showErrorMessage(
"Unfortunately we don't ship binaries for your platform yet. " +
"You need to manually clone rust-analyzer repository and " +
"run `cargo xtask install --server` to build the language server from sources. " +
"If you feel that your platform should be supported, please create an issue " +
"about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
"will consider it."
);
return undefined;
}
if (platform === "x86_64-unknown-linux-gnu" && isMusl()) {
platform = "x86_64-unknown-linux-musl";
}
const ext = platform.indexOf("-windows-") !== -1 ? ".exe" : "";
const dest = vscode.Uri.joinPath(config.globalStorageUri, `rust-analyzer-${platform}${ext}`);
const bundled = vscode.Uri.joinPath(config.installUri, "server", `rust-analyzer${ext}`);
const ext = process.platform === "win32" ? ".exe" : "";
const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `rust-analyzer${ext}`);
const bundledExists = await vscode.workspace.fs.stat(bundled).then(() => true, () => false);
let exists = await vscode.workspace.fs.stat(dest).then(() => true, () => false);
if (bundledExists) {
let server = bundled;
if (await isNixOs()) {
await vscode.workspace.fs.createDirectory(config.globalStorageUri).then();
const dest = vscode.Uri.joinPath(config.globalStorageUri, `rust-analyzer${ext}`);
let exists = await vscode.workspace.fs.stat(dest).then(() => true, () => false);
if (exists && config.package.version !== state.serverVersion) {
await vscode.workspace.fs.delete(dest);
exists = false;
}
if (!exists) {
await vscode.workspace.fs.copy(bundled, dest);
await patchelf(dest);
server = dest;
}
}
await state.updateServerVersion(config.package.version);
if (!await isNixOs()) {
return bundled.fsPath;
}
if (!exists) {
await vscode.workspace.fs.copy(bundled, dest);
await patchelf(dest);
exists = true;
}
}
if (!exists) {
await state.updateServerVersion(undefined);
return server.fsPath;
}
if (state.serverVersion === config.package.version) return dest.fsPath;
if (config.askBeforeDownload) {
const userResponse = await vscode.window.showInformationMessage(
`Language server version ${config.package.version} for rust-analyzer is not installed.`,
"Download now"
);
if (userResponse !== "Download now") return dest.fsPath;
}
const releaseTag = config.package.releaseTag;
const release = await downloadWithRetryDialog(state, async () => {
return await fetchRelease(releaseTag, state.githubToken, config.proxySettings);
});
const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
await downloadWithRetryDialog(state, async () => {
await download({
url: artifact.browser_download_url,
dest,
progressTitle: "Downloading rust-analyzer server",
gunzip: true,
mode: 0o755,
proxySettings: config.proxySettings,
});
});
// Patching executable if that's NixOS.
if (await isNixOs()) {
await patchelf(dest);
}
await state.updateServerVersion(config.package.version);
return dest.fsPath;
await state.updateServerVersion(undefined);
await vscode.window.showErrorMessage(
"Unfortunately we don't ship binaries for your platform yet. " +
"You need to manually clone the rust-analyzer repository and " +
"run `cargo xtask install --server` to build the language server from sources. " +
"If you feel that your platform should be supported, please create an issue " +
"about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
"will consider it."
);
return undefined;
}
function serverPath(config: Config): string | null {
@ -422,85 +275,14 @@ async function isNixOs(): Promise<boolean> {
}
}
function isMusl(): boolean {
// We can detect Alpine by checking `/etc/os-release` but not Void Linux musl.
// Instead, we run `ldd` since it advertises the libc which it belongs to.
const res = spawnSync("ldd", ["--version"]);
return res.stderr != null && res.stderr.indexOf("musl libc") >= 0;
}
async function downloadWithRetryDialog<T>(state: PersistentState, downloadFunc: () => Promise<T>): Promise<T> {
while (true) {
try {
return await downloadFunc();
} catch (e) {
const selected = await vscode.window.showErrorMessage("Failed to download: " + e.message, {}, {
title: "Update Github Auth Token",
updateToken: true,
}, {
title: "Retry download",
retry: true,
}, {
title: "Dismiss",
});
if (selected?.updateToken) {
await queryForGithubToken(state);
continue;
} else if (selected?.retry) {
continue;
}
throw e;
};
}
}
async function queryForGithubToken(state: PersistentState): Promise<void> {
const githubTokenOptions: vscode.InputBoxOptions = {
value: state.githubToken,
password: true,
prompt: `
This dialog allows to store a Github authorization token.
The usage of an authorization token will increase the rate
limit on the use of Github APIs and can thereby prevent getting
throttled.
Auth tokens can be created at https://github.com/settings/tokens`,
};
const newToken = await vscode.window.showInputBox(githubTokenOptions);
if (newToken === undefined) {
// The user aborted the dialog => Do not update the stored token
return;
}
if (newToken === "") {
log.info("Clearing github token");
await state.updateGithubToken(undefined);
} else {
log.info("Storing new github token");
await state.updateGithubToken(newToken);
}
}
function warnAboutExtensionConflicts() {
const conflicting = [
["rust-analyzer", "matklad.rust-analyzer"],
["Rust", "rust-lang.rust"],
["Rust", "kalitaalexey.vscode-rust"],
];
const found = conflicting.filter(
nameId => vscode.extensions.getExtension(nameId[1]) !== undefined);
if (found.length > 1) {
const fst = found[0];
const sec = found[1];
if (vscode.extensions.getExtension("rust-lang.rust")) {
vscode.window.showWarningMessage(
`You have both the ${fst[0]} (${fst[1]}) and ${sec[0]} (${sec[1]}) ` +
`You have both the rust-analyzer (matklad.rust-analyzer) and Rust (rust-lang.rust) ` +
"plugins enabled. These are known to conflict and cause various functions of " +
"both plugins to not work correctly. You should disable one of them.", "Got it")
.then(() => { }, console.error);
};
}
}
/**

View file

@ -1,228 +0,0 @@
import fetch from "node-fetch";
var HttpsProxyAgent = require('https-proxy-agent');
import * as vscode from "vscode";
import * as stream from "stream";
import * as crypto from "crypto";
import * as fs from "fs";
import * as zlib from "zlib";
import * as util from "util";
import * as path from "path";
import { log, assert } from "./util";
import * as https from "https";
import { ProxySettings } from "./config";
const pipeline = util.promisify(stream.pipeline);
const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
const OWNER = "rust-analyzer";
const REPO = "rust-analyzer";
function makeHttpAgent(proxy: string | null | undefined, options?: https.AgentOptions) {
if (proxy) {
return new HttpsProxyAgent({ ...options, ...new URL(proxy) });
} else {
return new https.Agent(options);
}
}
export async function fetchRelease(
releaseTag: string,
githubToken: string | null | undefined,
proxySettings: ProxySettings,
): 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 headers: Record<string, string> = { Accept: "application/vnd.github.v3+json" };
if (githubToken != null) {
headers.Authorization = "token " + githubToken;
}
const response = await (() => {
if (proxySettings.proxy) {
log.debug(`Fetching release metadata via proxy: ${proxySettings.proxy}`);
}
const options: any = {};
if (proxySettings.strictSSL) {
options["rejectUnauthorized"] = false;
}
const agent = makeHttpAgent(proxySettings.proxy, options);
return fetch(requestUrl, { headers: headers, agent: agent });
})();
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 `unknown` to `GithubRelease`)
const release = await response.json() as GithubRelease;
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: vscode.Uri;
}>;
}
interface DownloadOpts {
progressTitle: string;
url: vscode.Uri;
dest: vscode.Uri;
mode?: number;
gunzip?: boolean;
proxySettings: ProxySettings;
}
export async function download(opts: DownloadOpts) {
// Put artifact into a temporary file (in the same dir for simplicity)
// to prevent partially downloaded files when user kills vscode
// This also avoids overwriting running executables
const randomHex = crypto.randomBytes(5).toString("hex");
const rawDest = path.parse(opts.dest.fsPath);
const oldServerPath = vscode.Uri.joinPath(vscode.Uri.file(rawDest.dir), `${rawDest.name}-stale-${randomHex}${rawDest.ext}`);
const tempFilePath = vscode.Uri.joinPath(vscode.Uri.file(rawDest.dir), `${rawDest.name}${randomHex}`);
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
cancellable: false,
title: opts.progressTitle
},
async (progress, _cancellationToken) => {
let lastPercentage = 0;
await downloadFile(opts.url, tempFilePath, opts.mode, !!opts.gunzip, opts.proxySettings, (readBytes, totalBytes) => {
const newPercentage = Math.round((readBytes / totalBytes) * 100);
if (newPercentage !== lastPercentage) {
progress.report({
message: `${newPercentage.toFixed(0)}%`,
increment: newPercentage - lastPercentage
});
lastPercentage = newPercentage;
}
});
}
);
// Try to rename a running server to avoid EPERM on Windows
// NB: this can lead to issues if a running Code instance tries to restart the server.
try {
await vscode.workspace.fs.rename(opts.dest, oldServerPath, { overwrite: true });
log.info(`Renamed old server binary ${opts.dest.fsPath} to ${oldServerPath.fsPath}`);
} catch (err) {
const fsErr = err as vscode.FileSystemError;
// This is supposed to return `FileNotFound` (spelled as `EntryNotFound`)
// but instead `code` is `Unknown` and `name` is `EntryNotFound (FileSystemError) (FileSystemError)`.
// https://github.com/rust-analyzer/rust-analyzer/pull/10222
if (!fsErr.code || fsErr.code !== "EntryNotFound" && fsErr.name.indexOf("EntryNotFound") === -1) {
log.error(`Cannot rename existing server instance: ${err}"`);
}
}
try {
await vscode.workspace.fs.rename(tempFilePath, opts.dest, { overwrite: true });
} catch (err) {
log.error(`Cannot update server binary: ${err}`);
}
// Now try to remove any stale server binaries
const serverDir = vscode.Uri.file(rawDest.dir);
try {
const entries = await vscode.workspace.fs.readDirectory(serverDir);
for (const [entry, _] of entries) {
try {
if (entry.includes(`${rawDest.name}-stale-`)) {
const uri = vscode.Uri.joinPath(serverDir, entry);
try {
await vscode.workspace.fs.delete(uri);
log.info(`Removed old server binary ${uri.fsPath}`);
} catch (err) {
log.error(`Unable to remove old server binary ${uri.fsPath}`);
}
}
} catch (err) {
log.error(`Unable to parse ${entry}`);
}
}
} catch (err) {
log.error(`Unable to enumerate contents of ${serverDir.fsPath}`);
}
}
async function downloadFile(
url: vscode.Uri,
destFilePath: vscode.Uri,
mode: number | undefined,
gunzip: boolean,
proxySettings: ProxySettings,
onProgress: (readBytes: number, totalBytes: number) => void
): Promise<void> {
const urlString = url.toString();
const res = await (() => {
if (proxySettings.proxy) {
log.debug(`Downloading ${urlString} via proxy: ${proxySettings.proxy}`);
}
const options: any = {};
if (proxySettings.strictSSL) {
options["rejectUnauthorized"] = false;
}
const agent = makeHttpAgent(proxySettings.proxy, options);
return fetch(urlString, { agent: agent });
})();
if (!res.ok) {
log.error("Error", res.status, "while downloading file from", urlString);
log.error({ body: await res.text(), headers: res.headers });
throw new Error(`Got response ${res.status} when trying to download a file.`);
}
if (!res.body) {
log.error("Empty body while downloading file from", urlString);
log.error({ headers: res.headers });
throw new Error(`Got empty body 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", urlString, "to", destFilePath.fsPath);
let readBytes = 0;
res.body.on("data", (chunk: Buffer) => {
readBytes += chunk.length;
onProgress(readBytes, totalBytes);
});
const destFileStream = fs.createWriteStream(destFilePath.fsPath, { mode });
const srcStream = gunzip ? res.body.pipe(zlib.createGunzip()) : res.body;
await pipeline(srcStream, destFileStream);
}

View file

@ -3,34 +3,13 @@ import { log } from './util';
export class PersistentState {
constructor(private readonly globalState: vscode.Memento) {
const { lastCheck, nightlyReleaseId, serverVersion } = this;
log.info("PersistentState:", { lastCheck, nightlyReleaseId, serverVersion });
}
/**
* Used to check for *nightly* updates once an hour.
*/
get lastCheck(): number | undefined {
return this.globalState.get("lastCheck");
}
async updateLastCheck(value: number) {
await this.globalState.update("lastCheck", value);
}
/**
* Release id of the *nightly* extension.
* Used to check if we should update.
*/
get nightlyReleaseId(): number | undefined {
return this.globalState.get("releaseId");
}
async updateNightlyReleaseId(value: number | undefined) {
await this.globalState.update("releaseId", value);
const { serverVersion } = this;
log.info("PersistentState:", { serverVersion });
}
/**
* Version of the extension that installed the server.
* Used to check if we need to update the server.
* Used to check if we need to run patchelf again on NixOS.
*/
get serverVersion(): string | undefined {
return this.globalState.get("serverVersion");
@ -38,15 +17,4 @@ export class PersistentState {
async updateServerVersion(value: string | undefined) {
await this.globalState.update("serverVersion", value);
}
/**
* Github authorization token.
* This is used for API requests against the Github API.
*/
get githubToken(): string | undefined {
return this.globalState.get("githubToken");
}
async updateGithubToken(value: string | undefined) {
await this.globalState.update("githubToken", value);
}
}