Skip to content

[CollectLinux] Fix no-tty crash with console capability aware ProgressWriter#5771

Open
mdh1418 wants to merge 1 commit intodotnet:mainfrom
mdh1418:fix/collect-linux-cursor-position-crash
Open

[CollectLinux] Fix no-tty crash with console capability aware ProgressWriter#5771
mdh1418 wants to merge 1 commit intodotnet:mainfrom
mdh1418:fix/collect-linux-cursor-position-crash

Conversation

@mdh1418
Copy link
Member

@mdh1418 mdh1418 commented Mar 19, 2026

When CursorTop is 0 (e.g., in environments where cursor positioning isn't available), LineToClear was set to -1, causing SetCursorPosition to throw ArgumentOutOfRangeException on progress callbacks.

Extract the progress display concern from OutputHandler into a nested ProgressWriter class that:

  • Probes console capability once at construction
  • Interactive: rewrites status line in-place with 1s throttle
  • Non-interactive: prints a static message once, silently no-ops after
  • Owns LineRewriter and LineToClear internally

OutputHandler's status block reduces to progressWriter.Update(), fully decoupling it from LineRewriter. Matches CollectCommand's pattern of probing capability upfront and gating on it.

Also adds bounds validation to MockConsole.SetCursorPosition and two regression tests for the CursorTop=0 case.

…riter

When CursorTop is 0 (e.g., in environments where cursor positioning
isn't available), LineToClear was set to -1, causing SetCursorPosition
to throw ArgumentOutOfRangeException on progress callbacks.

Extract the progress display concern from OutputHandler into a nested
ProgressWriter class that:
- Probes console capability once at construction
- Interactive: rewrites status line in-place with 1s throttle
- Non-interactive: prints a static message once, silently no-ops after
- Owns LineRewriter and LineToClear internally

OutputHandler's status block reduces to progressWriter.Update(), fully
decoupling it from LineRewriter. Matches CollectCommand's pattern of
probing capability upfront and gating on it.

Also adds bounds validation to MockConsole.SetCursorPosition and two
regression tests for the CursorTop=0 case.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mdh1418 mdh1418 requested a review from a team as a code owner March 19, 2026 16:34
Copilot AI review requested due to automatic review settings March 19, 2026 16:34
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a crash in dotnet-trace collect-linux progress callbacks in environments where console cursor positioning isn’t available (or behaves unexpectedly), by centralizing progress output behavior behind a capability-aware writer.

Changes:

  • Refactors progress/status rendering in CollectLinuxCommand into a nested ProgressWriter that gates rewrites and throttles updates.
  • Adds functional regression tests intended to cover CursorTop == 0 / unsupported cursor repositioning scenarios.
  • Tightens MockConsole.SetCursorPosition by adding row bounds validation.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs Introduces ProgressWriter to manage progress output and console rewrite capability checks.
src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs Adds regression tests for cursor-top/cursor-repositioning edge cases.
src/tests/Common/MockConsole.cs Adds bounds checking to better surface invalid cursor positioning in tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +604 to +612
// Only capture the cursor position to rewrite once we've committed to writing progress output,
// otherwise the position becomes stale the moment any other console output occurs.
LineRewriter rewriter = new(_console);
rewriter.LineToClear = _console.CursorTop - 1;

if (rewriter.IsRewriteConsoleLineSupported)
{
_rewriter = rewriter;
}

_nextUpdateTimestamp = now + Stopwatch.Frequency;
_console.Out.WriteLine($"[{_stopwatch.Elapsed:dd\\:hh\\:mm\\:ss}]\tRecording trace.");
_console.Out.WriteLine("Press <Enter> or <Ctrl-C> to exit...");
Comment on lines +329 to +371
[ConditionalFact(nameof(IsCollectLinuxSupported))]
public void CollectLinuxCommand_DoesNotCrash_WhenCursorTopIsZero()
{
// Regression test: when CursorTop is 0 (e.g., no TTY), LineToClear = CursorTop - 1 = -1,
// which caused SetCursorPosition to throw ArgumentOutOfRangeException on the second
// progress callback when RewriteConsoleLine was called with the negative LineToClear.
MockConsole console = new(200, 30, _outputHelper);

var handler = new CollectLinuxCommandHandler(console);
handler.RecordTraceInvoker = (cmd, len, cb) => {
// Must send multiple callbacks — the crash occurred on the second one.
cb(3, IntPtr.Zero, UIntPtr.Zero);
cb(3, IntPtr.Zero, UIntPtr.Zero);
return 0;
};

int exitCode = handler.CollectLinux(TestArgs());
Assert.Equal((int)ReturnCode.Ok, exitCode);
}

[ConditionalFact(nameof(IsCollectLinuxSupported))]
public void CollectLinuxCommand_PrintsStatusOnce_WhenCursorRepositioningUnsupported()
{
// When cursor repositioning isn't supported, the status line should be
// printed exactly once — not spammed every second.
MockConsole console = new(200, 30, _outputHelper);

var handler = new CollectLinuxCommandHandler(console);
handler.RecordTraceInvoker = (cmd, len, cb) => {
for (int i = 0; i < 5; i++)
{
cb(3, IntPtr.Zero, UIntPtr.Zero);
}
return 0;
};

int exitCode = handler.CollectLinux(TestArgs());
Assert.Equal((int)ReturnCode.Ok, exitCode);

string[] lines = console.Lines;
int statusLineCount = lines.Count(l => l.Contains("Recording trace", StringComparison.OrdinalIgnoreCase));
Assert.Equal(1, statusLineCount);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(if we need control over timing we should use a TimeProvider rather than sleeps)

Comment on lines 72 to 82
public void SetCursorPosition(int col, int row)
{
if (row < 0 || row >= WindowHeight)
{
throw new ArgumentOutOfRangeException(nameof(row),
row,
"The value must be greater than or equal to zero and less than the console's buffer size in that dimension.");
}
CursorTop = row;
_cursorLeft = col;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants