using System.Globalization; using RioJoy.Core.Profiles; namespace RioJoy.Core.Hosting; /// /// The process a pod-bundled RIOJoy lives alongside (--exit-with): /// either a PID or an executable name, normalized the same way auto-switch /// triggers are (basename, no .exe, lower-case) so launch scripts can /// pass whatever they have. /// public sealed record CompanionTarget { public int? Pid { get; init; } /// Normalized executable name (when is null). public string? Name { get; init; } public static CompanionTarget Parse(string value) { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Companion target is required.", nameof(value)); return int.TryParse(value.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out int pid) ? new CompanionTarget { Pid = pid } : new CompanionTarget { Name = AutoSwitchResolver.Normalize(value) }; } public override string ToString() => Pid is int p ? $"pid {p}" : Name ?? "?"; } /// /// Pure decision core of --exit-with: RIOJoy should exit once its /// companion game has run and then gone away. Launch order is not guaranteed /// (the pod start script fires both), so a companion that has never /// been seen only triggers exit after a startup grace — covering both "game /// still loading" and "game failed to launch, don't linger forever". The /// caller polls (the tray's 1 s timer) and supplies elapsed time, so this /// stays clock-free and unit-testable. /// public sealed class CompanionExit { public static readonly TimeSpan DefaultStartupGrace = TimeSpan.FromSeconds(60); private readonly TimeSpan _grace; private bool _seen; public CompanionExit(TimeSpan? startupGrace = null) { _grace = startupGrace ?? DefaultStartupGrace; } /// True once RIOJoy should tear down and exit. public bool ShouldExit(bool companionRunning, TimeSpan elapsed) { if (companionRunning) { _seen = true; return false; } return _seen || elapsed >= _grace; } }