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