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.
This commit is contained in:
Tim Hockin 2019-10-25 21:14:18 -07:00
parent 98022e4fe8
commit 2a42fba009
3 changed files with 80 additions and 2 deletions

View File

@ -14,8 +14,6 @@
FROM {ARG_FROM} FROM {ARG_FROM}
MAINTAINER Tim Hockin <thockin@google.com>
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install \ && apt-get -y install \
ca-certificates \ ca-certificates \

View File

@ -40,6 +40,7 @@ import (
"github.com/go-logr/glogr" "github.com/go-logr/glogr"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"k8s.io/git-sync/pkg/pid1"
"k8s.io/git-sync/pkg/version" "k8s.io/git-sync/pkg/version"
) )
@ -184,6 +185,20 @@ func envDuration(key string, def time.Duration) time.Duration {
} }
func main() { 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() setFlagDefaults()
flag.Parse() flag.Parse()
@ -198,15 +213,18 @@ func main() {
flag.Usage() flag.Usage()
os.Exit(1) os.Exit(1)
} }
if *flDest == "" { if *flDest == "" {
parts := strings.Split(strings.Trim(*flRepo, "/"), "/") parts := strings.Split(strings.Trim(*flRepo, "/"), "/")
*flDest = parts[len(parts)-1] *flDest = parts[len(parts)-1]
} }
if strings.Contains(*flDest, "/") { if strings.Contains(*flDest, "/") {
fmt.Fprintf(os.Stderr, "ERROR: --dest must be a bare name\n") fmt.Fprintf(os.Stderr, "ERROR: --dest must be a bare name\n")
flag.Usage() flag.Usage()
os.Exit(1) os.Exit(1)
} }
if _, err := exec.LookPath(*flGitCmd); err != nil { if _, err := exec.LookPath(*flGitCmd); err != nil {
fmt.Fprintf(os.Stderr, "ERROR: git executable %q not found: %v\n", *flGitCmd, err) fmt.Fprintf(os.Stderr, "ERROR: git executable %q not found: %v\n", *flGitCmd, err)
os.Exit(1) os.Exit(1)

62
pkg/pid1/pid1.go Normal file
View File

@ -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.
}
}