Files
riojoy/src/RioJoy.Core/Hosting/CompanionExit.cs
T
CydandClaude Fable 5 97caf124a6 pod: bundled per-game deployment (portable config, --exit-with, build-pod)
Phase 10: RIO hardware exists only on pods + dev boxes, so production is one
RIOJoy copy inside each podized game folder, no resident tray. ConfigLocator
makes a config.json beside the exe win over %APPDATA%; --exit-with <exe|pid>
(CompanionTarget/CompanionExit, 60s startup grace) tears down and quits when
the game exits; a starting --exit-with instance waits up to 15s for the
predecessor mutex instead of silently exiting. deploy/build-pod.ps1 emits
the ~4.5MB drop-in (app + portable config wrapping the profile + start
script, no drivers) - verified against the shipped Descent profile. 455
tests; PLAN.md Phase 10 + INPUT-INTEGRATION.md pod section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 22:01:52 -05:00

64 lines
2.2 KiB
C#

using System.Globalization;
using RioJoy.Core.Profiles;
namespace RioJoy.Core.Hosting;
/// <summary>
/// The process a pod-bundled RIOJoy lives alongside (<c>--exit-with</c>):
/// either a PID or an executable name, normalized the same way auto-switch
/// triggers are (basename, no <c>.exe</c>, lower-case) so launch scripts can
/// pass whatever they have.
/// </summary>
public sealed record CompanionTarget
{
public int? Pid { get; init; }
/// <summary>Normalized executable name (when <see cref="Pid"/> is null).</summary>
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 ?? "?";
}
/// <summary>
/// Pure decision core of <c>--exit-with</c>: 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 <i>never</i>
/// 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.
/// </summary>
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;
}
/// <summary>True once RIOJoy should tear down and exit.</summary>
public bool ShouldExit(bool companionRunning, TimeSpan elapsed)
{
if (companionRunning)
{
_seen = true;
return false;
}
return _seen || elapsed >= _grace;
}
}