Startup took 44s and 42 of them were one line. Measured from startup.log: OnStartup 08:42:59 -> MainWindow loaded 08:43:43, with the whole gap inside LoadLayout, ending at TRAX: failed to load '\\192.168.50.86\llamaVault\Assets\Music\...': The network path was not found. (0x80070035) LoadTrax called MusicPlayer.Load synchronously on the UI thread, and new MediaFoundationReader(path) does not fail fast on a dead SMB session — it burns the full ARP/TCP/SMB timeout and only then throws. The NAS had moved to .87 after the power outage, so an unreachable path became a 40-second frozen window on every launch. The rule, from the creator: a probe may time out; a bind should return immediately. Only one side of that is achievable here, and the reason matters — an unreachable UNC path has no fast-fail at all, so probing it on the calling thread is the worst possible probe. (A device open is the opposite: it normally fails fast on a sharing violation, which is why this bug is network-specific.) So: IsTrackReachableAsync runs the probe on a thread-pool thread under a Task.WhenAny ceiling (TraxProbeBudget, 2s) and treats offline/missing/slow as a normal state rather than an error. Only a probe that passed gets the real bind, and that bind deliberately stays on the UI thread alongside every other MusicPlayer mutation — moving the whole load off-thread would have introduced a race against PlayTrax/PauseTrax that does not exist today. The orphan probe thread stays parked until SMB gives up; it is not the thread anyone waits on. One test, per the Good Dog rule: TraxProbeTests. AHangingProbeIsAbandonedAtTheBudget_AndReportsOffline stands in a probe that never returns and asserts the caller is released and told "offline". Drop the ceiling and it fails on elapsed time — which is the frozen window returning. Suite 381/381, 0 warnings.
51 lines
1.9 KiB
C#
51 lines
1.9 KiB
C#
using Xunit;
|
|
using ytLive.ViewModels;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// TRAX load-path guarding (2026-10-04). Startup used to freeze for ~40s when the
|
|
/// music track lived on an offline network share: <c>LoadLayout</c> called
|
|
/// <c>MusicPlayer.Load</c> synchronously, and <c>new MediaFoundationReader(path)</c>
|
|
/// does not fail fast on a dead SMB session — it burns the full timeout and then
|
|
/// throws 0x80070035. The fix is a bounded reachability probe on a background thread,
|
|
/// so an unreachable share can never sit on the path the creator waits on.
|
|
/// </summary>
|
|
public class TraxProbeTests
|
|
{
|
|
/// <summary>The regression this guards: a probe that hangs must be abandoned at the
|
|
/// budget and reported offline. Without the ceiling the caller inherits the SMB
|
|
/// timeout, which is the 40-second frozen startup window. The orphaned probe thread
|
|
/// is released at the end so the test does not leak it.</summary>
|
|
[Fact]
|
|
public async Task AHangingProbeIsAbandonedAtTheBudget_AndReportsOffline()
|
|
{
|
|
var orphan = new ManualResetEventSlim(false);
|
|
var started = DateTime.UtcNow;
|
|
|
|
try
|
|
{
|
|
var reachable = await MainViewModel.IsTrackReachableAsync(
|
|
@"\\llamavault\llamaVault\Assets\Music\VBI - Mayhem OST.mp3",
|
|
TimeSpan.FromMilliseconds(150),
|
|
// Stands in for a share that has gone away: the open never completes.
|
|
_ =>
|
|
{
|
|
orphan.Wait(TimeSpan.FromSeconds(30));
|
|
return true;
|
|
});
|
|
|
|
var elapsed = DateTime.UtcNow - started;
|
|
|
|
Assert.False(reachable);
|
|
Assert.True(
|
|
elapsed < TimeSpan.FromSeconds(5),
|
|
$"probe was not cut off at the budget — took {elapsed.TotalSeconds:0.0}s, which is the frozen-startup bug returning");
|
|
}
|
|
finally
|
|
{
|
|
orphan.Set();
|
|
}
|
|
}
|
|
}
|