]> git.lizzy.rs Git - rust.git/blob - editors/code/src/main.ts
Use retry dialog also for downloads
[rust.git] / editors / code / src / main.ts
1 import * as vscode from 'vscode';
2 import * as path from "path";
3 import * as os from "os";
4 import { promises as fs, PathLike } from "fs";
5
6 import * as commands from './commands';
7 import { activateInlayHints } from './inlay_hints';
8 import { Ctx } from './ctx';
9 import { Config, NIGHTLY_TAG } from './config';
10 import { log, assert, isValidExecutable } from './util';
11 import { PersistentState } from './persistent_state';
12 import { fetchRelease, download } from './net';
13 import { activateTaskProvider } from './tasks';
14 import { setContextValue } from './util';
15 import { exec } from 'child_process';
16
17 let ctx: Ctx | undefined;
18
19 const RUST_PROJECT_CONTEXT_NAME = "inRustProject";
20
21 export async function activate(context: vscode.ExtensionContext) {
22     // For some reason vscode not always shows pop-up error notifications
23     // when an extension fails to activate, so we do it explicitly by ourselves.
24     // FIXME: remove this bit of code once vscode fixes this issue: https://github.com/microsoft/vscode/issues/101242
25     await tryActivate(context).catch(err => {
26         void vscode.window.showErrorMessage(`Cannot activate rust-analyzer: ${err.message}`);
27         throw err;
28     });
29 }
30
31 async function tryActivate(context: vscode.ExtensionContext) {
32     // Register a "dumb" onEnter command for the case where server fails to
33     // start.
34     //
35     // FIXME: refactor command registration code such that commands are
36     // **always** registered, even if the server does not start. Use API like
37     // this perhaps?
38     //
39     // ```TypeScript
40     // registerCommand(
41     //    factory: (Ctx) => ((Ctx) => any),
42     //    fallback: () => any = () => vscode.window.showErrorMessage(
43     //        "rust-analyzer is not available"
44     //    ),
45     // )
46     const defaultOnEnter = vscode.commands.registerCommand(
47         'rust-analyzer.onEnter',
48         () => vscode.commands.executeCommand('default:type', { text: '\n' }),
49     );
50     context.subscriptions.push(defaultOnEnter);
51
52     const config = new Config(context);
53     const state = new PersistentState(context.globalState);
54     const serverPath = await bootstrap(config, state).catch(err => {
55         let message = "bootstrap error. ";
56
57         if (err.code === "EBUSY" || err.code === "ETXTBSY" || err.code === "EPERM") {
58             message += "Other vscode windows might be using rust-analyzer, ";
59             message += "you should close them and reload this window to retry. ";
60         }
61
62         message += 'See the logs in "OUTPUT > Rust Analyzer Client" (should open automatically). ';
63         message += 'To enable verbose logs use { "rust-analyzer.trace.extension": true }';
64
65         log.error("Bootstrap error", err);
66         throw new Error(message);
67     });
68
69     const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
70     if (workspaceFolder === undefined) {
71         throw new Error("no folder is opened");
72     }
73
74     // Note: we try to start the server before we activate type hints so that it
75     // registers its `onDidChangeDocument` handler before us.
76     //
77     // This a horribly, horribly wrong way to deal with this problem.
78     ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath);
79
80     setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
81
82     // Commands which invokes manually via command palette, shortcut, etc.
83
84     // Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895
85     ctx.registerCommand('reload', _ => async () => {
86         void vscode.window.showInformationMessage('Reloading rust-analyzer...');
87         await deactivate();
88         while (context.subscriptions.length > 0) {
89             try {
90                 context.subscriptions.pop()!.dispose();
91             } catch (err) {
92                 log.error("Dispose error:", err);
93             }
94         }
95         await activate(context).catch(log.error);
96     });
97
98     ctx.registerCommand('analyzerStatus', commands.analyzerStatus);
99     ctx.registerCommand('memoryUsage', commands.memoryUsage);
100     ctx.registerCommand('reloadWorkspace', commands.reloadWorkspace);
101     ctx.registerCommand('matchingBrace', commands.matchingBrace);
102     ctx.registerCommand('joinLines', commands.joinLines);
103     ctx.registerCommand('parentModule', commands.parentModule);
104     ctx.registerCommand('syntaxTree', commands.syntaxTree);
105     ctx.registerCommand('expandMacro', commands.expandMacro);
106     ctx.registerCommand('run', commands.run);
107     ctx.registerCommand('debug', commands.debug);
108     ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
109
110     defaultOnEnter.dispose();
111     ctx.registerCommand('onEnter', commands.onEnter);
112
113     ctx.registerCommand('ssr', commands.ssr);
114     ctx.registerCommand('serverVersion', commands.serverVersion);
115     ctx.registerCommand('toggleInlayHints', commands.toggleInlayHints);
116
117     // Internal commands which are invoked by the server.
118     ctx.registerCommand('runSingle', commands.runSingle);
119     ctx.registerCommand('debugSingle', commands.debugSingle);
120     ctx.registerCommand('showReferences', commands.showReferences);
121     ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
122     ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction);
123     ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
124     ctx.registerCommand('gotoLocation', commands.gotoLocation);
125
126     ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
127
128     activateInlayHints(ctx);
129
130     vscode.workspace.onDidChangeConfiguration(
131         _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
132         null,
133         ctx.subscriptions,
134     );
135 }
136
137 export async function deactivate() {
138     setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
139     await ctx?.client.stop();
140     ctx = undefined;
141 }
142
143 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
144     await fs.mkdir(config.globalStoragePath, { recursive: true });
145
146     await bootstrapExtension(config, state);
147     const path = await bootstrapServer(config, state);
148
149     return path;
150 }
151
152 async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
153     if (config.package.releaseTag === null) return;
154     if (config.channel === "stable") {
155         if (config.package.releaseTag === NIGHTLY_TAG) {
156             void vscode.window.showWarningMessage(
157                 `You are running a nightly version of rust-analyzer extension. ` +
158                 `To switch to stable, uninstall the extension and re-install it from the marketplace`
159             );
160         }
161         return;
162     };
163
164     const now = Date.now();
165     if (config.package.releaseTag === NIGHTLY_TAG) {
166         // Check if we should poll github api for the new nightly version
167         // if we haven't done it during the past hour
168         const lastCheck = state.lastCheck;
169
170         const anHour = 60 * 60 * 1000;
171         const shouldCheckForNewNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
172
173         if (!shouldCheckForNewNightly) return;
174     }
175
176     const release = await performDownloadWithRetryDialog(async () => {
177         return await fetchRelease("nightly", state.githubToken);
178     }, state).catch((e) => {
179         log.error(e);
180         if (state.releaseId === undefined) { // Show error only for the initial download
181             vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
182         }
183         return undefined;
184     });
185     if (release === undefined || release.id === state.releaseId) return;
186
187     const userResponse = await vscode.window.showInformationMessage(
188         "New version of rust-analyzer (nightly) is available (requires reload).",
189         "Update"
190     );
191     if (userResponse !== "Update") return;
192
193     const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
194     assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
195
196     const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
197
198     await performDownloadWithRetryDialog(async () => {
199         // Unlinking the exe file before moving new one on its place should prevent ETXTBSY error.
200         await fs.unlink(dest).catch(err => {
201             if (err.code !== "ENOENT") throw err;
202         });
203
204         await download({
205             url: artifact.browser_download_url,
206             dest,
207             progressTitle: "Downloading rust-analyzer extension",
208         });
209     }, state);
210
211     await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
212     await fs.unlink(dest);
213
214     await state.updateReleaseId(release.id);
215     await state.updateLastCheck(now);
216     await vscode.commands.executeCommand("workbench.action.reloadWindow");
217 }
218
219 async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
220     const path = await getServer(config, state);
221     if (!path) {
222         throw new Error(
223             "Rust Analyzer Language Server is not available. " +
224             "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
225         );
226     }
227
228     log.info("Using server binary at", path);
229
230     if (!isValidExecutable(path)) {
231         throw new Error(`Failed to execute ${path} --version`);
232     }
233
234     return path;
235 }
236
237 async function patchelf(dest: PathLike): Promise<void> {
238     await vscode.window.withProgress(
239         {
240             location: vscode.ProgressLocation.Notification,
241             title: "Patching rust-analyzer for NixOS"
242         },
243         async (progress, _) => {
244             const expression = `
245             {src, pkgs ? import <nixpkgs> {}}:
246                 pkgs.stdenv.mkDerivation {
247                     name = "rust-analyzer";
248                     inherit src;
249                     phases = [ "installPhase" "fixupPhase" ];
250                     installPhase = "cp $src $out";
251                     fixupPhase = ''
252                     chmod 755 $out
253                     patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
254                     '';
255                 }
256             `;
257             const origFile = dest + "-orig";
258             await fs.rename(dest, origFile);
259             progress.report({ message: "Patching executable", increment: 20 });
260             await new Promise((resolve, reject) => {
261                 const handle = exec(`nix-build -E - --arg src '${origFile}' -o ${dest}`,
262                     (err, stdout, stderr) => {
263                         if (err != null) {
264                             reject(Error(stderr));
265                         } else {
266                             resolve(stdout);
267                         }
268                     });
269                 handle.stdin?.write(expression);
270                 handle.stdin?.end();
271             });
272             await fs.unlink(origFile);
273         }
274     );
275 }
276
277 async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
278     const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
279     if (explicitPath) {
280         if (explicitPath.startsWith("~/")) {
281             return os.homedir() + explicitPath.slice("~".length);
282         }
283         return explicitPath;
284     };
285     if (config.package.releaseTag === null) return "rust-analyzer";
286
287     let platform: string | undefined;
288     if (process.arch === "x64" || process.arch === "ia32") {
289         if (process.platform === "linux") platform = "linux";
290         if (process.platform === "darwin") platform = "mac";
291         if (process.platform === "win32") platform = "windows";
292     }
293     if (platform === undefined) {
294         vscode.window.showErrorMessage(
295             "Unfortunately we don't ship binaries for your platform yet. " +
296             "You need to manually clone rust-analyzer repository and " +
297             "run `cargo xtask install --server` to build the language server from sources. " +
298             "If you feel that your platform should be supported, please create an issue " +
299             "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
300             "will consider it."
301         );
302         return undefined;
303     }
304     const ext = platform === "windows" ? ".exe" : "";
305     const dest = path.join(config.globalStoragePath, `rust-analyzer-${platform}${ext}`);
306     const exists = await fs.stat(dest).then(() => true, () => false);
307     if (!exists) {
308         await state.updateServerVersion(undefined);
309     }
310
311     if (state.serverVersion === config.package.version) return dest;
312
313     if (config.askBeforeDownload) {
314         const userResponse = await vscode.window.showInformationMessage(
315             `Language server version ${config.package.version} for rust-analyzer is not installed.`,
316             "Download now"
317         );
318         if (userResponse !== "Download now") return dest;
319     }
320
321     const releaseTag = config.package.releaseTag;
322     const release = await performDownloadWithRetryDialog(async () => {
323         return await fetchRelease(releaseTag, state.githubToken);
324     }, state);
325     const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
326     assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
327
328     await performDownloadWithRetryDialog(async () => {
329         // Unlinking the exe file before moving new one on its place should prevent ETXTBSY error.
330         await fs.unlink(dest).catch(err => {
331             if (err.code !== "ENOENT") throw err;
332         });
333
334         await download({
335             url: artifact.browser_download_url,
336             dest,
337             progressTitle: "Downloading rust-analyzer server",
338             gunzip: true,
339             mode: 0o755
340         });
341     }, state);
342
343     // Patching executable if that's NixOS.
344     if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) {
345         await patchelf(dest);
346     }
347
348     await state.updateServerVersion(config.package.version);
349     return dest;
350 }
351
352 async function performDownloadWithRetryDialog<T>(downloadFunc: () => Promise<T>, state: PersistentState): Promise<T> {
353     while (true) {
354         try {
355             return await downloadFunc();
356         } catch (e) {
357             const selected = await vscode.window.showErrorMessage("Failed perform download: " + e.message, {}, {
358                 title: "Update Github Auth Token",
359                 updateToken: true,
360             }, {
361                 title: "Retry download",
362                 retry: true,
363             }, {
364                 title: "Dismiss",
365             });
366
367             if (selected?.updateToken) {
368                 await queryForGithubToken(state);
369                 continue;
370             } else if (selected?.retry) {
371                 continue;
372             }
373             throw e;
374         };
375     }
376
377 }
378
379 async function queryForGithubToken(state: PersistentState): Promise<void> {
380     const githubTokenOptions: vscode.InputBoxOptions = {
381         value: state.githubToken,
382         password: true,
383         prompt: `
384             This dialog allows to store a Github authorization token.
385             The usage of an authorization token will increase the rate
386             limit on the use of Github APIs and can thereby prevent getting
387             throttled.
388             Auth tokens can be created at https://github.com/settings/tokens`,
389     };
390
391     const newToken = await vscode.window.showInputBox(githubTokenOptions);
392     if (newToken) {
393         log.info("Storing new github token");
394         await state.updateGithubToken(newToken);
395     }
396 }