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