Skip to content

Commit f1e5b6e

Browse files
authored
Cross platform file not found detection (#324)
1 parent 1cf255c commit f1e5b6e

File tree

4 files changed

+104
-27
lines changed

4 files changed

+104
-27
lines changed

packages/databricks-sdk-js/src/auth/fromAzureCli.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import * as child_process from "node:child_process";
2-
import {promisify} from "node:util";
1+
import {execFileWithShell, FileNotFoundException} from "../config/execUtils";
32
import {refreshableCredentialProvider} from "./refreshableCredentialProvider";
43
import {Token} from "./Token";
54
import {CredentialProvider, CredentialsProviderError} from "./types";
65

7-
const execFile = promisify(child_process.execFile);
8-
96
// Resource ID of the Azure application we need to log in.
107
const azureDatabricksLoginAppID = "2ff814a6-3304-4ab8-85cb-cd0e6f879c1d";
118

@@ -29,18 +26,14 @@ export const fromAzureCli = (host?: URL): CredentialProvider => {
2926
return refreshableCredentialProvider(async () => {
3027
let stdout = "";
3128
try {
32-
({stdout} = await execFile(
33-
"az",
34-
[
35-
"account",
36-
"get-access-token",
37-
"--resource",
38-
azureDatabricksLoginAppID,
39-
],
40-
{shell: true}
41-
));
29+
({stdout} = await execFileWithShell("az", [
30+
"account",
31+
"get-access-token",
32+
"--resource",
33+
azureDatabricksLoginAppID,
34+
]));
4235
} catch (e: any) {
43-
if (e.code === "ENOENT") {
36+
if (e instanceof FileNotFoundException) {
4437
throw new CredentialsProviderError(
4538
"Can't find 'az' command. Please install Azure CLI: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli'"
4639
);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as assert from "node:assert";
2+
import {execFileWithShell, FileNotFoundException} from "./execUtils";
3+
4+
describe(__filename, () => {
5+
it("should spawn a command", async () => {
6+
const {stdout} = await execFileWithShell("echo", ["hello"]);
7+
assert.match(stdout, /^hello\r?\n$/m);
8+
});
9+
10+
it("should detect if the command is not found", async () => {
11+
await assert.rejects(
12+
execFileWithShell("i_dont_exist", []),
13+
FileNotFoundException
14+
);
15+
});
16+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as child_process from "node:child_process";
2+
import {ExecException} from "node:child_process";
3+
import {promisify} from "node:util";
4+
5+
const execFile = promisify(child_process.execFile);
6+
7+
export interface ExecFileException extends ExecException {
8+
stdout?: string;
9+
stderr?: string;
10+
}
11+
12+
export class FileNotFoundException extends Error {}
13+
14+
export function isExecFileException(e: any): e is ExecFileException {
15+
return (
16+
e.code !== undefined ||
17+
e.stderr !== undefined ||
18+
e.stdout !== undefined ||
19+
e.signal !== undefined
20+
);
21+
}
22+
23+
function isFileNotFound(e: any): e is ExecFileException {
24+
// when using plain execFile
25+
if (e.code === "ENOENT") {
26+
return true;
27+
}
28+
29+
if (!isExecFileException(e)) {
30+
return false;
31+
}
32+
33+
// when using execFile with shell on Linux
34+
if (
35+
e.code === 127 &&
36+
e.stderr &&
37+
e.stderr.indexOf("command not found") >= 0
38+
) {
39+
return true;
40+
}
41+
42+
// when using execFile with shell on Windows
43+
if (
44+
e.code === 1 &&
45+
e.stderr &&
46+
e.stderr.indexOf(
47+
"is not recognized as an internal or external command"
48+
) >= 0
49+
) {
50+
return true;
51+
}
52+
53+
return false;
54+
}
55+
56+
export async function execFileWithShell(
57+
cmd: string,
58+
args: Array<string>
59+
): Promise<{
60+
stdout: string;
61+
stderr: string;
62+
}> {
63+
try {
64+
return await execFile(cmd, args, {shell: true});
65+
} catch (e) {
66+
if (isFileNotFound(e)) {
67+
throw new FileNotFoundException(e.message);
68+
} else {
69+
throw e;
70+
}
71+
}
72+
}

packages/databricks-vscode/src/configuration/AzureCliCheck.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import * as child_process from "node:child_process";
2-
import {promisify} from "node:util";
31
import {CurrentUserService} from "@databricks/databricks-sdk";
42
import {commands, Disposable, Uri, window} from "vscode";
53
import {ConnectionManager} from "./ConnectionManager";
64
import {AuthProvider} from "./AuthProvider";
7-
8-
const execFile = promisify(child_process.execFile);
5+
import {execFileWithShell} from "@databricks/databricks-sdk/dist/config/execUtils";
96

107
export type Step<S, N> = () => Promise<
118
SuccessResult<S> | NextResult<N> | ErrorResult
@@ -182,9 +179,9 @@ export class AzureCliCheck implements Disposable {
182179
// check if Azure CLI is installed
183180
public async hasAzureCli(): Promise<boolean> {
184181
try {
185-
const {stdout} = await execFile(this.azBinPath, ["--version"], {
186-
shell: true,
187-
});
182+
const {stdout} = await execFileWithShell(this.azBinPath, [
183+
"--version",
184+
]);
188185
if (stdout.indexOf("azure-cli") !== -1) {
189186
return true;
190187
}
@@ -215,11 +212,10 @@ export class AzureCliCheck implements Disposable {
215212
// check if Azure CLI is logged in
216213
public async isAzureCliLoggedIn(): Promise<boolean> {
217214
try {
218-
const {stdout, stderr} = await execFile(
219-
this.azBinPath,
220-
["account", "list"],
221-
{shell: true}
222-
);
215+
const {stdout, stderr} = await execFileWithShell(this.azBinPath, [
216+
"account",
217+
"list",
218+
]);
223219
if (stdout === "[]") {
224220
return false;
225221
}

0 commit comments

Comments
 (0)