]> Cypherpunks.ru repositories - goredo.git/blob - run.go
redo-sh tests under github.com/chriscool/sharness
[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         "errors"
25         "flag"
26         "fmt"
27         "io"
28         "os"
29         "os/exec"
30         "path"
31         "strings"
32         "sync"
33         "syscall"
34         "time"
35
36         "go.cypherpunks.ru/recfile"
37         "go.cypherpunks.ru/tai64n"
38         "golang.org/x/sys/unix"
39 )
40
41 const (
42         EnvDepFd        = "REDO_DEP_FD"
43         EnvDirPrefix    = "REDO_DIRPREFIX"
44         EnvBuildUUID    = "REDO_BUILD_UUID"
45         EnvStderrPrefix = "REDO_STDERR_PREFIX"
46         EnvTrace        = "REDO_TRACE"
47         EnvStderrKeep   = "REDO_LOGS"
48         EnvStderrSilent = "REDO_SILENT"
49         EnvNoSync       = "REDO_NO_SYNC"
50
51         RedoDir    = ".redo"
52         LockSuffix = ".lock"
53         DepSuffix  = ".dep"
54         TmpPrefix  = ".redo."
55         LogSuffix  = ".log"
56 )
57
58 var (
59         NoSync       bool = false
60         StderrKeep   bool = false
61         StderrSilent bool = false
62         StderrPrefix string
63         Jobs         sync.WaitGroup
64
65         flagTrace        = flag.Bool("x", false, "trace (sh -x) current targets")
66         flagTraceAll     = flag.Bool("xx", false, fmt.Sprintf("trace (sh -x) all targets (%s=1)", EnvTrace))
67         flagStderrKeep   = flag.Bool("logs", false, fmt.Sprintf("keep job's stderr (%s=1)", EnvStderrKeep))
68         flagStderrSilent = flag.Bool("silent", false, fmt.Sprintf("do not print job's stderr (%s=1)", EnvStderrSilent))
69 )
70
71 type RunErr struct {
72         Tgt      string
73         DoFile   string
74         Started  *time.Time
75         Finished *time.Time
76         Err      error
77 }
78
79 func (e RunErr) Unwrap() error { return e.Err }
80
81 func (e *RunErr) Name() string {
82         var name string
83         if e.DoFile == "" {
84                 name = e.Tgt
85         } else {
86                 name = fmt.Sprintf("%s (%s)", e.Tgt, e.DoFile)
87         }
88         if e.Finished == nil {
89                 return name
90         }
91         return fmt.Sprintf("%s (%fsec)", name, e.Finished.Sub(*e.Started).Seconds())
92 }
93
94 func (e RunErr) Error() string {
95         return fmt.Sprintf("%s: %s", e.Name(), e.Err)
96 }
97
98 func mkdirs(pth string) error {
99         if _, err := os.Stat(pth); err == nil {
100                 return nil
101         }
102         return os.MkdirAll(pth, os.FileMode(0777))
103 }
104
105 func isModified(cwd, redoDir, tgt string) (bool, string, error) {
106         fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix))
107         if err != nil {
108                 if os.IsNotExist(err) {
109                         return false, "", nil
110                 }
111                 return false, "", err
112         }
113         defer fdDep.Close()
114         r := recfile.NewReader(fdDep)
115         var ourTs string
116         for {
117                 m, err := r.NextMap()
118                 if err != nil {
119                         if err == io.EOF {
120                                 break
121                         }
122                         return false, "", err
123                 }
124                 if m["Target"] != tgt {
125                         continue
126                 }
127                 fd, err := os.Open(path.Join(cwd, tgt))
128                 if err != nil {
129                         if os.IsNotExist(err) {
130                                 return false, "", nil
131                         }
132                         return false, "", err
133                 }
134                 defer fd.Close()
135                 ourTs, err = fileCtime(fd)
136                 if err != nil {
137                         return false, "", err
138                 }
139                 if ourTs != m["Ctime"] {
140                         return true, ourTs, nil
141                 }
142                 break
143         }
144         return false, ourTs, nil
145 }
146
147 func syncDir(dir string) error {
148         fd, err := os.Open(dir)
149         if err != nil {
150                 return err
151         }
152         err = fd.Sync()
153         fd.Close()
154         return err
155 }
156
157 func runScript(tgtOrig string, errs chan error, traced bool) error {
158         cwd, tgt := cwdAndTgt(tgtOrig)
159         redoDir := path.Join(cwd, RedoDir)
160         if err := mkdirs(redoDir); err != nil {
161                 return TgtErr{tgtOrig, err}
162         }
163
164         // Acquire lock
165         fdLock, err := os.OpenFile(
166                 path.Join(redoDir, tgt+LockSuffix),
167                 os.O_WRONLY|os.O_TRUNC|os.O_CREATE,
168                 os.FileMode(0666),
169         )
170         if err != nil {
171                 return TgtErr{tgtOrig, err}
172         }
173         lockRelease := func() {
174                 trace(CLock, "LOCK_UN: %s", fdLock.Name())
175                 unix.Flock(int(fdLock.Fd()), unix.LOCK_UN)
176                 fdLock.Close()
177         }
178         trace(CLock, "LOCK_NB: %s", fdLock.Name())
179
180         // Waiting for job completion, already taken by someone else
181         if err = unix.Flock(int(fdLock.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
182                 if uintptr(err.(syscall.Errno)) != uintptr(unix.EWOULDBLOCK) {
183                         fdLock.Close()
184                         return TgtErr{tgtOrig, err}
185                 }
186                 Jobs.Add(1)
187                 trace(CDebug, "waiting: %s", tgtOrig)
188                 if FdStatus != nil {
189                         FdStatus.Write([]byte{StatusWait})
190                 }
191                 go func() {
192                         defer Jobs.Done()
193                         trace(CLock, "LOCK_EX: %s", fdLock.Name())
194                         unix.Flock(int(fdLock.Fd()), unix.LOCK_EX)
195                         lockRelease()
196                         trace(CDebug, "waiting done: %s", tgtOrig)
197                         if FdStatus != nil {
198                                 FdStatus.Write([]byte{StatusWaited})
199                         }
200                         var depInfo *DepInfo
201                         fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix))
202                         if err != nil {
203                                 if os.IsNotExist(err) {
204                                         err = errors.New("was not built: no .dep")
205                                 }
206                                 goto Finish
207                         }
208                         defer fdDep.Close()
209                         depInfo, err = depRead(fdDep)
210                         if err != nil {
211                                 goto Finish
212                         }
213                         if depInfo.build != BuildUUID {
214                                 err = errors.New("was not built: build differs")
215                         }
216                 Finish:
217                         if err != nil {
218                                 err = TgtErr{tgtOrig, err}
219                         }
220                         errs <- err
221                 }()
222                 return nil
223         }
224
225         // Check if target is not modified externally
226         modified, tsPrev, err := isModified(cwd, redoDir, tgt)
227         if err != nil {
228                 lockRelease()
229                 return TgtErr{tgtOrig, err}
230         }
231         if modified {
232                 trace(CWarn, "%s externally modified: not redoing", tgtOrig)
233                 lockRelease()
234                 go func() {
235                         errs <- nil
236                 }()
237                 return nil
238         }
239
240         // Start preparing .dep
241         fdDep, err := tempfile(redoDir, tgt+DepSuffix)
242         if err != nil {
243                 lockRelease()
244                 return TgtErr{tgtOrig, err}
245         }
246         cleanup := func() {
247                 lockRelease()
248                 fdDep.Close()
249                 os.Remove(fdDep.Name())
250         }
251         if _, err = recfile.NewWriter(fdDep).WriteFields(
252                 recfile.Field{Name: "Build", Value: BuildUUID},
253         ); err != nil {
254                 cleanup()
255                 return TgtErr{tgtOrig, err}
256         }
257
258         // Find .do
259         doFile, upLevels, err := findDo(fdDep, cwd, tgt)
260         if err != nil {
261                 cleanup()
262                 return TgtErr{tgtOrig, err}
263         }
264         if doFile == "" {
265                 cleanup()
266                 return TgtErr{tgtOrig, errors.New("no .do found")}
267         }
268
269         // Determine basename and DIRPREFIX
270         ents := strings.Split(cwd, "/")
271         ents = ents[len(ents)-upLevels:]
272         dirPrefix := path.Join(ents...)
273         cwdOrig := cwd
274         for i := 0; i < upLevels; i++ {
275                 cwd = path.Join(cwd, "..")
276         }
277         cwd = path.Clean(cwd)
278         doFilePath := path.Join(cwd, doFile)
279         basename := tgt
280         runErr := RunErr{Tgt: tgtOrig}
281         if strings.HasPrefix(doFile, "default.") {
282                 basename = tgt[:len(tgt)-(len(doFile)-len("default.")-len(".do"))-1]
283                 runErr.DoFile = doFile
284         }
285
286         if err = writeDep(fdDep, cwd, doFile); err != nil {
287                 cleanup()
288                 return TgtErr{tgtOrig, err}
289         }
290         trace(CWait, "%s", runErr.Name())
291
292         // Prepare command line
293         var cmdName string
294         var args []string
295         if err = unix.Access(doFilePath, unix.X_OK); err == nil {
296                 cmdName = doFilePath
297                 args = make([]string, 0, 3)
298         } else {
299                 cmdName = "/bin/sh"
300                 if traced {
301                         args = append(args, "-ex")
302                 } else {
303                         args = append(args, "-e")
304                 }
305                 args = append(args, doFile)
306         }
307
308         // Temporary file for stdout
309         fdStdout, err := tempfile(cwd, tgt)
310         if err != nil {
311                 cleanup()
312                 return TgtErr{tgtOrig, err}
313         }
314         tmpPath := fdStdout.Name() + ".3" // and for $3
315         args = append(args, tgt, basename, path.Base(tmpPath))
316
317         cmd := exec.Command(cmdName, args...)
318         cmd.Dir = cwd
319         cmd.Stdout = fdStdout
320         cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%d", EnvLevel, Level+1))
321         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", EnvDirPrefix, dirPrefix))
322         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", EnvBuildUUID, BuildUUID))
323
324         childStderrPrefix := tempsuffix()
325         cmd.Env = append(cmd.Env, fmt.Sprintf(
326                 "%s=%s", EnvStderrPrefix, childStderrPrefix,
327         ))
328
329         fdNum := 0
330         cmd.ExtraFiles = append(cmd.ExtraFiles, fdDep)
331         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvDepFd, 3+fdNum))
332         fdNum++
333
334         if FdStatus == nil {
335                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", EnvStatusFd))
336         } else {
337                 cmd.ExtraFiles = append(cmd.ExtraFiles, FdStatus)
338                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvStatusFd, 3+fdNum))
339                 fdNum++
340         }
341
342         if JSR == nil {
343                 // infinite jobs
344                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", EnvJSFd))
345         } else {
346                 cmd.ExtraFiles = append(cmd.ExtraFiles, JSR)
347                 cmd.ExtraFiles = append(cmd.ExtraFiles, JSW)
348                 cmd.Env = append(cmd.Env, fmt.Sprintf(
349                         "%s=%d,%d", EnvJSFd, 3+fdNum+0, 3+fdNum+1,
350                 ))
351                 fdNum += 2
352         }
353
354         // Preparing stderr
355         stderr, err := cmd.StderrPipe()
356         if err != nil {
357                 panic(err)
358         }
359         var fdStderr *os.File
360         if StderrKeep {
361                 fdStderr, err = os.OpenFile(
362                         path.Join(redoDir, tgt+LogSuffix),
363                         os.O_WRONLY|os.O_CREATE,
364                         os.FileMode(0666),
365                 )
366                 if err != nil {
367                         cleanup()
368                         return TgtErr{tgtOrig, err}
369                 }
370                 fdStderr.Truncate(0)
371         }
372         shCtx := fmt.Sprintf("sh: %s: %s %s [%s]", tgtOrig, cmdName, args, cwd)
373         trace(CDebug, "%s", shCtx)
374
375         Jobs.Add(1)
376         go func() {
377                 jsAcquire(shCtx)
378                 if FdStatus != nil {
379                         FdStatus.Write([]byte{StatusRun})
380                 }
381                 defer func() {
382                         jsRelease(shCtx)
383                         lockRelease()
384                         fdDep.Close()
385                         fdStdout.Close()
386                         if fdStderr != nil {
387                                 fdStderr.Close()
388                         }
389                         os.Remove(fdDep.Name())
390                         os.Remove(fdStdout.Name())
391                         os.Remove(tmpPath)
392                         os.Remove(fdLock.Name())
393                         if FdStatus != nil {
394                                 FdStatus.Write([]byte{StatusDone})
395                         }
396                         Jobs.Done()
397                 }()
398                 started := time.Now()
399                 runErr.Started = &started
400                 err := cmd.Start()
401                 if err != nil {
402                         runErr.Err = err
403                         errs <- runErr
404                         return
405                 }
406                 pid := fmt.Sprintf("[%d]", cmd.Process.Pid)
407                 trace(CDebug, "%s runs %s", tgtOrig, pid)
408
409                 stderrTerm := make(chan struct{}, 0)
410                 go func() {
411                         scanner := bufio.NewScanner(stderr)
412                         var line string
413                         ts := new(tai64n.TAI64N)
414                         for scanner.Scan() {
415                                 line = scanner.Text()
416                                 if strings.HasPrefix(line, childStderrPrefix) {
417                                         line = line[len(childStderrPrefix):]
418                                         os.Stderr.WriteString(StderrPrefix + line + "\n")
419                                         continue
420                                 }
421                                 if fdStderr != nil {
422                                         tai64n.FromTime(time.Now(), ts)
423                                         LogMutex.Lock()
424                                         fmt.Fprintf(fdStderr, "%s %s\n", ts.Encode(), line)
425                                         LogMutex.Unlock()
426                                 }
427                                 if StderrSilent {
428                                         continue
429                                 }
430                                 if MyPid == 0 {
431                                         trace(CNone, "%s", line)
432                                 } else {
433                                         trace(CNone, "%s %s", pid, line)
434                                 }
435                         }
436                         close(stderrTerm)
437                 }()
438
439                 // Wait for job completion
440                 <-stderrTerm
441                 err = cmd.Wait()
442                 finished := time.Now()
443                 runErr.Finished = &finished
444                 if err != nil {
445                         runErr.Err = err
446                         errs <- runErr
447                         return
448                 }
449
450                 // Was $1 touched?
451                 if fd, err := os.Open(path.Join(cwdOrig, tgt)); err == nil {
452                         ts, err := fileCtime(fd)
453                         fd.Close()
454                         if err == nil && ts != tsPrev {
455                                 runErr.Err = errors.New("$1 was explicitly touched")
456                                 errs <- runErr
457                                 fd.Close()
458                                 return
459                         }
460                 }
461
462                 // Does it produce both stdout and tmp?
463                 fiStdout, err := os.Stat(fdStdout.Name())
464                 if err != nil {
465                         runErr.Err = err
466                         errs <- runErr
467                         return
468                 }
469                 tmpExists := false
470                 _, err = os.Stat(tmpPath)
471                 if err == nil {
472                         if fiStdout.Size() > 0 {
473                                 runErr.Err = errors.New("created both tmp and stdout")
474                                 errs <- runErr
475                                 return
476                         }
477                         tmpExists = true
478                 } else if !os.IsNotExist(err) {
479                         runErr.Err = err
480                         errs <- runErr
481                         return
482                 }
483
484                 // Determine what file we must process at last
485                 var fd *os.File
486                 if tmpExists {
487                         fd, err = os.Open(tmpPath)
488                         if err != nil {
489                                 goto Finish
490                         }
491                         defer fd.Close()
492                 } else if fiStdout.Size() > 0 {
493                         fd = fdStdout
494                 }
495
496                 // Do we need to ifcreate it, of ifchange with renaming?
497                 if fd == nil {
498                         err = ifcreate(fdDep, tgt)
499                         if err != nil {
500                                 goto Finish
501                         }
502                 } else {
503                         if !NoSync {
504                                 err = fd.Sync()
505                                 if err != nil {
506                                         goto Finish
507                                 }
508                         }
509                         err = os.Rename(fd.Name(), path.Join(cwdOrig, tgt))
510                         if err != nil {
511                                 goto Finish
512                         }
513                         if !NoSync {
514                                 err = syncDir(cwdOrig)
515                                 if err != nil {
516                                         goto Finish
517                                 }
518                         }
519                         err = writeDep(fdDep, cwdOrig, tgt)
520                         if err != nil {
521                                 goto Finish
522                         }
523                 }
524
525                 // Commit .dep
526                 if !NoSync {
527                         err = fdDep.Sync()
528                         if err != nil {
529                                 goto Finish
530                         }
531                 }
532                 err = os.Rename(fdDep.Name(), path.Join(redoDir, tgt+DepSuffix))
533                 if err != nil {
534                         goto Finish
535                 }
536                 if !NoSync {
537                         err = syncDir(redoDir)
538                         if err != nil {
539                                 goto Finish
540                         }
541                 }
542         Finish:
543                 runErr.Err = err
544                 errs <- runErr
545         }()
546         return nil
547 }
548
549 func isOkRun(err error) bool {
550         if err == nil {
551                 return true
552         }
553         if err, ok := err.(RunErr); ok && err.Err == nil {
554                 trace(CRedo, "%s", err.Name())
555                 return true
556         }
557         trace(CErr, "%s", err)
558         return false
559 }