You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
LineOutputHandler never delivers a final line that has no trailing newline, and leaks it into the next run #88
LineOutputHandler.ProcessDataByLine (RunCommand/LineOutputHandler.cs, ~lines 46-57) adds each chunk to outputBuffer/errorBuffer and only calls the callback for text that ends in a line break. Nothing flushes those buffers when the process ends. AsyncProcessStreamReader returns and RunAsync completes, but the handler is never told the stream has ended.
This causes two problems:
Last line lost. If a command's final line has no line terminator, that line never reaches the callback.
Leak across runs. The buffers are instance fields. If the same LineOutputHandler is reused for another command, the text left over from the first run is glued onto the start of the next run's first line.
Repro
varlines=newList<string>();varhandler=newLineOutputHandler(l =>lines.Add(l));awaitRunCommand.ExecuteAsync("printf",["a\\nlast"],handler);// lines == ["a"] — "last" is never deliveredawaitRunCommand.ExecuteAsync("printf",["next\\n"],handler);// lines == ["a", "lastnext"]
I confirmed this on Linux/net10.0 against the current main. PR #87 changes how the streams are read but does not touch this, so it still happens with #87 applied.
Why it matters
Many tools print their last line, often the result, without a newline. Examples are printf-based scripts, git rev-parse --abbrev-ref HEAD when it is wrapped by some shells, prompts and progress lines, and many Windows tools. A caller that uses LineOutputHandler to collect a result gets nothing and sees no error. The existing test helper WriteTempFileInDirectoryWithSpaces in RunCommandTests.cs already adds a newline on purpose to get around this ("would sit unflushed in its buffer"). The README describes partial lines as held "until the rest of the line arrives", but once the process has ended, the rest never arrives.
Suggested fix
Add an internal completion hook to OutputHandler, for example internal virtual void Complete(), that does nothing by default.
Have RunAsync call it after the stream reader has finished draining, but not on the cancellation or abandon path.
In LineOutputHandler, override it to send any non-empty outputBuffer/errorBuffer to its callback, then clear both buffers.
Acceptance criteria
printf 'a\nlast' through LineOutputHandler delivers ["a", "last"].
Reusing one handler for two runs does not carry text from the first into the second.
An empty trailing buffer, where output ends exactly on a newline, does not emit an extra empty line.
Priority: Medium. Output is lost without any error. A caller that uses LineOutputHandler to collect a command's result gets nothing when the last line has no terminator, and a reused handler corrupts the first line of its next run. It is not High because it only affects the final unterminated line and handler reuse, and callers can work around it with a different OutputHandler.
Area / suggested assignee:LineOutputHandler buffering, plus the OutputHandler ↔ RunAsync completion contract. Maintainer: @matt-edmondson.
Next steps: Add the internal Complete() hook as the issue suggests, and call it only on the normal completion path. If #86 introduces a pending-CR state, Complete() must also resolve that, so a trailing lone \r flushes as a line ending rather than as text. Cover all three acceptance criteria with tests, including the "ends exactly on a newline emits nothing extra" case. The workaround comment in WriteTempFileInDirectoryWithSpaces can then be removed.
Activity
matt-edmondson commentedon Sep 26, 2026
Triage
LineOutputHandlerto collect a command's result gets nothing when the last line has no terminator, and a reused handler corrupts the first line of its next run. It is not High because it only affects the final unterminated line and handler reuse, and callers can work around it with a differentOutputHandler.LineOutputHandlerbuffering, plus theOutputHandler↔RunAsynccompletion contract. Maintainer: @matt-edmondson.LineOutputHandler's buffer handling (fixes LineOutputHandler emits a spurious empty line when a CRLF line ending is split across two reads #85).RunAsyncdrains the streams. The issue confirms this one does not fix the bug, but the newComplete()call belongs right after Read each output stream to end of stream, not just until exit #87's drain.Next steps: Add the internal
Complete()hook as the issue suggests, and call it only on the normal completion path. If #86 introduces a pending-CR state,Complete()must also resolve that, so a trailing lone\rflushes as a line ending rather than as text. Cover all three acceptance criteria with tests, including the "ends exactly on a newline emits nothing extra" case. The workaround comment inWriteTempFileInDirectoryWithSpacescan then be removed.Generated by Claude Code