namespace ScadaLink.CLI.Commands; /// /// Pure, testable helpers for the debug stream command. The SignalR-driven /// 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. /// internal static class DebugStreamHelpers { /// /// The maximum time waits for an in-flight /// TrySetResult (from OnStreamTerminated/Closed) to land after /// the wait was cancelled by Ctrl+C, so a termination racing with cancellation is /// observed deterministically rather than depending on scheduling. /// internal static readonly TimeSpan ExitGracePeriod = TimeSpan.FromMilliseconds(250); /// Outcome of classifying an exception thrown while connecting. internal readonly record struct ConnectFailure(bool IsCancellation, int ExitCode); /// /// Classifies an exception thrown by HubConnection.StartAsync. 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. /// 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); } /// /// Resolves the debug stream exit code from a single authoritative source — /// the exitTcs task. If a result was set by OnStreamTerminated or the /// Closed 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. /// internal static async Task ResolveStreamExitCodeAsync(Task 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; } }