Release v4.8: fix rare crash from endpoint thrash on a dual-address peer (#16)

The same singer hit a rare crash: their transmitter (COMP3) was reachable at two
addresses at once — the VPN address 10.8.0.1 they chose and that machine's wireless
192.168.3.245 — and the discovery-driven endpoint-follow ping-ponged the connection
between the two (the log shows four moves in 58 ms) right where the process died with
no shutdown line, no managed exception, no dialog: a hard crash from the receiver
audio-session teardown/rebuild churn the thrash caused.

Fix: the follow loop now never moves off an endpoint that's still answering heartbeats,
only follows once the current one has been unreachable for a sustained grace period
(6 s), only to an address that is itself answering, and never more than once per cooldown
(15 s) — so it can't thrash, and a peer reached on a working address stays put (honours
the singer's "just stay on 10.8.0.1"). New endpointUnreachableSinceUtc + lastEndpointMoveUtc
state; genuine DHCP/network moves are still followed a few seconds later.

Also: a global crash handler (Program.WriteCrashReport on AppDomain.UnhandledException +
TaskScheduler.UnobservedTaskException) writes a crash-*.txt into the logs folder, so a
future "RemSound just vanished" report leaves a stack behind. Removed a stale doc comment
left over from the v4.7 adoption removal. Manual gains a "RemSound closed unexpectedly"
troubleshooting entry. Version 4.8; About + RELEASE_NOTES updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-27 22:57:49 +01:00
co-authored by Claude Opus 4.8
parent 64d5cb5a86
commit 0df2a576fc
7 changed files with 119 additions and 57 deletions
+31
View File
@@ -12,6 +12,27 @@ internal static class Program
// window. volatile for cross-thread visibility; RestoreFromTray marshals to the UI thread.
private static volatile MainForm? activeMainForm;
// Writes an otherwise-fatal exception to a timestamped crash file in the logs folder, so a
// "RemSound just disappeared, no dialog" report (#16) leaves a stack behind to diagnose instead
// of nothing. Best-effort and self-contained — a crash handler must never throw.
private static void WriteCrashReport(string source, Exception? ex)
{
try
{
var dir = AppConfig.LogsDirectory;
Directory.CreateDirectory(dir);
var path = Path.Combine(dir, $"crash-{DateTime.Now:yyyyMMdd-HHmmss-fff}.txt");
var report =
$"RemSound crash report{Environment.NewLine}" +
$"Version: {typeof(Program).Assembly.GetName().Version}{Environment.NewLine}" +
$"Time: {DateTime.Now:O}{Environment.NewLine}" +
$"Source: {source}{Environment.NewLine}{Environment.NewLine}" +
(ex?.ToString() ?? "(no exception object)");
File.WriteAllText(path, report);
}
catch { /* a crash handler must never throw */ }
}
[STAThread]
private static void Main(string[] args)
{
@@ -25,6 +46,16 @@ internal static class Program
return;
}
// Capture otherwise-fatal background-thread exceptions to a crash file, so a "RemSound just
// vanished with no dialog" report (#16) leaves a stack behind instead of nothing. Best-effort.
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
WriteCrashReport("AppDomain.UnhandledException", e.ExceptionObject as Exception);
TaskScheduler.UnobservedTaskException += (_, e) =>
{
WriteCrashReport("TaskScheduler.UnobservedTaskException", e.Exception);
e.SetObserved();
};
// --config-dir <folder> (test / portable isolation): redirect ALL user state - config,
// profiles, logs, cue sounds - to an explicit folder for THIS process only. Applied first,
// before the layout migration and sound consolidation below read or write the default