1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
19 "git.arvados.org/arvados.git/lib/cmd"
20 "git.arvados.org/arvados.git/lib/crunchstat"
23 const MaxLogLine = 1 << 14 // Child stderr lines >16KiB will be split
26 signalOnDeadPPID int = 15
27 ppidCheckInterval = time.Second
31 type logger interface {
32 Printf(string, ...interface{})
36 reporter := crunchstat.Reporter{
37 Logger: log.New(os.Stderr, "crunchstat: ", 0),
40 flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
41 flags.StringVar(&reporter.CgroupRoot, "cgroup-root", "", "Root of cgroup tree")
42 flags.StringVar(&reporter.CgroupParent, "cgroup-parent", "", "Name of container parent under cgroup")
43 flags.StringVar(&reporter.CIDFile, "cgroup-cid", "", "Path to container id file")
44 flags.IntVar(&signalOnDeadPPID, "signal-on-dead-ppid", signalOnDeadPPID, "Signal to send child if crunchstat's parent process disappears (0 to disable)")
45 flags.DurationVar(&ppidCheckInterval, "ppid-check-interval", ppidCheckInterval, "Time between checks for parent process disappearance")
46 pollMsec := flags.Int64("poll", 1000, "Reporting interval, in milliseconds")
47 getVersion := flags.Bool("version", false, "Print version information and exit.")
49 if ok, code := cmd.ParseFlags(flags, os.Args[0], os.Args[1:], "program [args ...]", os.Stderr); !ok {
51 } else if *getVersion {
52 fmt.Printf("crunchstat %s\n", version)
54 } else if flags.NArg() == 0 {
55 fmt.Fprintf(os.Stderr, "missing required argument: program (try -help)\n")
59 reporter.Logger.Printf("crunchstat %s started", version)
61 if reporter.CgroupRoot == "" {
62 reporter.Logger.Printf("error: must provide -cgroup-root")
64 } else if signalOnDeadPPID < 0 {
65 reporter.Logger.Printf("-signal-on-dead-ppid=%d is invalid (use a positive signal number, or 0 to disable)", signalOnDeadPPID)
68 reporter.PollPeriod = time.Duration(*pollMsec) * time.Millisecond
71 err := runCommand(flags.Args(), reporter.Logger)
74 if err, ok := err.(*exec.ExitError); ok {
75 // The program has exited with an exit code != 0
77 // This works on both Unix and Windows. Although
78 // package syscall is generally platform dependent,
79 // WaitStatus is defined for both Unix and Windows and
80 // in both cases has an ExitStatus() method with the
82 if status, ok := err.Sys().(syscall.WaitStatus); ok {
83 os.Exit(status.ExitStatus())
85 reporter.Logger.Printf("ExitError without WaitStatus: %v", err)
88 } else if err != nil {
89 reporter.Logger.Printf("error running command: %v", err)
94 func runCommand(argv []string, logger logger) error {
95 cmd := exec.Command(argv[0], argv[1:]...)
97 logger.Printf("Running %v", argv)
99 // Child process will use our stdin and stdout pipes
100 // (we close our copies below)
102 cmd.Stdout = os.Stdout
104 // Forward SIGINT and SIGTERM to child process
105 sigChan := make(chan os.Signal, 1)
106 go func(sig <-chan os.Signal) {
108 if cmd.Process != nil {
109 cmd.Process.Signal(catch)
111 logger.Printf("notice: caught signal: %v", catch)
113 signal.Notify(sigChan, syscall.SIGTERM)
114 signal.Notify(sigChan, syscall.SIGINT)
116 // Kill our child proc if our parent process disappears
117 if signalOnDeadPPID != 0 {
118 go sendSignalOnDeadPPID(ppidCheckInterval, signalOnDeadPPID, os.Getppid(), cmd, logger)
121 // Funnel stderr through our channel
122 stderrPipe, err := cmd.StderrPipe()
124 logger.Printf("error in StderrPipe: %v", err)
129 if err := cmd.Start(); err != nil {
130 logger.Printf("error in cmd.Start: %v", err)
134 // Close stdin/stdout in this (parent) process
138 err = copyPipeToChildLog(stderrPipe, log.New(os.Stderr, "", 0))
147 func sendSignalOnDeadPPID(intvl time.Duration, signum, ppidOrig int, cmd *exec.Cmd, logger logger) {
148 ticker := time.NewTicker(intvl)
151 if ppid == ppidOrig {
154 if cmd.Process == nil {
155 // Child process isn't running yet
158 logger.Printf("notice: crunchstat ppid changed from %d to %d -- killing child pid %d with signal %d", ppidOrig, ppid, cmd.Process.Pid, signum)
159 err := cmd.Process.Signal(syscall.Signal(signum))
161 logger.Printf("error: sending signal: %s", err)
169 func copyPipeToChildLog(in io.ReadCloser, logger logger) error {
170 reader := bufio.NewReaderSize(in, MaxLogLine)
173 line, isPrefix, err := reader.ReadLine()
176 } else if err != nil {
177 return fmt.Errorf("error reading child stderr: %w", err)
183 logger.Printf("%s%s%s", prefix, string(line), suffix)
184 // Set up prefix for following line