package sync import ( "fmt" "os" "os/exec" "syscall" "time" "git.csclub.uwaterloo.ca/public/merlin/config" ) // SpawnProcess spawns a child process for the given repo. The process will // be stopped early if the repo receives a stop signal, or if the process // runs for longer than the repo's MaxTime. // It returns a channel through which a Cmd will be sent once it has finished, // or nil if it was unable to start a process. func spawnProcess(repo *config.Repo, args []string) (ch <-chan *exec.Cmd) { if len(args) == 0 { repo.Logger.Error("command given is of zero length") return } repo.Logger.Debug(fmt.Sprintf("Running the command: %v", args)) if repo.DryRun { repo.Logger.Debug("Dry running for 50 seconds") args = []string{"sleep", "50"} } cmd := exec.Command(args[0], args[1:]...) repo.Logger.Debug("Starting process") if err := cmd.Start(); err != nil { repo.Logger.Error(fmt.Errorf("could not start process for %s: %w", repo.Name, err).Error()) return } cmdChan := make(chan *exec.Cmd) ch = cmdChan cmdDoneChan := make(chan struct{}) killProcess := func() { err := cmd.Process.Signal(syscall.SIGTERM) if err != nil { repo.Logger.Error("Could not send signal to process:", err) return } select { case <-time.After(30 * time.Second): repo.Logger.Warning("Process still hasn't stopped after 30 seconds; sending SIGKILL") cmd.Process.Signal(syscall.SIGKILL) case <-cmdDoneChan: repo.Logger.Debug("Process has been stopped.") } } go func() { cmd.Wait() close(cmdDoneChan) }() go func() { defer func() { cmdChan <- cmd }() select { case <-cmdDoneChan: if !cmd.ProcessState.Success() { repo.Logger.Warning("Process ended with status code", cmd.ProcessState.ExitCode()) out, _ := cmd.CombinedOutput() repo.Logger.Debug(string(out)) } case <-repo.StopChan: repo.Logger.Debug("Received signal to stop, killing process...") killProcess() case <-time.After(time.Duration(repo.MaxTime) * time.Second): repo.Logger.Warning("Process has exceeded its max time; killing now") killProcess() } }() return } // spawns a process and waits for it complete before parsing the exit code and returning it func spawnProcessAndWait(repo *config.Repo, args []string) (status int) { status = config.FAILURE ch := spawnProcess(repo, args) if ch == nil { // spawnProcess will have already logged error return } cmd := <-ch switch cmd.ProcessState.ExitCode() { case 0: status = config.SUCCESS case -1: status = config.TERMINATED // default is already FAILURE } return } // returns true iff file contents are the same (for diff file contents, errors, empty return false) func diffFileContent(repo *config.Repo, file1, file2 string) bool { readFile := func(file string) string { f, err := os.ReadFile(file) if err != nil { repo.Logger.Debug("Error while trying to read file: " + file) return "" } return string(f) } content1 := readFile(file1) content2 := readFile(file2) if content1 == "" || content2 == "" { return false } return content1 == content2 } // returns true iff file times are the same (for diff file times and errors return false) func diffFileTime(repo *config.Repo, file1, file2 string) bool { statFile := func(file string) int64 { f, err := os.Stat(file) if err != nil { repo.Logger.Debug("Error while trying to stat file: " + file) return 0 } return f.ModTime().Unix() } time1 := statFile(file1) time2 := statFile(file2) if time1 == 0 || time2 == 0 { return false } return file1 == file2 }