2022-02-12 15:56:33 +01:00

142 lines
3.9 KiB
Go

// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.
package cmd
import (
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"github.com/gitpod-io/gitpod/common-go/log"
"github.com/spf13/cobra"
"golang.org/x/sys/unix"
)
var initCmd = &cobra.Command{
Use: "init",
Short: "init the supervisor",
Run: func(cmd *cobra.Command, args []string) {
log.Init(ServiceName, Version, true, false)
// Because we're reaping with PID -1, we'll catch the child process for
// which we've missed the notification anyways.
var (
sigInput = make(chan os.Signal, 1)
sigReaper = make(chan os.Signal, 1)
sigSupervisor = make(chan os.Signal, 1)
)
signal.Notify(sigInput, syscall.SIGCHLD, os.Interrupt, syscall.SIGTERM)
go func() {
for s := range sigInput {
switch s {
default:
sigSupervisor <- s
// the reaper needs all signals so that it can turn into
// a terminating reaper if need be.
fallthrough
case syscall.SIGCHLD:
// we don't want to blob the SIGINT/SIGTERM behaviour because
// the reaper is still busy.
select {
case sigReaper <- s:
default:
}
}
}
}()
go reaper(sigReaper)
supervisorPath, err := os.Executable()
if err != nil {
supervisorPath = "/.supervisor/supervisor"
}
runCommand := exec.Command(supervisorPath, "run")
runCommand.Args[0] = "supervisor"
runCommand.Stdin = os.Stdin
runCommand.Stdout = os.Stdout
runCommand.Stderr = os.Stderr
runCommand.Env = os.Environ()
err = runCommand.Start()
if err != nil {
log.WithError(err).Error("supervisor run start error")
return
}
supervisorDone := make(chan struct{})
go func() {
defer close(supervisorDone)
err := runCommand.Wait()
if err != nil && !(strings.Contains(err.Error(), "signal: interrupt") || strings.Contains(err.Error(), "no child processes")) {
log.WithError(err).Error("supervisor run error")
return
}
}()
select {
case <-supervisorDone:
// supervisor has ended - we're all done here
return
case s := <-sigSupervisor:
// we received a terminating signal - pass on to supervisor and wait for it to finish
_ = runCommand.Process.Signal(s)
<-supervisorDone
}
},
}
func init() {
rootCmd.AddCommand(initCmd)
}
func reaper(sigs <-chan os.Signal) {
// The reaper can be turned into a terminating reaper by writing true to this channel.
// When in terminating mode, the reaper will send SIGTERM to each child that gets reparented
// to us and is still running. We use this mechanism to send SIGTERM to a shell child processes
// that get reparented once their parent shell terminates during shutdown.
var terminating bool
for s := range sigs {
if s != syscall.SIGCHLD {
terminating = true
continue
}
for {
// wait on the process, hence remove it from the process table
pid, err := unix.Wait4(-1, nil, 0, nil)
// if we've been interrupted, try again until we're done
for err == syscall.EINTR {
pid, err = unix.Wait4(-1, nil, 0, nil)
}
// The calling process does not have any unwaited-for children. Let's wait for a SIGCHLD notification.
if err == unix.ECHILD {
break
}
if err != nil {
log.WithField("pid", pid).WithError(err).Debug("cannot call waitpid() for re-parented child")
}
if !terminating {
continue
}
proc, err := os.FindProcess(pid)
if err != nil {
log.WithField("pid", pid).WithError(err).Debug("cannot find re-parented process")
continue
}
err = proc.Signal(syscall.SIGTERM)
if err != nil {
if !strings.Contains(err.Error(), "os: process already finished") {
log.WithField("pid", pid).WithError(err).Debug("cannot send SIGTERM to re-parented process")
}
continue
}
log.WithField("pid", pid).Debug("SIGTERM'ed reparented child process")
}
}
}