package main // Rebuilding and refreshing, and the difference between them. // // r read the data again in this panel. Nothing is closed, nothing moves. // R rebuild every workspace and pane from the project file: close what the // layout owns, then build it again in order. // // A rebuild must survive the panel that started it, because that panel is one of // the things being replaced. So the rebuild runs detached, in its own session, // with its output in a log the Repair workspace can show. import ( "fmt" "os" "os/exec" "path/filepath" "syscall" "time" ) func rebuildLogPath() string { return filepath.Join(os.TempDir(), "paperclip-harness-rebuild.log") } // startRebuild launches bin/board --replace for this folder and returns at once. func startRebuild(folder string) error { board := filepath.Join(exeDir(), "board") if _, err := os.Stat(board); err != nil { return fmt.Errorf("no bin/board next to the program (%s)", board) } log, err := os.Create(rebuildLogPath()) if err != nil { return err } fmt.Fprintf(log, "rebuild started %s\n", time.Now().Format("2006-01-02 15:04:05")) cmd := exec.Command(board, "--folder", folder, "--replace") cmd.Stdout = log cmd.Stderr = log cmd.Stdin = nil // Its own session, so closing this pane does not take the rebuild with it. cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} if err := cmd.Start(); err != nil { log.Close() return err } go func() { cmd.Wait() fmt.Fprintf(log, "rebuild finished %s\n", time.Now().Format("15:04:05")) log.Close() }() return nil } func exeDir() string { p, err := os.Executable() if err != nil { return "." } return filepath.Dir(p) } // rebuildLogTail returns the end of the last rebuild, for the Repair workspace. func rebuildLogTail(lines int) []string { b, err := os.ReadFile(rebuildLogPath()) if err != nil { return []string{"", " no rebuild has been run since this machine started"} } all := splitLines(string(b)) start := max(0, len(all)-lines) return all[start:] } func splitLines(s string) []string { var out []string cur := "" for _, r := range s { if r == '\n' { out = append(out, cur) cur = "" continue } cur += string(r) } if cur != "" { out = append(out, cur) } return out } // mergeLogPath is where a merge writes what it did, so a panel can show it // without the person opening a shell or remembering a command. func mergeLogPath() string { return filepath.Join(os.TempDir(), "paperclip-harness-merge.log") } // startMerge runs bin/merge for one task, detached, and returns at once. The // Work workspace tails the log; the merge itself takes a lock, so starting one // twice by accident is refused rather than doubled. func startMerge(folder, identifier string) error { merge := filepath.Join(exeDir(), "merge") if _, err := os.Stat(merge); err != nil { return fmt.Errorf("no bin/merge next to the program (%s)", merge) } log, err := os.Create(mergeLogPath()) if err != nil { return err } fmt.Fprintf(log, "merge of %s started %s\n", identifier, time.Now().Format("15:04:05")) cmd := exec.Command(merge, identifier) cmd.Dir = folder cmd.Stdout = log cmd.Stderr = log cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} if err := cmd.Start(); err != nil { log.Close() return err } go func() { err := cmd.Wait() if err != nil { fmt.Fprintf(log, "\nmerge stopped: %v\n", err) } else { fmt.Fprintf(log, "\nmerge finished %s\n", time.Now().Format("15:04:05")) } log.Close() }() return nil } // mergeLogTail returns the end of the last merge, for the panel that shows it. func mergeLogTail(lines int) []string { b, err := os.ReadFile(mergeLogPath()) if err != nil { return []string{"", " no merge has been run from this screen yet"} } all := splitLines(string(b)) start := max(0, len(all)-lines) return all[start:] } // reviewLogPath is where a review started from the interface writes its output. func reviewLogPath() string { return filepath.Join(os.TempDir(), "paperclip-harness-review.log") } // startReview runs bin/review for one task and returns at once. The Review tab // follows the log, so a review takes no keypress more than starting it. func startReview(folder, identifier string) error { review := filepath.Join(exeDir(), "review") if _, err := os.Stat(review); err != nil { return fmt.Errorf("no bin/review next to the program (%s)", review) } log, err := os.Create(reviewLogPath()) if err != nil { return err } fmt.Fprintf(log, "review of %s started %s\n", identifier, time.Now().Format("15:04:05")) cmd := exec.Command(review, identifier, "--post") cmd.Dir = folder cmd.Stdout = log cmd.Stderr = log cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} if err := cmd.Start(); err != nil { log.Close() return err } go func() { err := cmd.Wait() if err != nil { fmt.Fprintf(log, "\nreview stopped: %v\n", err) } else { fmt.Fprintf(log, "\nreview finished %s\n", time.Now().Format("15:04:05")) } log.Close() }() return nil } func reviewLogTail(lines int) []string { b, err := os.ReadFile(reviewLogPath()) if err != nil { return []string{"", " no review has been run from this screen yet", "", " press v to review this task"} } all := splitLines(string(b)) start := max(0, len(all)-lines) return all[start:] }