]> Cypherpunks.ru repositories - goredo.git/blob - run.go
Fix JS deadlock and various optimizations
[goredo.git] / run.go
1 /*
2 goredo -- redo implementation on pure Go
3 Copyright (C) 2020 Sergey Matveev <stargrave@stargrave.org>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, version 3 of the License.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 // Targets runner
19
20 package main
21
22 import (
23         "bufio"
24         "encoding/hex"
25         "errors"
26         "fmt"
27         "io"
28         "os"
29         "os/exec"
30         "path"
31         "strconv"
32         "strings"
33         "syscall"
34         "time"
35
36         "go.cypherpunks.ru/recfile"
37         "golang.org/x/sys/unix"
38 )
39
40 const (
41         RedoDir    = ".redo"
42         LockSuffix = ".lock"
43         DepSuffix  = ".dep"
44         TmpPrefix  = ".redo."
45         LogSuffix  = ".log"
46 )
47
48 func mkdirs(pth string) error {
49         if _, err := os.Stat(pth); err == nil {
50                 return nil
51         }
52         return os.MkdirAll(pth, os.FileMode(0777))
53 }
54
55 func tempsuffix() string {
56         return strconv.FormatInt((time.Now().UnixNano()+int64(os.Getpid()))&0xFFFFFFFF, 16)
57 }
58
59 func tempfile(dir, prefix string) (*os.File, error) {
60         // It respects umask, unlike ioutil.TempFile
61         name := path.Join(dir, TmpPrefix+prefix+"."+tempsuffix())
62         return os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, os.FileMode(0666))
63 }
64
65 func isModified(cwd, redoDir, tgt string) (bool, error) {
66         fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix))
67         if err != nil {
68                 if os.IsNotExist(err) {
69                         return false, nil
70                 }
71                 return false, err
72         }
73         defer fdDep.Close()
74         r := recfile.NewReader(fdDep)
75         for {
76                 m, err := r.NextMap()
77                 if err != nil {
78                         if err == io.EOF {
79                                 break
80                         }
81                         return false, err
82                 }
83                 if m["Target"] != tgt {
84                         continue
85                 }
86                 fd, err := os.Open(path.Join(cwd, tgt))
87                 if err != nil {
88                         if os.IsNotExist(err) {
89                                 return false, nil
90                         }
91                         return false, err
92                 }
93                 defer fd.Close()
94                 ourTs, err := fileCtime(fd)
95                 if err != nil {
96                         return false, err
97                 }
98                 if ourTs != m["Ctime"] {
99                         return true, nil
100                 }
101                 break
102         }
103         return false, nil
104 }
105
106 type RunErr struct {
107         Tgt      string
108         DoFile   string
109         Started  *time.Time
110         Finished *time.Time
111         Err      error
112 }
113
114 func (e RunErr) Unwrap() error { return e.Err }
115
116 func (e *RunErr) Name() string {
117         var name string
118         if e.DoFile == "" {
119                 name = e.Tgt
120         } else {
121                 name = fmt.Sprintf("%s (%s)", e.Tgt, e.DoFile)
122         }
123         if e.Finished == nil {
124                 return name
125         }
126         return fmt.Sprintf("%s (%fsec)", name, e.Finished.Sub(*e.Started).Seconds())
127 }
128
129 func (e RunErr) Error() string {
130         return fmt.Sprintf("%s: %s", e.Name(), e.Err)
131 }
132
133 func syncDir(dir string) error {
134         fd, err := os.Open(dir)
135         if err != nil {
136                 return err
137         }
138         err = fd.Sync()
139         fd.Close()
140         return err
141 }
142
143 func runScript(tgt string, errs chan error) error {
144         tgtOrig := tgt
145         cwd, tgt := cwdAndTgt(tgt)
146         redoDir := path.Join(cwd, RedoDir)
147         if err := mkdirs(redoDir); err != nil {
148                 return TgtErr{tgtOrig, err}
149         }
150
151         // Acquire lock
152         fdLock, err := os.OpenFile(
153                 path.Join(redoDir, tgt+LockSuffix),
154                 os.O_WRONLY|os.O_TRUNC|os.O_CREATE,
155                 os.FileMode(0666),
156         )
157         if err != nil {
158                 return TgtErr{tgtOrig, err}
159         }
160         lockRelease := func() {
161                 trace(CLock, "LOCK_UN: %s", fdLock.Name())
162                 unix.Flock(int(fdLock.Fd()), unix.LOCK_UN)
163                 fdLock.Close()
164         }
165         trace(CLock, "LOCK_NB: %s", fdLock.Name())
166
167         // Waiting for job completion, already taken by someone else
168         if err = unix.Flock(int(fdLock.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
169                 if uintptr(err.(syscall.Errno)) != uintptr(unix.EWOULDBLOCK) {
170                         fdLock.Close()
171                         return TgtErr{tgtOrig, err}
172                 }
173                 trace(CDebug, "waiting: %s", tgtOrig)
174                 Jobs.Add(1)
175                 go func() {
176                         defer Jobs.Done()
177                         trace(CLock, "LOCK_EX: %s", fdLock.Name())
178                         unix.Flock(int(fdLock.Fd()), unix.LOCK_EX)
179                         lockRelease()
180                         trace(CDebug, "waiting done: %s", tgtOrig)
181                         var builtNow bool
182                         fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix))
183                         if err != nil {
184                                 if os.IsNotExist(err) {
185                                         err = errors.New("was not built")
186                                 }
187                                 goto Finish
188                         }
189                         builtNow, _, err = isBuiltNow(fdDep)
190                         if err != nil {
191                                 goto Finish
192                         }
193                         if !builtNow {
194                                 err = errors.New("was not built")
195                         }
196                 Finish:
197                         if err != nil {
198                                 err = TgtErr{tgtOrig, err}
199                         }
200                         errs <- err
201                 }()
202                 return nil
203         }
204
205         // Check if target is not modified externally
206         modified, err := isModified(cwd, redoDir, tgt)
207         if err != nil {
208                 lockRelease()
209                 return TgtErr{tgtOrig, err}
210         }
211         if modified {
212                 trace(CWarn, "%s externally modified: not redoing", tgtOrig)
213                 lockRelease()
214                 errs <- nil
215                 return TgtErr{tgtOrig, err}
216         }
217
218         // Start preparing .dep
219         fdDep, err := tempfile(redoDir, tgt+DepSuffix)
220         if err != nil {
221                 lockRelease()
222                 return TgtErr{tgtOrig, err}
223         }
224         cleanup := func() {
225                 lockRelease()
226                 fdDep.Close()
227                 os.Remove(fdDep.Name())
228         }
229         if _, err = recfile.NewWriter(fdDep).WriteFields(
230                 recfile.Field{Name: "Build", Value: BuildUUID},
231         ); err != nil {
232                 cleanup()
233                 return TgtErr{tgtOrig, err}
234         }
235
236         // Find .do
237         doFile, upLevels, err := findDo(fdDep, cwd, tgt)
238         if err != nil {
239                 cleanup()
240                 return TgtErr{tgtOrig, err}
241         }
242         if doFile == "" {
243                 cleanup()
244                 return TgtErr{tgtOrig, errors.New("no .do found")}
245         }
246         if err = writeDep(fdDep, cwd, doFile); err != nil {
247                 cleanup()
248                 return TgtErr{tgtOrig, err}
249         }
250
251         // Determine basename and DIRPREFIX
252         ents := strings.Split(cwd, "/")
253         ents = ents[len(ents)-upLevels:]
254         dirPrefix := path.Join(ents...)
255         cwdOrig := cwd
256         for i := 0; i < upLevels; i++ {
257                 cwd = path.Join(cwd, "..")
258         }
259         cwd = path.Clean(cwd)
260         basename := tgt
261         runErr := RunErr{Tgt: tgtOrig}
262         if strings.HasPrefix(doFile, "default.") {
263                 basename = tgt[:len(tgt)-(len(doFile)-len("default.")-len(".do"))-1]
264                 runErr.DoFile = doFile
265         }
266         trace(CWait, "%s", runErr.Name())
267         doFile = path.Base(doFile)
268
269         // Prepare command line
270         var cmdName string
271         var args []string
272         if err = unix.Access(path.Join(cwd, doFile), unix.X_OK); err == nil {
273                 // Ordinary executable file
274                 cmdName = doFile
275                 args = make([]string, 0, 3)
276         } else {
277                 fd, err := os.Open(path.Join(cwd, doFile))
278                 if err != nil {
279                         cleanup()
280                         return TgtErr{tgtOrig, err}
281                 }
282                 buf := make([]byte, 512)
283                 n, err := fd.Read(buf)
284                 if err != nil {
285                         cleanup()
286                         return TgtErr{tgtOrig, err}
287                 }
288                 if n > 3 && string(buf[:3]) == "#!/" {
289                         // Shebanged
290                         t := string(buf[2:n])
291                         nlIdx := strings.Index(t, "\n")
292                         if nlIdx == -1 {
293                                 cleanup()
294                                 return TgtErr{tgtOrig, errors.New("not fully read shebang")}
295                         }
296                         args = strings.Split(t[:nlIdx], " ")
297                         cmdName, args = args[0], args[1:]
298                 } else {
299                         // Shell
300                         cmdName = "/bin/sh"
301                         if Trace {
302                                 args = append(args, "-ex")
303                         } else {
304                                 args = append(args, "-e")
305                         }
306                 }
307                 args = append(args, doFile)
308         }
309
310         // Temporary file for stdout
311         fdStdout, err := tempfile(cwd, tgt)
312         if err != nil {
313                 cleanup()
314                 return TgtErr{tgtOrig, err}
315         }
316         tmpPath := fdStdout.Name() + ".3" // and for $3
317         args = append(args, tgt, basename, path.Base(tmpPath))
318
319         cmd := exec.Command(cmdName, args...)
320         cmd.Dir = cwd
321         cmd.Stdout = fdStdout
322         cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%d", RedoLevelEnv, Level+1))
323         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", RedoDirPrefixEnv, dirPrefix))
324         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", RedoBuildUUIDEnv, BuildUUID))
325         childStderrPrefix := tempsuffix()
326         cmd.Env = append(cmd.Env, fmt.Sprintf(
327                 "%s=%s", RedoStderrPrefixEnv, childStderrPrefix,
328         ))
329
330         cmd.ExtraFiles = append(cmd.ExtraFiles, fdDep)
331         fdNum := 0
332         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", RedoDepFdEnv, 3+fdNum))
333         fdNum++
334         if JSR == nil {
335                 // infinite jobs
336                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", RedoJSRFdEnv))
337                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", RedoJSWFdEnv))
338         } else {
339                 cmd.ExtraFiles = append(cmd.ExtraFiles, JSR)
340                 cmd.ExtraFiles = append(cmd.ExtraFiles, JSW)
341                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", RedoJSRFdEnv, 3+fdNum))
342                 fdNum++
343                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", RedoJSWFdEnv, 3+fdNum))
344                 fdNum++
345         }
346
347         // Preparing stderr
348         stderr, err := cmd.StderrPipe()
349         if err != nil {
350                 panic(err)
351         }
352         var fdStderr *os.File
353         if StderrKeep {
354                 fdStderr, err = os.OpenFile(
355                         path.Join(redoDir, tgt+LogSuffix),
356                         os.O_WRONLY|os.O_CREATE,
357                         os.FileMode(0666),
358                 )
359                 if err != nil {
360                         cleanup()
361                         return TgtErr{tgtOrig, err}
362                 }
363                 fdStderr.Truncate(0)
364         }
365         shCtx := fmt.Sprintf("sh: %s: %s %s [%s]", tgtOrig, cmdName, args, cwd)
366         trace(CDebug, "%s", shCtx)
367
368         Jobs.Add(1)
369         go func() {
370                 jsAcquire(shCtx)
371                 defer func() {
372                         jsRelease(shCtx)
373                         lockRelease()
374                         fdDep.Close()
375                         fdStdout.Close()
376                         if fdStderr != nil {
377                                 fdStderr.Close()
378                         }
379                         os.Remove(fdDep.Name())
380                         os.Remove(fdStdout.Name())
381                         os.Remove(tmpPath)
382                         os.Remove(fdLock.Name())
383                         Jobs.Done()
384                 }()
385                 started := time.Now()
386                 runErr.Started = &started
387                 err := cmd.Start()
388                 if err != nil {
389                         runErr.Err = err
390                         errs <- runErr
391                         return
392                 }
393                 pid := fmt.Sprintf("[%d]", cmd.Process.Pid)
394                 trace(CDebug, "%s runs %s", tgtOrig, pid)
395
396                 stderrTerm := make(chan struct{}, 0)
397                 go func() {
398                         scanner := bufio.NewScanner(stderr)
399                         var line string
400                         ts := new(TAI64N)
401                         for scanner.Scan() {
402                                 line = scanner.Text()
403                                 if strings.HasPrefix(line, childStderrPrefix) {
404                                         line = line[len(childStderrPrefix):]
405                                         os.Stderr.WriteString(StderrPrefix + line + "\n")
406                                         continue
407                                 }
408                                 if fdStderr != nil {
409                                         tai64nNow(ts)
410                                         fmt.Fprintf(fdStderr, "@%s %s\n", hex.EncodeToString(ts[:]), line)
411                                 }
412                                 if StderrSilent {
413                                         continue
414                                 }
415                                 if MyPid == 0 {
416                                         trace(CNone, "%s", line)
417                                 } else {
418                                         trace(CNone, "%s %s", pid, line)
419                                 }
420                         }
421                         close(stderrTerm)
422                 }()
423
424                 // Wait for job completion
425                 <-stderrTerm
426                 err = cmd.Wait()
427                 finished := time.Now()
428                 runErr.Finished = &finished
429                 if err != nil {
430                         runErr.Err = err
431                         errs <- runErr
432                         return
433                 }
434
435                 // Does it produce both stdout and tmp?
436                 fiStdout, err := os.Stat(fdStdout.Name())
437                 if err != nil {
438                         runErr.Err = err
439                         errs <- runErr
440                         return
441                 }
442                 tmpExists := false
443                 _, err = os.Stat(tmpPath)
444                 if err == nil {
445                         if fiStdout.Size() > 0 {
446                                 runErr.Err = errors.New("created both tmp and stdout")
447                                 errs <- runErr
448                                 return
449                         }
450                         tmpExists = true
451                 } else if !os.IsNotExist(err) {
452                         runErr.Err = err
453                         errs <- runErr
454                         return
455                 }
456
457                 // Determine what file we must process at last
458                 var fd *os.File
459                 if tmpExists {
460                         fd, err = os.Open(tmpPath)
461                         if err != nil {
462                                 goto Finish
463                         }
464                         defer fd.Close()
465                 } else if fiStdout.Size() > 0 {
466                         fd = fdStdout
467                 }
468
469                 // Do we need to ifcreate it, of ifchange with renaming?
470                 if fd == nil {
471                         err = ifcreate(fdDep, tgt)
472                         if err != nil {
473                                 goto Finish
474                         }
475                 } else {
476                         if !NoSync {
477                                 err = fd.Sync()
478                                 if err != nil {
479                                         goto Finish
480                                 }
481                         }
482                         err = os.Rename(fd.Name(), path.Join(cwdOrig, tgt))
483                         if err != nil {
484                                 goto Finish
485                         }
486                         if !NoSync {
487                                 err = syncDir(cwdOrig)
488                                 if err != nil {
489                                         goto Finish
490                                 }
491                         }
492                         err = writeDep(fdDep, cwdOrig, tgt)
493                         if err != nil {
494                                 goto Finish
495                         }
496                 }
497
498                 // Commit .dep
499                 if !NoSync {
500                         err = fdDep.Sync()
501                         if err != nil {
502                                 goto Finish
503                         }
504                 }
505                 err = os.Rename(fdDep.Name(), path.Join(redoDir, tgt+DepSuffix))
506                 if err != nil {
507                         goto Finish
508                 }
509                 if !NoSync {
510                         err = syncDir(redoDir)
511                         if err != nil {
512                                 goto Finish
513                         }
514                 }
515         Finish:
516                 runErr.Err = err
517                 errs <- runErr
518         }()
519         return nil
520 }