Skip to content

Commit fe25485

Browse files
bors[bot]Veetaha
andauthored
Merge #4963
4963: Download artifacts into tmp dir r=matklad a=Veetaha This should prevent partially downloaded files in cases when the user closes vsode before the download is complete. There is also a new more descriptive error message when the user has multiple vscode windows open and tries to download the server. Related: #4938 (comment) Co-authored-by: Veetaha <veetaha2@gmail.com>
2 parents 04d6426 + dceb818 commit fe25485

File tree

2 files changed

+60
-11
lines changed

2 files changed

+60
-11
lines changed

editors/code/src/main.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,16 @@ export async function activate(context: vscode.ExtensionContext) {
4242

4343
const config = new Config(context);
4444
const state = new PersistentState(context.globalState);
45-
const serverPath = await bootstrap(config, state);
45+
const serverPath = await bootstrap(config, state).catch(err => {
46+
let message = "Failed to bootstrap rust-analyzer.";
47+
if (err.code === "EBUSY" || err.code === "ETXTBSY") {
48+
message += " Other vscode windows might be using rust-analyzer, " +
49+
"you should close them and reload this window to retry.";
50+
}
51+
message += " Open \"Help > Toggle Developer Tools > Console\" to see the logs";
52+
log.error("Bootstrap error", err);
53+
throw new Error(message);
54+
});
4655

4756
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
4857
if (workspaceFolder === undefined) {
@@ -285,6 +294,11 @@ async function getServer(config: Config, state: PersistentState): Promise<string
285294
const artifact = release.assets.find(artifact => artifact.name === binaryName);
286295
assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
287296

297+
// Unlinking the exe file before moving new one on its place should prevent ETXTBSY error.
298+
await fs.unlink(dest).catch(err => {
299+
if (err.code !== "ENOENT") throw err;
300+
});
301+
288302
await download(artifact.browser_download_url, dest, "Downloading rust-analyzer server", { mode: 0o755 });
289303

290304
// Patching executable if that's NixOS.

editors/code/src/net.ts

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import fetch from "node-fetch";
22
import * as vscode from "vscode";
3-
import * as fs from "fs";
43
import * as stream from "stream";
4+
import * as fs from "fs";
5+
import * as os from "os";
6+
import * as path from "path";
57
import * as util from "util";
68
import { log, assert } from "./util";
79

@@ -87,7 +89,7 @@ export async function download(
8789
}
8890

8991
/**
90-
* Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`.
92+
* Downloads file from `url` and stores it at `destFilePath` with `mode` (unix permissions).
9193
* `onProgress` callback is called on recieveing each chunk of bytes
9294
* to track the progress of downloading, it gets the already read and total
9395
* amount of bytes to read as its parameters.
@@ -118,13 +120,46 @@ async function downloadFile(
118120
onProgress(readBytes, totalBytes);
119121
});
120122

121-
const destFileStream = fs.createWriteStream(destFilePath, { mode });
122-
123-
await pipeline(res.body, destFileStream);
124-
return new Promise<void>(resolve => {
125-
destFileStream.on("close", resolve);
126-
destFileStream.destroy();
127-
// This workaround is awaiting to be removed when vscode moves to newer nodejs version:
128-
// https://github.com/rust-analyzer/rust-analyzer/issues/3167
123+
// Put the artifact into a temporary folder to prevent partially downloaded files when user kills vscode
124+
await withTempFile(async tempFilePath => {
125+
const destFileStream = fs.createWriteStream(tempFilePath, { mode });
126+
await pipeline(res.body, destFileStream);
127+
await new Promise<void>(resolve => {
128+
destFileStream.on("close", resolve);
129+
destFileStream.destroy();
130+
// This workaround is awaiting to be removed when vscode moves to newer nodejs version:
131+
// https://github.com/rust-analyzer/rust-analyzer/issues/3167
132+
});
133+
await moveFile(tempFilePath, destFilePath);
129134
});
130135
}
136+
137+
async function withTempFile(scope: (tempFilePath: string) => Promise<void>) {
138+
// Based on the great article: https://advancedweb.hu/secure-tempfiles-in-nodejs-without-dependencies/
139+
140+
// `.realpath()` should handle the cases where os.tmpdir() contains symlinks
141+
const osTempDir = await fs.promises.realpath(os.tmpdir());
142+
143+
const tempDir = await fs.promises.mkdtemp(path.join(osTempDir, "rust-analyzer"));
144+
145+
try {
146+
return await scope(path.join(tempDir, "file"));
147+
} finally {
148+
// We are good citizens :D
149+
void fs.promises.rmdir(tempDir, { recursive: true }).catch(log.error);
150+
}
151+
};
152+
153+
async function moveFile(src: fs.PathLike, dest: fs.PathLike) {
154+
try {
155+
await fs.promises.rename(src, dest);
156+
} catch (err) {
157+
if (err.code === 'EXDEV') {
158+
// We are probably moving the file across partitions/devices
159+
await fs.promises.copyFile(src, dest);
160+
await fs.promises.unlink(src);
161+
} else {
162+
log.error(`Failed to rename the file ${src} -> ${dest}`, err);
163+
}
164+
}
165+
}

0 commit comments

Comments
 (0)