Skip to content

Commit af00440

Browse files
authored
chore: Promisify execFile (#384)
`await` is generally more readable than explicit use of promises. Also, this works towards flipping `strictNullChecks` to true, because TypeScript complains a bit less about the promisified `execFile` than about explicit uses of callbacks and promises. While at it, this commit also moves the `getBazelInfo` function from `debugger/client.ts` over to the `BazelInfo` class, where it can be more easily reused. This has the nice side-effect, that also this `info` call will now respect any `startupOptions` potentially configured by the user.
1 parent 2aea27e commit af00440

File tree

5 files changed

+111
-132
lines changed

5 files changed

+111
-132
lines changed

src/bazel/bazel_info.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,50 @@
1313
// limitations under the License.
1414

1515
import * as child_process from "child_process";
16+
import * as util from "util";
1617

1718
import { BazelCommand } from "./bazel_command";
1819

20+
const execFile = util.promisify(child_process.execFile);
21+
1922
/** Provides a promise-based API around the `bazel info` command. */
2023
export class BazelInfo extends BazelCommand {
2124
/**
22-
* Runs `bazel info <key>` and returns the output.
25+
* Gets the info for a single key by running `bazel info <key>`.
2326
*
2427
* @param key The info key to query.
2528
* @returns The output of `bazel info <key>`.
2629
*/
27-
public async run(key: string): Promise<string> {
28-
return new Promise((resolve, reject) => {
29-
child_process.execFile(
30-
this.bazelExecutable,
31-
this.execArgs([key]),
32-
{ cwd: this.workingDirectory },
33-
(error: Error, stdout: string) => {
34-
if (error) {
35-
reject(error);
36-
} else {
37-
resolve(stdout.trim());
38-
}
39-
},
40-
);
30+
public async getOne(key: string): Promise<string> {
31+
const execResult = await execFile(
32+
this.bazelExecutable,
33+
this.execArgs([key]),
34+
{
35+
cwd: this.workingDirectory,
36+
},
37+
);
38+
return execResult.stdout.trim();
39+
}
40+
41+
/**
42+
* Runs `bazel info` and returns the output.
43+
*
44+
* @returns All `bazel info` entries
45+
*/
46+
public async getAll(): Promise<Map<string, string>> {
47+
const execResult = await execFile(this.bazelExecutable, this.execArgs([]), {
48+
cwd: this.workingDirectory,
4149
});
50+
const keyValues = new Map<string, string>();
51+
const lines = execResult.stdout.trim().split("\n");
52+
for (const line of lines) {
53+
// Windows paths can have >1 ':', so can't use line.split(":", 2)
54+
const splitterIndex = line.indexOf(":");
55+
const key = line.substring(0, splitterIndex);
56+
const value = line.substring(splitterIndex + 1);
57+
keyValues.set(key.trim(), value.trim());
58+
}
59+
return keyValues;
4260
}
4361

4462
protected bazelCommand(): string {

src/bazel/bazel_query.ts

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ import * as child_process from "child_process";
1616
import * as crypto from "crypto";
1717
import * as os from "os";
1818
import * as path from "path";
19+
import * as util from "util";
1920
import * as vscode from "vscode";
2021

2122
import { blaze_query } from "../protos";
2223
import { BazelCommand } from "./bazel_command";
2324
import { getBazelWorkspaceFolder } from "./bazel_utils";
2425

26+
const execFile = util.promisify(child_process.execFile);
27+
2528
const protoOutputOptions = [
2629
"--proto:output_rule_attrs=''",
2730
"--noproto:rule_inputs_and_outputs",
@@ -106,14 +109,12 @@ export class BazelQuery extends BazelCommand {
106109
* Executes the command and returns a promise for the binary contents of
107110
* standard output.
108111
*
109-
* @param query The query to execute.
110-
* @param options
111-
* @param options.ignoresErrors `true` if errors from executing the query
112+
* @param options The options to pass to `bazel query`
113+
* @param ignoresErrors `true` if errors from executing the query
112114
* should be ignored.
113-
* @returns A promise that is resolved with the contents of the process's
114-
* standard output, or rejected if the command fails.
115+
* @returns The contents of the process's standard output
115116
*/
116-
protected run(
117+
protected async run(
117118
options: string[],
118119
{ ignoresErrors = false }: { ignoresErrors?: boolean } = {},
119120
): Promise<Buffer> {
@@ -142,30 +143,27 @@ export class BazelQuery extends BazelCommand {
142143
`--output_base=${queryOutputBase}`,
143144
]);
144145
}
145-
return new Promise((resolve, reject) => {
146-
const execOptions: child_process.ExecFileOptionsWithBufferEncoding = {
147-
cwd: this.workingDirectory,
148-
// A null encoding causes the callback below to receive binary data as a
149-
// Buffer instead of text data as strings.
150-
encoding: null,
151-
maxBuffer: Number.MAX_SAFE_INTEGER,
152-
};
153-
child_process.execFile(
154-
this.bazelExecutable,
155-
this.execArgs(options, additionalStartupOptions),
156-
execOptions,
157-
(error: Error, stdout: Buffer) => {
158-
if (error) {
159-
if (ignoresErrors) {
160-
resolve(new Buffer(0));
161-
} else {
162-
reject(error);
163-
}
164-
} else {
165-
resolve(stdout);
166-
}
167-
},
168-
);
169-
});
146+
const execOptions: child_process.ExecFileOptionsWithBufferEncoding = {
147+
cwd: this.workingDirectory,
148+
// A null encoding causes the callback below to receive binary data as a
149+
// Buffer instead of text data as strings.
150+
encoding: null,
151+
maxBuffer: Number.MAX_SAFE_INTEGER,
152+
};
153+
try {
154+
return (
155+
await execFile(
156+
this.bazelExecutable,
157+
this.execArgs(options, additionalStartupOptions),
158+
execOptions,
159+
)
160+
).stdout;
161+
} catch (e) {
162+
if (ignoresErrors) {
163+
return Buffer.alloc(0);
164+
} else {
165+
throw e;
166+
}
167+
}
170168
}
171169
}

src/buildifier/buildifier.ts

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,18 @@
1414

1515
import * as child_process from "child_process";
1616
import * as path from "path";
17+
import * as util from "util";
1718
import * as vscode from "vscode";
19+
1820
import { IBuildifierResult, IBuildifierWarning } from "./buildifier_result";
1921
import { getDefaultBazelExecutablePath } from "../extension/configuration";
2022

23+
const execFile = util.promisify(child_process.execFile);
24+
type PromiseExecFileException = child_process.ExecFileException & {
25+
stdout: string;
26+
stderr: string;
27+
};
28+
2129
/** Whether to warn about lint findings or fix them. */
2230
export type BuildifierLintMode = "fix" | "warn";
2331

@@ -196,52 +204,48 @@ export function getDefaultBuildifierJsonConfigPath(): string {
196204
* @param acceptNonSevereErrors If true, syntax/lint exit codes will not be
197205
* treated as severe tool errors.
198206
*/
199-
export function executeBuildifier(
207+
export async function executeBuildifier(
200208
fileContent: string,
201209
args: string[],
202210
acceptNonSevereErrors: boolean,
203211
): Promise<{ stdout: string; stderr: string }> {
204-
return new Promise((resolve, reject) => {
205-
// Determine the executable
206-
let executable = getDefaultBuildifierExecutablePath();
207-
const buildifierConfigJsonPath = getDefaultBuildifierJsonConfigPath();
208-
if (buildifierConfigJsonPath.length !== 0) {
209-
args = ["--config", buildifierConfigJsonPath, ...args];
210-
}
211-
// Paths starting with an `@` are referring to Bazel targets
212-
if (executable.startsWith("@")) {
213-
const targetName = executable;
214-
executable = getDefaultBazelExecutablePath();
215-
args = ["run", targetName, "--", ...args];
216-
}
217-
const execOptions = {
218-
maxBuffer: Number.MAX_SAFE_INTEGER,
219-
// Use the workspace folder as CWD, thereby allowing relative
220-
// paths. See #329
221-
cwd: vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath,
222-
};
212+
// Determine the executable
213+
let executable = getDefaultBuildifierExecutablePath();
214+
const buildifierConfigJsonPath = getDefaultBuildifierJsonConfigPath();
215+
if (buildifierConfigJsonPath.length !== 0) {
216+
args = ["--config", buildifierConfigJsonPath, ...args];
217+
}
218+
// Paths starting with an `@` are referring to Bazel targets
219+
if (executable.startsWith("@")) {
220+
const targetName = executable;
221+
executable = getDefaultBazelExecutablePath();
222+
args = ["run", targetName, "--", ...args];
223+
}
224+
const execOptions = {
225+
maxBuffer: Number.MAX_SAFE_INTEGER,
226+
// Use the workspace folder as CWD, thereby allowing relative
227+
// paths. See #329
228+
cwd: vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath,
229+
};
223230

224-
// Start buildifier
225-
const process = child_process.execFile(
226-
executable,
227-
args,
228-
execOptions,
229-
(error, stdout, stderr) => {
230-
if (
231-
!error ||
232-
(acceptNonSevereErrors && shouldTreatBuildifierErrorAsSuccess(error))
233-
) {
234-
resolve({ stdout, stderr });
235-
} else {
236-
reject(error);
237-
}
238-
},
239-
);
240-
// Write the file being linted/formatted to stdin and close the stream so
241-
// that the buildifier process continues.
242-
process.stdin.write(fileContent);
243-
process.stdin.end();
244-
});
231+
// Start buildifier
232+
const process = execFile(executable, args, execOptions);
233+
234+
// Write the file being linted/formatted to stdin and close the stream so
235+
// that the buildifier process continues.
236+
process.child.stdin?.write(fileContent);
237+
process.child.stdin?.end();
238+
239+
try {
240+
return await process;
241+
} catch (e) {
242+
const error = e as PromiseExecFileException;
243+
if (acceptNonSevereErrors && shouldTreatBuildifierErrorAsSuccess(error)) {
244+
return { stdout: error.stdout, stderr: error.stderr };
245+
} else {
246+
throw error;
247+
}
248+
}
245249
}
246250

247251
/**

src/debug-adapter/client.ts

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { DebugProtocol } from "vscode-debugprotocol";
3232
import { skylark_debugging } from "../protos";
3333
import { BazelDebugConnection } from "./connection";
3434
import { Handles } from "./handles";
35+
import { BazelInfo } from "../bazel/bazel_info";
3536

3637
/**
3738
* Returns a {@code number} equivalent to the given {@code number} or
@@ -166,7 +167,7 @@ class BazelDebugSession extends DebugSession {
166167
const verbose = args.verbose || false;
167168

168169
const bazelExecutable = this.bazelExecutable(args);
169-
this.bazelInfo = await this.getBazelInfo(bazelExecutable, args.cwd);
170+
this.bazelInfo = await new BazelInfo(bazelExecutable, args.cwd).getAll();
170171

171172
const fullArgs = args.bazelStartupOptions
172173
.concat([
@@ -531,48 +532,6 @@ class BazelDebugSession extends DebugSession {
531532
return bazelExecutable;
532533
}
533534

534-
/**
535-
* Invokes {@code bazel info} and returns the information in a map.
536-
*
537-
* @param bazelExecutable The name/path of the Bazel executable.
538-
* @param cwd The working directory in which Bazel should be launched.
539-
*/
540-
private getBazelInfo(
541-
bazelExecutable: string,
542-
cwd: string,
543-
): Promise<Map<string, string>> {
544-
return new Promise((resolve, reject) => {
545-
const execOptions = {
546-
cwd,
547-
// The maximum amount of data allowed on stdout. 500KB should be plenty
548-
// of `bazel info`, but if this becomes problematic we can switch to the
549-
// event-based `child_process` APIs instead.
550-
maxBuffer: 500 * 1024,
551-
};
552-
child_process.execFile(
553-
bazelExecutable,
554-
["info"],
555-
execOptions,
556-
(error: Error, stdout: string) => {
557-
if (error) {
558-
reject(error);
559-
} else {
560-
const keyValues = new Map<string, string>();
561-
const lines = stdout.trim().split("\n");
562-
for (const line of lines) {
563-
// Windows paths can have >1 ':', so can't use line.split(":", 2)
564-
const splitterIndex = line.indexOf(":");
565-
const key = line.substring(0, splitterIndex);
566-
const value = line.substring(splitterIndex + 1);
567-
keyValues.set(key.trim(), value.trim());
568-
}
569-
resolve(keyValues);
570-
}
571-
},
572-
);
573-
});
574-
}
575-
576535
/**
577536
* Launches the Bazel process to be debugged.
578537
*

src/extension/command_variables.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ async function bazelGetTargetOutput(
8787
const outputPath = await new BazelInfo(
8888
getDefaultBazelExecutablePath(),
8989
workspaceInfo.bazelWorkspacePath,
90-
).run("output_path");
90+
).getOne("output_path");
9191
const outputs = await new BazelCQuery(
9292
getDefaultBazelExecutablePath(),
9393
workspaceInfo.bazelWorkspacePath,
@@ -122,7 +122,7 @@ async function bazelInfo(key: string): Promise<string> {
122122
return new BazelInfo(
123123
getDefaultBazelExecutablePath(),
124124
workspaceInfo.bazelWorkspacePath,
125-
).run(key);
125+
).getOne(key);
126126
}
127127

128128
/**

0 commit comments

Comments
 (0)