Skip to content

Commit 151e4e5

Browse files
authored
Install dbconnect in a child process (#1216)
This avoids problems with command escaping when runnin in a shell.
1 parent 45e3a87 commit 151e4e5

File tree

2 files changed

+68
-62
lines changed

2 files changed

+68
-62
lines changed

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {EventEmitter, window} from "vscode";
1+
import {EventEmitter, OutputChannel, window} from "vscode";
22

33
import {Disposable} from "vscode";
44
import {MsPythonExtensionWrapper} from "./MsPythonExtensionWrapper";
@@ -9,29 +9,46 @@ export class EnvironmentDependenciesInstaller implements Disposable {
99
private onDidInstallAttemptEmitter = new EventEmitter<void>();
1010
public onDidTryInstallation = this.onDidInstallAttemptEmitter.event;
1111

12+
private _outputChannel?: OutputChannel;
13+
1214
constructor(private readonly pythonExtension: MsPythonExtensionWrapper) {}
1315

16+
get outputChannel() {
17+
if (!this._outputChannel) {
18+
this._outputChannel =
19+
window.createOutputChannel("Databricks Connect");
20+
this.disposables.push(this._outputChannel);
21+
}
22+
return this._outputChannel;
23+
}
24+
1425
dispose() {
1526
this.disposables.forEach((i) => i.dispose());
1627
}
1728

1829
async install(version?: string) {
1930
version = version ?? DATABRICKS_CONNECT_VERSION;
2031
try {
32+
this.outputChannel.clear();
33+
this.outputChannel.show();
2134
await this.pythonExtension.uninstallPackageFromEnvironment(
22-
"pyspark"
35+
"pyspark",
36+
this.outputChannel
2337
);
2438
await this.pythonExtension.uninstallPackageFromEnvironment(
25-
"databricks-connect"
39+
"databricks-connect",
40+
this.outputChannel
2641
);
2742
await this.pythonExtension.installPackageInEnvironment(
2843
"databricks-connect",
29-
version
44+
version,
45+
this.outputChannel
3046
);
3147
} catch (e: unknown) {
3248
if (e instanceof Error) {
3349
window.showErrorMessage(e.message);
3450
}
51+
this.outputChannel.show();
3552
}
3653
this.onDidInstallAttemptEmitter.fire();
3754
}

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

Lines changed: 47 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,12 @@ import {
44
Event,
55
window,
66
Disposable,
7-
workspace,
8-
RelativePattern,
97
Terminal,
108
commands,
9+
OutputChannel,
1110
} from "vscode";
1211
import {StateStorage} from "../vscode-objs/StateStorage";
1312
import {IExtensionApi as MsPythonExtensionApi} from "./MsPythonExtensionApi";
14-
import * as os from "node:os";
15-
import * as path from "node:path";
16-
import {mkdtemp, readFile} from "fs/promises";
1713
import {Mutex} from "../locking";
1814
import * as childProcess from "node:child_process";
1915
import {promisify} from "node:util";
@@ -88,40 +84,24 @@ export class MsPythonExtensionWrapper implements Disposable {
8884
);
8985
}
9086

91-
private async executeInTerminalE(command: string) {
92-
const dir = await mkdtemp(path.join(os.tmpdir(), "databricks-vscode-"));
93-
const filePath = path.join(dir, "python-terminal-output.txt");
94-
95-
const disposables: Disposable[] = [];
96-
const exitCodePromise = new Promise<number | undefined>((resolve) => {
97-
const fsWatcher = workspace.createFileSystemWatcher(
98-
new RelativePattern(dir, path.basename(filePath))
99-
);
100-
const handleFileChange = async () => {
101-
try {
102-
const fileData = await readFile(filePath, "utf-8");
103-
resolve(parseInt(fileData));
104-
} catch (e: unknown) {
105-
resolve(undefined);
87+
async runWithOutput(
88+
command: string,
89+
args: string[],
90+
outputChannel?: OutputChannel
91+
) {
92+
const cp = childProcess.execFile(command, args);
93+
cp.stdout?.on("data", (data) => outputChannel?.append(data));
94+
cp.stderr?.on("data", (data) => outputChannel?.append(data));
95+
return new Promise<void>((resolve, reject) => {
96+
cp.on("exit", (code) => {
97+
if (code === 0) {
98+
resolve();
99+
} else {
100+
reject(new Error(`Command exited with code ${code}`));
106101
}
107-
};
108-
disposables.push(
109-
fsWatcher,
110-
fsWatcher.onDidCreate(handleFileChange),
111-
fsWatcher.onDidChange(handleFileChange)
112-
);
102+
});
103+
cp.on("error", reject);
113104
});
114-
115-
try {
116-
await this.terminalMutex.wait();
117-
this.terminal.show();
118-
this.terminal.sendText(`${command}; echo $? > ${filePath}`);
119-
const exitCode = await exitCodePromise;
120-
return exitCode;
121-
} finally {
122-
disposables.forEach((i) => i.dispose());
123-
this.terminalMutex.signal();
124-
}
125105
}
126106

127107
async getLatestPackageVersion(name: string) {
@@ -190,41 +170,50 @@ export class MsPythonExtensionWrapper implements Disposable {
190170
);
191171
}
192172

193-
async installPackageInEnvironment(name: string, version?: string | RegExp) {
173+
async installPackageInEnvironment(
174+
name: string,
175+
version?: string | RegExp,
176+
outputChannel?: OutputChannel
177+
) {
194178
const executable = await this.getPythonExecutable();
195179
if (!executable) {
196180
throw Error("No python executable found");
197181
}
198182
if (version === "latest") {
199183
version = await this.getLatestPackageVersion(name);
200184
}
201-
const execCommand = `'${executable}' -m pip install '${name}${
202-
version ? `==${version}` : ""
203-
}' --disable-pip-version-check --no-python-version-warning`;
204-
205-
const exitCode = await this.executeInTerminalE(execCommand);
206-
if (exitCode) {
207-
throw new Error(
208-
`Error while installing ${name} package in the current python environment.`
209-
);
210-
}
185+
const args = [
186+
"-m",
187+
"pip",
188+
"install",
189+
`${name}${version ? `==${version}` : ""}`,
190+
"--disable-pip-version-check",
191+
"--no-python-version-warning",
192+
];
193+
outputChannel?.appendLine(`Running: ${executable} ${args.join(" ")}`);
194+
await this.runWithOutput(executable, args, outputChannel);
211195
}
212196

213-
async uninstallPackageFromEnvironment(name: string) {
197+
async uninstallPackageFromEnvironment(
198+
name: string,
199+
outputChannel?: OutputChannel
200+
) {
214201
const exists = await this.findPackageInEnvironment(name);
215202
const executable = await this.getPythonExecutable();
216-
217203
if (!exists || !executable) {
218204
return;
219205
}
220-
221-
const execCommand = `'${executable}' -m pip uninstall '${name}' --disable-pip-version-check --no-python-version-warning -y`;
222-
const exitCode = await this.executeInTerminalE(execCommand);
223-
if (exitCode) {
224-
throw new Error(
225-
`Error while un-installing ${name} package from the current python environment.`
226-
);
227-
}
206+
const args = [
207+
"-m",
208+
"pip",
209+
"uninstall",
210+
name,
211+
"--disable-pip-version-check",
212+
"--no-python-version-warning",
213+
"-y",
214+
];
215+
outputChannel?.appendLine(`Running: ${executable} ${args.join(" ")}`);
216+
await this.runWithOutput(executable, args, outputChannel);
228217
}
229218

230219
async selectPythonInterpreter() {

0 commit comments

Comments
 (0)