Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 105 additions & 29 deletions reexec/reexec.go
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would move example to a separate file, example_test.go or so. Having example in comments means that it is never even compiled, which will make it outdated.

Go examples are at least compiled.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah; I did that for the other one; but was looking for a "here's how it would be used for multi-call binary", and of course the symlinks aren't really something to do in an example test.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and of course the symlinks aren't really something to do in an example test.

I think it can still be done, you just create the binary first using go test -c and then go create symlinks. I haven't tested this, but in case your example has // Output: line at the end of the function, it will actually be run during tests (to check the output matches).

I agree it can be cumbersome though, so if you want it to leave as it, I'm fine with it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's possible to test probably, but I don't think that works for pkg.go.dev to do something similar to that 🤔

I think this one is fine to keep; I'm adding a second "mocked" example for this scenario in #208

//
// 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 (
Expand All @@ -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...)
}
Expand All @@ -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"
Expand Down
Loading