Skip to content

Commit 5dc0308

Browse files
Allow CLI path to have spaces (#1295)
## Changes <!-- Summary of your changes that are easy to understand --> ## Tests <!-- How is this tested? -->
1 parent c96b05f commit 5dc0308

File tree

11 files changed

+189
-124
lines changed

11 files changed

+189
-124
lines changed

packages/databricks-vscode/src/bundle/BundleInitWizard.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {Events, Telemetry} from "../telemetry";
1818
import {OverrideableConfigModel} from "../configuration/models/OverrideableConfigModel";
1919
import {writeFile, mkdir} from "fs/promises";
2020
import path from "path";
21+
import {escapePathArgument} from "../utils/shellUtils";
2122

2223
export async function promptToOpenSubProjects(
2324
projects: {absolute: Uri; relative: string}[],
@@ -188,12 +189,12 @@ export class BundleInitWizard {
188189
"bundle",
189190
"init",
190191
"--output-dir",
191-
this.cli.escapePathArgument(parentFolder.fsPath),
192+
escapePathArgument(parentFolder.fsPath),
192193
].join(" ");
193194
const initialPrompt = `clear; echo "Executing: databricks ${args}\nFollow the steps below to create your new Databricks project.\n"`;
194195
const finalPrompt = `echo "\nPress any key to close the terminal and continue ..."; ${ShellUtils.readCmd()}; exit`;
195196
terminal.sendText(
196-
`${initialPrompt}; ${this.cli.cliPath} ${args}; ${finalPrompt}`
197+
`${initialPrompt}; ${this.cli.escapedCliPath} ${args}; ${finalPrompt}`
197198
);
198199
return new Promise<void>((resolve) => {
199200
const closeEvent = window.onDidCloseTerminal(async (t) => {

packages/databricks-vscode/src/cli/CliWrapper.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,6 @@ nothing = true
250250
PATH: process.env.PATH,
251251
/* eslint-enable @typescript-eslint/naming-convention */
252252
}),
253-
shell: true,
254253
},
255254
};
256255
try {

packages/databricks-vscode/src/cli/CliWrapper.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,42 @@ import {quote} from "shell-quote";
1919
import {BundleVariableModel} from "../bundle/models/BundleVariableModel";
2020
import {MsPythonExtensionWrapper} from "../language/MsPythonExtensionWrapper";
2121
import path from "path";
22+
import {isPowershell} from "../utils/shellUtils";
2223

2324
const withLogContext = logging.withLogContext;
24-
const execFile = promisify(execFileCb);
25+
function getEscapedCommandAndAgrs(
26+
cmd: string,
27+
args: string[],
28+
options: SpawnOptionsWithoutStdio
29+
) {
30+
if (process.platform === "win32") {
31+
args = [
32+
"/d", //Disables execution of AutoRun commands, which are like .bashrc commands.
33+
"/c", //Carries out the command specified by <string> and then exits the command processor.
34+
`""${cmd}" ${args.map((a) => `"${a}"`).join(" ")}"`,
35+
];
36+
cmd = "cmd.exe";
37+
options = {...options, windowsVerbatimArguments: true};
38+
}
39+
return {cmd, args, options};
40+
}
41+
42+
export const execFile = async (
43+
file: string,
44+
args: string[],
45+
options: any = {}
46+
): Promise<{
47+
stdout: string;
48+
stderr: string;
49+
}> => {
50+
const {
51+
cmd,
52+
args: escapedArgs,
53+
options: escapedOptions,
54+
} = getEscapedCommandAndAgrs(file, args, options);
55+
const res = await promisify(execFileCb)(cmd, escapedArgs, escapedOptions);
56+
return {stdout: res.stdout.toString(), stderr: res.stderr.toString()};
57+
};
2558

2659
export interface Command {
2760
command: string;
@@ -154,13 +187,14 @@ async function runBundleCommand(
154187
});
155188

156189
logger?.debug(quote([cmd, ...args]), {bundleOpName});
190+
let options: SpawnOptionsWithoutStdio = {
191+
cwd: workspaceFolder.fsPath,
192+
env: removeUndefinedKeys(env),
193+
};
157194

195+
({cmd, args, options} = getEscapedCommandAndAgrs(cmd, args, options));
158196
try {
159-
const p = spawn(cmd, args, {
160-
cwd: workspaceFolder.fsPath,
161-
env: removeUndefinedKeys(env),
162-
shell: true,
163-
});
197+
const p = spawn(cmd, args, options);
164198

165199
const {stdout, stderr} = await waitForProcess(p, onStdOut, onStdError);
166200
logger?.info(displayLogs.end, {
@@ -236,8 +270,10 @@ export class CliWrapper {
236270
};
237271
}
238272

239-
escapePathArgument(arg: string): string {
240-
return `"${arg.replaceAll('"', '\\"')}"`;
273+
get escapedCliPath(): string {
274+
return isPowershell()
275+
? `& "${this.cliPath.replace('"', '\\"')}"`
276+
: `'${this.cliPath.replaceAll("'", "\\'")}'`;
241277
}
242278

243279
/**
@@ -300,6 +336,8 @@ export class CliWrapper {
300336
"Failed to parse Databricks Config File, please make sure it's in the correct ini format";
301337
} else if (e.message.includes("spawn UNKNOWN")) {
302338
msg = `Failed to parse Databricks Config File using databricks CLI, please make sure you have permissions to execute this binary: "${this.cliPath}"`;
339+
} else {
340+
msg += e.message;
303341
}
304342
}
305343
ctx?.logger?.error(msg, e);
@@ -593,7 +631,6 @@ export class CliWrapper {
593631
options: {
594632
cwd: workspaceFolder.fsPath,
595633
env,
596-
shell: true,
597634
},
598635
};
599636
}

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

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
ValidationMessageType,
1313
} from "../ui/MultiStepInputWizard";
1414
import {CliWrapper, ConfigEntry} from "../cli/CliWrapper";
15-
import {workspaceConfigs} from "../vscode-objs/WorkspaceConfigs";
1615
import {
1716
AuthProvider,
1817
AuthType,
@@ -31,6 +30,8 @@ import ini from "ini";
3130
import {appendFile, copyFile} from "fs/promises";
3231
import path from "path";
3332
import os from "os";
33+
import {createFile} from "fs-extra";
34+
import {getDatabricksConfigFilePath} from "../utils/fileUtils";
3435

3536
interface AuthTypeQuickPickItem extends QuickPickItem {
3637
authType?: SdkAuthType;
@@ -384,25 +385,36 @@ export async function saveNewProfile(
384385
throw new Error("Can't save empty auth provider to a profile");
385386
}
386387

387-
const {path: configFilePath} = await loadConfigFile(
388-
workspaceConfigs.databrickscfgLocation
389-
);
388+
const configFilePath: string = getDatabricksConfigFilePath().fsPath;
389+
let shouldBackup = true;
390+
try {
391+
await loadConfigFile(configFilePath);
392+
} catch (e) {
393+
shouldBackup = false;
394+
await createFile(configFilePath);
395+
window.showInformationMessage(
396+
`Created a new .databrickscfg file at ${configFilePath}`
397+
);
398+
}
390399

391400
const profile: any = {};
392401
profile[profileName] = Object.fromEntries(
393402
Object.entries(iniData).filter((kv) => kv[1] !== undefined)
394403
);
395404
const iniStr = ini.stringify(profile);
396405
const finalStr = `${os.EOL};This profile is autogenerated by the Databricks Extension for VS Code${os.EOL}${iniStr}`;
397-
// Create a backup for .databrickscfg
398-
const backup = path.join(
399-
path.dirname(configFilePath),
400-
`.databrickscfg.${Date.now()}.bak`
401-
);
402-
await copyFile(configFilePath, backup);
403-
window.showInformationMessage(
404-
`Created a backup for .databrickscfg at ${backup}`
405-
);
406+
407+
if (shouldBackup) {
408+
// Create a backup for .databrickscfg
409+
const backup = path.join(
410+
path.dirname(configFilePath),
411+
`.databrickscfg.${Date.now()}.bak`
412+
);
413+
await copyFile(configFilePath, backup);
414+
window.showInformationMessage(
415+
`Created a backup for .databrickscfg at ${backup}`
416+
);
417+
}
406418

407419
// Write the new profile to .databrickscfg
408420
await appendFile(configFilePath, finalStr);

packages/databricks-vscode/src/configuration/auth/DatabricksCliCheck.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
ExecUtils,
32
ProductVersion,
43
WorkspaceClient,
54
logging,
@@ -8,6 +7,7 @@ import {Disposable, window} from "vscode";
87
import {DatabricksCliAuthProvider} from "./AuthProvider";
98
import {orchestrate, OrchestrationLoopError, Step} from "./orchestrate";
109
import {Loggers} from "../../logger";
10+
import {execFile} from "../../cli/CliWrapper";
1111

1212
// eslint-disable-next-line @typescript-eslint/no-var-requires
1313
const extensionVersion = require("../../../package.json")
@@ -92,7 +92,7 @@ export class DatabricksCliCheck implements Disposable {
9292

9393
private async login(): Promise<void> {
9494
try {
95-
await ExecUtils.execFile(this.authProvider.cliPath, [
95+
await execFile(this.authProvider.cliPath, [
9696
"auth",
9797
"login",
9898
"--host",

packages/databricks-vscode/src/extension.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ export async function activate(
322322
bundleRemoteStateModel,
323323
configModel,
324324
connectionManager,
325+
commands.registerCommand("databricks.internal.showOutput", () => {
326+
loggerManager.showOutputChannel("Databricks Logs");
327+
}),
325328
connectionManager.onDidChangeState(async () => {
326329
telemetry.setMetadata(
327330
Metadata.USER,

packages/databricks-vscode/src/language/MsPythonExtensionWrapper.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ import {StateStorage} from "../vscode-objs/StateStorage";
1212
import {IExtensionApi as MsPythonExtensionApi} from "./MsPythonExtensionApi";
1313
import {Mutex} from "../locking";
1414
import * as childProcess from "node:child_process";
15-
import {promisify} from "node:util";
1615
import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager";
17-
export const execFile = promisify(childProcess.execFile);
16+
import {execFile} from "../cli/CliWrapper";
1817

1918
export class MsPythonExtensionWrapper implements Disposable {
2019
public readonly api: MsPythonExtensionApi;

packages/databricks-vscode/src/language/notebooks/NotebookInitScriptManager.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,12 @@ import {Loggers} from "../../logger";
1717
import {Context, context} from "@databricks/databricks-sdk/dist/context";
1818
import {Mutex} from "../../locking";
1919
import {MsPythonExtensionWrapper} from "../MsPythonExtensionWrapper";
20-
import {execFile as ef} from "child_process";
21-
import {promisify} from "util";
2220
import {FileUtils} from "../../utils";
2321
import {workspaceConfigs} from "../../vscode-objs/WorkspaceConfigs";
2422
import {LocalUri} from "../../sync/SyncDestination";
2523
import {DatabricksEnvFileManager} from "../../file-managers/DatabricksEnvFileManager";
2624
import {WorkspaceFolderManager} from "../../vscode-objs/WorkspaceFolderManager";
27-
28-
const execFile = promisify(ef);
25+
import {execFile} from "../../cli/CliWrapper";
2926

3027
async function isDbnbTextEditor(editor?: TextEditor) {
3128
try {

packages/databricks-vscode/src/test/e2e/bundle_init.e2e.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,16 @@ describe("Bundle Init", async function () {
6767
expect(await pick.getLabel()).toBe(parentDir);
6868
await pick.select();
6969

70+
await dismissNotifications();
71+
7072
// Wait for the databricks cli terminal window to pop up and select all
7173
// default options for the default template
7274
const editorView = workbench.getEditorView();
7375
const title = "Databricks Project Init";
7476
const initTab = await getTabByTitle(title);
7577
assert(initTab, "Can't find a tab for project-init terminal wizard");
7678
await initTab.select();
79+
await sleep(1000);
7780

7881
//select temaplate type
7982
await browser.keys("default-python".split(""));

0 commit comments

Comments
 (0)