max / alloy
- Co-Authored-By
- Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 file changed,
+33 insertions,
-8 deletions
| @@ -95,12 +95,7 @@ | |||
| 95 | 95 | fn drain(&self, into: &mut Vec<String>) { | |
| 96 | 96 | loop { | |
| 97 | 97 | match self.lines.try_recv() { | |
| 98 | - | Ok(line) => { | |
| 99 | - | if into.len() == SCROLLBACK { | |
| 100 | - | into.remove(0); | |
| 101 | - | } | |
| 102 | - | into.push(line); | |
| 103 | - | } | |
| 98 | + | Ok(line) => push_line(into, line), | |
| 104 | 99 | // Disconnected means both readers are done, which is a fact | |
| 105 | 100 | // about the pipes rather than about the process: the child may | |
| 106 | 101 | // still be exiting. `finished` is what decides that. | |
| @@ -109,12 +104,39 @@ | |||
| 109 | 104 | } | |
| 110 | 105 | } | |
| 111 | 106 | ||
| 107 | + | /// Drain everything the command will ever produce, blocking until the | |
| 108 | + | /// readers are done. | |
| 109 | + | /// | |
| 110 | + | /// Only correct once the child has exited, and only safe then: reaping the | |
| 111 | + | /// child is not the same event as its output arriving. `try_wait` can reap | |
| 112 | + | /// a process whose last lines are still sitting in the pipe, unread by the | |
| 113 | + | /// pump threads, and a `try_recv` at that moment reports `Empty` and takes | |
| 114 | + | /// the lines to the grave with the `Running` that gets dropped next. | |
| 115 | + | /// | |
| 116 | + | /// Blocking to `Disconnected` waits for the one event that means all | |
| 117 | + | /// output has been forwarded: both pump threads reaching EOF and dropping | |
| 118 | + | /// their senders. The wait is bounded because the child is already dead, | |
| 119 | + | /// so both pipes are closed and EOF is already on its way. | |
| 120 | + | fn drain_to_end(&self, into: &mut Vec<String>) { | |
| 121 | + | while let Ok(line) = self.lines.recv() { | |
| 122 | + | push_line(into, line); | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | ||
| 112 | 126 | /// The exit status if the child is done, `None` while it runs. | |
| 113 | 127 | fn finished(&mut self) -> Option<std::io::Result<ExitStatus>> { | |
| 114 | 128 | self.child.try_wait().transpose() | |
| 115 | 129 | } | |
| 116 | 130 | } | |
| 117 | 131 | ||
| 132 | + | /// Append a line, holding the buffer to [`SCROLLBACK`]. | |
| 133 | + | fn push_line(into: &mut Vec<String>, line: String) { | |
| 134 | + | if into.len() == SCROLLBACK { | |
| 135 | + | into.remove(0); | |
| 136 | + | } | |
| 137 | + | into.push(line); | |
| 138 | + | } | |
| 139 | + | ||
| 118 | 140 | /// A queue of commands, run one at a time, with their output streamed. | |
| 119 | 141 | /// | |
| 120 | 142 | /// Owns the whole sequence rather than one command because the install is a | |
| @@ -199,10 +221,13 @@ | |||
| 199 | 221 | // The pipes can still hold output written just before exit, | |
| 200 | 222 | // so drain once more after reaping. Without this the last | |
| 201 | 223 | // lines of a failing command — the ones that say why — are | |
| 202 | - | // the ones that get lost. | |
| 224 | + | // the ones that get lost, and a resolver reading this | |
| 225 | + | // command's output parses whatever happened to arrive in | |
| 226 | + | // time. Blocking, because a non-blocking drain here is a | |
| 227 | + | // race against the pump threads that the pumps often lose. | |
| 203 | 228 | let mut lines = std::mem::take(&mut self.output); | |
| 204 | 229 | let mut finished = self.current.take().expect("current was Some"); | |
| 205 | - | finished.drain(&mut lines); | |
| 230 | + | finished.drain_to_end(&mut lines); | |
| 206 | 231 | self.output = lines; | |
| 207 | 232 | self.done_count += 1; | |
| 208 | 233 |