380 lines
12 KiB
C#
380 lines
12 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),
|
|
_ => 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 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 string ToWireValue(PlayoutKtapConnectState state) => state switch
|
|
{
|
|
PlayoutKtapConnectState.NotAttempted => "not-attempted",
|
|
PlayoutKtapConnectState.Attempted => "attempted",
|
|
PlayoutKtapConnectState.AcceptedUnconfirmed => "accepted-unconfirmed",
|
|
PlayoutKtapConnectState.Failed => "failed",
|
|
_ => "not-attempted"
|
|
};
|
|
|
|
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 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]");
|
|
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.");
|
|
return 64;
|
|
}
|