//go:build linux && cgo
// +build linux,cgo

package cmds

import (
	"io"
	"os"
	"os/exec"
	"os/signal"
	"syscall"

	systemd "github.com/coreos/go-systemd/v22/daemon"
	"github.com/k3s-io/k3s/pkg/proctitle"
	"github.com/k3s-io/k3s/pkg/signals"
	"github.com/k3s-io/k3s/pkg/version"
	"github.com/natefinch/lumberjack"
	pkgerrors "github.com/pkg/errors"
	"golang.org/x/sys/unix"
)

// forkIfLoggingOrReaping handles forking off the actual k3s process if it is necessary to
// capture log output, or reap child processes. Reaping is only necessary when running
// as pid 1.
func forkIfLoggingOrReaping() error {
	var stdout, stderr io.Writer = os.Stdout, os.Stderr
	enableLogRedirect := LogConfig.LogFile != "" && os.Getenv("_K3S_LOG_REEXEC_") == ""
	enableReaping := os.Getpid() == 1

	if enableLogRedirect {
		var l io.Writer = &lumberjack.Logger{
			Filename:   LogConfig.LogFile,
			MaxSize:    50,
			MaxBackups: 3,
			MaxAge:     28,
			Compress:   true,
		}
		if LogConfig.AlsoLogToStderr {
			l = io.MultiWriter(l, os.Stderr)
		}
		stdout = l
		stderr = l
	}

	if enableLogRedirect || enableReaping {
		proctitle.SetProcTitle(os.Args[0] + " init")

		pwd, err := os.Getwd()
		if err != nil {
			return pkgerrors.WithMessage(err, "failed to get working directory")
		}

		if enableReaping {
			// If we're running as pid 1 we need to reap child processes or defunct containerd-shim
			// child processes will accumulate.
			unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, uintptr(1), 0, 0, 0)
			go reapChildren()
		}

		args := append([]string{version.Program}, os.Args[1:]...)
		env := append(os.Environ(), "_K3S_LOG_REEXEC_=true", "NOTIFY_SOCKET=")
		ctx := signals.SetupSignalContext()
		cmd := exec.CommandContext(ctx, "/proc/self/exe")
		cmd.Args = args
		cmd.Dir = pwd
		cmd.Env = env
		cmd.Stdout = stdout
		cmd.Stderr = stderr
		cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: unix.SIGTERM, Setpgid: true}
		cmd.Cancel = func() error { return cmd.Process.Signal(unix.SIGINT) }

		if err := cmd.Start(); err != nil {
			return err
		}

		// The child process won't be allowed to notify, so we send one for it as soon as it's started,
		// and then wait for it to exit and pass along the exit code.
		systemd.SdNotify(true, "READY=1\n")

		cmd.Wait()
		os.Exit(cmd.ProcessState.ExitCode())
	}
	return nil
}

// reapChildren calls Wait4 whenever SIGCHLD is received
func reapChildren() {
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, unix.SIGCHLD)
	for {
		select {
		case <-sigs:
		}
		for {
			var wstatus syscall.WaitStatus
			_, err := syscall.Wait4(-1, &wstatus, 0, nil)
			for err == syscall.EINTR {
				_, err = syscall.Wait4(-1, &wstatus, 0, nil)
			}
			if err == nil || err == syscall.ECHILD {
				break
			}
		}
	}
}
