From 2a42fba009c2b2f6c07de3b036fb27f2ccb9f939 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Fri, 25 Oct 2019 21:14:18 -0700 Subject: [PATCH] Handle running as pid 1 This detects when it is running as pid 1, and becomes an init process. Specifically this means handling SIGCHLD and reaping processes (otherwise they become zombies) and forwarding signals to "real" process. We fork and re-exec ourselves so that we only get *this* SIGCHLD for orphaned processes (re-parented to 1) and not the real events from running things like git or ssh. --- Dockerfile.in | 2 -- cmd/git-sync/main.go | 18 +++++++++++++ pkg/pid1/pid1.go | 62 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 pkg/pid1/pid1.go diff --git a/Dockerfile.in b/Dockerfile.in index 5d49aeb..7ebdcf5 100644 --- a/Dockerfile.in +++ b/Dockerfile.in @@ -14,8 +14,6 @@ FROM {ARG_FROM} -MAINTAINER Tim Hockin - RUN apt-get update \ && apt-get -y install \ ca-certificates \ diff --git a/cmd/git-sync/main.go b/cmd/git-sync/main.go index cb2292e..2b62846 100644 --- a/cmd/git-sync/main.go +++ b/cmd/git-sync/main.go @@ -40,6 +40,7 @@ import ( "github.com/go-logr/glogr" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "k8s.io/git-sync/pkg/pid1" "k8s.io/git-sync/pkg/version" ) @@ -184,6 +185,20 @@ func envDuration(key string, def time.Duration) time.Duration { } func main() { + // In case we come up as pid 1, act as init. + if os.Getpid() == 1 { + fmt.Fprintf(os.Stderr, "INFO: detected pid 1, running init handler\n") + err := pid1.ReRun() + if err == nil { + os.Exit(0) + } + if exerr, ok := err.(*exec.ExitError); ok { + os.Exit(exerr.ExitCode()) + } + fmt.Fprintf(os.Stderr, "ERROR: unhandled pid1 error: %v\n", err) + os.Exit(127) + } + setFlagDefaults() flag.Parse() @@ -198,15 +213,18 @@ func main() { flag.Usage() os.Exit(1) } + if *flDest == "" { parts := strings.Split(strings.Trim(*flRepo, "/"), "/") *flDest = parts[len(parts)-1] } + if strings.Contains(*flDest, "/") { fmt.Fprintf(os.Stderr, "ERROR: --dest must be a bare name\n") flag.Usage() os.Exit(1) } + if _, err := exec.LookPath(*flGitCmd); err != nil { fmt.Fprintf(os.Stderr, "ERROR: git executable %q not found: %v\n", *flGitCmd, err) os.Exit(1) diff --git a/pkg/pid1/pid1.go b/pkg/pid1/pid1.go new file mode 100644 index 0000000..19cc84f --- /dev/null +++ b/pkg/pid1/pid1.go @@ -0,0 +1,62 @@ +package pid1 + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "syscall" +) + +// ReRun converts the current process into a bare-bones init, runs the current +// commandline as a child process, and waits for it to complete. The new child +// process shares stdin/stdout/stderr with the parent. When the child exits, +// this will return the same value as exec.Command.Run(). If there is an error +// in reaping children that this can not handle, it will panic. +func ReRun() error { + bin, err := os.Readlink("/proc/self/exe") + if err != nil { + return err + } + cmd := exec.Command(bin, os.Args[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + return err + } + go runInit(cmd.Process.Pid) + return cmd.Wait() +} + +// runInit runs a bare-bones init process. This will never return. In case of +// truly unknown errors it will panic. +func runInit(pid int) { + sigs := make(chan os.Signal, 8) + signal.Notify(sigs) + for sig := range sigs { + if sig == syscall.SIGCHLD { + sigchld() + } else { + // Pass it on to the real process. + syscall.Kill(pid, sig.(syscall.Signal)) + } + } +} + +// sigchld handles a SIGCHLD. +func sigchld() { + // Loop to handle multiple child processes. + for { + var status syscall.WaitStatus + pid, err := syscall.Wait4(-1, &status, syscall.WNOHANG, nil) + if err != nil { + panic(fmt.Sprintf("failed to wait4(): %v\n", err)) + } + if pid <= 0 { + // No more children to reap. + break + } + // Must have found one, see if there are more. + } +}