Skip to content

Commit 98b5a31

Browse files
fix: localstack installation on ubuntu/i3
1 parent fef3279 commit 98b5a31

File tree

2 files changed

+129
-24
lines changed

2 files changed

+129
-24
lines changed

src/utils/install.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,13 @@ fi
475475
`Could not detect your shell profile. To use localstack CLI from terminal ensure ~/.local/bin is in your PATH.`,
476476
);
477477
}
478+
479+
// Update PATH for the current VSCode process so LocalStack is immediately available
480+
const localBinPath = path.join(homeDir, ".local", "bin");
481+
const currentPath = process.env.PATH || "";
482+
if (!currentPath.split(":").includes(localBinPath)) {
483+
process.env.PATH = `${localBinPath}:${currentPath}`;
484+
}
478485
} catch (err) {
479486
window.showInformationMessage(
480487
`Could not update your shell profile. To use localstack CLI from terminal ensure ~/.local/bin is in your PATH.`,
@@ -515,8 +522,9 @@ async function installLocalWindows(temporaryDirname: string) {
515522
await move(`${temporaryDirname}/localstack`, LOCAL_CLI_INSTALLATION_DIRNAME);
516523
await exec(`setx PATH "%PATH%;${LOCAL_CLI_INSTALLATION_DIRNAME}"`);
517524

518-
// // Update PATH for current process (setx only updates for new processes, including vscode)
519-
// process.env.PATH = `${process.env.PATH};${CLI_INSTALLATION_DIRNAME}`;
525+
// Update PATH for the current VSCode process so LocalStack is immediately available
526+
// (setx only updates for new processes, including future VSCode instances)
527+
process.env.PATH = `${process.env.PATH};${LOCAL_CLI_INSTALLATION_DIRNAME}`;
520528

521529
window.showInformationMessage("LocalStack CLI installed for current user.");
522530
}
@@ -551,17 +559,23 @@ async function installGlobalLinux(
551559
) {
552560
// Use elevated privileges to install globally on linux
553561

554-
const { cancelled } = await spawnElevatedLinux({
555-
//TODO consider loading script from a file
556-
script: `rm -rf /usr/local/localstack && mv ${temporaryDirname}/localstack /usr/local/localstack && ln -sf /usr/local/localstack/localstack /usr/local/bin/localstack`,
557-
outputChannel,
558-
outputLabel: "install",
559-
cancellationToken,
560-
});
561-
if (cancelled) {
562-
//TODO check if progress can be used instead of window
563-
window.showErrorMessage("The installation was cancelled by the user");
564-
return { cancelled: true };
562+
try {
563+
const { cancelled } = await spawnElevatedLinux({
564+
//TODO consider loading script from a file
565+
script: `rm -rf /usr/local/localstack && mv ${temporaryDirname}/localstack /usr/local/localstack && ln -sf /usr/local/localstack/localstack /usr/local/bin/localstack`,
566+
outputChannel,
567+
outputLabel: "install",
568+
cancellationToken,
569+
});
570+
if (cancelled) {
571+
//TODO check if progress can be used instead of window
572+
window.showErrorMessage("The installation was cancelled by the user");
573+
return { cancelled: true };
574+
}
575+
} catch (error) {
576+
const message = error instanceof Error ? error.message : String(error);
577+
window.showErrorMessage(`Installation failed: ${message}`);
578+
throw error;
565579
}
566580
}
567581

src/utils/prompts.ts

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import * as childProcess from "node:child_process";
12
import { appendFile, rm } from "node:fs/promises";
23
import { tmpdir } from "node:os";
34
import path from "node:path";
45

6+
import * as vscode from "vscode";
57
import type { CancellationToken, LogOutputChannel } from "vscode";
68

7-
import { spawn, SpawnError } from "./spawn.ts";
9+
import { pipeToLogOutputChannel, spawn, SpawnError } from "./spawn.ts";
810

911
export interface SpawnElevatedDarwinOptions {
1012
script: string;
@@ -50,28 +52,117 @@ export interface SpawnElevatedLinuxOptions {
5052
cancellationToken?: CancellationToken;
5153
}
5254

53-
export async function spawnElevatedLinux(
55+
async function tryPkexec(
5456
options: SpawnElevatedLinuxOptions,
55-
): Promise<{ cancelled: boolean }> {
57+
): Promise<{ success: boolean; cancelled: boolean }> {
58+
const scriptArg = JSON.stringify(options.script);
5659
try {
57-
await spawn("pkexec", ["sh", "-c", `${JSON.stringify(options.script)}`], {
60+
await spawn("pkexec", ["sh", "-c", scriptArg], {
5861
outputChannel: options.outputChannel,
5962
outputLabel: options.outputLabel,
6063
cancellationToken: options.cancellationToken,
6164
});
62-
return {
63-
cancelled: false,
64-
};
65+
return { success: true, cancelled: false };
6566
} catch (error) {
66-
if (error instanceof SpawnError && error.code === 126) {
67-
return { cancelled: true };
67+
if (error instanceof SpawnError) {
68+
// User cancelled (pkexec returns 126)
69+
if (error.code === 126) {
70+
return { success: true, cancelled: true };
71+
}
72+
// Other failures (no polkit agent, TTY error, etc.) - signal to try fallback
73+
return { success: false, cancelled: false };
6874
}
69-
70-
options.outputChannel.error(error instanceof Error ? error : String(error));
7175
throw error;
7276
}
7377
}
7478

79+
async function spawnWithSudoPassword(
80+
options: SpawnElevatedLinuxOptions,
81+
): Promise<{ cancelled: boolean }> {
82+
const password = await vscode.window.showInputBox({
83+
prompt: "Enter your sudo password",
84+
password: true,
85+
ignoreFocusOut: true,
86+
});
87+
88+
if (password === undefined) {
89+
return { cancelled: true };
90+
}
91+
92+
const outputLabel = options.outputLabel ? `[${options.outputLabel}]: ` : "";
93+
94+
return new Promise((resolve, reject) => {
95+
let stderrOutput = "";
96+
97+
const child = childProcess.spawn(
98+
"sudo",
99+
["-S", "sh", "-c", options.script],
100+
{
101+
stdio: ["pipe", "pipe", "pipe"],
102+
},
103+
);
104+
105+
// Capture stderr to detect wrong password
106+
child.stderr?.on("data", (data: Buffer) => {
107+
stderrOutput += data.toString();
108+
});
109+
110+
// Write password to stdin and close it
111+
child.stdin.write(`${password}\n`);
112+
child.stdin.end();
113+
114+
// Redirect the child process stdout/stderr to VSCode's output channel for visibility
115+
pipeToLogOutputChannel(child, options.outputChannel, outputLabel);
116+
117+
const disposeCancel = options.cancellationToken?.onCancellationRequested(
118+
() => {
119+
child.kill("SIGINT");
120+
reject(new Error("Command cancelled"));
121+
},
122+
);
123+
124+
child.on("close", (code) => {
125+
disposeCancel?.dispose();
126+
if (code === 0) {
127+
resolve({ cancelled: false });
128+
} else {
129+
// Detect wrong password from stderr
130+
const isWrongPassword =
131+
stderrOutput.includes("Sorry, try again") ||
132+
stderrOutput.includes("incorrect password") ||
133+
stderrOutput.includes("Authentication failure");
134+
135+
const errorMessage = isWrongPassword
136+
? "Incorrect sudo password"
137+
: `sudo command failed with exit code ${code}`;
138+
139+
reject(new Error(errorMessage));
140+
}
141+
});
142+
143+
child.on("error", (error) => {
144+
disposeCancel?.dispose();
145+
reject(error);
146+
});
147+
});
148+
}
149+
150+
export async function spawnElevatedLinux(
151+
options: SpawnElevatedLinuxOptions,
152+
): Promise<{ cancelled: boolean }> {
153+
// Try pkexec first (works with graphical polkit agents)
154+
const pkexecResult = await tryPkexec(options);
155+
if (pkexecResult.success) {
156+
return { cancelled: pkexecResult.cancelled };
157+
}
158+
159+
// Fallback: prompt for sudo password via VS Code input
160+
options.outputChannel.info(
161+
"pkexec not available, falling back to sudo password prompt",
162+
);
163+
return spawnWithSudoPassword(options);
164+
}
165+
75166
export async function spawnElevatedWindows({
76167
script,
77168
outputChannel,

0 commit comments

Comments
 (0)