using System.Diagnostics; using System.Text.Json; using MBN_STOCK_WEBVIEW.Core.Playout; using MBN_STOCK_WEBVIEW.Playout; using MBN_STOCK_WEBVIEW.Playout.Configuration; using MBN_STOCK_WEBVIEW.Playout.Diagnostics; using MBN_STOCK_WEBVIEW.Playout.Registration; using MBN_STOCK_WEBVIEW.PlayoutSmoke; var parsed = SmokeCommandLine.Parse(args); if (!parsed.IsValid) { return Usage(parsed.ErrorCode); } using var cancellation = new CancellationTokenSource(); Console.CancelKeyPress += (_, eventArgs) => { eventArgs.Cancel = true; cancellation.Cancel(); }; return parsed.Invocation.Kind switch { SmokeCommandKind.Probe => Probe(), SmokeCommandKind.DryRun => await DryRunAsync(), SmokeCommandKind.TestPlan => TestPlan(parsed.Invocation), SmokeCommandKind.TestConnect or SmokeCommandKind.TestSequence => await RunIsolatedTestCommandAsync(parsed.Invocation, cancellation.Token), SmokeCommandKind.PgmConnectDiagnostic => await RunPgmConnectDiagnosticAsync(parsed.Invocation, cancellation.Token), SmokeCommandKind.PgmCutsSequence => await RunPgmCutsSequenceAsync(parsed.Invocation, cancellation.Token), _ => Usage("unknown-command") }; static int Probe() { var runtimeRegistration = new K3dRegistrationProbe().Probe(); var processInspectionAvailable = true; var tornadoProcessCount = 0; var programWindowDetected = false; Process[] processes; try { processes = Process.GetProcesses(); } catch { processes = []; processInspectionAvailable = false; } foreach (var process in processes) { try { if (!process.ProcessName.StartsWith("Tornado2", StringComparison.OrdinalIgnoreCase)) { continue; } tornadoProcessCount++; var title = process.MainWindowTitle; if (SmokeCommandLine.IsUnsafeTornadoWindowTitle(title)) { programWindowDetected = true; } } catch { // A process may exit or deny inspection between enumeration and reading. } finally { process.Dispose(); } } WriteJson(new { command = "probe", architecture = Environment.Is64BitProcess ? "x64" : "x86", connectRequestIssued = false, comActivationAttempted = false, lastKtapConnectState = "not-attempted", ktapConnectAttempted = false, ktapConnectAccepted = false, ktapHelloObserved = (bool?)null, networkMonitoringRecordExpected = false, networkMonitoringCheckRequired = false, networkMonitoringVerified = (bool?)null, runtimeRegistration = new { ready = runtimeRegistration.IsReady, issues = runtimeRegistration.Issues.ToString(), engineClassReady = runtimeRegistration.IsEngineClassReady, eventHandlerClassReady = runtimeRegistration.IsEventHandlerClassReady, message = runtimeRegistration.Message }, tornado = new { inspectionAvailable = processInspectionAvailable, processCount = tornadoProcessCount, programWindowDetected }, safety = programWindowDetected ? "PROGRAM or ambiguous Tornado window detected; no COM activation was attempted." : "Read-only probe complete; no COM activation was attempted." }); return runtimeRegistration.IsReady && Environment.Is64BitProcess ? 0 : 2; } static async Task DryRunAsync() { var options = new PlayoutOptions { Mode = PlayoutMode.DryRun }; await using var engine = PlayoutEngineFactory.Create(options); var cue = new PlayoutCue("DRY_RUN_ONLY.t2s", "DRY_RUN_ONLY"); var results = new[] { await engine.ConnectAsync(), await engine.PrepareAsync(cue), await engine.TakeInAsync(), await engine.NextAsync(cue with { SceneName = "DRY_RUN_NEXT" }), await engine.TakeOutAsync(PlayoutTakeOutScope.All), await engine.DisconnectAsync() }; WriteJson(new { command = "dry-run", mode = engine.Status.Mode.ToString(), state = engine.Status.State.ToString(), connectRequestIssued = true, comActivationAttempted = false, lastKtapConnectState = "not-attempted", ktapConnectAttempted = false, ktapConnectAccepted = false, ktapHelloObserved = (bool?)null, networkMonitoringRecordExpected = false, networkMonitoringCheckRequired = false, networkMonitoringVerified = (bool?)null, commands = results.Select(result => new { operation = result.Operation.ToString(), code = result.Code.ToString(), message = result.Message }) }); return results.All(result => result.IsSuccess) ? 0 : 3; } static int TestPlan(SmokeCommandInvocation invocation) { try { var plan = IsolatedTestCommandPreflight.Create(invocation); WriteJson(new { command = "test-plan", mode = "Test", preflightValidated = true, runtimeProcessGateChecked = false, connectRequestIssued = false, comActivationAttempted = false, lastKtapConnectState = "not-attempted", ktapConnectAttempted = false, ktapConnectAccepted = false, ktapHelloObserved = (bool?)null, networkMonitoringRecordExpected = false, networkMonitoringCheckRequired = false, networkMonitoringVerified = (bool?)null, sceneCount = plan.PrepareCue is null ? 0 : 2, automaticReconnectEnabled = plan.Options.ReconnectEnabled, safety = "Configuration and scene assets validated only; no engine or COM was created." }); return 0; } catch (IsolatedTestPreflightException exception) { WritePreflightFailure("test-plan", exception.ErrorCode); return 65; } catch { WritePreflightFailure("test-plan", "internal-preflight-failure"); return 70; } } static async Task RunIsolatedTestCommandAsync( SmokeCommandInvocation invocation, CancellationToken cancellationToken) { var command = invocation.Kind == SmokeCommandKind.TestConnect ? "test-connect" : "test-sequence"; IsolatedTestCommandPlan plan; try { plan = IsolatedTestCommandPreflight.Create(invocation); } catch (IsolatedTestPreflightException exception) { WritePreflightFailure(command, exception.ErrorCode); return 65; } catch { WritePreflightFailure(command, "internal-preflight-failure"); return 70; } var report = await IsolatedTestCommandExecutor.ExecuteAsync(plan, cancellationToken); WriteJson(report); if (report.Completed && !report.OutcomeUnknown) { return 0; } return report.OutcomeUnknown ? 5 : 4; } static async Task RunPgmConnectDiagnosticAsync( SmokeCommandInvocation invocation, CancellationToken cancellationToken) { if (IsolatedTestCommandPreflight.HasPlayoutEnvironmentOverride( Environment.GetEnvironmentVariables())) { WritePgmDiagnosticFailure("environment-override-present"); return 65; } var request = new PgmConnectDiagnosticRequest( invocation.Host ?? string.Empty, invocation.Port ?? 0, invocation.ExpectedPgmWindowTitle ?? string.Empty); PgmConnectDiagnosticReport report; try { report = await new PgmConnectDiagnosticRunner() .ExecuteAsync(request, cancellationToken) .ConfigureAwait(false); } catch { WritePgmDiagnosticFailure("internal-diagnostic-failure"); return 70; } WriteJson(new { command = "pgm-connect-diagnostic", report.RequestValidated, report.ProcessGatePassed, report.ListenerOwnershipConfirmed, report.RegistrationReady, report.ConnectRequestIssued, report.ComActivationAttempted, lastKtapConnectState = ToWireValue(report.LastKtapConnectState), report.KtapConnectAttempted, report.KtapConnectAccepted, report.KtapHelloObserved, report.NetworkMonitoringRecordExpected, report.NetworkMonitoringCheckRequired, report.NetworkMonitoringVerified, report.DisconnectAttempted, report.DisconnectCode, report.Completed, report.OutcomeUnknown, report.RenderCommandSurfaceExposed, report.RenderCommandAttempted, report.AutomaticReconnectEnabled, report.ErrorCode }); if (report.Completed && !report.OutcomeUnknown) { return 0; } return report.OutcomeUnknown ? 5 : 4; } static async Task RunPgmCutsSequenceAsync( SmokeCommandInvocation invocation, CancellationToken cancellationToken) { if (IsolatedTestCommandPreflight.HasPlayoutEnvironmentOverride( Environment.GetEnvironmentVariables())) { WritePgmCutsSequenceFailure("environment-override-present"); return 65; } var request = new PgmCutsSequenceRequest( invocation.Host ?? string.Empty, invocation.Port ?? 0, invocation.ExpectedPgmWindowTitle ?? string.Empty, invocation.SceneRoot ?? string.Empty, invocation.ObservationMilliseconds ?? 0); PgmCutsSequenceReport report; try { report = await new PgmCutsSequenceRunner() .ExecuteAsync( request, stage => { WriteProgressJson(new { @event = "observation-start", stage = ToObservationWireValue(stage) }); }, cancellationToken) .ConfigureAwait(false); } catch { WritePgmCutsSequenceFailure("internal-sequence-failure"); return 70; } WriteJson(new { command = "pgm-cuts-sequence", report.RequestValidated, report.HashPinsPresent, report.TargetGatePassed, report.EngineCreated, report.RenderCommandAttempted, report.DisconnectAttempted, report.QuarantineAttempted, report.QuarantineCompleted, report.Completed, report.OutcomeUnknown, commands = report.Commands.Select(command => new { stage = ToSequenceStageWireValue(command.Stage), code = command.Code.ToString() }), report.ErrorCode }); if (report.Completed && !report.OutcomeUnknown) { return 0; } return report.OutcomeUnknown ? 5 : 4; } static void WritePgmDiagnosticFailure(string errorCode) => WriteJson(new { command = "pgm-connect-diagnostic", requestValidated = false, processGatePassed = false, listenerOwnershipConfirmed = false, registrationReady = false, connectRequestIssued = false, comActivationAttempted = false, lastKtapConnectState = "not-attempted", ktapConnectAttempted = false, ktapConnectAccepted = false, ktapHelloObserved = (bool?)null, networkMonitoringRecordExpected = false, networkMonitoringCheckRequired = false, networkMonitoringVerified = (bool?)null, disconnectAttempted = false, disconnectCode = "not-attempted", completed = false, outcomeUnknown = false, renderCommandSurfaceExposed = false, renderCommandAttempted = false, automaticReconnectEnabled = false, errorCode }); static void WritePgmCutsSequenceFailure(string errorCode) => WriteJson(new { command = "pgm-cuts-sequence", requestValidated = false, hashPinsPresent = false, targetGatePassed = false, engineCreated = false, renderCommandAttempted = false, disconnectAttempted = false, quarantineAttempted = false, quarantineCompleted = false, completed = false, outcomeUnknown = false, commands = Array.Empty(), errorCode }); static string ToWireValue(PlayoutKtapConnectState state) => state switch { PlayoutKtapConnectState.NotAttempted => "not-attempted", PlayoutKtapConnectState.Attempted => "attempted", PlayoutKtapConnectState.AcceptedUnconfirmed => "accepted-unconfirmed", PlayoutKtapConnectState.Failed => "failed", _ => "not-attempted" }; static string ToObservationWireValue(PgmCutsObservationStage stage) => stage switch { PgmCutsObservationStage.First => "first", PgmCutsObservationStage.Next => "next", PgmCutsObservationStage.TakeOut => "take-out", _ => "unknown" }; static string ToSequenceStageWireValue(PgmCutsSequenceStage stage) => stage switch { PgmCutsSequenceStage.Connect => "connect", PgmCutsSequenceStage.PrepareFirst => "prepare-first", PgmCutsSequenceStage.TakeIn => "take-in", PgmCutsSequenceStage.Next => "next", PgmCutsSequenceStage.TakeOut => "take-out", PgmCutsSequenceStage.Disconnect => "disconnect", _ => "unknown" }; static void WritePreflightFailure(string command, string errorCode) => WriteJson(new { command, mode = "Test", preflightValidated = false, runtimeProcessGateChecked = false, connectRequestIssued = false, comActivationAttempted = false, lastKtapConnectState = "not-attempted", ktapConnectAttempted = false, ktapConnectAccepted = false, ktapHelloObserved = (bool?)null, networkMonitoringRecordExpected = false, networkMonitoringCheckRequired = false, networkMonitoringVerified = (bool?)null, errorCode, safety = "Rejected before engine creation; no COM activation was attempted." }); static void WriteJson(T value) => Console.WriteLine(JsonSerializer.Serialize(value, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase })); static void WriteProgressJson(T value) { Console.WriteLine(JsonSerializer.Serialize(value, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })); Console.Out.Flush(); } static int Usage(string? errorCode) { WriteJson(new { command = "invalid", connectRequestIssued = false, comActivationAttempted = false, lastKtapConnectState = "not-attempted", ktapConnectAttempted = false, ktapConnectAccepted = false, ktapHelloObserved = (bool?)null, networkMonitoringRecordExpected = false, networkMonitoringCheckRequired = false, networkMonitoringVerified = (bool?)null, errorCode = errorCode ?? "invalid-command-line" }); Console.Error.WriteLine( "Usage: MBN_STOCK_WEBVIEW.PlayoutSmoke [--probe|--dry-run|--test-plan|--test-connect|--test-sequence|--pgm-connect-diagnostic|--pgm-cuts-sequence]"); Console.Error.WriteLine( $"Test commands require {SmokeCommandLine.AcknowledgementFlag} and --config ."); Console.Error.WriteLine( "--test-plan and --test-sequence also require --prepare --next --observe-ms <1000..30000>."); Console.Error.WriteLine( "--test-connect and --test-sequence may request render-capable COM after all Test safety gates pass."); Console.Error.WriteLine( $"--pgm-connect-diagnostic requires {SmokeCommandLine.PgmConnectAcknowledgementFlag}, --host, --port and --expected-pgm-window-title; it exposes no render command."); Console.Error.WriteLine( $"--pgm-cuts-sequence requires {SmokeCommandLine.PgmCutsSequenceAcknowledgementFlag}, --host, --port, --expected-pgm-window-title and --scene-root; --observe-ms may only be 5000."); return 64; }