From c2e4698dfcc1a6692e3c3890a0c31e6372cf5892 Mon Sep 17 00:00:00 2001 From: mantrakp04 Date: Wed, 18 Mar 2026 15:33:44 -0700 Subject: [PATCH] Refactor local emulator configuration management and remove obsolete file bridge - Removed the `readConfigFileContentIfExists` and `writeConfigToFile` functions from the local emulator module, simplifying file handling. - Updated the `readConfigFromFile` function to utilize environment variables for configuration content, enhancing flexibility. - Refactored tests to align with the new configuration management approach, ensuring accurate validation of file reading and handling. - Consolidated Docker environment variables into a new `base.env` file for improved organization and maintainability. - Removed the host file bridge script and related logic, streamlining the emulator's architecture and reducing complexity. These changes significantly enhance the local emulator's usability and maintainability for developers. --- .../internal/local-emulator/project/route.tsx | 7 - apps/backend/src/lib/config.tsx | 10 +- apps/backend/src/lib/local-emulator.test.ts | 82 ++------ apps/backend/src/lib/local-emulator.ts | 112 ++--------- docker/local-emulator/base.env | 66 +++++++ docker/local-emulator/docker-compose.yaml | 75 +------- docker/local-emulator/qemu/build-image.sh | 12 +- .../qemu/cloud-init/emulator/user-data | 175 +++--------------- .../local-emulator/qemu/cloud-init/meta-data | 2 - .../local-emulator/qemu/cloud-init/user-data | 6 - .../local-emulator/qemu/host-file-bridge.mjs | 139 -------------- .../local-emulator/qemu/prepare-app-bundle.sh | 5 - .../qemu/prepare-image-bundle.sh | 36 ---- docker/local-emulator/qemu/run-emulator.sh | 156 ++++------------ .../demo/src/app/emulator-status/page.tsx | 2 +- 15 files changed, 167 insertions(+), 718 deletions(-) create mode 100644 docker/local-emulator/base.env delete mode 100644 docker/local-emulator/qemu/cloud-init/meta-data delete mode 100644 docker/local-emulator/qemu/cloud-init/user-data delete mode 100644 docker/local-emulator/qemu/host-file-bridge.mjs delete mode 100755 docker/local-emulator/qemu/prepare-app-bundle.sh delete mode 100755 docker/local-emulator/qemu/prepare-image-bundle.sh diff --git a/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx b/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx index 996039a84..0635cd388 100644 --- a/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx +++ b/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx @@ -4,9 +4,7 @@ import { LOCAL_EMULATOR_ONLY_ENDPOINT_MESSAGE, LOCAL_EMULATOR_OWNER_TEAM_ID, isLocalEmulatorEnabled, - readConfigFileContentIfExists, readConfigFromFile, - writeConfigToFile, } from "@/lib/local-emulator"; import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from "@/lib/tenancies"; import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client"; @@ -193,11 +191,6 @@ export const POST = createSmartRouteHandler({ const absoluteFilePath = path.resolve(req.body.absolute_file_path); - const fileContent = await readConfigFileContentIfExists(absoluteFilePath) ?? ""; - if (fileContent.trim() === "") { - await writeConfigToFile(absoluteFilePath, {}); - } - await assertLocalEmulatorOwnerTeamReadiness(); const projectId = await getOrCreateLocalEmulatorProjectId(absoluteFilePath); diff --git a/apps/backend/src/lib/config.tsx b/apps/backend/src/lib/config.tsx index 078dd9be8..c1c90f410 100644 --- a/apps/backend/src/lib/config.tsx +++ b/apps/backend/src/lib/config.tsx @@ -11,7 +11,7 @@ import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { deindent, stringCompare } from "@stackframe/stack-shared/dist/utils/strings"; import * as yup from "yup"; import { RawQuery, globalPrismaClient, rawQuery } from "../prisma-client"; -import { LOCAL_EMULATOR_ENV_CONFIG_BLOCKED_MESSAGE, getLocalEmulatorFilePath, isLocalEmulatorEnabled, isLocalEmulatorProject, readConfigFromFile, writeConfigToFile } from "./local-emulator"; +import { LOCAL_EMULATOR_ENV_CONFIG_BLOCKED_MESSAGE, isLocalEmulatorEnabled, isLocalEmulatorProject, readConfigFromFile } from "./local-emulator"; import { listPermissionDefinitionsFromConfig } from "./permissions"; type BranchConfigSourceApi = yup.InferType; @@ -266,14 +266,6 @@ export async function setBranchConfigOverride(options: { }): Promise { const newConfig = migrateConfigOverride("branch", options.branchConfigOverride); - if (isLocalEmulatorEnabled() && await isLocalEmulatorProject(options.projectId)) { - const filePath = await getLocalEmulatorFilePath(options.projectId); - if (filePath) { - await writeConfigToFile(filePath, newConfig); - return; - } - } - // large configs make our DB slow; let's prevent them early const newConfigString = JSON.stringify(newConfig); if (newConfigString.length > 1_000_000) { diff --git a/apps/backend/src/lib/local-emulator.test.ts b/apps/backend/src/lib/local-emulator.test.ts index 4fbdf4d7d..028c5000b 100644 --- a/apps/backend/src/lib/local-emulator.test.ts +++ b/apps/backend/src/lib/local-emulator.test.ts @@ -1,91 +1,33 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { readConfigFileContentIfExists, readConfigFromFile, writeConfigToFile } from "./local-emulator"; +import { readConfigFromFile } from "./local-emulator"; -describe("local emulator file bridge", () => { +describe("local emulator config", () => { afterEach(() => { vi.unstubAllEnvs(); vi.restoreAllMocks(); - vi.unstubAllGlobals(); }); - it("reads config files through the host file bridge when configured", async () => { - vi.stubEnv("STACK_LOCAL_EMULATOR_FILE_BRIDGE_URL", "http://127.0.0.1:8116"); - vi.stubEnv("STACK_LOCAL_EMULATOR_FILE_BRIDGE_TOKEN", "bridge-token"); + it("reads config from STACK_LOCAL_EMULATOR_CONFIG_CONTENT env var when set", async () => { + const content = `export const config = { auth: { allowLocalhost: true } };\n`; + vi.stubEnv("STACK_LOCAL_EMULATOR_CONFIG_CONTENT", Buffer.from(content).toString("base64")); - const fetchMock = vi.fn(async () => { - return new Response(JSON.stringify({ - exists: true, - content: "export const config = { auth: { allowLocalhost: true } };\n", - }), { - status: 200, - headers: { - "Content-Type": "application/json", - }, - }); - }); - vi.stubGlobal("fetch", fetchMock); - - await expect(readConfigFromFile("/Users/tester/example/stack.config.ts")).resolves.toMatchInlineSnapshot(` + await expect(readConfigFromFile("/irrelevant/path/stack.config.ts")).resolves.toMatchInlineSnapshot(` { "auth": { "allowLocalhost": true, }, } `); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith( - new URL("/read", "http://127.0.0.1:8116"), - expect.objectContaining({ - method: "POST", - headers: expect.objectContaining({ - "Content-Type": "application/json", - "X-Stack-Emulator-Token": "bridge-token", - }), - }), - ); }); - it("returns null for missing files when the host file bridge reports they do not exist", async () => { - vi.stubEnv("STACK_LOCAL_EMULATOR_FILE_BRIDGE_URL", "http://127.0.0.1:8116"); - vi.stubEnv("STACK_LOCAL_EMULATOR_FILE_BRIDGE_TOKEN", "bridge-token"); - - vi.stubGlobal("fetch", vi.fn(async () => { - return new Response(JSON.stringify({ exists: false }), { - status: 200, - headers: { - "Content-Type": "application/json", - }, - }); - })); - - await expect(readConfigFileContentIfExists("/Users/tester/example/missing-stack.config.ts")).resolves.toBeNull(); + it("returns empty object when env var is not set and file does not exist", async () => { + await expect(readConfigFromFile("/nonexistent/stack.config.ts")).resolves.toEqual({}); }); - it("writes config files through the host file bridge when configured", async () => { - vi.stubEnv("STACK_LOCAL_EMULATOR_FILE_BRIDGE_URL", "http://127.0.0.1:8116"); - vi.stubEnv("STACK_LOCAL_EMULATOR_FILE_BRIDGE_TOKEN", "bridge-token"); + it("returns empty object when env var content is empty", async () => { + const content = ``; + vi.stubEnv("STACK_LOCAL_EMULATOR_CONFIG_CONTENT", Buffer.from(content).toString("base64")); - const fetchMock = vi.fn(async () => { - return new Response(JSON.stringify({ ok: true }), { - status: 200, - headers: { - "Content-Type": "application/json", - }, - }); - }); - vi.stubGlobal("fetch", fetchMock); - - await writeConfigToFile("/Users/tester/example/stack.config.ts", { teams: { enabled: true } }); - - expect(fetchMock).toHaveBeenCalledWith( - new URL("/write", "http://127.0.0.1:8116"), - expect.objectContaining({ - method: "POST", - body: JSON.stringify({ - path: "/Users/tester/example/stack.config.ts", - content: `export const config = ${JSON.stringify({ teams: { enabled: true } }, null, 2)};\n`, - }), - }), - ); + await expect(readConfigFromFile("/irrelevant/path/stack.config.ts")).resolves.toEqual({}); }); }); diff --git a/apps/backend/src/lib/local-emulator.ts b/apps/backend/src/lib/local-emulator.ts index a13815f05..690a5a493 100644 --- a/apps/backend/src/lib/local-emulator.ts +++ b/apps/backend/src/lib/local-emulator.ts @@ -3,7 +3,7 @@ import path from "path"; import { createJiti } from "jiti"; import { isValidConfig } from "@stackframe/stack-shared/dist/config/format"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { globalPrismaClient } from "@/prisma-client"; export const LOCAL_EMULATOR_ADMIN_USER_ID = "63abbc96-5329-454a-ba56-e0460173c6c1"; @@ -36,102 +36,17 @@ export async function isLocalEmulatorProject(projectId: string) { return project !== null; } -export async function getLocalEmulatorFilePath(projectId: string): Promise { - const result = await globalPrismaClient.localEmulatorProject.findUnique({ - where: { projectId }, - select: { absoluteFilePath: true }, - }); - return result?.absoluteFilePath ?? null; -} - -function getLocalEmulatorFileBridgeConfig() { - const url = getEnvVariable("STACK_LOCAL_EMULATOR_FILE_BRIDGE_URL", ""); - const token = getEnvVariable("STACK_LOCAL_EMULATOR_FILE_BRIDGE_TOKEN", ""); - if (url === "") { - return null; - } - if (token === "") { - throw new StackAssertionError("STACK_LOCAL_EMULATOR_FILE_BRIDGE_TOKEN must be set when STACK_LOCAL_EMULATOR_FILE_BRIDGE_URL is configured."); - } - return { url, token }; -} - -function isObject(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -async function requestLocalEmulatorFileBridge(pathname: string, body: Record): Promise { - const bridgeConfig = getLocalEmulatorFileBridgeConfig(); - if (bridgeConfig === null) { - throw new StackAssertionError("Local emulator file bridge is not configured."); - } - - const response = await fetch(new URL(pathname, bridgeConfig.url), { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Stack-Emulator-Token": bridgeConfig.token, - }, - body: JSON.stringify(body), - }); - - const responseText = await response.text(); - if (!response.ok) { - throw new StackAssertionError(`Local emulator file bridge request failed: ${response.status} ${responseText || response.statusText}`); - } - - try { - return JSON.parse(responseText) as unknown; - } catch { - throw new StackAssertionError(`Local emulator file bridge returned invalid JSON for ${pathname}.`); - } -} - -export async function readConfigFileContentIfExists(filePath: string): Promise { - const bridgeConfig = getLocalEmulatorFileBridgeConfig(); - if (bridgeConfig !== null) { - const responseJson = await requestLocalEmulatorFileBridge("/read", { path: filePath }); - if (!isObject(responseJson) || typeof responseJson.exists !== "boolean") { - throw new StackAssertionError("Local emulator file bridge returned an invalid read response.", { responseJson }); - } - if (!responseJson.exists) { - return null; - } - if (typeof responseJson.content !== "string") { - throw new StackAssertionError("Local emulator file bridge read response is missing file content.", { responseJson }); - } - return responseJson.content; - } - - try { - return await fs.readFile(filePath, "utf-8"); - } catch (error: unknown) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") { - return null; - } - throw error; - } -} - -async function writeConfigFileContent(filePath: string, content: string): Promise { - const bridgeConfig = getLocalEmulatorFileBridgeConfig(); - if (bridgeConfig !== null) { - const responseJson = await requestLocalEmulatorFileBridge("/write", { path: filePath, content }); - if (!isObject(responseJson) || responseJson.ok !== true) { - throw new StackAssertionError("Local emulator file bridge returned an invalid write response.", { responseJson }); - } - return; - } - - const dir = path.dirname(filePath); - await fs.mkdir(dir, { recursive: true }); - await fs.writeFile(filePath, content, "utf-8"); -} - export async function readConfigFromFile(filePath: string): Promise> { - const content = await readConfigFileContentIfExists(filePath); - if (content === null) { - throw new StatusError(StatusError.BadRequest, `Config file not found: ${filePath}`); + const configContentBase64 = getEnvVariable("STACK_LOCAL_EMULATOR_CONFIG_CONTENT", ""); + const content = configContentBase64 !== "" + ? Buffer.from(configContentBase64, "base64").toString("utf-8") + : await fs.readFile(filePath, "utf-8").catch((error: unknown) => { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; + throw error; + }); + + if (content === null || content.trim() === "") { + return {}; } const jiti = createJiti(import.meta.url, { cache: false }); @@ -142,8 +57,3 @@ export async function readConfigFromFile(filePath: string): Promise): Promise { - const content = `export const config = ${JSON.stringify(config, null, 2)};\n`; - await writeConfigFileContent(filePath, content); -} diff --git a/docker/local-emulator/base.env b/docker/local-emulator/base.env new file mode 100644 index 000000000..b5bca9b83 --- /dev/null +++ b/docker/local-emulator/base.env @@ -0,0 +1,66 @@ +NEXT_PUBLIC_STACK_DOCS_BASE_URL=https://docs.stack-auth.com +NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true +NEXT_PUBLIC_STACK_PROJECT_ID=internal +NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only +STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only +STACK_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo +STACK_CHANGELOG_URL=https://raw.githubusercontent.com/stack-auth/stack-auth/refs/heads/dev/CHANGELOG.md +STACK_SEED_ENABLE_DUMMY_PROJECT=true +STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=true +STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED=true +STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=true +STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS=github,spotify,google,microsoft +STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=true +STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only +STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only +STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only +STACK_SVIX_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTUxNDA2MzksImV4cCI6MTk3MDUwMDYzOSwibmJmIjoxNjU1MTQwNjM5LCJpc3MiOiJzdml4LXNlcnZlciIsInN1YiI6Im9yZ18yM3JiOFlkR3FNVDBxSXpwZ0d3ZFhmSGlyTXUifQ.En8w77ZJWbd0qrMlHHupHUB-4cx17RfzFykseg95SUk +STACK_OPENAI_API_KEY=mock_openai_api_key +STACK_OPENROUTER_API_KEY=FORWARD_TO_PRODUCTION +STACK_STRIPE_SECRET_KEY=sk_test_mockstripekey +STACK_STRIPE_WEBHOOK_SECRET=mock_stripe_webhook_secret +STACK_RESEND_API_KEY=mock_resend_api_key +STACK_RESEND_WEBHOOK_SECRET=mock_resend_webhook_secret +STACK_DNSIMPLE_API_TOKEN=mock_dnsimple_api_token +STACK_DNSIMPLE_ACCOUNT_ID=mock_dnsimple_account_id +STACK_DNSIMPLE_API_BASE_URL=https://api.dnsimple.com/v2 +STACK_FREESTYLE_API_KEY=mock_stack_freestyle_key +STACK_VERCEL_SANDBOX_TOKEN=vercel_sandbox_disabled_for_local_development +CRON_SECRET=mock_cron_secret +STACK_S3_REGION=us-east-1 +STACK_S3_ACCESS_KEY_ID=s3mockroot +STACK_S3_SECRET_ACCESS_KEY=s3mockroot +STACK_S3_BUCKET=stack-storage +STACK_S3_PRIVATE_BUCKET=stack-storage-private +STACK_AWS_REGION=us-east-1 +STACK_AWS_ACCESS_KEY_ID=test +STACK_AWS_SECRET_ACCESS_KEY=test +STACK_QSTASH_TOKEN=eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0= +STACK_QSTASH_CURRENT_SIGNING_KEY=sig_7kYjw48mhY7kAjqNGcy6cr29RJ6r +STACK_QSTASH_NEXT_SIGNING_KEY=sig_5ZB6DVzB1wjE8S6rZ7eenA8Pdnhs +STACK_CLICKHOUSE_ADMIN_USER=stackframe +STACK_CLICKHOUSE_ADMIN_PASSWORD=PASSWORD-PLACEHOLDER--9gKyMxJeMx +STACK_CLICKHOUSE_EXTERNAL_PASSWORD=PASSWORD-PLACEHOLDER--EZeHscBMzE +STACK_EMAIL_PORT=2500 +STACK_EMAIL_SECURE=false +STACK_EMAIL_USERNAME=does-not-matter +STACK_EMAIL_PASSWORD=does-not-matter +STACK_EMAIL_SENDER=noreply@example.com +STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR=10000 +STACK_EMAIL_MONITOR_PROJECT_ID=internal +STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only +STACK_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN=stack-generated.example.com +STACK_EMAIL_MONITOR_RESEND_EMAIL_API_KEY=this-is-a-fake-key +STACK_EMAIL_MONITOR_SECRET_TOKEN=this-secret-token-is-for-local-development-only +STACK_EMAIL_MONITOR_USE_INBUCKET=true +STACK_FEATUREBASE_JWT_SECRET=secret-value +STACK_FORWARD_MOCK_OAUTH_SERVER=false +STACK_GITHUB_CLIENT_ID=MOCK +STACK_GITHUB_CLIENT_SECRET=MOCK +STACK_GOOGLE_CLIENT_ID=MOCK +STACK_GOOGLE_CLIENT_SECRET=MOCK +STACK_MICROSOFT_CLIENT_ID=MOCK +STACK_MICROSOFT_CLIENT_SECRET=MOCK +STACK_SPOTIFY_CLIENT_ID=MOCK +STACK_SPOTIFY_CLIENT_SECRET=MOCK +STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS=true diff --git a/docker/local-emulator/docker-compose.yaml b/docker/local-emulator/docker-compose.yaml index 7d5158b59..116cc7df0 100644 --- a/docker/local-emulator/docker-compose.yaml +++ b/docker/local-emulator/docker-compose.yaml @@ -46,87 +46,26 @@ services: volumes: - "${HOME}:${HOME}" - "/tmp:/tmp" + env_file: ./base.env environment: + # Port-prefixed URLs (host-specific) NEXT_PUBLIC_STACK_PORT_PREFIX: "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}" NEXT_PUBLIC_STACK_API_URL: "http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02" NEXT_PUBLIC_STACK_DASHBOARD_URL: "http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01" - NEXT_PUBLIC_STACK_DOCS_BASE_URL: "https://docs.stack-auth.com" - NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR: "true" - NEXT_PUBLIC_STACK_PROJECT_ID: "internal" - NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: "this-publishable-client-key-is-for-local-development-only" - STACK_SECRET_SERVER_KEY: "this-secret-server-key-is-for-local-development-only" - STACK_SERVER_SECRET: "23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo" NEXT_PUBLIC_STACK_SVIX_SERVER_URL: "http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}13" - STACK_CHANGELOG_URL: "https://raw.githubusercontent.com/stack-auth/stack-auth/refs/heads/dev/CHANGELOG.md" - STACK_SEED_ENABLE_DUMMY_PROJECT: "true" - STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED: "true" - STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED: "true" - STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST: "true" - STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS: "github,spotify,google,microsoft" - STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS: "true" - STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY: "this-publishable-client-key-is-for-local-development-only" - STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY: "this-secret-server-key-is-for-local-development-only" - STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY: "this-super-secret-admin-key-is-for-local-development-only" + STACK_S3_PUBLIC_ENDPOINT: "http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}21/stack-storage" + STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL: "http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01/handler/email-verification" STACK_OAUTH_MOCK_URL: "http://host.docker.internal:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}14" - STACK_GITHUB_CLIENT_ID: "MOCK" - STACK_GITHUB_CLIENT_SECRET: "MOCK" - STACK_GOOGLE_CLIENT_ID: "MOCK" - STACK_GOOGLE_CLIENT_SECRET: "MOCK" - STACK_MICROSOFT_CLIENT_ID: "MOCK" - STACK_MICROSOFT_CLIENT_SECRET: "MOCK" - STACK_SPOTIFY_CLIENT_ID: "MOCK" - STACK_SPOTIFY_CLIENT_SECRET: "MOCK" - STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS: "true" + BACKEND_PORT: "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02" + DASHBOARD_PORT: "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01" + # Deps service hostnames (stack-deps container, not localhost) STACK_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@stack-deps:5432/stackframe" STACK_EMAIL_HOST: "stack-deps" - STACK_EMAIL_PORT: "2500" - STACK_EMAIL_SECURE: "false" - STACK_EMAIL_USERNAME: "does-not-matter" - STACK_EMAIL_PASSWORD: "does-not-matter" - STACK_EMAIL_SENDER: "noreply@example.com" - STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR: "10000" STACK_SVIX_SERVER_URL: "http://stack-deps:8071" - STACK_SVIX_API_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTUxNDA2MzksImV4cCI6MTk3MDUwMDYzOSwibmJmIjoxNjU1MTQwNjM5LCJpc3MiOiJzdml4LXNlcnZlciIsInN1YiI6Im9yZ18yM3JiOFlkR3FNVDBxSXpwZ0d3ZFhmSGlyTXUifQ.En8w77ZJWbd0qrMlHHupHUB-4cx17RfzFykseg95SUk" - STACK_OPENAI_API_KEY: "mock_openai_api_key" - STACK_OPENROUTER_API_KEY: "FORWARD_TO_PRODUCTION" - STACK_STRIPE_SECRET_KEY: "sk_test_mockstripekey" - STACK_STRIPE_WEBHOOK_SECRET: "mock_stripe_webhook_secret" - STACK_RESEND_API_KEY: "mock_resend_api_key" - STACK_RESEND_WEBHOOK_SECRET: "mock_resend_webhook_secret" - STACK_DNSIMPLE_API_TOKEN: "mock_dnsimple_api_token" - STACK_DNSIMPLE_ACCOUNT_ID: "mock_dnsimple_account_id" - STACK_DNSIMPLE_API_BASE_URL: "https://api.dnsimple.com/v2" - STACK_FREESTYLE_API_KEY: "mock_stack_freestyle_key" - STACK_VERCEL_SANDBOX_TOKEN: "vercel_sandbox_disabled_for_local_development" - CRON_SECRET: "mock_cron_secret" STACK_S3_ENDPOINT: "http://stack-deps:9090" - STACK_S3_PUBLIC_ENDPOINT: "http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}21/stack-storage" - STACK_S3_REGION: "us-east-1" - STACK_S3_ACCESS_KEY_ID: "s3mockroot" - STACK_S3_SECRET_ACCESS_KEY: "s3mockroot" - STACK_S3_BUCKET: "stack-storage" - STACK_S3_PRIVATE_BUCKET: "stack-storage-private" - STACK_AWS_REGION: "us-east-1" - STACK_AWS_ACCESS_KEY_ID: "test" - STACK_AWS_SECRET_ACCESS_KEY: "test" STACK_QSTASH_URL: "http://stack-deps:8080" - STACK_QSTASH_TOKEN: "eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=" - STACK_QSTASH_CURRENT_SIGNING_KEY: "sig_7kYjw48mhY7kAjqNGcy6cr29RJ6r" - STACK_QSTASH_NEXT_SIGNING_KEY: "sig_5ZB6DVzB1wjE8S6rZ7eenA8Pdnhs" STACK_CLICKHOUSE_URL: "http://stack-deps:8123" - STACK_CLICKHOUSE_ADMIN_USER: "stackframe" - STACK_CLICKHOUSE_ADMIN_PASSWORD: "PASSWORD-PLACEHOLDER--9gKyMxJeMx" - STACK_CLICKHOUSE_EXTERNAL_PASSWORD: "PASSWORD-PLACEHOLDER--EZeHscBMzE" - STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL: "http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01/handler/email-verification" - STACK_EMAIL_MONITOR_PROJECT_ID: "internal" - STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY: "this-publishable-client-key-is-for-local-development-only" - STACK_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN: "stack-generated.example.com" - STACK_EMAIL_MONITOR_RESEND_EMAIL_API_KEY: "this-is-a-fake-key" STACK_EMAIL_MONITOR_INBUCKET_API_URL: "http://stack-deps:9001" - STACK_EMAIL_MONITOR_USE_INBUCKET: "true" - STACK_EMAIL_MONITOR_SECRET_TOKEN: "this-secret-token-is-for-local-development-only" - STACK_FEATUREBASE_JWT_SECRET: "secret-value" - STACK_FORWARD_MOCK_OAUTH_SERVER: "false" extra_hosts: - "host.docker.internal:host-gateway" healthcheck: diff --git a/docker/local-emulator/qemu/build-image.sh b/docker/local-emulator/qemu/build-image.sh index d5c78adfa..77c714e22 100755 --- a/docker/local-emulator/qemu/build-image.sh +++ b/docker/local-emulator/qemu/build-image.sh @@ -7,7 +7,6 @@ source "$SCRIPT_DIR/common.sh" IMAGE_DIR="$SCRIPT_DIR/images" CLOUD_INIT_ROOT="$SCRIPT_DIR/cloud-init" -PREPARE_IMAGE_BUNDLE_SCRIPT="$SCRIPT_DIR/prepare-image-bundle.sh" DEBIAN_VERSION="${DEBIAN_VERSION:-13}" DISK_SIZE="${EMULATOR_DISK_SIZE:-12G}" @@ -140,7 +139,16 @@ prepare_bundle_artifacts() { fi log "Creating Docker image bundle (${arch})..." - "$PREPARE_IMAGE_BUNDLE_SCRIPT" "$bundle_tgz" "${DOCKER_IMAGES[@]}" + for img in "${DOCKER_IMAGES[@]}"; do + if ! docker image inspect "$img" >/dev/null 2>&1; then + err "Missing Docker image: $img. Build the local emulator images first, then rerun the QEMU image build." + exit 1 + fi + done + local tmp_bundle="${bundle_tgz}.tmp" + rm -f "$tmp_bundle" + docker save "${DOCKER_IMAGES[@]}" | gzip -c > "$tmp_bundle" + mv "$tmp_bundle" "$bundle_tgz" printf "%s" "$current_ids" > "$bundle_meta" } diff --git a/docker/local-emulator/qemu/cloud-init/emulator/user-data b/docker/local-emulator/qemu/cloud-init/emulator/user-data index ef163c659..a126eb2ee 100644 --- a/docker/local-emulator/qemu/cloud-init/emulator/user-data +++ b/docker/local-emulator/qemu/cloud-init/emulator/user-data @@ -15,7 +15,7 @@ chpasswd: stack:stack-emulator expire: false -ssh_pwauth: true +ssh_pwauth: false package_update: true package_upgrade: false @@ -28,68 +28,6 @@ packages: - qemu-guest-agent write_files: - - path: /etc/stack-auth/local-emulator.base.env - content: | - NEXT_PUBLIC_STACK_DOCS_BASE_URL=https://docs.stack-auth.com - NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true - NEXT_PUBLIC_STACK_PROJECT_ID=internal - NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only - STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only - STACK_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo - STACK_CHANGELOG_URL=https://raw.githubusercontent.com/stack-auth/stack-auth/refs/heads/dev/CHANGELOG.md - STACK_SEED_ENABLE_DUMMY_PROJECT=true - STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=true - STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED=true - STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=true - STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS=github,spotify,google,microsoft - STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=true - STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only - STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only - STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only - STACK_SVIX_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTUxNDA2MzksImV4cCI6MTk3MDUwMDYzOSwibmJmIjoxNjU1MTQwNjM5LCJpc3MiOiJzdml4LXNlcnZlciIsInN1YiI6Im9yZ18yM3JiOFlkR3FNVDBxSXpwZ0d3ZFhmSGlyTXUifQ.En8w77ZJWbd0qrMlHHupHUB-4cx17RfzFykseg95SUk - STACK_OPENAI_API_KEY=mock_openai_api_key - STACK_OPENROUTER_API_KEY=FORWARD_TO_PRODUCTION - STACK_STRIPE_SECRET_KEY=sk_test_mockstripekey - STACK_STRIPE_WEBHOOK_SECRET=mock_stripe_webhook_secret - STACK_RESEND_API_KEY=mock_resend_api_key - STACK_RESEND_WEBHOOK_SECRET=mock_resend_webhook_secret - STACK_DNSIMPLE_API_TOKEN=mock_dnsimple_api_token - STACK_DNSIMPLE_ACCOUNT_ID=mock_dnsimple_account_id - STACK_DNSIMPLE_API_BASE_URL=https://api.dnsimple.com/v2 - STACK_FREESTYLE_API_KEY=mock_stack_freestyle_key - STACK_VERCEL_SANDBOX_TOKEN=vercel_sandbox_disabled_for_local_development - CRON_SECRET=mock_cron_secret - STACK_S3_REGION=us-east-1 - STACK_S3_ACCESS_KEY_ID=s3mockroot - STACK_S3_SECRET_ACCESS_KEY=s3mockroot - STACK_S3_BUCKET=stack-storage - STACK_S3_PRIVATE_BUCKET=stack-storage-private - STACK_AWS_REGION=us-east-1 - STACK_AWS_ACCESS_KEY_ID=test - STACK_AWS_SECRET_ACCESS_KEY=test - STACK_QSTASH_TOKEN=eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0= - STACK_QSTASH_CURRENT_SIGNING_KEY=sig_7kYjw48mhY7kAjqNGcy6cr29RJ6r - STACK_QSTASH_NEXT_SIGNING_KEY=sig_5ZB6DVzB1wjE8S6rZ7eenA8Pdnhs - STACK_CLICKHOUSE_ADMIN_USER=stackframe - STACK_CLICKHOUSE_ADMIN_PASSWORD=PASSWORD-PLACEHOLDER--9gKyMxJeMx - STACK_CLICKHOUSE_EXTERNAL_PASSWORD=PASSWORD-PLACEHOLDER--EZeHscBMzE - STACK_EMAIL_MONITOR_PROJECT_ID=internal - STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only - STACK_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN=stack-generated.example.com - STACK_EMAIL_MONITOR_RESEND_EMAIL_API_KEY=this-is-a-fake-key - STACK_EMAIL_MONITOR_SECRET_TOKEN=this-secret-token-is-for-local-development-only - STACK_FEATUREBASE_JWT_SECRET=secret-value - STACK_FORWARD_MOCK_OAUTH_SERVER=false - STACK_GITHUB_CLIENT_ID=MOCK - STACK_GITHUB_CLIENT_SECRET=MOCK - STACK_GOOGLE_CLIENT_ID=MOCK - STACK_GOOGLE_CLIENT_SECRET=MOCK - STACK_MICROSOFT_CLIENT_ID=MOCK - STACK_MICROSOFT_CLIENT_SECRET=MOCK - STACK_SPOTIFY_CLIENT_ID=MOCK - STACK_SPOTIFY_CLIENT_SECRET=MOCK - STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS=true - - path: /usr/local/bin/install-emulator-containers permissions: '0755' content: | @@ -135,103 +73,43 @@ write_files: set -a source /mnt/stack-runtime/runtime.env - source /etc/stack-auth/local-emulator.base.env + source /mnt/stack-runtime/base.env set +a # Deps runs in the same VM, so always localhost DEPS_HOST=127.0.0.1 + P="$STACK_EMULATOR_PORT_PREFIX" - cat > /run/stack-auth/local-emulator.env < /run/stack-auth/local-emulator.env - path: /usr/local/bin/run-stack-app-container permissions: '0755' @@ -296,7 +174,8 @@ write_files: WantedBy=multi-user.target runcmd: - - mkdir -p /etc/stack-auth + - systemctl disable --now ssh || true + - systemctl mask ssh || true - bash /usr/local/bin/install-emulator-containers - systemctl daemon-reload - systemctl enable stack-deps.service diff --git a/docker/local-emulator/qemu/cloud-init/meta-data b/docker/local-emulator/qemu/cloud-init/meta-data deleted file mode 100644 index 15c4b5b8e..000000000 --- a/docker/local-emulator/qemu/cloud-init/meta-data +++ /dev/null @@ -1,2 +0,0 @@ -instance-id: stack-emulator-split -local-hostname: stack-emulator diff --git a/docker/local-emulator/qemu/cloud-init/user-data b/docker/local-emulator/qemu/cloud-init/user-data deleted file mode 100644 index 6a39bc4d1..000000000 --- a/docker/local-emulator/qemu/cloud-init/user-data +++ /dev/null @@ -1,6 +0,0 @@ -#cloud-config -write_files: - - path: /etc/motd - content: | - Stack Auth local emulator uses the unified cloud-init definition in - docker/local-emulator/qemu/cloud-init/emulator/. diff --git a/docker/local-emulator/qemu/host-file-bridge.mjs b/docker/local-emulator/qemu/host-file-bridge.mjs deleted file mode 100644 index 22d14c5c3..000000000 --- a/docker/local-emulator/qemu/host-file-bridge.mjs +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env node - -import http from "node:http"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; - -const port = Number.parseInt(process.env.STACK_QEMU_FILE_BRIDGE_PORT ?? "", 10) || 8116; -const host = process.env.STACK_QEMU_FILE_BRIDGE_HOST ?? "0.0.0.0"; -const token = process.env.STACK_QEMU_FILE_BRIDGE_TOKEN ?? ""; - -if (token === "") { - console.error("STACK_QEMU_FILE_BRIDGE_TOKEN is required"); - process.exit(1); -} - -const allowedRoots = [os.homedir(), "/tmp"].map((root) => path.resolve(root)); - -function isWithinRoot(filePath, rootPath) { - return filePath === rootPath || filePath.startsWith(`${rootPath}${path.sep}`); -} - -function isAllowedPath(filePath) { - return allowedRoots.some((rootPath) => isWithinRoot(filePath, rootPath)); -} - -function sendJson(res, statusCode, body) { - res.writeHead(statusCode, { "Content-Type": "application/json" }); - res.end(JSON.stringify(body)); -} - -function sendText(res, statusCode, message) { - res.writeHead(statusCode, { "Content-Type": "text/plain; charset=utf-8" }); - res.end(message); -} - -async function readBody(req) { - let body = ""; - for await (const chunk of req) { - body += chunk.toString(); - } - return body; -} - -function parseRequestBody(bodyText) { - try { - const body = JSON.parse(bodyText); - return typeof body === "object" && body !== null ? body : null; - } catch { - return null; - } -} - -async function handleRead(res, body) { - const requestedPath = body.path; - if (typeof requestedPath !== "string") { - sendText(res, 400, "Body must include a string 'path'."); - return; - } - - const filePath = path.resolve(requestedPath); - if (!isAllowedPath(filePath)) { - sendText(res, 403, `Path is not allowed: ${filePath}`); - return; - } - - try { - const content = await fs.readFile(filePath, "utf-8"); - sendJson(res, 200, { exists: true, content }); - } catch (error) { - if (error?.code === "ENOENT") { - sendJson(res, 200, { exists: false }); - return; - } - throw error; - } -} - -async function handleWrite(res, body) { - const requestedPath = body.path; - const content = body.content; - if (typeof requestedPath !== "string" || typeof content !== "string") { - sendText(res, 400, "Body must include string 'path' and 'content' fields."); - return; - } - - const filePath = path.resolve(requestedPath); - if (!isAllowedPath(filePath)) { - sendText(res, 403, `Path is not allowed: ${filePath}`); - return; - } - - await fs.mkdir(path.dirname(filePath), { recursive: true }); - await fs.writeFile(filePath, content, "utf-8"); - sendJson(res, 200, { ok: true }); -} - -const server = http.createServer(async (req, res) => { - try { - if (req.url === "/health") { - sendText(res, 200, "ok"); - return; - } - - if (req.method !== "POST") { - sendText(res, 405, "Method not allowed"); - return; - } - - if (req.headers["x-stack-emulator-token"] !== token) { - sendText(res, 401, "Unauthorized"); - return; - } - - const requestBody = parseRequestBody(await readBody(req)); - if (requestBody === null) { - sendText(res, 400, "Invalid JSON body."); - return; - } - - if (req.url === "/read") { - await handleRead(res, requestBody); - return; - } - - if (req.url === "/write") { - await handleWrite(res, requestBody); - return; - } - - sendText(res, 404, "Not found"); - } catch (error) { - sendText(res, 500, error instanceof Error ? error.message : String(error)); - } -}); - -server.listen(port, host, () => { - console.log(`stack-qemu-file-bridge listening on ${host}:${port}`); -}); diff --git a/docker/local-emulator/qemu/prepare-app-bundle.sh b/docker/local-emulator/qemu/prepare-app-bundle.sh deleted file mode 100755 index 6f89115e2..000000000 --- a/docker/local-emulator/qemu/prepare-app-bundle.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -exec "$SCRIPT_DIR/prepare-image-bundle.sh" "${1:-}" stack-local-emulator-app diff --git a/docker/local-emulator/qemu/prepare-image-bundle.sh b/docker/local-emulator/qemu/prepare-image-bundle.sh deleted file mode 100755 index daec7a106..000000000 --- a/docker/local-emulator/qemu/prepare-image-bundle.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -OUTPUT_PATH="${1:-}" -shift || true - -if [ -z "$OUTPUT_PATH" ] || [ "$#" -eq 0 ]; then - echo "Usage: $0 [docker-image...]" >&2 - exit 1 -fi - -if ! command -v docker >/dev/null 2>&1; then - echo "docker is required to package emulator images" >&2 - exit 1 -fi - -for IMAGE_NAME in "$@"; do - if ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then - cat >&2 < "$tmp_output" -mv "$tmp_output" "$OUTPUT_PATH" diff --git a/docker/local-emulator/qemu/run-emulator.sh b/docker/local-emulator/qemu/run-emulator.sh index 45a2d50bf..6a14932f7 100755 --- a/docker/local-emulator/qemu/run-emulator.sh +++ b/docker/local-emulator/qemu/run-emulator.sh @@ -7,13 +7,12 @@ source "$SCRIPT_DIR/common.sh" IMAGE_DIR="$SCRIPT_DIR/images" RUN_DIR="/tmp/stack-emulator-run" -FILE_BRIDGE_SCRIPT="$SCRIPT_DIR/host-file-bridge.mjs" VM_RAM="${EMULATOR_RAM:-4096}" VM_CPUS="${EMULATOR_CPUS:-4}" PORT_PREFIX="${PORT_PREFIX:-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}}" -FILE_BRIDGE_PORT="${EMULATOR_FILE_BRIDGE_PORT:-${PORT_PREFIX}16}" READY_TIMEOUT="${EMULATOR_READY_TIMEOUT:-240}" +CONFIG_FILE="" RED='\033[0;31m' GREEN='\033[0;32m' @@ -26,113 +25,6 @@ warn() { echo -e "${YELLOW}[emulator]${NC} $*"; } err() { echo -e "${RED}[emulator]${NC} $*" >&2; } info() { echo -e "${CYAN}[emulator]${NC} $*"; } -file_bridge_pidfile() { - echo "$RUN_DIR/host-file-bridge.pid" -} - -file_bridge_logfile() { - echo "$RUN_DIR/host-file-bridge.log" -} - -file_bridge_tokenfile() { - echo "$RUN_DIR/host-file-bridge.token" -} - -ensure_file_bridge_token() { - local token_file - token_file="$(file_bridge_tokenfile)" - # Deterministic token so snapshots can reuse the same value across restarts - FILE_BRIDGE_TOKEN="$(printf 'stack-local-emulator-%s' "$PORT_PREFIX" | shasum -a 256 | head -c 48)" - mkdir -p "$RUN_DIR" - printf "%s" "$FILE_BRIDGE_TOKEN" > "$token_file" -} - -is_file_bridge_running() { - local pidfile - pidfile="$(file_bridge_pidfile)" - if [ ! -f "$pidfile" ]; then - return 1 - fi - local pid - pid="$(cat "$pidfile")" - kill -0 "$pid" 2>/dev/null -} - -start_file_bridge() { - ensure_file_bridge_token - if is_file_bridge_running; then - return 0 - fi - - if [ ! -f "$FILE_BRIDGE_SCRIPT" ]; then - err "Missing host file bridge script: $FILE_BRIDGE_SCRIPT" - exit 1 - fi - - local pid - pid="$( - STACK_QEMU_FILE_BRIDGE_PORT="$FILE_BRIDGE_PORT" \ - STACK_QEMU_FILE_BRIDGE_HOST="0.0.0.0" \ - STACK_QEMU_FILE_BRIDGE_TOKEN="$FILE_BRIDGE_TOKEN" \ - python3 - "$FILE_BRIDGE_SCRIPT" "$(file_bridge_logfile)" <<'PY' -import os -import subprocess -import sys - -script_path = sys.argv[1] -log_path = sys.argv[2] - -with open(log_path, "ab", buffering=0) as log_file: - process = subprocess.Popen( - ["node", script_path], - stdin=subprocess.DEVNULL, - stdout=log_file, - stderr=log_file, - start_new_session=True, - env=os.environ.copy(), - close_fds=True, - ) - -print(process.pid) -PY - )" - echo "$pid" > "$(file_bridge_pidfile)" - - local elapsed=0 - while [ "$elapsed" -lt 15 ]; do - if curl -sf "http://127.0.0.1:${FILE_BRIDGE_PORT}/health" >/dev/null 2>&1; then - return 0 - fi - if ! kill -0 "$pid" 2>/dev/null; then - err "Host file bridge exited unexpectedly." - tail -40 "$(file_bridge_logfile)" 2>/dev/null || true - exit 1 - fi - sleep 1 - elapsed=$((elapsed + 1)) - done - - err "Timed out waiting for host file bridge on port ${FILE_BRIDGE_PORT}." - tail -40 "$(file_bridge_logfile)" 2>/dev/null || true - exit 1 -} - -stop_file_bridge() { - local pidfile - pidfile="$(file_bridge_pidfile)" - if [ ! -f "$pidfile" ]; then - return 0 - fi - - local pid - pid="$(cat "$pidfile")" - if kill -0 "$pid" 2>/dev/null; then - kill "$pid" 2>/dev/null || true - sleep 1 - kill -9 "$pid" 2>/dev/null || true - fi - rm -f "$pidfile" "$(file_bridge_logfile)" "$(file_bridge_tokenfile)" -} detect_host ARCH="${EMULATOR_ARCH:-$HOST_ARCH}" @@ -221,11 +113,13 @@ prepare_runtime_config_iso() { cfg_iso="$(runtime_iso_path)" rm -rf "$cfg_dir" mkdir -p "$cfg_dir" - cat > "$cfg_dir/runtime.env" < "$cfg_dir/runtime.env" + cp "$SCRIPT_DIR/../base.env" "$cfg_dir/base.env" make_iso_from_dir "$cfg_iso" "STACKCFG" "$cfg_dir" } @@ -332,7 +226,6 @@ build_qemu_cmd() { esac local netdev="user,id=net0" - netdev+=",hostfwd=tcp::${PORT_PREFIX}22-:22" # Deps services netdev+=",hostfwd=tcp::${PORT_PREFIX}28-:5432" netdev+=",hostfwd=tcp::${PORT_PREFIX}29-:2500" @@ -393,7 +286,7 @@ tail_vm_logs() { } ensure_ports_free() { - local ports=("${PORT_PREFIX}01" "${PORT_PREFIX}02" "${PORT_PREFIX}05" "${PORT_PREFIX}13" "${PORT_PREFIX}16" "${PORT_PREFIX}21" "${PORT_PREFIX}25" "${PORT_PREFIX}28" "${PORT_PREFIX}29" "${PORT_PREFIX}30" "${PORT_PREFIX}36" "${PORT_PREFIX}37") + local ports=("${PORT_PREFIX}01" "${PORT_PREFIX}02" "${PORT_PREFIX}05" "${PORT_PREFIX}13" "${PORT_PREFIX}21" "${PORT_PREFIX}25" "${PORT_PREFIX}28" "${PORT_PREFIX}29" "${PORT_PREFIX}30" "${PORT_PREFIX}36" "${PORT_PREFIX}37") local port for port in "${ports[@]}"; do if lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then @@ -435,9 +328,13 @@ stop_vm() { } cmd_start() { + if [ -n "$CONFIG_FILE" ] && [ ! -f "$CONFIG_FILE" ]; then + err "Config file not found: $CONFIG_FILE" + exit 1 + fi + ensure_ports_free mkdir -p "$RUN_DIR" - start_file_bridge IS_SNAPSHOT_RESTORE=false @@ -477,7 +374,6 @@ cmd_start() { cmd_stop() { stop_vm - stop_file_bridge log "QEMU emulator stopped." } @@ -513,7 +409,6 @@ cmd_status() { print_service_status "Backend" "${PORT_PREFIX}02" http "/health?db=1" print_service_status "PostgreSQL" "${PORT_PREFIX}28" tcp print_service_status "Inbucket HTTP" "${PORT_PREFIX}05" http / - print_service_status "Host File Bridge" "${FILE_BRIDGE_PORT}" http /health print_service_status "Svix" "${PORT_PREFIX}13" http /api/v1/health/ print_service_status "MinIO" "${PORT_PREFIX}21" http /minio/health/live print_service_status "QStash" "${PORT_PREFIX}25" http / 401 @@ -541,7 +436,24 @@ print(f"Startup time: {end_time - start_time:.1f}s") PY } -ACTION="${1:-start}" +ACTION="start" + +while [[ $# -gt 0 ]]; do + case "$1" in + --config-file) + CONFIG_FILE="$2" + shift 2 + ;; + start|stop|reset|status|bench) + ACTION="$1" + shift + ;; + *) + echo "Usage: $0 [start|stop|reset|status|bench] [--config-file ]" + exit 1 + ;; + esac +done case "$ACTION" in start) cmd_start ;; @@ -549,8 +461,4 @@ case "$ACTION" in reset) cmd_reset ;; status) cmd_status ;; bench) cmd_bench ;; - *) - echo "Usage: $0 [start|stop|reset|status|bench]" - exit 1 - ;; esac diff --git a/examples/demo/src/app/emulator-status/page.tsx b/examples/demo/src/app/emulator-status/page.tsx index 542bff846..32b1a9d9a 100644 --- a/examples/demo/src/app/emulator-status/page.tsx +++ b/examples/demo/src/app/emulator-status/page.tsx @@ -137,7 +137,7 @@ export default function EmulatorStatusPage() { {summary.down > 0 && ( {summary.down} down )} - {data != null && updated {new Date(data.timestamp).toLocaleTimeString()}} + updated {new Date(data.timestamp).toLocaleTimeString()} ) : null}