Skip to content

LineOutputHandler never delivers a final line that has no trailing newline, and leaks it into the next run #88

Description

@matt-edmondson

What's wrong

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:

  1. Last line lost. If a command's final line has no line terminator, that line never reaches the callback.
  2. 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

var lines = new List<string>();
var handler = new LineOutputHandler(l => lines.Add(l));
await RunCommand.ExecuteAsync("printf", ["a\\nlast"], handler);
// lines == ["a"]  — "last" is never delivered

await RunCommand.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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions