/* goredo -- redo implementation on pure Go Copyright (C) 2020 Sergey Matveev This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ // Targets runner package main import ( "bufio" "encoding/hex" "errors" "fmt" "io" "os" "os/exec" "path" "strconv" "strings" "syscall" "time" "go.cypherpunks.ru/recfile" "golang.org/x/sys/unix" ) const ( RedoDir = ".redo" LockSuffix = ".lock" DepSuffix = ".dep" TmpPrefix = ".redo." LogSuffix = ".log" ) func mkdirs(pth string) error { if _, err := os.Stat(pth); err == nil { return nil } return os.MkdirAll(pth, os.FileMode(0777)) } func tempsuffix() string { return strconv.FormatInt((time.Now().UnixNano()+int64(os.Getpid()))&0xFFFFFFFF, 16) } func tempfile(dir, prefix string) (*os.File, error) { // It respects umask, unlike ioutil.TempFile name := path.Join(dir, TmpPrefix+prefix+"."+tempsuffix()) return os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, os.FileMode(0666)) } func isModified(cwd, redoDir, tgt string) (bool, error) { fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix)) if err != nil { if os.IsNotExist(err) { return false, nil } return false, err } defer fdDep.Close() r := recfile.NewReader(fdDep) for { m, err := r.NextMap() if err != nil { if err == io.EOF { break } return false, err } if m["Target"] != tgt { continue } fd, err := os.Open(path.Join(cwd, tgt)) if err != nil { if os.IsNotExist(err) { return false, nil } return false, err } defer fd.Close() ourTs, err := fileCtime(fd) if err != nil { return false, err } if ourTs != m["Ctime"] { return true, nil } break } return false, nil } func runScript(tgt string, errs chan error) error { tgtOrig := tgt cwd, tgt := cwdAndTgt(tgt) redoDir := path.Join(cwd, RedoDir) errf := func(err error) error { return fmt.Errorf("%s: %s", tgtOrig, err) } if err := mkdirs(redoDir); err != nil { return errf(err) } // Acquire lock fdLock, err := os.OpenFile( path.Join(redoDir, "lock."+tgt), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.FileMode(0666), ) if err != nil { return errf(err) } lockRelease := func() { trace(CLock, "LOCK_UN: %s", fdLock.Name()) unix.Flock(int(fdLock.Fd()), unix.LOCK_UN) fdLock.Close() } trace(CLock, "LOCK_NB: %s", fdLock.Name()) if err = unix.Flock(int(fdLock.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil { if uintptr(err.(syscall.Errno)) != uintptr(unix.EWOULDBLOCK) { fdLock.Close() return errf(err) } trace(CDebug, "waiting: %s", tgtOrig) Jobs.Add(1) go func() { defer Jobs.Done() trace(CLock, "LOCK_EX: %s", fdLock.Name()) unix.Flock(int(fdLock.Fd()), unix.LOCK_EX) lockRelease() trace(CDebug, "waiting done: %s", tgtOrig) var builtNow bool fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix)) if err != nil { if os.IsNotExist(err) { err = fmt.Errorf("%s is not built", tgtOrig) } goto Finish } builtNow, _, err = isBuiltNow(fdDep) if err != nil { goto Finish } if !builtNow { err = fmt.Errorf("%s is not built", tgtOrig) } Finish: if err != nil { err = errf(err) } errs <- err }() return nil } // Check if target is not modified externally modified, err := isModified(cwd, redoDir, tgt) if err != nil { lockRelease() return errf(err) } if modified { trace(CWarn, "%s externally modified: not redoing", tgtOrig) lockRelease() Jobs.Add(1) go func() { errs <- nil Jobs.Done() }() return nil } // Start preparing dep. fdDep, err := tempfile(redoDir, tgt+DepSuffix) if err != nil { lockRelease() return errf(err) } cleanup := func() { lockRelease() fdDep.Close() os.Remove(fdDep.Name()) } if _, err = recfile.NewWriter(fdDep).WriteFields( recfile.Field{Name: "Build", Value: BuildUUID}, ); err != nil { cleanup() return errf(err) } // Find .do doFile, upLevels, err := findDo(fdDep, cwd, tgt) if err != nil { cleanup() return errf(err) } if doFile == "" { cleanup() return errf(errors.New("no .do found")) } if err = writeDep(fdDep, cwd, doFile); err != nil { cleanup() return errf(err) } // Determine basename and DIRPREFIX ents := strings.Split(cwd, "/") ents = ents[len(ents)-upLevels:] dirPrefix := path.Join(ents...) cwdOrig := cwd for i := 0; i < upLevels; i++ { cwd = path.Join(cwd, "..") } cwd = path.Clean(cwd) basename := tgt if strings.HasPrefix(doFile, "default.") { basename = tgt[:len(tgt)-(len(doFile)-len("default.")-len(".do"))-1] trace(CRedo, "%s (%s)", tgtOrig, doFile) } else { trace(CRedo, "%s", tgtOrig) } doFile = path.Base(doFile) // Prepare command line var cmdName string var args []string if err = unix.Access(path.Join(cwd, doFile), unix.X_OK); err == nil { // Ordinary executable file cmdName = doFile args = make([]string, 0, 3) } else { fd, err := os.Open(path.Join(cwd, doFile)) if err != nil { cleanup() return errf(err) } buf := make([]byte, 512) n, err := fd.Read(buf) if err != nil { cleanup() return errf(err) } if n > 3 && string(buf[:3]) == "#!/" { // Shebanged t := string(buf[2:n]) nlIdx := strings.Index(t, "\n") if nlIdx == -1 { cleanup() return errf(errors.New("not fully read shebang")) } args = strings.Split(t[:nlIdx], " ") cmdName, args = args[0], args[1:] } else { // Shell cmdName = "/bin/sh" if Trace { args = append(args, "-ex") } else { args = append(args, "-e") } } args = append(args, doFile) } // Temporary file for stdout fdStdout, err := tempfile(cwd, tgt) if err != nil { cleanup() return errf(err) } tmpPath := fdStdout.Name() + ".3" // and for $3 args = append(args, tgt, basename, path.Base(tmpPath)) cmd := exec.Command(cmdName, args...) cmd.Dir = cwd cmd.Stdout = fdStdout cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%d", RedoLevelEnv, Level+1)) cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", RedoDirPrefixEnv, dirPrefix)) cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", RedoBuildUUIDEnv, BuildUUID)) childStderrPrefix := tempsuffix() cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", RedoStderrPrefixEnv, childStderrPrefix)) cmd.ExtraFiles = append(cmd.ExtraFiles, fdDep) cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", RedoDepFdEnv, 3+0)) if JSR == nil { // infinite jobs cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", RedoJSRFdEnv)) cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", RedoJSWFdEnv)) } else { cmd.ExtraFiles = append(cmd.ExtraFiles, JSR) cmd.ExtraFiles = append(cmd.ExtraFiles, JSW) cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", RedoJSRFdEnv, 3+1)) cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", RedoJSWFdEnv, 3+2)) } // Preparing stderr stderr, err := cmd.StderrPipe() if err != nil { panic(err) } var fdStderr *os.File if StderrKeep { fdStderr, err = os.OpenFile( path.Join(redoDir, tgt+LogSuffix), os.O_WRONLY|os.O_CREATE, os.FileMode(0666), ) if err != nil { cleanup() return errf(err) } fdStderr.Truncate(0) } trace(CDebug, "sh: %s: %s %s [%s]", tgtOrig, cmdName, args, cwd) Jobs.Add(1) go func() { jsAcquire() started := time.Now() defer func() { jsRelease() lockRelease() fdDep.Close() fdStdout.Close() if fdStderr != nil { fdStderr.Close() } os.Remove(fdDep.Name()) os.Remove(fdStdout.Name()) os.Remove(tmpPath) os.Remove(fdLock.Name()) finished := time.Now() trace(CDone, "%s (%fsec)", tgtOrig, finished.Sub(started).Seconds()) Jobs.Done() }() err := cmd.Start() if err != nil { errs <- errf(err) return } pid := fmt.Sprintf("[%d]", cmd.Process.Pid) trace(CDebug, "%s runs %s", tgtOrig, pid) stderrTerm := make(chan struct{}, 0) go func() { scanner := bufio.NewScanner(stderr) var line string ts := new(TAI64N) for scanner.Scan() { line = scanner.Text() if strings.HasPrefix(line, childStderrPrefix) { line = line[len(childStderrPrefix):] os.Stderr.WriteString(StderrPrefix + line + "\n") continue } if fdStderr != nil { tai64nNow(ts) fmt.Fprintf(fdStderr, "@%s %s\n", hex.EncodeToString(ts[:]), line) } if StderrSilent { continue } if MyPid == 0 { trace(CNone, "%s", line) } else { trace(CNone, "%s %s", pid, line) } } close(stderrTerm) }() // Wait for job completion <-stderrTerm err = cmd.Wait() if err != nil { errs <- errf(err) return } // Does it produce both stdout and tmp? fiStdout, err := os.Stat(fdStdout.Name()) if err != nil { errs <- errf(err) return } tmpExists := false _, err = os.Stat(tmpPath) if err == nil { if fiStdout.Size() > 0 { errs <- errf(fmt.Errorf("%s created both tmp and stdout", tgtOrig)) return } tmpExists = true } else if !os.IsNotExist(err) { errs <- errf(err) return } // Determine what file we must process at last var fd *os.File if tmpExists { fd, err = os.Open(tmpPath) if err != nil { errs <- errf(err) return } defer fd.Close() } else if fiStdout.Size() > 0 { fd = fdStdout } // Do we need to ifcreate it, of ifchange with renaming? if fd == nil { if err = ifcreate(fdDep, tgt); err != nil { errs <- errf(err) return } } else { if !NoSync { if err = fd.Sync(); err != nil { errs <- errf(err) return } } if err = os.Rename(fd.Name(), path.Join(cwdOrig, tgt)); err != nil { errs <- errf(err) return } if !NoSync { fdDir, err := os.Open(cwdOrig) if err != nil { errs <- errf(err) return } defer fdDir.Close() if err = fdDir.Sync(); err != nil { errs <- errf(err) return } } if err = writeDep(fdDep, cwdOrig, tgt); err != nil { errs <- errf(err) return } } // Commit dep. if !NoSync { if err = fdDep.Sync(); err != nil { errs <- errf(err) return } } if err = os.Rename(fdDep.Name(), path.Join(redoDir, tgt+DepSuffix)); err != nil { errs <- errf(err) return } if !NoSync { fdDir, err := os.Open(redoDir) if err != nil { errs <- errf(err) return } defer fdDir.Close() if err = fdDir.Sync(); err != nil { errs <- errf(err) return } } errs <- nil }() return nil }