Skip to content

Commit d7e3aba

Browse files
committed
handle LS CLI failures using text heuristic
1 parent d64ca89 commit d7e3aba

File tree

5 files changed

+59
-30
lines changed

5 files changed

+59
-30
lines changed

src/plugins/manage.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@ export default createPlugin(
1717
);
1818

1919
context.subscriptions.push(
20-
commands.registerCommand("localstack.start", () => {
20+
commands.registerCommand("localstack.start", async () => {
2121
if (localStackStatusTracker.status() !== "stopped") {
2222
window.showInformationMessage("LocalStack is already running.");
2323
return;
2424
}
25-
localStackStatusTracker.forceStarting();
26-
void startLocalStack(outputChannel, telemetry);
25+
localStackStatusTracker.forceContainerStatus("running");
26+
try {
27+
await startLocalStack(outputChannel, telemetry);
28+
} catch {
29+
localStackStatusTracker.forceContainerStatus("stopped");
30+
}
2731
}),
2832
);
2933

@@ -33,7 +37,7 @@ export default createPlugin(
3337
window.showInformationMessage("LocalStack is not running.");
3438
return;
3539
}
36-
localStackStatusTracker.forceStopping();
40+
localStackStatusTracker.forceContainerStatus("stopping");
3741
void stopLocalStack(outputChannel, telemetry);
3842
}),
3943
);

src/utils/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CLI_PATHS } from "../constants.ts";
88

99
import { exec } from "./exec.ts";
1010
import { spawn } from "./spawn.ts";
11+
import type { SpawnOptions } from "./spawn.ts";
1112

1213
const IMAGE_NAME = "localstack/localstack-pro";
1314
const LOCALSTACK_LDM_PREVIEW = "1";
@@ -68,6 +69,7 @@ export const spawnLocalStack = async (
6869
options: {
6970
outputChannel: LogOutputChannel;
7071
cancellationToken?: CancellationToken;
72+
onStderr?: SpawnOptions["onStderr"];
7173
},
7274
) => {
7375
const cli = await findLocalStack();
@@ -81,5 +83,6 @@ export const spawnLocalStack = async (
8183
IMAGE_NAME,
8284
LOCALSTACK_LDM_PREVIEW,
8385
},
86+
onStderr: options.onStderr,
8487
});
8588
};

src/utils/localstack-status.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ export type LocalStackStatus = "starting" | "running" | "stopping" | "stopped";
1111

1212
export interface LocalStackStatusTracker extends Disposable {
1313
status(): LocalStackStatus;
14-
// setStatus(status: LocalStackStatus): void;
15-
forceStarting(): void;
16-
forceStopping(): void;
14+
forceContainerStatus(status: ContainerStatus): void;
1715
onChange(callback: (status: LocalStackStatus) => void): void;
1816
}
1917

@@ -66,15 +64,9 @@ export async function createLocalStackStatusTracker(
6664
// biome-ignore lint/style/noNonNullAssertion: false positive
6765
return status!;
6866
},
69-
forceStarting() {
70-
if (containerStatus !== "running") {
71-
containerStatus = "running";
72-
deriveStatus();
73-
}
74-
},
75-
forceStopping() {
76-
if (containerStatus !== "stopping") {
77-
containerStatus = "stopping";
67+
forceContainerStatus(newContainerStatus) {
68+
if (containerStatus !== newContainerStatus) {
69+
containerStatus = newContainerStatus;
7870
deriveStatus();
7971
}
8072
},

src/utils/manage.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { commands, env, Uri, window } from "vscode";
55
import { checkIsLicenseValid } from "./authenticate.ts";
66
import { spawnLocalStack } from "./cli.ts";
77
import { exec } from "./exec.ts";
8+
import { spawn } from "./spawn.ts";
89
import type { Telemetry } from "./telemetry.ts";
910

1011
export type LocalstackStatus = "running" | "starting" | "stopping" | "stopped";
@@ -85,7 +86,7 @@ export async function getLocalstackStatus(): Promise<LocalstackStatus> {
8586
export async function startLocalStack(
8687
outputChannel: LogOutputChannel,
8788
telemetry: Telemetry,
88-
) {
89+
): Promise<void> {
8990
void showInformationMessage("Starting LocalStack.", {
9091
title: "View Logs",
9192
command: "localstack.viewLogs",
@@ -105,6 +106,17 @@ export async function startLocalStack(
105106
],
106107
{
107108
outputChannel,
109+
onStderr(data: Buffer, context) {
110+
const text = data.toString();
111+
if (
112+
text.includes(
113+
"localstack.utils.container_utils.container_client.ContainerException",
114+
)
115+
) {
116+
context.abort();
117+
throw new Error("ContainerException");
118+
}
119+
},
108120
},
109121
);
110122

@@ -129,6 +141,7 @@ export async function startLocalStack(
129141
title: "View Logs",
130142
command: "localstack.viewLogs",
131143
});
144+
throw error;
132145
}
133146

134147
telemetry.track({

src/utils/spawn.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ export class SpawnError extends Error {
133133
}
134134
}
135135

136+
export interface SpawnOptions {
137+
outputLabel?: string;
138+
outputChannel: LogOutputChannel;
139+
cancellationToken?: CancellationToken;
140+
environment?: Record<string, string | undefined> | undefined;
141+
onStderr?: (data: Buffer, context: { abort: () => void }) => void;
142+
}
143+
136144
/**
137145
* Spawns a new process using the given `command`, with command-line arguments in `args`.
138146
* - All output is appended to the `options.outputChannel`, optionally prefixed by `options.outputLabel`.
@@ -143,12 +151,7 @@ export class SpawnError extends Error {
143151
export const spawn = (
144152
command: string,
145153
args: string[],
146-
options: {
147-
outputLabel?: string;
148-
outputChannel: LogOutputChannel;
149-
cancellationToken?: CancellationToken;
150-
environment?: Record<string, string | undefined> | undefined;
151-
},
154+
options: SpawnOptions,
152155
) => {
153156
return new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
154157
(resolve, reject) => {
@@ -169,24 +172,38 @@ export const spawn = (
169172

170173
const child = childProcess.spawn(command, args, spawnOptions);
171174

175+
const killChild = () => {
176+
// Use SIGINT on Unix, 'SIGTERM' on Windows
177+
const isWindows = os.platform() === "win32";
178+
if (isWindows) {
179+
child.kill("SIGTERM");
180+
} else {
181+
child.kill("SIGINT");
182+
}
183+
};
184+
172185
const disposeCancel = options.cancellationToken?.onCancellationRequested(
173186
() => {
174187
outputChannel.appendLine(
175188
`${outputLabel}Command cancelled: ${commandLine}`,
176189
);
177-
// Use SIGINT on Unix, 'SIGTERM' on Windows
178-
const isWindows = os.platform() === "win32";
179-
if (isWindows) {
180-
child.kill("SIGTERM");
181-
} else {
182-
child.kill("SIGINT");
183-
}
190+
killChild();
184191
reject(new Error("Command cancelled"));
185192
},
186193
);
187194

188195
pipeToLogOutputChannel(child, outputChannel, outputLabel);
189196

197+
if (options.onStderr) {
198+
child.stderr?.on("data", (data: Buffer) =>
199+
options.onStderr?.(data, {
200+
abort() {
201+
killChild();
202+
},
203+
}),
204+
);
205+
}
206+
190207
child.on("close", (code, signal) => {
191208
disposeCancel?.dispose();
192209

0 commit comments

Comments
 (0)