diff --git a/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts b/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts index 4dab2dafd3..5da2648cd3 100644 --- a/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts +++ b/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts @@ -738,7 +738,7 @@ describe("legacy functions serve integration", () => { toDockerPath(tempRoot.current), ]); expect(dockerRun.args[dockerRun.args.length - 1]).toBe( - "edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n", + "exec edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n", ); const envs = yield* Effect.promise(() => extractDockerEnvEntries(dockerRun)); @@ -1738,7 +1738,7 @@ describe("legacy functions serve integration", () => { const commandScript = dockerRun.args[dockerRun.args.length - 1] ?? ""; expect(commandScript).toBe( - "edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n", + "exec edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n", ); const cp = deployMockState.runCalls.find( diff --git a/apps/cli/src/legacy/commands/start/services/kong.service.ts b/apps/cli/src/legacy/commands/start/services/kong.service.ts index ec72cd667c..1dbd11c7af 100644 --- a/apps/cli/src/legacy/commands/start/services/kong.service.ts +++ b/apps/cli/src/legacy/commands/start/services/kong.service.ts @@ -37,8 +37,8 @@ * disk when TLS is enabled AND both `cert_path`/`key_path` are configured — * see {@link LegacyKongContainerSpecInput.tlsCertContent}'s doc comment. * {@link legacyBuildKongEntrypointScript} reproduces the remaining - * `custom_nginx.template` heredoc + exec line byte-for-byte; see its doc - * comment for the exact shell mechanics. + * `custom_nginx.template` heredoc; the final command is `exec`'d so Kong + * is PID 1 and `docker stop` reaches it directly. * * Kong mints no JWTs of its own: `BearerToken`/`QueryToken` are Kong * `request-transformer`/lua expression STRINGS built from the four @@ -174,7 +174,7 @@ export function legacyBuildKongEmailTemplateBind( const LEGACY_KONG_ENTRYPOINT_HEAD = "cat <<'EOF' > /home/kong/custom_nginx.template && \\\n" + - "./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n"; + "exec ./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n"; /** * Builds the surviving (non-secret) half of the Kong entrypoint: only the diff --git a/apps/cli/src/legacy/commands/start/services/kong.service.unit.test.ts b/apps/cli/src/legacy/commands/start/services/kong.service.unit.test.ts index 61f219e2b0..1e80c2cee5 100644 --- a/apps/cli/src/legacy/commands/start/services/kong.service.unit.test.ts +++ b/apps/cli/src/legacy/commands/start/services/kong.service.unit.test.ts @@ -117,7 +117,7 @@ describe("legacyBuildKongEntrypointScript", () => { const script = legacyBuildKongEntrypointScript("NGINX_TEMPLATE"); expect(script).toBe( "cat <<'EOF' > /home/kong/custom_nginx.template && \\\n" + - "./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n" + + "exec ./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template\n" + "NGINX_TEMPLATE\nEOF\n", ); }); diff --git a/apps/cli/src/legacy/commands/start/services/logflare.service.ts b/apps/cli/src/legacy/commands/start/services/logflare.service.ts index 229ccef273..d51ad00fb1 100644 --- a/apps/cli/src/legacy/commands/start/services/logflare.service.ts +++ b/apps/cli/src/legacy/commands/start/services/logflare.service.ts @@ -50,9 +50,23 @@ const LEGACY_LOGFLARE_API_KEY = "api-key"; * the `unless-stopped` restart policy retries until the db is ready — * running Logflare against an unmigrated database lets Oban die on the * missing `public.oban_jobs` table instead. + * + * `run.sh` stays PID 1 on purpose: a plain `exec` of `beam.smp` still burned + * Docker's 10s SIGTERM grace (upstream hang). Forward TERM, wait 3s, then + * KILL. Interrupted `wait` is >128; a second `wait` recovers the BEAM's + * status unless it was already reaped (127). */ const LEGACY_LOGFLARE_ENTRYPOINT_SCRIPT = - "cat <<'EOF' > run.sh && sh run.sh\n./logflare eval Logflare.Release.migrate &&\n./logflare start --sname logflare\nEOF\n"; + "cat <<'EOF' > run.sh && exec sh run.sh\n" + + "./logflare eval Logflare.Release.migrate || exit $?\n" + + "./logflare start --sname logflare &\n" + + "BEAM_PID=$!\n" + + 'trap \'kill -TERM "$BEAM_PID" 2>/dev/null; n=0; while [ "$n" -lt 3 ] && kill -0 "$BEAM_PID" 2>/dev/null; do n=$((n+1)); sleep 1; done; kill -KILL "$BEAM_PID" 2>/dev/null\' TERM\n' + + 'wait "$BEAM_PID"\n' + + "code=$?\n" + + 'if [ "$code" -gt 128 ]; then wait "$BEAM_PID" 2>/dev/null; code2=$?; [ "$code2" -ne 127 ] && code=$code2; fi\n' + + 'exit "$code"\n' + + "EOF\n"; export interface LegacyLogflareContainerSpecInput { /** diff --git a/apps/cli/src/legacy/commands/start/services/logflare.service.unit.test.ts b/apps/cli/src/legacy/commands/start/services/logflare.service.unit.test.ts index 7a9cf7aa93..33b5db08cb 100644 --- a/apps/cli/src/legacy/commands/start/services/logflare.service.unit.test.ts +++ b/apps/cli/src/legacy/commands/start/services/logflare.service.unit.test.ts @@ -32,7 +32,16 @@ describe("legacyBuildLogflareContainerSpec", () => { expect(spec.entrypoint).toBe("sh"); expect(spec.cmd).toEqual([ "-c", - "cat <<'EOF' > run.sh && sh run.sh\n./logflare eval Logflare.Release.migrate &&\n./logflare start --sname logflare\nEOF\n", + "cat <<'EOF' > run.sh && exec sh run.sh\n" + + "./logflare eval Logflare.Release.migrate || exit $?\n" + + "./logflare start --sname logflare &\n" + + "BEAM_PID=$!\n" + + 'trap \'kill -TERM "$BEAM_PID" 2>/dev/null; n=0; while [ "$n" -lt 3 ] && kill -0 "$BEAM_PID" 2>/dev/null; do n=$((n+1)); sleep 1; done; kill -KILL "$BEAM_PID" 2>/dev/null\' TERM\n' + + 'wait "$BEAM_PID"\n' + + "code=$?\n" + + 'if [ "$code" -gt 128 ]; then wait "$BEAM_PID" 2>/dev/null; code2=$?; [ "$code2" -ne 127 ] && code=$code2; fi\n' + + 'exit "$code"\n' + + "EOF\n", ]); expect(spec.exposedPorts).toEqual([{ containerPort: "4000" }]); expect(spec.ports).toEqual([{ hostPort: "54327", containerPort: "4000" }]); diff --git a/apps/cli/src/legacy/commands/start/services/vector.service.ts b/apps/cli/src/legacy/commands/start/services/vector.service.ts index 496f740566..d1813c2604 100644 --- a/apps/cli/src/legacy/commands/start/services/vector.service.ts +++ b/apps/cli/src/legacy/commands/start/services/vector.service.ts @@ -279,18 +279,18 @@ const LEGACY_VECTOR_HEALTHCHECK = { /** * Builds the Vector entrypoint script: writes the rendered `vector.yaml` via - * a `cat <<'EOF'` heredoc, then blocks on a `wget`-poll loop against - * Logflare's own `/health` endpoint (Vector's `docker_logs`/HTTP sinks would - * otherwise start before Logflare is ready to receive them) before finally - * exec'ing `vector`. + * a `cat <<'EOF'` heredoc, waits on Logflare `/health` (sinks would otherwise + * start too early), then `exec`s Vector so it is PID 1. A TERM trap covers + * the wait so `docker stop` does not burn 10s if Logflare is still down; + * `-T 2` bounds each probe so a hung health endpoint cannot defer the trap. */ export function legacyBuildVectorEntrypointScript(vectorYaml: string, logflareId: string): string { return ( "cat <<'EOF' > /etc/vector/vector.yaml\n" + vectorYaml + - "\nEOF\nuntil wget --no-verbose --tries=1 --spider http://" + + "\nEOF\ntrap 'exit 143' TERM\nuntil wget --no-verbose --tries=1 -T 2 --spider http://" + logflareId + - ":4000/health 2>/dev/null; do sleep 2; done\nvector --config /etc/vector/vector.yaml\n" + ":4000/health 2>/dev/null; do sleep 2; done\ntrap - TERM\nexec vector --config /etc/vector/vector.yaml\n" ); } diff --git a/apps/cli/src/legacy/commands/start/services/vector.service.unit.test.ts b/apps/cli/src/legacy/commands/start/services/vector.service.unit.test.ts index 69429ea9dd..24762e5f5b 100644 --- a/apps/cli/src/legacy/commands/start/services/vector.service.unit.test.ts +++ b/apps/cli/src/legacy/commands/start/services/vector.service.unit.test.ts @@ -225,9 +225,9 @@ describe("legacyBuildVectorEntrypointScript", () => { expect(legacyBuildVectorEntrypointScript("VECTOR_YAML", "supabase_analytics_proj")).toBe( "cat <<'EOF' > /etc/vector/vector.yaml\n" + "VECTOR_YAML" + - "\nEOF\nuntil wget --no-verbose --tries=1 --spider http://" + + "\nEOF\ntrap 'exit 143' TERM\nuntil wget --no-verbose --tries=1 -T 2 --spider http://" + "supabase_analytics_proj" + - ":4000/health 2>/dev/null; do sleep 2; done\nvector --config /etc/vector/vector.yaml\n", + ":4000/health 2>/dev/null; do sleep 2; done\ntrap - TERM\nexec vector --config /etc/vector/vector.yaml\n", ); }); }); diff --git a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts index 1c6531d6af..d3dca2ce53 100644 --- a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts +++ b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts @@ -95,16 +95,14 @@ export function legacyBuildEdgeRuntimeStartCmd(opts: { /** * Builds the `sh -c` entrypoint body that writes each file via a here-document - * (so contents may contain `EOF`) and then runs `cmd`. Byte-for-byte port of - * `buildEdgeRuntimeEntrypoint` (`apps/cli-go/internal/utils/edgeruntime.go`): - * all heredoc openers are joined with `&&` before the bodies so the shell stacks - * them in declaration order; each body ends with a unique sentinel. + * (so contents may contain `EOF`) and then `exec`s `cmd` so edge-runtime is + * PID 1 and an early `docker stop` reaches it directly. */ export function legacyBuildEdgeRuntimeEntrypoint( files: ReadonlyArray, cmd: string, ): string { - if (files.length === 0) return `${cmd}\n`; + if (files.length === 0) return `exec ${cmd}\n`; let head = ""; let bodies = ""; files.forEach((file, index) => { @@ -112,5 +110,5 @@ export function legacyBuildEdgeRuntimeEntrypoint( head += `cat <<'${sentinel}' > ${file.name} && `; bodies += `${file.content}\n${sentinel}\n`; }); - return `${head}${cmd}\n${bodies}`; + return `${head}exec ${cmd}\n${bodies}`; } diff --git a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.unit.test.ts b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.unit.test.ts index e8d668a0ad..9a6cc505eb 100644 --- a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.unit.test.ts @@ -37,7 +37,9 @@ describe("legacyBuildEdgeRuntimeStartCmd", () => { describe("legacyBuildEdgeRuntimeEntrypoint", () => { it("returns just the command (newline-terminated) when there are no files", () => { - expect(legacyBuildEdgeRuntimeEntrypoint([], "edge-runtime start")).toBe("edge-runtime start\n"); + expect(legacyBuildEdgeRuntimeEntrypoint([], "edge-runtime start")).toBe( + "exec edge-runtime start\n", + ); }); it("writes a single file via a sentinel here-document then runs the command", () => { @@ -45,10 +47,8 @@ describe("legacyBuildEdgeRuntimeEntrypoint", () => { [{ name: "index.ts", content: "console.log(1);" }], "edge-runtime start --main-service=. --port=5", ); - // Byte-for-byte port of Go's buildEdgeRuntimeEntrypoint: openers (joined with - // ` && `) precede the command, then the bodies with their sentinels. expect(out).toBe( - "cat <<'__EDGE_RT_FILE_0__' > index.ts && edge-runtime start --main-service=. --port=5\n" + + "cat <<'__EDGE_RT_FILE_0__' > index.ts && exec edge-runtime start --main-service=. --port=5\n" + "console.log(1);\n__EDGE_RT_FILE_0__\n", ); }); @@ -62,13 +62,15 @@ describe("legacyBuildEdgeRuntimeEntrypoint", () => { "CMD", ); expect(out).toBe( - "cat <<'__EDGE_RT_FILE_0__' > index.ts && cat <<'__EDGE_RT_FILE_1__' > .npmrc && CMD\n" + + "cat <<'__EDGE_RT_FILE_0__' > index.ts && cat <<'__EDGE_RT_FILE_1__' > .npmrc && exec CMD\n" + "A\n__EDGE_RT_FILE_0__\nB\n__EDGE_RT_FILE_1__\n", ); }); it("preserves file contents that themselves contain EOF-like text", () => { const out = legacyBuildEdgeRuntimeEntrypoint([{ name: "index.ts", content: "EOF\nmore" }], "C"); - expect(out).toBe("cat <<'__EDGE_RT_FILE_0__' > index.ts && C\nEOF\nmore\n__EDGE_RT_FILE_0__\n"); + expect(out).toBe( + "cat <<'__EDGE_RT_FILE_0__' > index.ts && exec C\nEOF\nmore\n__EDGE_RT_FILE_0__\n", + ); }); }); diff --git a/apps/cli/src/shared/functions/serve.ts b/apps/cli/src/shared/functions/serve.ts index 479889d723..a5f6b4e5fb 100644 --- a/apps/cli/src/shared/functions/serve.ts +++ b/apps/cli/src/shared/functions/serve.ts @@ -1471,7 +1471,8 @@ export function buildServeEntrypointCommand( command: ReadonlyArray, multilineEnvScriptPath?: string, ) { - return `${multilineEnvScriptPath === undefined ? "" : `. ${multilineEnvScriptPath}\n`}${command.join(" ")} + // `exec` so edge-runtime is PID 1; sourced env survives into the replacement process. + return `${multilineEnvScriptPath === undefined ? "" : `. ${multilineEnvScriptPath}\n`}exec ${command.join(" ")} `; } diff --git a/apps/cli/src/shared/functions/serve.unit.test.ts b/apps/cli/src/shared/functions/serve.unit.test.ts index cd4d3af2d6..ef457001ae 100644 --- a/apps/cli/src/shared/functions/serve.unit.test.ts +++ b/apps/cli/src/shared/functions/serve.unit.test.ts @@ -7,13 +7,13 @@ import { buildServeEntrypointCommand } from "./serve.ts"; describe("buildServeEntrypointCommand", () => { it("returns the runtime command without embedding the template body", () => { const script = buildServeEntrypointCommand(["edge-runtime", "start"]); - expect(script).toBe("edge-runtime start\n"); + expect(script).toBe("exec edge-runtime start\n"); expect(script).not.toContain("Deno.serve"); }); it("sources the multiline env script before the runtime command when provided", () => { const script = buildServeEntrypointCommand(["edge-runtime", "start"], "/root/env.sh"); - expect(script).toContain(". /root/env.sh\nedge-runtime start"); + expect(script).toContain(". /root/env.sh\nexec edge-runtime start"); }); it("keeps the spawned command short even with the real bundled template", async () => { diff --git a/packages/stack/src/services/vector.ts b/packages/stack/src/services/vector.ts index de4fc9bf93..7dc003a8cb 100644 --- a/packages/stack/src/services/vector.ts +++ b/packages/stack/src/services/vector.ts @@ -106,7 +106,7 @@ export const makeVectorServiceDocker = (opts: DockerVectorOptions) => { entrypoint: "sh", cmd: [ "-c", - `cat <<'EOF' > /etc/vector/vector.yaml && vector --config /etc/vector/vector.yaml + `cat <<'EOF' > /etc/vector/vector.yaml && exec vector --config /etc/vector/vector.yaml ${vectorConfig( opts.serviceHost, opts.analyticsPort,