Skip to content

Commit

Permalink
Auto merge of #18281 - darichey:async-subprocess, r=lnicola
Browse files Browse the repository at this point in the history
Run subprocesses async in vscode extension

Extensions should not block the vscode extension host. Replace uses of `spawnSync` with `spawnAsync`, a convenience wrapper around `spawn`.

These `spawnSync`s are unlikely to cause a real issue in practice, because they spawn very short-lived processes, so we aren't blocking for very long. That said, blocking the extension host is poor practice, and if they _do_ block for too long for whatever reason, vscode becomes useless.
  • Loading branch information
bors committed Oct 12, 2024
2 parents b551482 + 0260e41 commit d7628c0
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 32 deletions.
39 changes: 19 additions & 20 deletions editors/code/src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as vscode from "vscode";
import * as os from "os";
import type { Config } from "./config";
import { type Env, log } from "./util";
import { type Env, log, spawnAsync } from "./util";
import type { PersistentState } from "./persistent_state";
import { exec, spawnSync } from "child_process";
import { exec } from "child_process";
import { TextDecoder } from "node:util";

export async function bootstrap(
Expand Down Expand Up @@ -61,13 +61,12 @@ async function getServer(
// if so, use the rust-analyzer component
const toolchainUri = vscode.Uri.joinPath(workspaceFolder.uri, "rust-toolchain.toml");
if (await hasToolchainFileWithRaDeclared(toolchainUri)) {
const res = spawnSync("rustup", ["which", "rust-analyzer"], {
encoding: "utf8",
const res = await spawnAsync("rustup", ["which", "rust-analyzer"], {
env: { ...process.env },
cwd: workspaceFolder.uri.fsPath,
});
if (!res.error && res.status === 0) {
toolchainServerPath = earliestToolchainPath(
toolchainServerPath = await earliestToolchainPath(
toolchainServerPath,
res.stdout.trim(),
raVersionResolver,
Expand Down Expand Up @@ -114,10 +113,8 @@ async function getServer(
}

// Given a path to a rust-analyzer executable, resolve its version and return it.
function raVersionResolver(path: string): string | undefined {
const res = spawnSync(path, ["--version"], {
encoding: "utf8",
});
async function raVersionResolver(path: string): Promise<string | undefined> {
const res = await spawnAsync(path, ["--version"]);
if (!res.error && res.status === 0) {
return res.stdout;
} else {
Expand All @@ -126,13 +123,16 @@ function raVersionResolver(path: string): string | undefined {
}

// Given a path to two rust-analyzer executables, return the earliest one by date.
function earliestToolchainPath(
async function earliestToolchainPath(
path0: string | undefined,
path1: string,
raVersionResolver: (path: string) => string | undefined,
): string {
raVersionResolver: (path: string) => Promise<string | undefined>,
): Promise<string> {
if (path0) {
if (orderFromPath(path0, raVersionResolver) < orderFromPath(path1, raVersionResolver)) {
if (
(await orderFromPath(path0, raVersionResolver)) <
(await orderFromPath(path1, raVersionResolver))
) {
return path0;
} else {
return path1;
Expand All @@ -150,11 +150,11 @@ function earliestToolchainPath(
// nightly - /Users/myuser/.rustup/toolchains/nightly-2022-11-22-aarch64-apple-darwin/bin/rust-analyzer
// versioned - /Users/myuser/.rustup/toolchains/1.72.1-aarch64-apple-darwin/bin/rust-analyzer
// stable - /Users/myuser/.rustup/toolchains/stable-aarch64-apple-darwin/bin/rust-analyzer
function orderFromPath(
async function orderFromPath(
path: string,
raVersionResolver: (path: string) => string | undefined,
): string {
const raVersion = raVersionResolver(path);
raVersionResolver: (path: string) => Promise<string | undefined>,
): Promise<string> {
const raVersion = await raVersionResolver(path);
const raDate = raVersion?.match(/^rust-analyzer .*\(.* (\d{4}-\d{2}-\d{2})\)$/);
if (raDate?.length === 2) {
const precedence = path.includes("nightly-") ? "0" : "1";
Expand Down Expand Up @@ -184,11 +184,10 @@ async function hasToolchainFileWithRaDeclared(uri: vscode.Uri): Promise<boolean>
}
}

export function isValidExecutable(path: string, extraEnv: Env): boolean {
export async function isValidExecutable(path: string, extraEnv: Env): Promise<boolean> {
log.debug("Checking availability of a binary at", path);

const res = spawnSync(path, ["--version"], {
encoding: "utf8",
const res = await spawnAsync(path, ["--version"], {
env: { ...process.env, ...extraEnv },
});

Expand Down
54 changes: 53 additions & 1 deletion editors/code/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as vscode from "vscode";
import { strict as nativeAssert } from "assert";
import { exec, type ExecOptions } from "child_process";
import { exec, spawn, type SpawnOptionsWithoutStdio, type ExecOptions } from "child_process";
import { inspect } from "util";
import type { CargoRunnableArgs, ShellRunnableArgs } from "./lsp_ext";

Expand Down Expand Up @@ -233,3 +233,55 @@ export function expectNotUndefined<T>(input: Undefinable<T>, msg: string): NotUn
export function unwrapUndefinable<T>(input: Undefinable<T>): NotUndefined<T> {
return expectNotUndefined(input, `unwrapping \`undefined\``);
}

interface SpawnAsyncReturns {
stdout: string;
stderr: string;
status: number | null;
error?: Error | undefined;
}

export async function spawnAsync(
path: string,
args?: ReadonlyArray<string>,
options?: SpawnOptionsWithoutStdio,
): Promise<SpawnAsyncReturns> {
const child = spawn(path, args, options);
const stdout: Array<Buffer> = [];
const stderr: Array<Buffer> = [];
try {
const res = await new Promise<{ stdout: string; stderr: string; status: number | null }>(
(resolve, reject) => {
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
child.on("error", (error) =>
reject({
stdout: Buffer.concat(stdout).toString("utf8"),
stderr: Buffer.concat(stderr).toString("utf8"),
error,
}),
);
child.on("close", (status) =>
resolve({
stdout: Buffer.concat(stdout).toString("utf8"),
stderr: Buffer.concat(stderr).toString("utf8"),
status,
}),
);
},
);

return {
stdout: res.stdout,
stderr: res.stderr,
status: res.status,
};
} catch (e: any) {
return {
stdout: e.stdout,
stderr: e.stderr,
status: e.status,
error: e.error,
};
}
}
22 changes: 11 additions & 11 deletions editors/code/tests/unit/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ export async function getTests(ctx: Context) {
await ctx.suite("Bootstrap/Select toolchain RA", (suite) => {
suite.addTest("Order of nightly RA", async () => {
assert.deepStrictEqual(
_private.orderFromPath(
await _private.orderFromPath(
"/Users/myuser/.rustup/toolchains/nightly-2022-11-22-aarch64-apple-darwin/bin/rust-analyzer",
function (path: string) {
async function (path: string) {
assert.deepStrictEqual(
path,
"/Users/myuser/.rustup/toolchains/nightly-2022-11-22-aarch64-apple-darwin/bin/rust-analyzer",
Expand All @@ -22,9 +22,9 @@ export async function getTests(ctx: Context) {

suite.addTest("Order of versioned RA", async () => {
assert.deepStrictEqual(
_private.orderFromPath(
await _private.orderFromPath(
"/Users/myuser/.rustup/toolchains/1.72.1-aarch64-apple-darwin/bin/rust-analyzer",
function (path: string) {
async function (path: string) {
assert.deepStrictEqual(
path,
"/Users/myuser/.rustup/toolchains/1.72.1-aarch64-apple-darwin/bin/rust-analyzer",
Expand All @@ -38,9 +38,9 @@ export async function getTests(ctx: Context) {

suite.addTest("Order of versioned RA when unable to obtain version date", async () => {
assert.deepStrictEqual(
_private.orderFromPath(
await _private.orderFromPath(
"/Users/myuser/.rustup/toolchains/1.72.1-aarch64-apple-darwin/bin/rust-analyzer",
function () {
async function () {
return "rust-analyzer 1.72.1";
},
),
Expand All @@ -50,9 +50,9 @@ export async function getTests(ctx: Context) {

suite.addTest("Order of stable RA", async () => {
assert.deepStrictEqual(
_private.orderFromPath(
await _private.orderFromPath(
"/Users/myuser/.rustup/toolchains/stable-aarch64-apple-darwin/bin/rust-analyzer",
function (path: string) {
async function (path: string) {
assert.deepStrictEqual(
path,
"/Users/myuser/.rustup/toolchains/stable-aarch64-apple-darwin/bin/rust-analyzer",
Expand All @@ -66,7 +66,7 @@ export async function getTests(ctx: Context) {

suite.addTest("Order with invalid path to RA", async () => {
assert.deepStrictEqual(
_private.orderFromPath("some-weird-path", function () {
await _private.orderFromPath("some-weird-path", async function () {
return undefined;
}),
"2",
Expand All @@ -75,10 +75,10 @@ export async function getTests(ctx: Context) {

suite.addTest("Earliest RA between nightly and stable", async () => {
assert.deepStrictEqual(
_private.earliestToolchainPath(
await _private.earliestToolchainPath(
"/Users/myuser/.rustup/toolchains/stable-aarch64-apple-darwin/bin/rust-analyzer",
"/Users/myuser/.rustup/toolchains/nightly-2022-11-22-aarch64-apple-darwin/bin/rust-analyzer",
function (path: string) {
async function (path: string) {
if (
path ===
"/Users/myuser/.rustup/toolchains/nightly-2022-11-22-aarch64-apple-darwin/bin/rust-analyzer"
Expand Down

0 comments on commit d7628c0

Please sign in to comment.