Review sweep: fix real bugs found across the service + per-app + settings changes
Parallel code review of this session's changes surfaced several real bugs; fixed the substantive ones (judgment/cleanup calls held for Ed): - HIGH Clearing "remembered peers" was resurrected on the next launch: the per-profile -> global migration re-ran every startup and re-unioned the profile file's stale copy. Added a one-time AppConfig.RememberedPeersMigrated marker so migration runs once and a cleared list stays cleared. Self-test pins the clear-then-reload scenario. - MED PushModeWasapiBackend.Start rethrew on a device-open failure; nothing up the stack wraps it, so device churn (a push-eligible single WASAPI source unplugged mid-open) could crash the app. Now logs and stays stopped like MixingEngine/ASIO; the device watcher / self-heal re-open when a device returns. - MED Service self-heal: (a) the re-open "no send sources" path left PerformanceMode ON and presence up while streaming nothing - now releases cleanly; (b) the 3-attempt ladder never refunded, so 3 hiccups over a days-long stint meant permanent silence - now refunds when real audio is heard, and resets on a device hot-plug. - MED ApplyProfile resolved peers (DNS) and enumerated devices INSIDE the gate lock - a boot-time DNS hang as SYSTEM stalled Suspend()/yield/self-heal. Moved outside the lock. - LOW AudioSessionStartWatcher leaked the AudioSessionManager on every Rehook (the WASAPI handle-leak fingerprint) - now disposed. New lifecycle self-test. - LOW stale docstrings (send-all master toggle; ServiceUpdate in-place scheme; Profile .SendAllApplications "neither reads nor writes"). Gate: 42/42 (added peers-migration + session-watcher-lifecycle tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4b2baa58b5
commit
984bcd042e
@@ -137,7 +137,9 @@ public sealed class ServiceSendHost : IDisposable
|
||||
// silence keepalive feeding back, which is why the peak matters.
|
||||
var peak = sender.TakeMaxSenderPreEncodePeak();
|
||||
if (peak > pulsePeakMax) pulsePeakMax = peak;
|
||||
if (peak >= SilentPeak) { everHeardAudio = true; deafSinceTick = 0; }
|
||||
// Real audio proves the capture works — refund the self-heal ladder so a LATER hiccup (hours or
|
||||
// days into an always-on stream) gets fresh re-open attempts instead of finding the budget spent.
|
||||
if (peak >= SilentPeak) { everHeardAudio = true; deafSinceTick = 0; reopenAttempts = 0; }
|
||||
pulseFramesSent += sender.TakeSenderAudioFramesSent();
|
||||
|
||||
// What is the DEVICE playing? The endpoint's own meter, independent of our capture stream.
|
||||
@@ -256,7 +258,20 @@ public sealed class ServiceSendHost : IDisposable
|
||||
{
|
||||
sender.Stop();
|
||||
var specs = BuildSendSpecs(profile); // re-resolve (the default device may have moved)
|
||||
if (specs.Count == 0) { log?.Invoke("service: re-open found no send sources — capture left stopped"); running = false; return; }
|
||||
if (specs.Count == 0)
|
||||
{
|
||||
// The source went away (e.g. the only loopback device was unplugged). Release the whole
|
||||
// send stack — presence, meter readers, the session watcher AND the perf-mode overrides
|
||||
// — instead of sitting "running" with High priority / EcoQoS-off held while streaming
|
||||
// nothing. The device-change watcher re-opens (via ApplyProfile) when a device returns.
|
||||
log?.Invoke("service: re-open found no send sources — releasing until a device returns");
|
||||
try { presence.Stop(); } catch { }
|
||||
SwapMeterDevices(Array.Empty<CaptureSourceSpec>());
|
||||
try { sessionKick?.Dispose(); } catch { } sessionKick = null;
|
||||
try { PerformanceMode.Apply(false, msg => log?.Invoke($"service: {msg}")); } catch { }
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
sender.Configure(specs);
|
||||
sender.Start();
|
||||
SwapMeterDevices(specs);
|
||||
@@ -297,11 +312,16 @@ public sealed class ServiceSendHost : IDisposable
|
||||
/// false (and stays stopped) if the profile has nothing to send or no reachable peers.</summary>
|
||||
public bool ApplyProfile(Profile profile)
|
||||
{
|
||||
// Resolve sources + peer addresses OUTSIDE the lock. BuildEndpoints does DNS (Dns.GetHostAddresses)
|
||||
// and BuildSendSpecs enumerates devices — either can block for seconds at boot as SYSTEM before the
|
||||
// network/audio stack is fully up. Doing that while holding `gate` would stall Suspend() (yielding
|
||||
// to the interactive app), the RunLoop tick and the self-heal for the whole timeout.
|
||||
if (disposed) return false;
|
||||
var specs = BuildSendSpecs(profile);
|
||||
var endpoints = BuildEndpoints(profile);
|
||||
lock (gate)
|
||||
{
|
||||
if (disposed) return false;
|
||||
var specs = BuildSendSpecs(profile);
|
||||
var endpoints = BuildEndpoints(profile);
|
||||
if (specs.Count == 0) { log?.Invoke("service: profile has no WASAPI send sources — nothing to stream"); return false; }
|
||||
if (endpoints.Count == 0) { log?.Invoke("service: profile has no reachable peers — nothing to stream to"); return false; }
|
||||
|
||||
@@ -536,6 +556,10 @@ public sealed class ServiceSendHost : IDisposable
|
||||
}
|
||||
var profile = loadProfile();
|
||||
if (profile is null) return;
|
||||
// A device hot-plug re-plumbs the audio graph much like a power resume or a fresh boot — refill the
|
||||
// self-heal ladder so a brand-new device that comes up momentarily deaf still gets its re-opens.
|
||||
everHeardAudio = false;
|
||||
reopenAttempts = 0;
|
||||
Suspend();
|
||||
ApplyProfile(profile);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user