Migrate remaining legacy operator workflows
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.Infrastructure.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// Converts closed runtime states into closed audit records and suppresses identical
|
||||
/// consecutive states independently for Oracle, MariaDB, and Tornado.
|
||||
/// </summary>
|
||||
public sealed class NativeOperatorLogTransitionTracker
|
||||
{
|
||||
private readonly object _sync = new();
|
||||
private NativeOperatorDatabaseState? _oracleState;
|
||||
private NativeOperatorDatabaseState? _mariaDbState;
|
||||
private NativeOperatorTornadoState? _tornadoState;
|
||||
|
||||
public bool TryObserveDatabaseState(
|
||||
DataSourceKind source,
|
||||
DatabaseHealthState state,
|
||||
out NativeOperatorLogRecord record)
|
||||
{
|
||||
record = default;
|
||||
if (!TryMapDatabaseState(state, out var mapped))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
switch (source)
|
||||
{
|
||||
case DataSourceKind.Oracle when _oracleState != mapped:
|
||||
_oracleState = mapped;
|
||||
record = NativeOperatorLogRecord.ForOracleStateChanged(mapped);
|
||||
return true;
|
||||
case DataSourceKind.MariaDb when _mariaDbState != mapped:
|
||||
_mariaDbState = mapped;
|
||||
record = NativeOperatorLogRecord.ForMariaDbStateChanged(mapped);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryObserveTornadoState(
|
||||
PlayoutConnectionState state,
|
||||
out NativeOperatorLogRecord record)
|
||||
{
|
||||
record = default;
|
||||
if (!TryMapTornadoState(state, out var mapped))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_tornadoState == mapped)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_tornadoState = mapped;
|
||||
record = NativeOperatorLogRecord.ForTornadoStateChanged(mapped);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryMapDatabaseState(
|
||||
DatabaseHealthState state,
|
||||
out NativeOperatorDatabaseState mapped)
|
||||
{
|
||||
mapped = state switch
|
||||
{
|
||||
DatabaseHealthState.NotConfigured => NativeOperatorDatabaseState.NotConfigured,
|
||||
DatabaseHealthState.Healthy => NativeOperatorDatabaseState.Healthy,
|
||||
DatabaseHealthState.Unhealthy => NativeOperatorDatabaseState.Unhealthy,
|
||||
DatabaseHealthState.TimedOut => NativeOperatorDatabaseState.TimedOut,
|
||||
_ => default
|
||||
};
|
||||
return mapped != default;
|
||||
}
|
||||
|
||||
internal static bool TryMapTornadoState(
|
||||
PlayoutConnectionState state,
|
||||
out NativeOperatorTornadoState mapped)
|
||||
{
|
||||
mapped = state switch
|
||||
{
|
||||
PlayoutConnectionState.Disabled => NativeOperatorTornadoState.Disabled,
|
||||
PlayoutConnectionState.DryRunReady => NativeOperatorTornadoState.DryRunReady,
|
||||
PlayoutConnectionState.Disconnected => NativeOperatorTornadoState.Disconnected,
|
||||
PlayoutConnectionState.Connecting => NativeOperatorTornadoState.Connecting,
|
||||
PlayoutConnectionState.Connected => NativeOperatorTornadoState.Connected,
|
||||
PlayoutConnectionState.Reconnecting => NativeOperatorTornadoState.Reconnecting,
|
||||
PlayoutConnectionState.Disconnecting => NativeOperatorTornadoState.Disconnecting,
|
||||
PlayoutConnectionState.Faulted => NativeOperatorTornadoState.Faulted,
|
||||
PlayoutConnectionState.OutcomeUnknown => NativeOperatorTornadoState.OutcomeUnknown,
|
||||
PlayoutConnectionState.Disposed => NativeOperatorTornadoState.Disposed,
|
||||
_ => default
|
||||
};
|
||||
return mapped != default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the single serialized operator command without retaining request IDs or cue data.
|
||||
/// Exactly one terminal record can be emitted for each accepted command.
|
||||
/// </summary>
|
||||
public sealed class NativeOperatorCommandLogTracker
|
||||
{
|
||||
private readonly object _sync = new();
|
||||
private NativeOperatorPlayoutCommand? _currentCommand;
|
||||
private bool _terminalRecorded;
|
||||
|
||||
public bool TryBegin(
|
||||
NativeOperatorPlayoutCommand command,
|
||||
out NativeOperatorLogRecord record)
|
||||
{
|
||||
record = default;
|
||||
if (!Enum.IsDefined(command))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
_currentCommand = command;
|
||||
_terminalRecorded = false;
|
||||
record = NativeOperatorLogRecord.ForPlayoutCommandRequested(command);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryComplete(
|
||||
NativeOperatorPlayoutCommand command,
|
||||
NativeOperatorPlayoutCompletion completion,
|
||||
out NativeOperatorLogRecord record)
|
||||
{
|
||||
record = default;
|
||||
if (!Enum.IsDefined(command) || !Enum.IsDefined(completion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_currentCommand != command || _terminalRecorded)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_terminalRecorded = true;
|
||||
record = NativeOperatorLogRecord.ForPlayoutCommandCompleted(command, completion);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryMarkOutcomeUnknown(
|
||||
NativeOperatorPlayoutCommand command,
|
||||
out NativeOperatorLogRecord record)
|
||||
{
|
||||
record = default;
|
||||
if (!Enum.IsDefined(command))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
return TryMarkOutcomeUnknownLocked(command, out record);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryMarkCurrentOutcomeUnknown(out NativeOperatorLogRecord record)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_currentCommand is not { } command)
|
||||
{
|
||||
record = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryMarkOutcomeUnknownLocked(command, out record);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryMarkOutcomeUnknownLocked(
|
||||
NativeOperatorPlayoutCommand command,
|
||||
out NativeOperatorLogRecord record)
|
||||
{
|
||||
record = default;
|
||||
if (_currentCommand != command || _terminalRecorded)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_terminalRecorded = true;
|
||||
record = NativeOperatorLogRecord.ForOutcomeUnknown(command);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.Infrastructure.Diagnostics;
|
||||
|
||||
public enum NativeOperatorLogEvent
|
||||
{
|
||||
Startup = 1,
|
||||
Shutdown = 2,
|
||||
OracleStateChanged = 3,
|
||||
MariaDbStateChanged = 4,
|
||||
TornadoStateChanged = 5,
|
||||
PlayoutCommandRequested = 6,
|
||||
PlayoutCommandCompleted = 7,
|
||||
OutcomeUnknown = 8
|
||||
}
|
||||
|
||||
public enum NativeOperatorDatabaseState
|
||||
{
|
||||
NotConfigured = 1,
|
||||
Healthy = 2,
|
||||
Unhealthy = 3,
|
||||
TimedOut = 4
|
||||
}
|
||||
|
||||
public enum NativeOperatorTornadoState
|
||||
{
|
||||
Disabled = 1,
|
||||
DryRunReady = 2,
|
||||
ProcessMissing = 3,
|
||||
Disconnected = 4,
|
||||
Connecting = 5,
|
||||
Connected = 6,
|
||||
Reconnecting = 7,
|
||||
Disconnecting = 8,
|
||||
Faulted = 9,
|
||||
OutcomeUnknown = 10,
|
||||
Disposed = 11
|
||||
}
|
||||
|
||||
public enum NativeOperatorPlayoutCommand
|
||||
{
|
||||
Connect = 1,
|
||||
Disconnect = 2,
|
||||
Prepare = 3,
|
||||
TakeIn = 4,
|
||||
Next = 5,
|
||||
TakeOut = 6,
|
||||
UpdateOnAir = 7
|
||||
}
|
||||
|
||||
public enum NativeOperatorPlayoutCompletion
|
||||
{
|
||||
Succeeded = 1,
|
||||
Rejected = 2,
|
||||
Cancelled = 3,
|
||||
Unavailable = 4,
|
||||
Failed = 5
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A closed operator-audit record. It deliberately has no string, path, SQL, scene,
|
||||
/// exception, or arbitrary numeric field, so untrusted/free-form data cannot enter the log.
|
||||
/// </summary>
|
||||
public readonly record struct NativeOperatorLogRecord
|
||||
{
|
||||
private NativeOperatorLogRecord(
|
||||
NativeOperatorLogEvent logEvent,
|
||||
NativeOperatorDatabaseState? databaseState = null,
|
||||
NativeOperatorTornadoState? tornadoState = null,
|
||||
NativeOperatorPlayoutCommand? command = null,
|
||||
NativeOperatorPlayoutCompletion? completion = null)
|
||||
{
|
||||
Event = logEvent;
|
||||
DatabaseState = databaseState;
|
||||
TornadoState = tornadoState;
|
||||
Command = command;
|
||||
Completion = completion;
|
||||
}
|
||||
|
||||
public NativeOperatorLogEvent Event { get; }
|
||||
|
||||
public NativeOperatorDatabaseState? DatabaseState { get; }
|
||||
|
||||
public NativeOperatorTornadoState? TornadoState { get; }
|
||||
|
||||
public NativeOperatorPlayoutCommand? Command { get; }
|
||||
|
||||
public NativeOperatorPlayoutCompletion? Completion { get; }
|
||||
|
||||
public static NativeOperatorLogRecord ForStartup() =>
|
||||
new(NativeOperatorLogEvent.Startup);
|
||||
|
||||
public static NativeOperatorLogRecord ForShutdown() =>
|
||||
new(NativeOperatorLogEvent.Shutdown);
|
||||
|
||||
public static NativeOperatorLogRecord ForOracleStateChanged(
|
||||
NativeOperatorDatabaseState state) =>
|
||||
new(NativeOperatorLogEvent.OracleStateChanged, databaseState: state);
|
||||
|
||||
public static NativeOperatorLogRecord ForMariaDbStateChanged(
|
||||
NativeOperatorDatabaseState state) =>
|
||||
new(NativeOperatorLogEvent.MariaDbStateChanged, databaseState: state);
|
||||
|
||||
public static NativeOperatorLogRecord ForTornadoStateChanged(
|
||||
NativeOperatorTornadoState state) =>
|
||||
new(NativeOperatorLogEvent.TornadoStateChanged, tornadoState: state);
|
||||
|
||||
public static NativeOperatorLogRecord ForPlayoutCommandRequested(
|
||||
NativeOperatorPlayoutCommand command) =>
|
||||
new(NativeOperatorLogEvent.PlayoutCommandRequested, command: command);
|
||||
|
||||
public static NativeOperatorLogRecord ForPlayoutCommandCompleted(
|
||||
NativeOperatorPlayoutCommand command,
|
||||
NativeOperatorPlayoutCompletion completion) =>
|
||||
new(
|
||||
NativeOperatorLogEvent.PlayoutCommandCompleted,
|
||||
command: command,
|
||||
completion: completion);
|
||||
|
||||
public static NativeOperatorLogRecord ForOutcomeUnknown(
|
||||
NativeOperatorPlayoutCommand command) =>
|
||||
new(NativeOperatorLogEvent.OutcomeUnknown, command: command);
|
||||
}
|
||||
|
||||
public interface INativeOperatorLogWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts one bounded append. No logging failure is allowed to escape to broadcast logic.
|
||||
/// </summary>
|
||||
bool TryWrite(NativeOperatorLogRecord record);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes closed native operator events beneath
|
||||
/// %LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Logs\Log_MMdd.log.
|
||||
/// </summary>
|
||||
public sealed class NativeOperatorLogWriter : INativeOperatorLogWriter
|
||||
{
|
||||
internal const int DefaultMaximumFileBytes = 4 * 1024 * 1024;
|
||||
internal const int DefaultRetentionDays = 31;
|
||||
internal const int MaximumEncodedLineBytes = 512;
|
||||
|
||||
private const string ApplicationDirectoryName = "MBN_STOCK_WEBVIEW";
|
||||
private const string LogDirectoryName = "Logs";
|
||||
private static readonly UTF8Encoding Utf8 = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
|
||||
private static readonly ConcurrentDictionary<string, object> PathGates =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly string _localApplicationDataRoot;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly int _maximumFileBytes;
|
||||
private readonly int _retentionDays;
|
||||
|
||||
public NativeOperatorLogWriter()
|
||||
: this(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
TimeProvider.System,
|
||||
DefaultMaximumFileBytes,
|
||||
DefaultRetentionDays)
|
||||
{
|
||||
}
|
||||
|
||||
internal NativeOperatorLogWriter(
|
||||
string localApplicationDataRoot,
|
||||
TimeProvider timeProvider,
|
||||
int maximumFileBytes = DefaultMaximumFileBytes,
|
||||
int retentionDays = DefaultRetentionDays)
|
||||
{
|
||||
_localApplicationDataRoot = localApplicationDataRoot;
|
||||
_timeProvider = timeProvider;
|
||||
_maximumFileBytes = maximumFileBytes;
|
||||
_retentionDays = retentionDays;
|
||||
}
|
||||
|
||||
public bool TryWrite(NativeOperatorLogRecord record)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_timeProvider is null ||
|
||||
_maximumFileBytes <= 0 ||
|
||||
_retentionDays <= 0 ||
|
||||
!TryFormatPayload(record, out var payload) ||
|
||||
!TryResolvePaths(out var localRoot, out var applicationDirectory, out var logDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var gate = PathGates.GetOrAdd(logDirectory, static _ => new object());
|
||||
lock (gate)
|
||||
{
|
||||
var localNow = _timeProvider.GetLocalNow();
|
||||
if (!TryPrepareDirectories(localRoot, applicationDirectory, logDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var directoryLease = global::MBN_STOCK_WEBVIEW.Infrastructure
|
||||
.TrustedManualDirectory.AcquirePrivateWritableLease(logDirectory);
|
||||
if (!TryApplyRetention(logDirectory, localNow.UtcDateTime))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var fileName = $"Log_{localNow:MMdd}.log";
|
||||
var logPath = Path.GetFullPath(Path.Combine(logDirectory, fileName));
|
||||
if (!IsImmediateChild(logDirectory, logPath) ||
|
||||
!string.Equals(Path.GetFileName(logPath), fileName, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var line = $"[{localNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", CultureInfo.InvariantCulture)}] {payload}{Environment.NewLine}";
|
||||
var encodedLine = Utf8.GetBytes(line);
|
||||
if (encodedLine.Length == 0 || encodedLine.Length > MaximumEncodedLineBytes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryAppend(logDirectory, logPath, encodedLine);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Audit logging is best effort and must never interrupt playout or database work.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolvePaths(
|
||||
out string localRoot,
|
||||
out string applicationDirectory,
|
||||
out string logDirectory)
|
||||
{
|
||||
localRoot = string.Empty;
|
||||
applicationDirectory = string.Empty;
|
||||
logDirectory = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_localApplicationDataRoot) ||
|
||||
!Path.IsPathFullyQualified(_localApplicationDataRoot))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
localRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(_localApplicationDataRoot));
|
||||
applicationDirectory = Path.GetFullPath(Path.Combine(localRoot, ApplicationDirectoryName));
|
||||
logDirectory = Path.GetFullPath(Path.Combine(applicationDirectory, LogDirectoryName));
|
||||
return IsImmediateChild(localRoot, applicationDirectory) &&
|
||||
IsImmediateChild(applicationDirectory, logDirectory);
|
||||
}
|
||||
|
||||
private static bool TryPrepareDirectories(
|
||||
string localRoot,
|
||||
string applicationDirectory,
|
||||
string logDirectory)
|
||||
{
|
||||
if (!Directory.Exists(localRoot) ||
|
||||
!TryGetSafeDirectoryAttributes(localRoot))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var prepared = global::MBN_STOCK_WEBVIEW.Infrastructure.TrustedManualDirectory
|
||||
.PreparePrivateWritableDirectory(logDirectory);
|
||||
if (!string.Equals(
|
||||
Path.TrimEndingDirectorySeparator(prepared),
|
||||
Path.TrimEndingDirectorySeparator(logDirectory),
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The lease acquired by the caller pins the chain after this validation.
|
||||
return TryGetSafeDirectoryAttributes(localRoot) &&
|
||||
TryGetSafeDirectoryAttributes(applicationDirectory) &&
|
||||
TryGetSafeDirectoryAttributes(logDirectory);
|
||||
}
|
||||
|
||||
private bool TryApplyRetention(string logDirectory, DateTime utcNow)
|
||||
{
|
||||
var cutoff = utcNow.AddDays(-_retentionDays);
|
||||
foreach (var entry in Directory.EnumerateFileSystemEntries(
|
||||
logDirectory,
|
||||
"Log_????.log",
|
||||
SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
var fullPath = Path.GetFullPath(entry);
|
||||
var fileName = Path.GetFileName(fullPath);
|
||||
if (!IsImmediateChild(logDirectory, fullPath) ||
|
||||
!IsLogFileName(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var attributes = File.GetAttributes(fullPath);
|
||||
if (!IsSafeFileAttributes(attributes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (File.GetLastWriteTimeUtc(fullPath) < cutoff)
|
||||
{
|
||||
File.Delete(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return TryGetSafeDirectoryAttributes(logDirectory);
|
||||
}
|
||||
|
||||
private bool TryAppend(string logDirectory, string logPath, byte[] encodedLine)
|
||||
{
|
||||
if (File.Exists(logPath) && !TryGetSafeFileAttributes(logPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var stream = new FileStream(
|
||||
logPath,
|
||||
FileMode.OpenOrCreate,
|
||||
FileAccess.Write,
|
||||
FileShare.Read,
|
||||
bufferSize: 4096,
|
||||
FileOptions.SequentialScan);
|
||||
|
||||
if (!string.Equals(Path.GetFullPath(stream.Name), logPath, StringComparison.OrdinalIgnoreCase) ||
|
||||
!TryGetSafeDirectoryAttributes(logDirectory) ||
|
||||
!TryGetSafeFileAttributes(logPath) ||
|
||||
stream.Length > _maximumFileBytes - encodedLine.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
global::MBN_STOCK_WEBVIEW.Infrastructure.TrustedDirectoryLease
|
||||
.EnsureRegularFileHandle(
|
||||
stream.SafeFileHandle,
|
||||
logPath,
|
||||
_maximumFileBytes,
|
||||
allowEmpty: true);
|
||||
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
stream.Write(encodedLine, 0, encodedLine.Length);
|
||||
stream.Flush(flushToDisk: false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryFormatPayload(
|
||||
NativeOperatorLogRecord record,
|
||||
out string payload)
|
||||
{
|
||||
payload = string.Empty;
|
||||
switch (record.Event)
|
||||
{
|
||||
case NativeOperatorLogEvent.Startup
|
||||
when HasNoDetails(record):
|
||||
payload = "event=Startup";
|
||||
return true;
|
||||
case NativeOperatorLogEvent.Shutdown
|
||||
when HasNoDetails(record):
|
||||
payload = "event=Shutdown";
|
||||
return true;
|
||||
case NativeOperatorLogEvent.OracleStateChanged
|
||||
when record.DatabaseState is { } oracleState && HasOnlyDatabaseState(record):
|
||||
return TryDatabaseStateToken(oracleState, out payload, "OracleStateChanged");
|
||||
case NativeOperatorLogEvent.MariaDbStateChanged
|
||||
when record.DatabaseState is { } mariaDbState && HasOnlyDatabaseState(record):
|
||||
return TryDatabaseStateToken(mariaDbState, out payload, "MariaDbStateChanged");
|
||||
case NativeOperatorLogEvent.TornadoStateChanged
|
||||
when record.TornadoState is { } tornadoState && HasOnlyTornadoState(record):
|
||||
return TryTornadoStateToken(tornadoState, out payload);
|
||||
case NativeOperatorLogEvent.PlayoutCommandRequested
|
||||
when record.Command is { } requestedCommand && HasOnlyCommand(record):
|
||||
return TryCommandToken(requestedCommand, out payload, "PlayoutCommandRequested");
|
||||
case NativeOperatorLogEvent.PlayoutCommandCompleted
|
||||
when record.Command is { } completedCommand &&
|
||||
record.Completion is { } completion &&
|
||||
HasOnlyCommandAndCompletion(record):
|
||||
return TryCompletionToken(completedCommand, completion, out payload);
|
||||
case NativeOperatorLogEvent.OutcomeUnknown
|
||||
when record.Command is { } unknownCommand && HasOnlyCommand(record):
|
||||
return TryCommandToken(unknownCommand, out payload, "OutcomeUnknown");
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasNoDetails(NativeOperatorLogRecord record) =>
|
||||
record.DatabaseState is null &&
|
||||
record.TornadoState is null &&
|
||||
record.Command is null &&
|
||||
record.Completion is null;
|
||||
|
||||
private static bool HasOnlyDatabaseState(NativeOperatorLogRecord record) =>
|
||||
record.DatabaseState is not null &&
|
||||
record.TornadoState is null &&
|
||||
record.Command is null &&
|
||||
record.Completion is null;
|
||||
|
||||
private static bool HasOnlyTornadoState(NativeOperatorLogRecord record) =>
|
||||
record.DatabaseState is null &&
|
||||
record.TornadoState is not null &&
|
||||
record.Command is null &&
|
||||
record.Completion is null;
|
||||
|
||||
private static bool HasOnlyCommand(NativeOperatorLogRecord record) =>
|
||||
record.DatabaseState is null &&
|
||||
record.TornadoState is null &&
|
||||
record.Command is not null &&
|
||||
record.Completion is null;
|
||||
|
||||
private static bool HasOnlyCommandAndCompletion(NativeOperatorLogRecord record) =>
|
||||
record.DatabaseState is null &&
|
||||
record.TornadoState is null &&
|
||||
record.Command is not null &&
|
||||
record.Completion is not null;
|
||||
|
||||
private static bool TryDatabaseStateToken(
|
||||
NativeOperatorDatabaseState state,
|
||||
out string payload,
|
||||
string eventName)
|
||||
{
|
||||
var stateToken = state switch
|
||||
{
|
||||
NativeOperatorDatabaseState.NotConfigured => "NotConfigured",
|
||||
NativeOperatorDatabaseState.Healthy => "Healthy",
|
||||
NativeOperatorDatabaseState.Unhealthy => "Unhealthy",
|
||||
NativeOperatorDatabaseState.TimedOut => "TimedOut",
|
||||
_ => null
|
||||
};
|
||||
payload = stateToken is null ? string.Empty : $"event={eventName} state={stateToken}";
|
||||
return stateToken is not null;
|
||||
}
|
||||
|
||||
private static bool TryTornadoStateToken(
|
||||
NativeOperatorTornadoState state,
|
||||
out string payload)
|
||||
{
|
||||
var stateToken = state switch
|
||||
{
|
||||
NativeOperatorTornadoState.Disabled => "Disabled",
|
||||
NativeOperatorTornadoState.DryRunReady => "DryRunReady",
|
||||
NativeOperatorTornadoState.ProcessMissing => "ProcessMissing",
|
||||
NativeOperatorTornadoState.Disconnected => "Disconnected",
|
||||
NativeOperatorTornadoState.Connecting => "Connecting",
|
||||
NativeOperatorTornadoState.Connected => "Connected",
|
||||
NativeOperatorTornadoState.Reconnecting => "Reconnecting",
|
||||
NativeOperatorTornadoState.Disconnecting => "Disconnecting",
|
||||
NativeOperatorTornadoState.Faulted => "Faulted",
|
||||
NativeOperatorTornadoState.OutcomeUnknown => "OutcomeUnknown",
|
||||
NativeOperatorTornadoState.Disposed => "Disposed",
|
||||
_ => null
|
||||
};
|
||||
payload = stateToken is null
|
||||
? string.Empty
|
||||
: $"event=TornadoStateChanged state={stateToken}";
|
||||
return stateToken is not null;
|
||||
}
|
||||
|
||||
private static bool TryCommandToken(
|
||||
NativeOperatorPlayoutCommand command,
|
||||
out string payload,
|
||||
string eventName)
|
||||
{
|
||||
var commandToken = CommandToken(command);
|
||||
payload = commandToken is null
|
||||
? string.Empty
|
||||
: $"event={eventName} command={commandToken}";
|
||||
return commandToken is not null;
|
||||
}
|
||||
|
||||
private static bool TryCompletionToken(
|
||||
NativeOperatorPlayoutCommand command,
|
||||
NativeOperatorPlayoutCompletion completion,
|
||||
out string payload)
|
||||
{
|
||||
var commandToken = CommandToken(command);
|
||||
var completionToken = completion switch
|
||||
{
|
||||
NativeOperatorPlayoutCompletion.Succeeded => "Succeeded",
|
||||
NativeOperatorPlayoutCompletion.Rejected => "Rejected",
|
||||
NativeOperatorPlayoutCompletion.Cancelled => "Cancelled",
|
||||
NativeOperatorPlayoutCompletion.Unavailable => "Unavailable",
|
||||
NativeOperatorPlayoutCompletion.Failed => "Failed",
|
||||
_ => null
|
||||
};
|
||||
payload = commandToken is null || completionToken is null
|
||||
? string.Empty
|
||||
: $"event=PlayoutCommandCompleted command={commandToken} result={completionToken}";
|
||||
return commandToken is not null && completionToken is not null;
|
||||
}
|
||||
|
||||
private static string? CommandToken(NativeOperatorPlayoutCommand command) =>
|
||||
command switch
|
||||
{
|
||||
NativeOperatorPlayoutCommand.Connect => "Connect",
|
||||
NativeOperatorPlayoutCommand.Disconnect => "Disconnect",
|
||||
NativeOperatorPlayoutCommand.Prepare => "Prepare",
|
||||
NativeOperatorPlayoutCommand.TakeIn => "TakeIn",
|
||||
NativeOperatorPlayoutCommand.Next => "Next",
|
||||
NativeOperatorPlayoutCommand.TakeOut => "TakeOut",
|
||||
NativeOperatorPlayoutCommand.UpdateOnAir => "UpdateOnAir",
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static bool TryGetSafeDirectoryAttributes(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Directory.Exists(path) &&
|
||||
IsSafeDirectoryAttributes(File.GetAttributes(path));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetSafeFileAttributes(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.Exists(path) &&
|
||||
IsSafeFileAttributes(File.GetAttributes(path));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsSafeDirectoryAttributes(FileAttributes attributes) =>
|
||||
attributes.HasFlag(FileAttributes.Directory) &&
|
||||
!attributes.HasFlag(FileAttributes.ReparsePoint) &&
|
||||
!attributes.HasFlag(FileAttributes.Device);
|
||||
|
||||
internal static bool IsSafeFileAttributes(FileAttributes attributes) =>
|
||||
!attributes.HasFlag(FileAttributes.Directory) &&
|
||||
!attributes.HasFlag(FileAttributes.ReparsePoint) &&
|
||||
!attributes.HasFlag(FileAttributes.Device);
|
||||
|
||||
private static bool IsImmediateChild(string parent, string candidate)
|
||||
{
|
||||
var expectedParent = Path.TrimEndingDirectorySeparator(Path.GetFullPath(parent));
|
||||
var actualParent = Path.GetDirectoryName(Path.GetFullPath(candidate));
|
||||
return string.Equals(expectedParent, actualParent, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsLogFileName(string fileName) =>
|
||||
fileName.Length == 12 &&
|
||||
fileName.StartsWith("Log_", StringComparison.Ordinal) &&
|
||||
fileName.EndsWith(".log", StringComparison.Ordinal) &&
|
||||
fileName.AsSpan(4, 4).IndexOfAnyExceptInRange('0', '9') < 0;
|
||||
}
|
||||
@@ -247,8 +247,14 @@ public sealed class OperatorCatalogMutationExecutor : IOperatorCatalogMutationEx
|
||||
OperatorCatalogMutationKind.CreateTheme or
|
||||
OperatorCatalogMutationKind.ReplaceTheme or
|
||||
OperatorCatalogMutationKind.DeleteTheme;
|
||||
var identityLength = themeMutation ? 8 : 4;
|
||||
if (transaction.IdentityCode.Length != identityLength ||
|
||||
var validIdentityLength = transaction.Kind switch
|
||||
{
|
||||
OperatorCatalogMutationKind.CreateTheme => transaction.IdentityCode.Length == 8,
|
||||
OperatorCatalogMutationKind.ReplaceTheme or OperatorCatalogMutationKind.DeleteTheme =>
|
||||
transaction.IdentityCode.Length is >= 3 and <= 8,
|
||||
_ => transaction.IdentityCode.Length == 4
|
||||
};
|
||||
if (!validIdentityLength ||
|
||||
transaction.IdentityCode.Any(static character => !char.IsAsciiDigit(character)) ||
|
||||
!themeMutation && transaction.Source != DataSourceKind.Oracle)
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user