fix(cli): resolve CLI-008..013 — format validation, exit-code semantics, debug-stream cancellation/disposal, test coverage
This commit is contained in:
54
src/ScadaLink.CLI/Commands/DebugStreamHelpers.cs
Normal file
54
src/ScadaLink.CLI/Commands/DebugStreamHelpers.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace ScadaLink.CLI.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Pure, testable helpers for the <c>debug stream</c> command. The SignalR-driven
|
||||
/// <see cref="DebugCommands"/> body itself cannot be unit-tested without a live hub, so
|
||||
/// the decision logic — connect-failure classification (CLI-010) and exit-code
|
||||
/// resolution after stream termination (CLI-012) — is extracted here.
|
||||
/// </summary>
|
||||
internal static class DebugStreamHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum time <see cref="ResolveStreamExitCodeAsync"/> waits for an in-flight
|
||||
/// <c>TrySetResult</c> (from <c>OnStreamTerminated</c>/<c>Closed</c>) to land after
|
||||
/// the wait was cancelled by Ctrl+C, so a termination racing with cancellation is
|
||||
/// observed deterministically rather than depending on scheduling.
|
||||
/// </summary>
|
||||
internal static readonly TimeSpan ExitGracePeriod = TimeSpan.FromMilliseconds(250);
|
||||
|
||||
/// <summary>Outcome of classifying an exception thrown while connecting.</summary>
|
||||
internal readonly record struct ConnectFailure(bool IsCancellation, int ExitCode);
|
||||
|
||||
/// <summary>
|
||||
/// Classifies an exception thrown by <c>HubConnection.StartAsync</c>. A
|
||||
/// cancellation exception that coincides with a user-requested cancellation
|
||||
/// (Ctrl+C during connect) is a graceful shutdown — exit 0, no error printed.
|
||||
/// Anything else is a genuine connection failure — exit 1.
|
||||
/// </summary>
|
||||
internal static ConnectFailure ClassifyConnectFailure(Exception ex, bool cancellationRequested)
|
||||
{
|
||||
if (cancellationRequested && ex is OperationCanceledException)
|
||||
return new ConnectFailure(IsCancellation: true, ExitCode: 0);
|
||||
|
||||
return new ConnectFailure(IsCancellation: false, ExitCode: 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the <c>debug stream</c> exit code from a single authoritative source —
|
||||
/// the <c>exitTcs</c> task. If a result was set by <c>OnStreamTerminated</c> or the
|
||||
/// <c>Closed</c> handler it is always preferred (even when Ctrl+C also fired);
|
||||
/// a brief grace period covers a termination that races with cancellation. If no
|
||||
/// result is ever produced (pure Ctrl+C), the stream ended gracefully — exit 0.
|
||||
/// </summary>
|
||||
internal static async Task<int> ResolveStreamExitCodeAsync(Task<int> exitTask)
|
||||
{
|
||||
if (exitTask.IsCompletedSuccessfully)
|
||||
return exitTask.Result;
|
||||
|
||||
var completed = await Task.WhenAny(exitTask, Task.Delay(ExitGracePeriod));
|
||||
if (ReferenceEquals(completed, exitTask) && exitTask.IsCompletedSuccessfully)
|
||||
return exitTask.Result;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user