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