Files
MBN_STOCK_WEBVIEW/tools/MBN_STOCK_WEBVIEW.PlayoutSmoke/Program.cs

499 lines
16 KiB
C#

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<int> 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<int> 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<int> 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<int> 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<object>(),
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>(T value) =>
Console.WriteLine(JsonSerializer.Serialize(value, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}));
static void WriteProgressJson<T>(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 <absolute-path>.");
Console.Error.WriteLine(
"--test-plan and --test-sequence also require --prepare <scene-code> --next <scene-code> --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;
}