20259: Add documentation for banner and tooltip features
[arvados.git] / services / crunchstat / crunchstat.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "bufio"
9         "flag"
10         "fmt"
11         "io"
12         "log"
13         "os"
14         "os/exec"
15         "os/signal"
16         "syscall"
17         "time"
18
19         "git.arvados.org/arvados.git/lib/cmd"
20         "git.arvados.org/arvados.git/lib/crunchstat"
21 )
22
23 const MaxLogLine = 1 << 14 // Child stderr lines >16KiB will be split
24
25 var (
26         signalOnDeadPPID  int = 15
27         ppidCheckInterval     = time.Second
28         version               = "dev"
29 )
30
31 type logger interface {
32         Printf(string, ...interface{})
33 }
34
35 func main() {
36         reporter := crunchstat.Reporter{
37                 Logger: log.New(os.Stderr, "crunchstat: ", 0),
38         }
39
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.")
48
49         if ok, code := cmd.ParseFlags(flags, os.Args[0], os.Args[1:], "program [args ...]", os.Stderr); !ok {
50                 os.Exit(code)
51         } else if *getVersion {
52                 fmt.Printf("crunchstat %s\n", version)
53                 return
54         } else if flags.NArg() == 0 {
55                 fmt.Fprintf(os.Stderr, "missing required argument: program (try -help)\n")
56                 os.Exit(2)
57         }
58
59         reporter.Logger.Printf("crunchstat %s started", version)
60
61         if reporter.CgroupRoot == "" {
62                 reporter.Logger.Printf("error: must provide -cgroup-root")
63                 os.Exit(2)
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)
66                 os.Exit(2)
67         }
68         reporter.PollPeriod = time.Duration(*pollMsec) * time.Millisecond
69
70         reporter.Start()
71         err := runCommand(flags.Args(), reporter.Logger)
72         reporter.Stop()
73
74         if err, ok := err.(*exec.ExitError); ok {
75                 // The program has exited with an exit code != 0
76
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
81                 // same signature.
82                 if status, ok := err.Sys().(syscall.WaitStatus); ok {
83                         os.Exit(status.ExitStatus())
84                 } else {
85                         reporter.Logger.Printf("ExitError without WaitStatus: %v", err)
86                         os.Exit(1)
87                 }
88         } else if err != nil {
89                 reporter.Logger.Printf("error running command: %v", err)
90                 os.Exit(1)
91         }
92 }
93
94 func runCommand(argv []string, logger logger) error {
95         cmd := exec.Command(argv[0], argv[1:]...)
96
97         logger.Printf("Running %v", argv)
98
99         // Child process will use our stdin and stdout pipes
100         // (we close our copies below)
101         cmd.Stdin = os.Stdin
102         cmd.Stdout = os.Stdout
103
104         // Forward SIGINT and SIGTERM to child process
105         sigChan := make(chan os.Signal, 1)
106         go func(sig <-chan os.Signal) {
107                 catch := <-sig
108                 if cmd.Process != nil {
109                         cmd.Process.Signal(catch)
110                 }
111                 logger.Printf("notice: caught signal: %v", catch)
112         }(sigChan)
113         signal.Notify(sigChan, syscall.SIGTERM)
114         signal.Notify(sigChan, syscall.SIGINT)
115
116         // Kill our child proc if our parent process disappears
117         if signalOnDeadPPID != 0 {
118                 go sendSignalOnDeadPPID(ppidCheckInterval, signalOnDeadPPID, os.Getppid(), cmd, logger)
119         }
120
121         // Funnel stderr through our channel
122         stderrPipe, err := cmd.StderrPipe()
123         if err != nil {
124                 logger.Printf("error in StderrPipe: %v", err)
125                 return err
126         }
127
128         // Run subprocess
129         if err := cmd.Start(); err != nil {
130                 logger.Printf("error in cmd.Start: %v", err)
131                 return err
132         }
133
134         // Close stdin/stdout in this (parent) process
135         os.Stdin.Close()
136         os.Stdout.Close()
137
138         err = copyPipeToChildLog(stderrPipe, log.New(os.Stderr, "", 0))
139         if err != nil {
140                 cmd.Process.Kill()
141                 return err
142         }
143
144         return cmd.Wait()
145 }
146
147 func sendSignalOnDeadPPID(intvl time.Duration, signum, ppidOrig int, cmd *exec.Cmd, logger logger) {
148         ticker := time.NewTicker(intvl)
149         for range ticker.C {
150                 ppid := os.Getppid()
151                 if ppid == ppidOrig {
152                         continue
153                 }
154                 if cmd.Process == nil {
155                         // Child process isn't running yet
156                         continue
157                 }
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))
160                 if err != nil {
161                         logger.Printf("error: sending signal: %s", err)
162                         continue
163                 }
164                 ticker.Stop()
165                 break
166         }
167 }
168
169 func copyPipeToChildLog(in io.ReadCloser, logger logger) error {
170         reader := bufio.NewReaderSize(in, MaxLogLine)
171         var prefix string
172         for {
173                 line, isPrefix, err := reader.ReadLine()
174                 if err == io.EOF {
175                         break
176                 } else if err != nil {
177                         return fmt.Errorf("error reading child stderr: %w", err)
178                 }
179                 var suffix string
180                 if isPrefix {
181                         suffix = "[...]"
182                 }
183                 logger.Printf("%s%s%s", prefix, string(line), suffix)
184                 // Set up prefix for following line
185                 if isPrefix {
186                         prefix = "[...]"
187                 } else {
188                         prefix = ""
189                 }
190         }
191         return in.Close()
192 }