Documentation
¶
Overview ¶
Package proc is a stdlib-only fabric slice of the spec contract module (github.com/opencharly/spec/proc, #55 step1) holding generic host-process lifecycle + launch plumbing: catchable-signal-triggered cleanup hooks, a stale-temp-file sweep, the current-binary subcommand runner, and the process-group shutdown + remote-launch-command renderers (launch.go). It is a host primitive a plugin physically cannot hold as a shared library, so by #55 Rule 1 it lives in a contained spec slice (deps: os/os-signal/os-exec + spec value types + spec.ShellQuote — stdlib-weight only, Rule-2-free) that charly core AND every plugin can import directly, without pulling any mechanism kit. Relocated here from sdk/proclifecycle (cleanup + run) and sdk/kit/process.go (the launch helpers) — a genuinely shared leaf.
Index ¶
- Constants
- Variables
- func CreateTempHeld(dir, pattern string) (f *os.File, release func(), err error)
- func InstallSignalHandler()
- func MergeRepoOverrides(existing, add string) string
- func MkdirTempHeld(dir, pattern string) (path string, release func(), err error)
- func RegisterShutdownHook(fn func())
- func RegisterTempCleanup(path string)
- func RemoteLaunchCommand(launch spec.ProcessLaunch) string
- func SelfSuperprojectOverridePair(projectDir string) string
- func ShutdownProcessGroup(cmd *exec.Cmd, stdin io.Closer, done <-chan struct{})
- func SortedEnvPairs(env map[string]string) []string
- func SweepStaleTemps()
- func TempIsHeld(path string) bool
- func UnregisterTempCleanup(path string)
Constants ¶
const ProcessShutdownGrace = 2 * time.Second
ProcessShutdownGrace bounds each stage of the graceful process-group shutdown (ShutdownProcessGroup). Named + documented, not a tuned magic value: it is the guarantee that closing a stdio-carried process ALWAYS terminates instead of wedging the caller on a child that ignores its stdin EOF.
const RepoOverrideEnv = "CHARLY_REPO_OVERRIDE"
RepoOverrideEnv configures RDD local-overrides: it points a remote `@github` repo ref at a LOCAL working tree (Go-`replace`-style), so an UNCOMMITTED candy / charly.yml change can be built and `charly check`'d by ANY consumer — across submodule boundaries — BEFORE it is committed and pushed. This is the supported "verify before you push to main" mechanism (no cache hacks, no producer-first tag churn).
Value: a comma-separated list of `repoPath=localDir` pairs. repoPath matches the repo-root form every `@github` candy/namespace/image ref resolves through (`github.com/<org>/<repo>`); a bare `<org>/<repo>` is accepted too (auto `github.com/` prefix, same rule as `--repo`). Example:
CHARLY_REPO_OVERRIDE=opencharly/charly=/home/me/oc-charly \
charly -C box/ubuntu box build ubuntu-coder
The matched directory resolves verbatim (leading `~/` expanded); the ref's `:vTAG` is IGNORED — an override ALWAYS resolves to the dev's current tree.
Variables ¶
var RunCharlySubcommand = func(args ...string) error { exe, err := os.Executable() if err != nil { exe = os.Args[0] } cmd := exec.Command(exe, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() }
RunCharlySubcommand shells out to the CURRENT binary with args, inheriting stdin/stdout/stderr. Relocated from charly core (run_subcommand.go) as a stdlib-only host-exec leaf: the host-side update/deploy orchestration (podUpdateCmd, the unified-target Update/Rebuild methods, per-kind R10 sequences, `charly vm cycle`, fleet member bring-up/teardown) spawns child `charly <args…>` invocations through it, and resolving the build-under-test means an update loop (or a member fork) picks up the local build automatically.
The child binary resolves via os.Executable() — an ABSOLUTE, chdir-immune path to the running process's binary — falling back to os.Args[0] only on os.Executable()'s error (a genuinely rare OS-level failure, e.g. the exe was deleted mid-run on some platforms). os.Args[0] ALONE is wrong here: it is whatever string invoked the process — a bare `charly` (PATH-resolved, historically the only thing operators ran, so the bug stayed masked) or a RELATIVE path like `./bin/charly`. A relative os.Args[0] resolves against the process's CURRENT working directory at fork time, not the directory it had when os.Args[0] was captured — so a caller that os.Chdir()s (e.g. `charly -C box/fedora …`) before this var forks a child (e.g. fleet-member bring-up's `charly config <member>`) sends the child hunting for box/fedora/bin/charly, which doesn't exist (ENOENT). os.Executable() has no such relative-path hazard — it is resolved once, absolutely, at the OS level.
A package var (not a plain func) so tests can stub the child-process boundary (e.g. record the image-build / vm-cp-box calls a deploy makes without actually spawning the binary).
Functions ¶
func CreateTempHeld ¶ added in v0.2026217.301
CreateTempHeld is the file twin of MkdirTempHeld. The hold rides a SEPARATE descriptor from the returned *os.File on purpose: callers routinely write the file, close it, and then hand the PATH to a subprocess that reads it much later (a gocryptfs extpass script sitting at a passphrase prompt, a multi-GB image tarball being loaded back). Locking the caller's own descriptor would release the moment they closed it — precisely when the exposure begins.
As with MkdirTempHeld, release drops only the hold; closing, deleting and unregistering the file remain the caller's, unchanged.
func InstallSignalHandler ¶
func InstallSignalHandler()
InstallSignalHandler arms the global signal handler exactly once. Subsequent calls are no-ops. On SIGTERM/SIGINT/SIGHUP it runs the registered temp cleanups AND the registered shutdown hooks (plugin-client reaping, etc.), then re-raises the signal so the process exits with the conventional 128+signum status code.
SIGKILL is not catchable (kernel-level). Leftover temps from a SIGKILL'd charly are caught by the next invocation's SweepStaleTemps; orphaned plugin servers from a SIGKILL'd charly self-terminate via the plugin SDK's parent-death watch (see plugin/sdk/parentwatch.go).
func MergeRepoOverrides ¶ added in v0.2026217.301
MergeRepoOverrides combines an existing CHARLY_REPO_OVERRIDE value with an auto-added pair. The existing (operator-set) entries are placed FIRST so an explicit operator override for a repo WINS over the auto pair — repoOverrideDir returns the FIRST matching entry. Either argument may be empty.
func MkdirTempHeld ¶ added in v0.2026217.301
MkdirTempHeld is os.MkdirTemp plus the two things every sweepable-namespace creator already did by hand: register the path for signal-driven cleanup, and — new here — hold it against the sweep for the operation's lifetime.
release drops ONLY the hold. It deliberately does not delete the temp and does not unregister the cleanup, because those two policies differ per caller and folding them in would silently change behavior: a site that keeps its artifacts on success (the localpkg and pkgdep builders) keeps the cleanup registration too, so a later SIGTERM still reaps them. Callers keep their existing RemoveAll / UnregisterTempCleanup defers exactly as they were; only the hold is new.
Defer it immediately after creation, before the caller's own removal defer, so that LIFO ordering deletes the tree while the hold is still active and releases afterwards.
This is the single creation path for every swept namespace (R3). Before it existed each site paired a bare os.MkdirTemp with its own RegisterTempCleanup and nothing held the tree, which is how a live build could be deleted underneath itself.
func RegisterShutdownHook ¶
func RegisterShutdownHook(fn func())
RegisterShutdownHook registers fn to run on a catchable shutdown signal (SIGTERM/SIGINT/SIGHUP), in the signal handler, before it re-raises the signal and the process exits. It is the package-boundary seam that lets package main reap resources this package's signal handler cannot reference directly — notably charly's connected out-of-process plugin clients (providerRegistry.Close): a Ctrl-C'd / `systemctl stop`ped charly kills its plugin servers instead of orphaning them (the 77h-orphan leak). Hooks run in registration order; each MUST be best-effort and bounded (the handler is synchronous). A nil fn is a no-op.
func RegisterTempCleanup ¶
func RegisterTempCleanup(path string)
RegisterTempCleanup registers path for cleanup on graceful shutdown (SIGTERM/SIGINT/SIGHUP). Existing `defer os.Remove(...)` callers keep working for normal exit; this is a SUPPLEMENTAL safety net for the ungraceful-but-catchable signal path. Empty path is a no-op.
func RemoteLaunchCommand ¶
func RemoteLaunchCommand(launch spec.ProcessLaunch) string
RemoteLaunchCommand renders one exact-argv launch as the single command string OpenSSH must receive: POSIX single-quoting each token preserves the argv boundary and prevents interpolation by the remote login shell. The working directory and environment belong to the TARGET side, so they ride inside the remote command (`cd <dir> && env <K=V>… <argv>`); applying them to the local ssh carrier would conflate controller and target state.
func SelfSuperprojectOverridePair ¶ added in v0.2026217.301
SelfSuperprojectOverridePair returns a CHARLY_REPO_OVERRIDE pair (`<repo-identity>=<superproject-dir>`) that points a bed project's OWN superproject `@github` refs at the local working tree, or "" when projectDir is not a git submodule of a charly superproject. A check bed (a `disposable: true` fleet) living in a `box/<distro>` submodule references its parent repo's shared candies via `@github.com/<org>/<parent>/candy/<name>:<tag>`; without this override the bed would build the PINNED REMOTE candy and so test STALE code — the candy-ref analogue of why the bed runner builds the toolchain with `--dev-local-pkg`. The override IGNORES the ref's `:vTAG`, so the bed always tests the dev's current tree. Returns "" when projectDir is its own root (its candies already resolve from the local tree) or when git / the superproject identity is unavailable.
func ShutdownProcessGroup ¶
ShutdownProcessGroup stops one Setpgid child process gracefully-first and reaps it. The escalation ladder is:
- close stdin — the polite EOF signal every stdio-carried protocol answers by exiting on its own;
- one ProcessShutdownGrace for the process to exit;
- SIGTERM to the whole process group — the explicit termination request, so a well-behaved child can still shut down cleanly;
- a second ProcessShutdownGrace;
- only then SIGKILL to the group — the final escalation a child cannot ignore — and reap.
The ladder never opens with a force-kill: SIGKILL orphans nothing (the whole group dies), but it also gives no child the chance to flush protocol state, so it stays the last resort.
done must be closed by the caller's cmd.Wait() monitor once the process is reaped; ShutdownProcessGroup returns only after that, so no zombie or running group member survives the call. Call it once per process — wrap in sync.Once when several paths may close the same process.
func SortedEnvPairs ¶
SortedEnvPairs renders an environment map as a deterministically ordered "KEY=value" slice. Sorting makes launched-process environments and rendered remote commands byte-reproducible across runs (map iteration order is not).
func SweepStaleTemps ¶
func SweepStaleTemps()
SweepStaleTemps removes /tmp/<prefix>* entries that are (a) NOT held open by any running process AND (b) NOT flock-held by a live operation (TempIsHeld, temphold.go) AND (c) older than sweepSafetyFloor AND (d) owned by the current user. Best-effort: never errors, never blocks. Called once from main().
Guard (b) is the load-bearing one for a long build: (a) and (c) BOTH answer "stale" for a live makepkg stage tree, which is how this sweep deleted two beds' builds out from under them. See temphold.go for the mechanism and the evidence.
func TempIsHeld ¶ added in v0.2026217.301
TempIsHeld reports whether some process is holding path with the flock MkdirTempHeld / CreateTempHeld take — i.e. whether an operation is still live inside it. Used by SweepStaleTemps to skip a busy temp.
An unreadable path answers false: the sweep's other guards (ownership, age, open descriptors) still apply, so a path this probe cannot open is treated exactly as it was before.
func UnregisterTempCleanup ¶
func UnregisterTempCleanup(path string)
UnregisterTempCleanup removes a path from the registry. Called by the existing defer-cleanup paths after they successfully os.Remove the file, so the registry doesn't accumulate stale entries in long- running charly processes (e.g. `charly mcp serve`).
Types ¶
This section is empty.