turbo-editors/turbo-corepublic Fork 0
d662cebdb65b319885da903daf7eff9ab1bfbb78
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

run.go · 199 lines · 5.3 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 21h ago1package tools
2
3import (
4 "bufio"
5 "errors"
6 "fmt"
7 "io"
8 "os/exec"
9 "sync"
10 "time"
11)
12
13// group is whatever a platform offers for stopping a command *and everything
14// it started*: a process group on Unix, a job object on Windows. It is what
15// Stop acts on, because killing the shell alone leaves its children running
16// and holding the output pipe.
17type group interface {
18 // kill ends the command and its descendants.
19 kill()
20 // release gives back whatever the group held, once the command has ended.
21 release()
22}
23
24// stopGrace is how long Wait keeps reading after the command has been killed.
25//
26// Killing a process group should close the output pipe at once, so this is the
27// backstop for anything that escaped it — a process that changed its own group,
28// or a platform with no groups at all. Without it, one stray grandchild holding
29// the pipe leaves the reading goroutine blocked for as long as it lives.
30const stopGrace = 2 * time.Second
31
32// maxLines is how much of a command's output is kept.
33//
34// A runaway command can print without end, and a dialog holding all of it would
35// grow until the editor did. Ten thousand lines is far past what anyone reads
36// and far short of what hurts; past it the oldest go, because the tail of a
37// failing build is the part that matters.
38const maxLines = 10000
39
40// Run is a command in flight, and then the record of one that has finished.
41//
42// Its output is read on a goroutine of its own, so everything the caller can
43// see is behind a lock.
44//
45// run, err := tools.Start("go test ./...", ".", wakeTheEventLoop)
46// if err != nil {
47// return err
48// }
49// if finished, code := run.Done(); finished {
50// fmt.Println(code, run.Lines())
51// }
52type Run struct {
53 command string
54
55 mu sync.Mutex
56 lines []string
57 dropped int
58 finished bool
59 exit int
60
61 process *exec.Cmd
62 group group
63 onLine func()
64}
65
66// Start runs a shell command in a directory and returns it in flight.
67//
68// Standard output and standard error are merged, in the order the command
69// wrote them, because a compiler's errors and its progress belong together.
70//
71// onLine is called whenever there is more to see and once when the command
72// ends. It runs on the reading goroutine, so it must only wake an event loop —
73// never touch what that loop owns. It is a parameter rather than a field
74// because this starts the goroutine that calls it, and a field assigned
75// afterwards would be a data race.
76func Start(command, dir string, onLine func()) (*Run, error) {
77 process := exec.Command(Shell(), ShellArgs(command)...)
78 process.Dir = dir
79 process.SysProcAttr = childAttributes(process, command)
80 process.WaitDelay = stopGrace
81
82 output, err := process.StdoutPipe()
83 if err != nil {
84 return nil, fmt.Errorf("running %s: %w", command, err)
85 }
86 process.Stderr = process.Stdout
87
88 if err := process.Start(); err != nil {
89 return nil, fmt.Errorf("running %s: %w", command, err)
90 }
91
92 r := &Run{command: command, process: process, group: newGroup(process.Process), onLine: onLine}
93 go r.read(output)
94 return r, nil
95}
96
97// Command returns the command line being run.
98func (r *Run) Command() string { return r.command }
99
100// read collects the output until the command has gone.
101func (r *Run) read(output io.Reader) {
102 scanner := bufio.NewScanner(output)
103 scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
104
105 for scanner.Scan() {
106 r.append(scanner.Text())
107 r.notify()
108 }
109 r.finish()
110}
111
112// append adds one line, dropping the oldest once there are too many.
113func (r *Run) append(line string) {
114 r.mu.Lock()
115 defer r.mu.Unlock()
116
117 if len(r.lines) >= maxLines {
118 r.lines = r.lines[1:]
119 r.dropped++
120 }
121 r.lines = append(r.lines, line)
122}
123
124// finish waits for the command and records how it went.
125//
126// Why it failed does not matter to the caller beyond the code: a command that
127// exited non-zero and one that could not be waited for both mean "this did not
128// work", and the output already says which.
129func (r *Run) finish() {
130 code := 0
131 if err := r.process.Wait(); err != nil {
132 var exit *exec.ExitError
133 if errors.As(err, &exit) {
134 code = exit.ExitCode()
135 } else {
136 code = -1
137 }
138 }
139
140 r.group.release()
141
142 r.mu.Lock()
143 r.finished, r.exit = true, code
144 r.mu.Unlock()
145
146 r.notify()
147}
148
149// notify tells the caller there is something new.
150func (r *Run) notify() {
151 if r.onLine != nil {
152 r.onLine()
153 }
154}
155
156// Lines returns what the command has printed so far, oldest first.
157//
158// The slice is a copy, so a caller can hold it while the command goes on
159// writing.
160func (r *Run) Lines() []string {
161 r.mu.Lock()
162 defer r.mu.Unlock()
163
164 out := make([]string, len(r.lines))
165 copy(out, r.lines)
166 return out
167}
168
169// Dropped returns how many lines were thrown away for being too old, which is
170// zero for any command anyone would run on purpose.
171func (r *Run) Dropped() int {
172 r.mu.Lock()
173 defer r.mu.Unlock()
174 return r.dropped
175}
176
177// Done reports whether the command has ended, and with what exit code.
178//
179// The code is meaningless until finished is true.
180//
181// if finished, code := run.Done(); finished && code != 0 {
182// // it failed
183// }
184func (r *Run) Done() (finished bool, exit int) {
185 r.mu.Lock()
186 defer r.mu.Unlock()
187 return r.finished, r.exit
188}
189
190// Stop kills the command, which is what closing a dialog while it is still
191// running does.
192//
193// A command that has already ended is left alone, so stopping twice is safe.
194func (r *Run) Stop() {
195 if finished, _ := r.Done(); finished {
196 return
197 }
198 r.group.kill()
199}