Merge branch '14873-google-api-client-update'
[arvados.git] / services / crunch-run / background.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         "encoding/json"
9         "fmt"
10         "io"
11         "os"
12         "os/exec"
13         "path/filepath"
14         "strings"
15         "syscall"
16         "time"
17 )
18
19 var (
20         lockdir    = "/var/lock"
21         lockprefix = "crunch-run-"
22         locksuffix = ".lock"
23 )
24
25 // procinfo is saved in each process's lockfile.
26 type procinfo struct {
27         UUID string
28         PID  int
29 }
30
31 // Detach acquires a lock for the given uuid, and starts the current
32 // program as a child process (with -no-detach prepended to the given
33 // arguments so the child knows not to detach again). The lock is
34 // passed along to the child process.
35 //
36 // Stdout and stderr in the child process are sent to the systemd
37 // journal using the systemd-cat program.
38 func Detach(uuid string, args []string, stdout, stderr io.Writer) int {
39         return exitcode(stderr, detach(uuid, args, stdout, stderr))
40 }
41 func detach(uuid string, args []string, stdout, stderr io.Writer) error {
42         lockfile, err := func() (*os.File, error) {
43                 // We must hold the dir-level lock between
44                 // opening/creating the lockfile and acquiring LOCK_EX
45                 // on it, to avoid racing with the ListProcess's
46                 // alive-checking and garbage collection.
47                 dirlock, err := lockall()
48                 if err != nil {
49                         return nil, err
50                 }
51                 defer dirlock.Close()
52                 lockfilename := filepath.Join(lockdir, lockprefix+uuid+locksuffix)
53                 lockfile, err := os.OpenFile(lockfilename, os.O_CREATE|os.O_RDWR, 0700)
54                 if err != nil {
55                         return nil, fmt.Errorf("open %s: %s", lockfilename, err)
56                 }
57                 err = syscall.Flock(int(lockfile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
58                 if err != nil {
59                         lockfile.Close()
60                         return nil, fmt.Errorf("lock %s: %s", lockfilename, err)
61                 }
62                 return lockfile, nil
63         }()
64         if err != nil {
65                 return err
66         }
67         defer lockfile.Close()
68         lockfile.Truncate(0)
69
70         cmd := exec.Command("systemd-cat", append([]string{"--identifier=crunch-run", args[0], "-no-detach"}, args[1:]...)...)
71         // Child inherits lockfile.
72         cmd.ExtraFiles = []*os.File{lockfile}
73         // Ensure child isn't interrupted even if we receive signals
74         // from parent (sshd) while sending lockfile content to
75         // caller.
76         cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
77         err = cmd.Start()
78         if err != nil {
79                 return fmt.Errorf("exec %s: %s", cmd.Path, err)
80         }
81
82         w := io.MultiWriter(stdout, lockfile)
83         return json.NewEncoder(w).Encode(procinfo{
84                 UUID: uuid,
85                 PID:  cmd.Process.Pid,
86         })
87 }
88
89 // KillProcess finds the crunch-run process corresponding to the given
90 // uuid, and sends the given signal to it. It then waits up to 1
91 // second for the process to die. It returns 0 if the process is
92 // successfully killed or didn't exist in the first place.
93 func KillProcess(uuid string, signal syscall.Signal, stdout, stderr io.Writer) int {
94         return exitcode(stderr, kill(uuid, signal, stdout, stderr))
95 }
96
97 func kill(uuid string, signal syscall.Signal, stdout, stderr io.Writer) error {
98         path := filepath.Join(lockdir, lockprefix+uuid+locksuffix)
99         f, err := os.Open(path)
100         if os.IsNotExist(err) {
101                 return nil
102         } else if err != nil {
103                 return fmt.Errorf("open %s: %s", path, err)
104         }
105         defer f.Close()
106
107         var pi procinfo
108         err = json.NewDecoder(f).Decode(&pi)
109         if err != nil {
110                 return fmt.Errorf("decode %s: %s\n", path, err)
111         }
112
113         if pi.UUID != uuid || pi.PID == 0 {
114                 return fmt.Errorf("%s: bogus procinfo: %+v", path, pi)
115         }
116
117         proc, err := os.FindProcess(pi.PID)
118         if err != nil {
119                 // FindProcess should have succeeded, even if the
120                 // process does not exist.
121                 return fmt.Errorf("%s: find process %d: %s", uuid, pi.PID, err)
122         }
123
124         // Send the requested signal once, then send signal 0 a few
125         // times.  When proc.Signal() returns an error (process no
126         // longer exists), return success.  If that doesn't happen
127         // within 1 second, return an error.
128         err = proc.Signal(signal)
129         for deadline := time.Now().Add(time.Second); err == nil && time.Now().Before(deadline); time.Sleep(time.Second / 100) {
130                 err = proc.Signal(syscall.Signal(0))
131         }
132         if err == nil {
133                 // Reached deadline without a proc.Signal() error.
134                 return fmt.Errorf("%s: pid %d: sent signal %d (%s) but process is still alive", uuid, pi.PID, signal, signal)
135         }
136         fmt.Fprintf(stderr, "%s: pid %d: %s\n", uuid, pi.PID, err)
137         return nil
138 }
139
140 // List UUIDs of active crunch-run processes.
141 func ListProcesses(stdout, stderr io.Writer) int {
142         // filepath.Walk does not follow symlinks, so we must walk
143         // lockdir+"/." in case lockdir itself is a symlink.
144         walkdir := lockdir + "/."
145         return exitcode(stderr, filepath.Walk(walkdir, func(path string, info os.FileInfo, err error) error {
146                 if info.IsDir() && path != walkdir {
147                         return filepath.SkipDir
148                 }
149                 if name := info.Name(); !strings.HasPrefix(name, lockprefix) || !strings.HasSuffix(name, locksuffix) {
150                         return nil
151                 }
152                 if info.Size() == 0 {
153                         // race: process has opened/locked but hasn't yet written pid/uuid
154                         return nil
155                 }
156
157                 f, err := os.Open(path)
158                 if err != nil {
159                         return nil
160                 }
161                 defer f.Close()
162
163                 // Ensure other processes don't acquire this lockfile
164                 // after we have decided it is abandoned but before we
165                 // have deleted it.
166                 dirlock, err := lockall()
167                 if err != nil {
168                         return err
169                 }
170                 err = syscall.Flock(int(f.Fd()), syscall.LOCK_SH|syscall.LOCK_NB)
171                 if err == nil {
172                         // lockfile is stale
173                         err := os.Remove(path)
174                         dirlock.Close()
175                         if err != nil {
176                                 fmt.Fprintf(stderr, "unlink %s: %s\n", f.Name(), err)
177                         }
178                         return nil
179                 }
180                 dirlock.Close()
181
182                 var pi procinfo
183                 err = json.NewDecoder(f).Decode(&pi)
184                 if err != nil {
185                         fmt.Fprintf(stderr, "%s: %s\n", path, err)
186                         return nil
187                 }
188                 if pi.UUID == "" || pi.PID == 0 {
189                         fmt.Fprintf(stderr, "%s: bogus procinfo: %+v", path, pi)
190                         return nil
191                 }
192
193                 fmt.Fprintln(stdout, pi.UUID)
194                 return nil
195         }))
196 }
197
198 // If err is nil, return 0 ("success"); otherwise, print err to stderr
199 // and return 1.
200 func exitcode(stderr io.Writer, err error) int {
201         if err != nil {
202                 fmt.Fprintln(stderr, err)
203                 return 1
204         }
205         return 0
206 }
207
208 // Acquire a dir-level lock. Must be held while creating or deleting
209 // container-specific lockfiles, to avoid races during the intervals
210 // when those container-specific lockfiles are open but not locked.
211 //
212 // Caller releases the lock by closing the returned file.
213 func lockall() (*os.File, error) {
214         lockfile := filepath.Join(lockdir, lockprefix+"all"+locksuffix)
215         f, err := os.OpenFile(lockfile, os.O_CREATE|os.O_RDWR, 0700)
216         if err != nil {
217                 return nil, fmt.Errorf("open %s: %s", lockfile, err)
218         }
219         err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX)
220         if err != nil {
221                 f.Close()
222                 return nil, fmt.Errorf("lock %s: %s", lockfile, err)
223         }
224         return f, nil
225 }