Skip to content

Commit b5da9d5

Browse files
committed
reexec: touch-up docs and add some example uses
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1 parent 2f71e8f commit b5da9d5

File tree

1 file changed

+105
-29
lines changed

1 file changed

+105
-29
lines changed

reexec/reexec.go

Lines changed: 105 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,72 @@
1-
// Package reexec facilitates the busybox style reexec of a binary.
1+
// Package reexec implements a BusyBox-style “multi-call binary” re-execution
2+
// pattern for Go programs.
23
//
3-
// Handlers can be registered with a name and the argv 0 of the exec of
4-
// the binary will be used to find and execute custom init paths.
4+
// Callers register named entrypoints through [Register]. When the current
5+
// process starts, [Init] compares filepath.Base(os.Args[0]) against the set of
6+
// registered entrypoints. If it matches, Init runs that entrypoint and returns
7+
// true.
58
//
6-
// It is used to work around forking limitations when using Go.
9+
// A matched entrypoint is analogous to main for that invocation mode: callers
10+
// typically call Init at the start of main and return immediately when it
11+
// reports a match.
12+
//
13+
// Example uses:
14+
//
15+
// - For multi-call binaries: multiple symlinks/hardlinks point at one binary,
16+
// and argv[0] (see [execve(2)]) selects the entrypoint.
17+
// - For programmatic re-exec: a parent launches the current binary (via
18+
// [Command] or [CommandContext]) with argv[0] set to a registered entrypoint
19+
// name to run a specific mode.
20+
//
21+
// The programmatic re-exec pattern is commonly used as a safe alternative to
22+
// fork-without-exec in Go, since the runtime is not fork-safe once multiple
23+
// threads exist (see [os.StartProcess] and https://go.dev/issue/27505).
24+
//
25+
// Example (multi-call binary):
26+
//
27+
// package main
28+
//
29+
// import (
30+
// "fmt"
31+
//
32+
// "github.com/moby/sys/reexec"
33+
// )
34+
//
35+
// func init() {
36+
// reexec.Register("example-foo", func() {
37+
// fmt.Println("Hello from entrypoint example-foo")
38+
// })
39+
// reexec.Register("example-bar", entrypointBar)
40+
// }
41+
//
42+
// func main() {
43+
// if reexec.Init() {
44+
// // Matched a reexec entrypoint; stop normal main execution.
45+
// return
46+
// }
47+
// fmt.Println("Hello main")
48+
// }
49+
//
50+
// func entrypointBar() {
51+
// fmt.Println("Hello from entrypoint example-bar")
52+
// }
53+
//
54+
// To try it:
55+
//
56+
// go build -o example .
57+
// ln -s example example-foo
58+
// ln -s example example-bar
59+
//
60+
// ./example
61+
// # Hello main
62+
//
63+
// ./example-foo
64+
// # Hello from entrypoint example-foo
65+
//
66+
// ./example-bar
67+
// # Hello from entrypoint example-bar
68+
//
69+
// [execve(2)]: https://man7.org/linux/man-pages/man2/execve.2.html
770
package reexec
871

972
import (
@@ -17,40 +80,55 @@ import (
1780
"github.com/moby/sys/reexec/internal/reexecoverride"
1881
)
1982

20-
var registeredInitializers = make(map[string]func())
83+
var entrypoints = make(map[string]func())
2184

22-
// Register adds an initialization func under the specified name. It panics
23-
// if the given name is already registered.
24-
func Register(name string, initializer func()) {
85+
// Register associates name with an entrypoint function to be executed when
86+
// the current binary is invoked with argv[0] equal to name.
87+
//
88+
// Register is not safe for concurrent use; entrypoints must be registered
89+
// during program initialization, before calling [Init].
90+
// It panics if name contains a path component or is already registered.
91+
func Register(name string, entrypoint func()) {
2592
if filepath.Base(name) != name {
2693
panic(fmt.Sprintf("reexec func does not expect a path component: %q", name))
2794
}
28-
if _, exists := registeredInitializers[name]; exists {
95+
if _, exists := entrypoints[name]; exists {
2996
panic(fmt.Sprintf("reexec func already registered under name %q", name))
3097
}
3198

32-
registeredInitializers[name] = initializer
99+
entrypoints[name] = entrypoint
33100
}
34101

35-
// Init is called as the first part of the exec process and returns true if an
36-
// initialization function was called.
102+
// Init checks whether the current process was invoked under a registered name
103+
// (based on filepath.Base(os.Args[0])).
104+
//
105+
// If a matching entrypoint is found, it is executed and Init returns true. In
106+
// that case, the caller should stop normal main execution. If no match is found,
107+
// Init returns false and normal execution should continue.
37108
func Init() bool {
38-
if initializer, ok := registeredInitializers[filepath.Base(os.Args[0])]; ok {
39-
initializer()
109+
if entrypoint, ok := entrypoints[filepath.Base(os.Args[0])]; ok {
110+
entrypoint()
40111
return true
41112
}
42113
return false
43114
}
44115

45-
// Command returns an [*exec.Cmd] with its Path set to the path of the current
46-
// binary using the result of [Self].
116+
// Command returns an [*exec.Cmd] configured to re-execute the current binary,
117+
// using the path returned by [Self].
118+
//
119+
// The first element of args becomes argv[0] of the new process and is used by
120+
// [Init] to select a registered entrypoint.
121+
//
122+
// On Linux, the Pdeathsig of [*exec.Cmd.SysProcAttr] is set to SIGTERM. This
123+
// signal is sent to the child process when the OS thread that created it dies,
124+
// which helps ensure the child does not outlive its parent unexpectedly. See
125+
// [PR_SET_PDEATHSIG(2const)] and [go.dev/issue/27505] for details.
47126
//
48-
// On Linux, the Pdeathsig of [*exec.Cmd.SysProcAttr] is set to SIGTERM.
49-
// This signal is sent to the process when the OS thread that created
50-
// the process dies.
127+
// It is the caller's responsibility to ensure that the creating thread is not
128+
// terminated prematurely.
51129
//
52-
// It is the caller's responsibility to ensure that the creating thread is
53-
// not terminated prematurely. See https://go.dev/issue/27505 for more details.
130+
// [PR_SET_PDEATHSIG(2const)]: https://man7.org/linux/man-pages/man2/PR_SET_PDEATHSIG.2const.html
131+
// [go.dev/issue/27505]: https://go.dev/issue/27505
54132
func Command(args ...string) *exec.Cmd {
55133
return command(args...)
56134
}
@@ -69,16 +147,14 @@ func CommandContext(ctx context.Context, args ...string) *exec.Cmd {
69147
return commandContext(ctx, args...)
70148
}
71149

72-
// Self returns the path to the current process's binary.
150+
// Self returns the executable path of the current process.
73151
//
74-
// On Linux, it returns "/proc/self/exe", which provides the in-memory version
75-
// of the current binary. This makes it safe to delete or replace the on-disk
76-
// binary (os.Args[0]).
152+
// On Linux, it returns "/proc/self/exe", which references the in-memory image
153+
// of the running binary. This allows the on-disk binary (os.Args[0]) to be
154+
// replaced or deleted without affecting re-execution.
77155
//
78-
// On Other platforms, it attempts to look up the absolute path for os.Args[0],
79-
// or otherwise returns os.Args[0] as-is. For example if current binary is
80-
// "my-binary" at "/usr/bin/" (or "my-binary.exe" at "C:\" on Windows),
81-
// then it returns "/usr/bin/my-binary" and "C:\my-binary.exe" respectively.
156+
// On other platforms, it attempts to resolve os.Args[0] to an absolute path.
157+
// If resolution fails, it returns os.Args[0] unchanged.
82158
func Self() string {
83159
if argv0, ok := reexecoverride.Argv0(); ok {
84160
return naiveSelf(argv0)

0 commit comments

Comments
 (0)