From 6b1a49c672352fb52117e0b69d973a51df4a15fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Tue, 9 Sep 2025 15:54:50 +0200 Subject: [PATCH] feat: use file watchers to track the LS CLI status License status for LS CLI is now checked via file watchers instead of periodic checks. --- src/utils/license.ts | 29 +++++++++++++++++++++++++++++ src/utils/setup-status.ts | 26 ++++++++++++++++++++++++++ src/utils/setup.ts | 5 +---- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/utils/license.ts b/src/utils/license.ts index e41350f..631d01e 100644 --- a/src/utils/license.ts +++ b/src/utils/license.ts @@ -1,7 +1,36 @@ +import { homedir, platform } from "node:os"; +import { join } from "node:path"; + import type { CancellationToken, LogOutputChannel } from "vscode"; import { execLocalStack } from "./cli.ts"; +/** + * See https://github.com/localstack/localstack/blob/de861e1f656a52eaa090b061bd44fc1a7069715e/localstack-core/localstack/utils/files.py#L38-L55. + * @returns The cache directory for the current platform. + */ +const cacheDirectory = () => { + switch (platform()) { + case "win32": + return join(process.env.LOCALAPPDATA!, "cache"); + case "darwin": + return join(homedir(), "Library", "Caches"); + default: + return process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"); + } +}; + +/** + * The file that contains the license information of the LocalStack CLI. + * + * The license file is stored in the cache directory for the current platform. + */ +export const LICENSE_FILENAME = join( + cacheDirectory(), + "localstack-cli", + "license.json", +); + const LICENSE_VALIDITY_MARKER = "license validity: valid"; export async function checkIsLicenseValid(outputChannel: LogOutputChannel) { diff --git a/src/utils/setup-status.ts b/src/utils/setup-status.ts index 0fab112..0b96dbc 100644 --- a/src/utils/setup-status.ts +++ b/src/utils/setup-status.ts @@ -13,6 +13,7 @@ import { } from "./configure-aws.ts"; import { createEmitter } from "./emitter.ts"; import { immediateOnce } from "./immediate-once.ts"; +import { checkIsLicenseValid, LICENSE_FILENAME } from "./license.ts"; import type { UnwrapPromise } from "./promises.ts"; import { checkSetupStatus } from "./setup.ts"; import type { TimeTracker } from "./time-tracker.ts"; @@ -39,6 +40,7 @@ export async function createSetupStatusTracker( const awsProfileTracker = createAwsProfileStatusTracker(outputChannel); const localStackAuthenticationTracker = createLocalStackAuthenticationStatusTracker(outputChannel); + const licenseTracker = createLicenseStatusTracker(outputChannel); const end = Date.now(); outputChannel.trace( `[setup-status]: Initialized dependencies in ${ms(end - start, { long: true })}`, @@ -51,6 +53,7 @@ export async function createSetupStatusTracker( Object.values(statuses), awsProfileTracker.status() === "ok", localStackAuthenticationTracker.status() === "ok", + licenseTracker.status() === "ok", ].some((check) => check === false); const newStatus = setupRequired ? "setup_required" : "ok"; @@ -75,6 +78,10 @@ export async function createSetupStatusTracker( checkStatus(); }); + licenseTracker.onChange(() => { + checkStatus(); + }); + let timeout: NodeJS.Timeout | undefined; const startChecking = () => { checkStatus(); @@ -225,3 +232,22 @@ function createLocalStackAuthenticationStatusTracker( async () => ((await checkIsAuthenticated()) ? "ok" : "setup_required"), ); } + +/** + * Creates a status tracker that monitors the LocalStack license file for changes. + * When the file is changed, the provided check function is called to determine the current setup status. + * Emits status changes to registered listeners. + * + * @param outputChannel - Channel for logging output and trace messages. + * @returns A {@link StatusTracker} instance for querying status, subscribing to changes, and disposing resources. + */ +function createLicenseStatusTracker( + outputChannel: LogOutputChannel, +): StatusTracker { + return createFileStatusTracker( + outputChannel, + "[setup-status.license]", + [LICENSE_FILENAME], + async () => ((await checkIsLicenseValid(outputChannel)) ? "ok" : "setup_required"), + ); +} diff --git a/src/utils/setup.ts b/src/utils/setup.ts index 14becc7..535acc2 100644 --- a/src/utils/setup.ts +++ b/src/utils/setup.ts @@ -5,18 +5,15 @@ import { LOCALSTACK_DOCKER_IMAGE_NAME } from "../constants.ts"; import { exec } from "./exec.ts"; import { checkLocalstackInstalled } from "./install.ts"; -import { checkIsLicenseValid } from "./license.ts"; import { spawn } from "./spawn.ts"; export async function checkSetupStatus(outputChannel: LogOutputChannel) { - const [isInstalled, isLicenseValid] = await Promise.all([ + const [isInstalled] = await Promise.all([ checkLocalstackInstalled(outputChannel), - checkIsLicenseValid(outputChannel), ]); return { isInstalled, - isLicenseValid, }; }