Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions cli/daemon/run/proc_groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package run

import (
"context"
"fmt"
"io"
"net"
"net/http"
Expand Down Expand Up @@ -379,6 +380,11 @@ type Proc struct {
listenAddr netip.AddrPort // The port the HTTP server of the process should listen on
httpProxy *httputil.ReverseProxy // The reverse proxy for the HTTP server of the process

// parentPipeW is the write end of the liveness pipe. Kept open for
// the lifetime of the process; when closed (or when this process dies),
// the child detects EOF and exits.
parentPipeW *os.File

// The following fields are only valid after Start() has been called.
Started atomic.Bool // whether the process has started
StartedAt time.Time // when the process started
Expand All @@ -403,9 +409,29 @@ func (p *Proc) start() error {
return nil
}

// Create a liveness pipe. The read end is passed to the child process;
// when this (parent) process dies for any reason, the OS closes the write end,
// and the child detects EOF and exits. This works cross-platform and handles
// cases where the parent is killed without a chance to run cleanup (e.g. SIGKILL).
pipeR, pipeW, err := os.Pipe()
if err != nil {
return errors.Wrap(err, "could not create liveness pipe")
}
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, pipeR)
// The fd number is 3 + index in ExtraFiles (0=stdin, 1=stdout, 2=stderr).
fd := 3 + len(p.cmd.ExtraFiles) - 1
p.cmd.Env = append(p.cmd.Env, fmt.Sprintf("ENCORE_PARENT_FD=%d", fd))

if err := p.cmd.Start(); err != nil {
pipeR.Close()
pipeW.Close()
return errors.Wrap(err, "could not start process")
}

// Close the read end in the parent; only the child needs it.
// Keep pipeW open — it closes when this process exits (or is killed).
pipeR.Close()
p.parentPipeW = pipeW
p.log.Info().Str("addr", p.listenAddr.String()).Msg("process started")
p.group.runningProcs++

Expand Down Expand Up @@ -449,6 +475,11 @@ func (p *Proc) start() error {
// Close closes the process and waits for it to exit.
// It is safe to call Close multiple times.
func (p *Proc) Close() {
// Close the liveness pipe to signal the child that we're shutting down.
if p.parentPipeW != nil {
p.parentPipeW.Close()
}

if err := p.cmd.Process.Signal(os.Interrupt); err != nil {
// If there's an error sending the signal, just kill the process.
// This might happen because Interrupt is not supported on Windows.
Expand Down
23 changes: 23 additions & 0 deletions runtimes/js/encore.dev/internal/appinit/mod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Socket } from "node:net";
import { isMainThread, Worker, workerData } from "node:worker_threads";
import { Gateway } from "../../api/gateway";
import { Middleware, MiddlewareRequest, HandlerResponse } from "../../api/mod";
Expand Down Expand Up @@ -32,6 +33,11 @@ export function registerGateways(gateways: Gateway[]) {

export async function run(entrypoint: string) {
if (isMainThread) {
// Watch the liveness pipe from the parent process. When the parent dies
// (for any reason, including SIGKILL), the OS closes the write end of the
// pipe and we get EOF, triggering a clean exit. This is cross-platform.
watchParentLiveness();

const metricsBuffer = initGlobalMetricsBuffer();
const extraWorkers = runtime.RT.numWorkerThreads() - 1;
if (extraWorkers > 0) {
Expand Down Expand Up @@ -237,3 +243,20 @@ function toResponse(
return new HandlerResponse(payload).__internalToResponse();
}
}

function watchParentLiveness() {
const fdStr = process.env.ENCORE_PARENT_FD;
if (!fdStr) return;

const fd = parseInt(fdStr, 10);
if (isNaN(fd)) return;

const sock = new Socket({ fd, readable: true, writable: false });
// Prevent the socket from keeping the event loop alive on its own,
// so it doesn't block normal shutdown.
sock.unref();
sock.on("close", () => {
process.exit(0);
});
sock.resume();
}
Loading