From ed57227943f73d5162a216d5a139ff7a697e5b6d Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 1 Mar 2026 14:30:26 +0100 Subject: [PATCH] reexec: touch-up docs and add some example uses Signed-off-by: Sebastiaan van Stijn --- reexec/reexec.go | 134 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 105 insertions(+), 29 deletions(-) diff --git a/reexec/reexec.go b/reexec/reexec.go index b97a0aa..5bb8836 100644 --- a/reexec/reexec.go +++ b/reexec/reexec.go @@ -1,9 +1,72 @@ -// Package reexec facilitates the busybox style reexec of a binary. +// Package reexec implements a BusyBox-style “multi-call binary” re-execution +// pattern for Go programs. // -// Handlers can be registered with a name and the argv 0 of the exec of -// the binary will be used to find and execute custom init paths. +// Callers register named entrypoints through [Register]. When the current +// process starts, [Init] compares filepath.Base(os.Args[0]) against the set of +// registered entrypoints. If it matches, Init runs that entrypoint and returns +// true. // -// It is used to work around forking limitations when using Go. +// A matched entrypoint is analogous to main for that invocation mode: callers +// typically call Init at the start of main and return immediately when it +// reports a match. +// +// Example uses: +// +// - For multi-call binaries: multiple symlinks/hardlinks point at one binary, +// and argv[0] (see [execve(2)]) selects the entrypoint. +// - For programmatic re-exec: a parent launches the current binary (via +// [Command] or [CommandContext]) with argv[0] set to a registered entrypoint +// name to run a specific mode. +// +// The programmatic re-exec pattern is commonly used as a safe alternative to +// fork-without-exec in Go, since the runtime is not fork-safe once multiple +// threads exist (see [os.StartProcess] and https://go.dev/issue/27505). +// +// Example (multi-call binary): +// +// package main +// +// import ( +// "fmt" +// +// "github.com/moby/sys/reexec" +// ) +// +// func init() { +// reexec.Register("example-foo", func() { +// fmt.Println("Hello from entrypoint example-foo") +// }) +// reexec.Register("example-bar", entrypointBar) +// } +// +// func main() { +// if reexec.Init() { +// // Matched a reexec entrypoint; stop normal main execution. +// return +// } +// fmt.Println("Hello main") +// } +// +// func entrypointBar() { +// fmt.Println("Hello from entrypoint example-bar") +// } +// +// To try it: +// +// go build -o example . +// ln -s example example-foo +// ln -s example example-bar +// +// ./example +// # Hello main +// +// ./example-foo +// # Hello from entrypoint example-foo +// +// ./example-bar +// # Hello from entrypoint example-bar +// +// [execve(2)]: https://man7.org/linux/man-pages/man2/execve.2.html package reexec import ( @@ -15,40 +78,55 @@ import ( "runtime" ) -var registeredInitializers = make(map[string]func()) +var entrypoints = make(map[string]func()) -// Register adds an initialization func under the specified name. It panics -// if the given name is already registered. -func Register(name string, initializer func()) { +// Register associates name with an entrypoint function to be executed when +// the current binary is invoked with argv[0] equal to name. +// +// Register is not safe for concurrent use; entrypoints must be registered +// during program initialization, before calling [Init]. +// It panics if name contains a path component or is already registered. +func Register(name string, entrypoint func()) { if filepath.Base(name) != name { panic(fmt.Sprintf("reexec func does not expect a path component: %q", name)) } - if _, exists := registeredInitializers[name]; exists { + if _, exists := entrypoints[name]; exists { panic(fmt.Sprintf("reexec func already registered under name %q", name)) } - registeredInitializers[name] = initializer + entrypoints[name] = entrypoint } -// Init is called as the first part of the exec process and returns true if an -// initialization function was called. +// Init checks whether the current process was invoked under a registered name +// (based on filepath.Base(os.Args[0])). +// +// If a matching entrypoint is found, it is executed and Init returns true. In +// that case, the caller should stop normal main execution. If no match is found, +// Init returns false and normal execution should continue. func Init() bool { - if initializer, ok := registeredInitializers[filepath.Base(os.Args[0])]; ok { - initializer() + if entrypoint, ok := entrypoints[filepath.Base(os.Args[0])]; ok { + entrypoint() return true } return false } -// Command returns an [*exec.Cmd] with its Path set to the path of the current -// binary using the result of [Self]. +// Command returns an [*exec.Cmd] configured to re-execute the current binary, +// using the path returned by [Self]. +// +// The first element of args becomes argv[0] of the new process and is used by +// [Init] to select a registered entrypoint. +// +// On Linux, the Pdeathsig of [*exec.Cmd.SysProcAttr] is set to SIGTERM. This +// signal is sent to the child process when the OS thread that created it dies, +// which helps ensure the child does not outlive its parent unexpectedly. See +// [PR_SET_PDEATHSIG(2const)] and [go.dev/issue/27505] for details. // -// On Linux, the Pdeathsig of [*exec.Cmd.SysProcAttr] is set to SIGTERM. -// This signal is sent to the process when the OS thread that created -// the process dies. +// It is the caller's responsibility to ensure that the creating thread is not +// terminated prematurely. // -// It is the caller's responsibility to ensure that the creating thread is -// not terminated prematurely. See https://go.dev/issue/27505 for more details. +// [PR_SET_PDEATHSIG(2const)]: https://man7.org/linux/man-pages/man2/PR_SET_PDEATHSIG.2const.html +// [go.dev/issue/27505]: https://go.dev/issue/27505 func Command(args ...string) *exec.Cmd { return command(args...) } @@ -67,16 +145,14 @@ func CommandContext(ctx context.Context, args ...string) *exec.Cmd { return commandContext(ctx, args...) } -// Self returns the path to the current process's binary. +// Self returns the executable path of the current process. // -// On Linux, it returns "/proc/self/exe", which provides the in-memory version -// of the current binary. This makes it safe to delete or replace the on-disk -// binary (os.Args[0]). +// On Linux, it returns "/proc/self/exe", which references the in-memory image +// of the running binary. This allows the on-disk binary (os.Args[0]) to be +// replaced or deleted without affecting re-execution. // -// On Other platforms, it attempts to look up the absolute path for os.Args[0], -// or otherwise returns os.Args[0] as-is. For example if current binary is -// "my-binary" at "/usr/bin/" (or "my-binary.exe" at "C:\" on Windows), -// then it returns "/usr/bin/my-binary" and "C:\my-binary.exe" respectively. +// On other platforms, it attempts to resolve os.Args[0] to an absolute path. +// If resolution fails, it returns os.Args[0] unchanged. func Self() string { if runtime.GOOS == "linux" { return "/proc/self/exe"