From 75e907558cb20050862cf8ef42e9c354f822246c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 12:15:21 +0200 Subject: [PATCH 01/28] feat: reflect localstack status immediately after running start and stop commands Makes the status bar display that LS is either starting or stopping immediately after executing the respective LS commands. --- src/utils/localstack-status.ts | 37 ++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/utils/localstack-status.ts b/src/utils/localstack-status.ts index f9ac65b..2b169e4 100644 --- a/src/utils/localstack-status.ts +++ b/src/utils/localstack-status.ts @@ -11,6 +11,9 @@ export type LocalStackStatus = "starting" | "running" | "stopping" | "stopped"; export interface LocalStackStatusTracker extends Disposable { status(): LocalStackStatus; + // setStatus(status: LocalStackStatus): void; + forceStarting(): void; + forceStopping(): void; onChange(callback: (status: LocalStackStatus) => void): void; } @@ -22,30 +25,36 @@ export async function createLocalStackStatusTracker( outputChannel: LogOutputChannel, timeTracker: TimeTracker, ): Promise { + let containerStatus: ContainerStatus | undefined; let status: LocalStackStatus | undefined; const emitter = createEmitter(outputChannel); let healthCheck: boolean | undefined; - const updateStatus = () => { - const newStatus = getLocalStackStatus( - containerStatusTracker.status(), - healthCheck, - ); + const setStatus = (newStatus: LocalStackStatus) => { if (status !== newStatus) { status = newStatus; + outputChannel.debug(`[localstack.status]: ${status}`); void emitter.emit(status); } }; - containerStatusTracker.onChange(() => { - updateStatus(); + const deriveStatus = () => { + const newStatus = getLocalStackStatus(containerStatus, healthCheck); + setStatus(newStatus); + }; + + containerStatusTracker.onChange((newContainerStatus) => { + if (containerStatus !== newContainerStatus) { + containerStatus = newContainerStatus; + deriveStatus(); + } }); let healthCheckTimeout: NodeJS.Timeout | undefined; const startHealthCheck = async () => { healthCheck = await fetchHealth(); - updateStatus(); + deriveStatus(); healthCheckTimeout = setTimeout(() => void startHealthCheck(), 1_000); }; @@ -58,6 +67,18 @@ export async function createLocalStackStatusTracker( // biome-ignore lint/style/noNonNullAssertion: false positive return status!; }, + forceStarting() { + if (containerStatus !== "running") { + containerStatus = "running"; + deriveStatus(); + } + }, + forceStopping() { + if (containerStatus !== "stopping") { + containerStatus = "stopping"; + deriveStatus(); + } + }, onChange(callback) { emitter.on(callback); if (status) { From 20fee3c627f744fd2006138119bbb712570d039e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 12:15:21 +0200 Subject: [PATCH 02/28] feat: reflect localstack status immediately after running start and stop commands Makes the status bar display that LS is either starting or stopping immediately after executing the respective LS commands. From e1b114f405a652d40da32d4ded320e1f89017d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 12:15:21 +0200 Subject: [PATCH 03/28] feat: reflect localstack status immediately after running start and stop commands Makes the status bar display that LS is either starting or stopping immediately after executing the respective LS commands. From dd3748ca9846c94d46fa4e6a85cde6b6f2da430a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 12:15:21 +0200 Subject: [PATCH 04/28] feat: reflect localstack status immediately after running start and stop commands Makes the status bar display that LS is either starting or stopping immediately after executing the respective LS commands. From 2d9d8f40868a66b9cd607854d22e21181db1b686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 13:28:09 +0200 Subject: [PATCH 05/28] wip --- src/plugins/manage.ts | 14 ++++++++++++-- src/utils/localstack-status.ts | 1 - 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/plugins/manage.ts b/src/plugins/manage.ts index 9eca357..47a27c3 100644 --- a/src/plugins/manage.ts +++ b/src/plugins/manage.ts @@ -1,4 +1,4 @@ -import { commands } from "vscode"; +import { commands, window } from "vscode"; import { createPlugin } from "../plugins.ts"; import { @@ -9,7 +9,7 @@ import { export default createPlugin( "manage", - ({ context, outputChannel, telemetry }) => { + ({ context, outputChannel, telemetry, localStackStatusTracker }) => { context.subscriptions.push( commands.registerCommand("localstack.viewLogs", () => { outputChannel.show(true); @@ -18,12 +18,22 @@ export default createPlugin( context.subscriptions.push( commands.registerCommand("localstack.start", () => { + if (localStackStatusTracker.status() !== "stopped") { + window.showInformationMessage("LocalStack is already running."); + return; + } + localStackStatusTracker.forceStarting(); void startLocalStack(outputChannel, telemetry); }), ); context.subscriptions.push( commands.registerCommand("localstack.stop", () => { + if (localStackStatusTracker.status() !== "running") { + window.showInformationMessage("LocalStack is not running."); + return; + } + localStackStatusTracker.forceStopping(); void stopLocalStack(outputChannel, telemetry); }), ); diff --git a/src/utils/localstack-status.ts b/src/utils/localstack-status.ts index 2b169e4..56456f5 100644 --- a/src/utils/localstack-status.ts +++ b/src/utils/localstack-status.ts @@ -34,7 +34,6 @@ export async function createLocalStackStatusTracker( const setStatus = (newStatus: LocalStackStatus) => { if (status !== newStatus) { status = newStatus; - outputChannel.debug(`[localstack.status]: ${status}`); void emitter.emit(status); } }; From 9d38c6652846e765937c43b29327f6251bd36a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 13:28:09 +0200 Subject: [PATCH 06/28] wip From 95d818422613c713899079d47ef54a0465251fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 13:28:09 +0200 Subject: [PATCH 07/28] wip From 395af433b606772779f3500311e1c64c1348b0a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 13:28:09 +0200 Subject: [PATCH 08/28] wip From dddee7d9041b8c8896f4af97e330db8a49ca2d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 14:44:04 +0200 Subject: [PATCH 09/28] handle LS CLI failures using text heuristic --- src/plugins/manage.ts | 12 ++++++---- src/utils/cli.ts | 3 +++ src/utils/localstack-status.ts | 16 ++++--------- src/utils/manage.ts | 15 +++++++++++- src/utils/spawn.ts | 43 ++++++++++++++++++++++++---------- 5 files changed, 59 insertions(+), 30 deletions(-) diff --git a/src/plugins/manage.ts b/src/plugins/manage.ts index 47a27c3..0f02549 100644 --- a/src/plugins/manage.ts +++ b/src/plugins/manage.ts @@ -17,13 +17,17 @@ export default createPlugin( ); context.subscriptions.push( - commands.registerCommand("localstack.start", () => { + commands.registerCommand("localstack.start", async () => { if (localStackStatusTracker.status() !== "stopped") { window.showInformationMessage("LocalStack is already running."); return; } - localStackStatusTracker.forceStarting(); - void startLocalStack(outputChannel, telemetry); + localStackStatusTracker.forceContainerStatus("running"); + try { + await startLocalStack(outputChannel, telemetry); + } catch { + localStackStatusTracker.forceContainerStatus("stopped"); + } }), ); @@ -33,7 +37,7 @@ export default createPlugin( window.showInformationMessage("LocalStack is not running."); return; } - localStackStatusTracker.forceStopping(); + localStackStatusTracker.forceContainerStatus("stopping"); void stopLocalStack(outputChannel, telemetry); }), ); diff --git a/src/utils/cli.ts b/src/utils/cli.ts index 8075827..99b740c 100644 --- a/src/utils/cli.ts +++ b/src/utils/cli.ts @@ -8,6 +8,7 @@ import { CLI_PATHS } from "../constants.ts"; import { exec } from "./exec.ts"; import { spawn } from "./spawn.ts"; +import type { SpawnOptions } from "./spawn.ts"; const IMAGE_NAME = "localstack/localstack-pro"; const LOCALSTACK_LDM_PREVIEW = "1"; @@ -68,6 +69,7 @@ export const spawnLocalStack = async ( options: { outputChannel: LogOutputChannel; cancellationToken?: CancellationToken; + onStderr?: SpawnOptions["onStderr"]; }, ) => { const cli = await findLocalStack(); @@ -81,5 +83,6 @@ export const spawnLocalStack = async ( IMAGE_NAME, LOCALSTACK_LDM_PREVIEW, }, + onStderr: options.onStderr, }); }; diff --git a/src/utils/localstack-status.ts b/src/utils/localstack-status.ts index 56456f5..f89525c 100644 --- a/src/utils/localstack-status.ts +++ b/src/utils/localstack-status.ts @@ -11,9 +11,7 @@ export type LocalStackStatus = "starting" | "running" | "stopping" | "stopped"; export interface LocalStackStatusTracker extends Disposable { status(): LocalStackStatus; - // setStatus(status: LocalStackStatus): void; - forceStarting(): void; - forceStopping(): void; + forceContainerStatus(status: ContainerStatus): void; onChange(callback: (status: LocalStackStatus) => void): void; } @@ -66,15 +64,9 @@ export async function createLocalStackStatusTracker( // biome-ignore lint/style/noNonNullAssertion: false positive return status!; }, - forceStarting() { - if (containerStatus !== "running") { - containerStatus = "running"; - deriveStatus(); - } - }, - forceStopping() { - if (containerStatus !== "stopping") { - containerStatus = "stopping"; + forceContainerStatus(newContainerStatus) { + if (containerStatus !== newContainerStatus) { + containerStatus = newContainerStatus; deriveStatus(); } }, diff --git a/src/utils/manage.ts b/src/utils/manage.ts index 42eae83..0fab790 100644 --- a/src/utils/manage.ts +++ b/src/utils/manage.ts @@ -5,6 +5,7 @@ import { commands, env, Uri, window } from "vscode"; import { checkIsLicenseValid } from "./authenticate.ts"; import { spawnLocalStack } from "./cli.ts"; import { exec } from "./exec.ts"; +import { spawn } from "./spawn.ts"; import type { Telemetry } from "./telemetry.ts"; export type LocalstackStatus = "running" | "starting" | "stopping" | "stopped"; @@ -85,7 +86,7 @@ export async function getLocalstackStatus(): Promise { export async function startLocalStack( outputChannel: LogOutputChannel, telemetry: Telemetry, -) { +): Promise { void showInformationMessage("Starting LocalStack.", { title: "View Logs", command: "localstack.viewLogs", @@ -105,6 +106,17 @@ export async function startLocalStack( ], { outputChannel, + onStderr(data: Buffer, context) { + const text = data.toString(); + if ( + text.includes( + "localstack.utils.container_utils.container_client.ContainerException", + ) + ) { + context.abort(); + throw new Error("ContainerException"); + } + }, }, ); @@ -129,6 +141,7 @@ export async function startLocalStack( title: "View Logs", command: "localstack.viewLogs", }); + throw error; } telemetry.track({ diff --git a/src/utils/spawn.ts b/src/utils/spawn.ts index 08bcafc..565eaf2 100644 --- a/src/utils/spawn.ts +++ b/src/utils/spawn.ts @@ -133,6 +133,14 @@ export class SpawnError extends Error { } } +export interface SpawnOptions { + outputLabel?: string; + outputChannel: LogOutputChannel; + cancellationToken?: CancellationToken; + environment?: Record | undefined; + onStderr?: (data: Buffer, context: { abort: () => void }) => void; +} + /** * Spawns a new process using the given `command`, with command-line arguments in `args`. * - All output is appended to the `options.outputChannel`, optionally prefixed by `options.outputLabel`. @@ -143,12 +151,7 @@ export class SpawnError extends Error { export const spawn = ( command: string, args: string[], - options: { - outputLabel?: string; - outputChannel: LogOutputChannel; - cancellationToken?: CancellationToken; - environment?: Record | undefined; - }, + options: SpawnOptions, ) => { return new Promise<{ code: number | null; signal: NodeJS.Signals | null }>( (resolve, reject) => { @@ -169,24 +172,38 @@ export const spawn = ( const child = childProcess.spawn(command, args, spawnOptions); + const killChild = () => { + // Use SIGINT on Unix, 'SIGTERM' on Windows + const isWindows = os.platform() === "win32"; + if (isWindows) { + child.kill("SIGTERM"); + } else { + child.kill("SIGINT"); + } + }; + const disposeCancel = options.cancellationToken?.onCancellationRequested( () => { outputChannel.appendLine( `${outputLabel}Command cancelled: ${commandLine}`, ); - // Use SIGINT on Unix, 'SIGTERM' on Windows - const isWindows = os.platform() === "win32"; - if (isWindows) { - child.kill("SIGTERM"); - } else { - child.kill("SIGINT"); - } + killChild(); reject(new Error("Command cancelled")); }, ); pipeToLogOutputChannel(child, outputChannel, outputLabel); + if (options.onStderr) { + child.stderr?.on("data", (data: Buffer) => + options.onStderr?.(data, { + abort() { + killChild(); + }, + }), + ); + } + child.on("close", (code, signal) => { disposeCancel?.dispose(); From 9943cb3ffd1325e2ce96228fb612aeaecf73c7c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 14:44:04 +0200 Subject: [PATCH 10/28] handle LS CLI failures using text heuristic From 18bcef264e1acd36fce58937e6346b62f69b748c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 14:44:04 +0200 Subject: [PATCH 11/28] handle LS CLI failures using text heuristic From 68c2423e354bfca0ea939988509ff4c19de4c0c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 14:44:04 +0200 Subject: [PATCH 12/28] handle LS CLI failures using text heuristic From ef66da95d178fa788f073ccf06178e1e12be102a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 14:51:45 +0200 Subject: [PATCH 13/28] wip --- src/utils/manage.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/manage.ts b/src/utils/manage.ts index 0fab790..eb7c8cb 100644 --- a/src/utils/manage.ts +++ b/src/utils/manage.ts @@ -108,6 +108,8 @@ export async function startLocalStack( outputChannel, onStderr(data: Buffer, context) { const text = data.toString(); + // Currently, the LocalStack CLI does not return a non-zero exit code if the container fails to start. + // As a workaround, we look for a specific error message in the output to determine if the container failed to start. if ( text.includes( "localstack.utils.container_utils.container_client.ContainerException", From 9ce74e85daf68cc6f82000303c2cb7aaf4f9c7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 14:51:45 +0200 Subject: [PATCH 14/28] wip From 81cb350eed3456a0f5378a29f6d28b0c9530bd86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 14:51:45 +0200 Subject: [PATCH 15/28] wip From 891f053a6640085fb66669ef7857354060478417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 14:51:45 +0200 Subject: [PATCH 16/28] wip From 995d8eeaa93e5d0f2cde38b0d7386f785a5f6756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 14:52:28 +0200 Subject: [PATCH 17/28] wip --- src/utils/manage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/manage.ts b/src/utils/manage.ts index eb7c8cb..217a4e5 100644 --- a/src/utils/manage.ts +++ b/src/utils/manage.ts @@ -115,6 +115,7 @@ export async function startLocalStack( "localstack.utils.container_utils.container_client.ContainerException", ) ) { + // Abort the process if we detect a ContainerException, otherwise it will hang indefinitely. context.abort(); throw new Error("ContainerException"); } From 40e7c486c7345859a585d1238924889050071042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 14:52:28 +0200 Subject: [PATCH 18/28] wip From 7aa4672dcc7faac504c7f1b31e7afe94f22da769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 14:52:28 +0200 Subject: [PATCH 19/28] wip From 6699d2b4f9bf3f502f3a3e93337a7e390fd7f154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 14:52:28 +0200 Subject: [PATCH 20/28] wip From 6f065ced038b36f598c878aaf579f453c5e863c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 16:27:02 +0200 Subject: [PATCH 21/28] wip --- src/utils/manage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/manage.ts b/src/utils/manage.ts index 217a4e5..63eec25 100644 --- a/src/utils/manage.ts +++ b/src/utils/manage.ts @@ -108,7 +108,7 @@ export async function startLocalStack( outputChannel, onStderr(data: Buffer, context) { const text = data.toString(); - // Currently, the LocalStack CLI does not return a non-zero exit code if the container fails to start. + // Currently, the LocalStack CLI does not exit if the container fails to start in specific scenarios. // As a workaround, we look for a specific error message in the output to determine if the container failed to start. if ( text.includes( From ebde06ac7c94f31acd804fb6caf5c24324d05136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 16:27:02 +0200 Subject: [PATCH 22/28] wip From 40efd181106fc1c8075e9dbd98223855bfc89f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 16:27:02 +0200 Subject: [PATCH 23/28] wip From 0baef45df5c61940e4a34d8946e0288e041f9e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 16:27:02 +0200 Subject: [PATCH 24/28] wip From 68c222d8b14182ef09a2912be2cec680582bc9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 16:27:02 +0200 Subject: [PATCH 25/28] wip From d94c40c39fc7c7d654f51b31b145f5b2a6919650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 16:27:02 +0200 Subject: [PATCH 26/28] wip From e20ec766fad901d9d5c34070d73a71ba45d464b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 16:27:02 +0200 Subject: [PATCH 27/28] wip From 3c4c3da0a84dda6c94b161449bfc4c326690ffcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 4 Sep 2025 16:27:02 +0200 Subject: [PATCH 28/28] wip