Fix: service startup-volume re-punching on every restart + misleading suspend log
Ed reported the service pulling his volume to 20% repeatedly. Diagnosis from the service logs: NOT a runaway timer - one service process ran untouched for 36 hours, proving there's no periodic restart. The volume-to-20 punches came from GENUINE service restarts (deploys, the one self-update, profile saves, handovers during active use), each re-applying because the mode was "every service restart". Two real faults found and fixed: 1. Re-apply burst guard. "Every service restart" (and boot-only as belt-and-braces) now skips a re-apply within ReapplyCooldown (5 min) of the last successful apply, persisted as startup-volume-last.txt. This kills the double-apply we saw in the log at 08:20:48 then 08:21:02 (a self-update restart immediately followed by a follow-on start, 14s apart, both punching the volume), and any rapid churn from self-update / profile-save / deploy. ShouldApply gains lastApplied+now params; future-dated stamp (clock moved back) can't wedge it. 2. Misleading log. ServiceSendHost.Suspend() hard-coded "interactive app present" on EVERY suspend, including plain shutdown - which sent this very investigation chasing phantom app-handovers. Suspend now takes a reason; the run-loop-ending path says so, only a real app yield says "interactive app present". readme: recommend "first start after boot" as the set-and-forget mode and explain that "every restart" also fires on routine internal restarts (and is now burst- guarded). Self-test extended: cooldown skip in both modes, 14s double-apply guard, future-stamp safety. Gate 71/71 + 7 relay tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,14 +17,35 @@ internal static class StartupVolume
|
||||
/// granularity, clock adjustments); a real reboot separates instants by minutes at least.</summary>
|
||||
internal static readonly TimeSpan SameBootTolerance = TimeSpan.FromMinutes(2);
|
||||
|
||||
/// <summary>Minimum gap between two volume applications, whatever the mode (2026-07-27). The
|
||||
/// service can restart in quick succession for reasons the user never asked for — a self-update
|
||||
/// (we saw two applies 14 s apart: the update restart, then a follow-on start), a profile save,
|
||||
/// a test deploy, the app handing back. "Every service restart" must not machine-gun the volume
|
||||
/// down on each of those, so a fresh apply inside this window is skipped. A genuine "I restarted
|
||||
/// the service to reset the volume" a few minutes later still applies; a reboot (boot-only) is
|
||||
/// unaffected. NOTE: this is a burst guard — the set-and-forget choice is "first start after
|
||||
/// boot", which applies once per boot and never re-punches.</summary>
|
||||
internal static readonly TimeSpan ReapplyCooldown = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>When THIS boot began (UTC), from the monotonic uptime counter.</summary>
|
||||
public static DateTime CurrentBootUtc() => DateTime.UtcNow - TimeSpan.FromMilliseconds(Environment.TickCount64);
|
||||
|
||||
/// <summary>Pure decision core, pinned by the self-test: apply when enabled, and — in boot-only
|
||||
/// mode — when the recorded marker belongs to a DIFFERENT boot (or there is no marker yet).</summary>
|
||||
internal static bool ShouldApply(bool enabled, bool bootOnly, DateTime? markerBootUtc, DateTime currentBootUtc)
|
||||
/// <summary>Pure decision core, pinned by the self-test. Never apply within the re-apply cooldown
|
||||
/// of the last successful apply (the burst guard — stops rapid/automatic restarts from re-punching
|
||||
/// the volume). Otherwise: boot-only mode applies only when the boot marker belongs to a DIFFERENT
|
||||
/// boot (or none yet); every-restart mode applies on any start past the cooldown.</summary>
|
||||
internal static bool ShouldApply(bool enabled, bool bootOnly, DateTime? markerBootUtc, DateTime currentBootUtc,
|
||||
DateTime? lastAppliedUtc, DateTime nowUtc)
|
||||
{
|
||||
if (!enabled) return false;
|
||||
// Burst guard first, both modes: a fresh apply within the cooldown of the last one is skipped.
|
||||
// Guard the negative case too (clock moved backwards) — treat only a positive, sub-cooldown
|
||||
// gap as "too soon"; anything else falls through to the normal decision.
|
||||
if (lastAppliedUtc is { } last)
|
||||
{
|
||||
var since = nowUtc - last;
|
||||
if (since >= TimeSpan.Zero && since < ReapplyCooldown) return false;
|
||||
}
|
||||
if (!bootOnly) return true;
|
||||
if (markerBootUtc is null) return true;
|
||||
return (currentBootUtc - markerBootUtc.Value).Duration() > SameBootTolerance;
|
||||
@@ -40,9 +61,13 @@ internal static class StartupVolume
|
||||
var (enabled, percent, bootOnly) = ServiceStore.LoadStartupVolume();
|
||||
if (!enabled) return;
|
||||
var boot = CurrentBootUtc();
|
||||
if (!ShouldApply(enabled, bootOnly, ServiceStore.LoadStartupVolumeBootMarker(), boot))
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
if (!ShouldApply(enabled, bootOnly, ServiceStore.LoadStartupVolumeBootMarker(), boot,
|
||||
ServiceStore.LoadStartupVolumeLastAppliedUtc(), nowUtc))
|
||||
{
|
||||
log?.Invoke("service: startup volume skipped — already applied this boot (boot-only mode)");
|
||||
log?.Invoke(bootOnly
|
||||
? "service: startup volume skipped — already applied this boot (boot-only mode)"
|
||||
: "service: startup volume skipped — applied within the last few minutes (burst guard; a restart just happened)");
|
||||
return;
|
||||
}
|
||||
var ok = SystemVolumeHelper.TrySetVolumeAndUnmute(percent);
|
||||
@@ -53,9 +78,13 @@ internal static class StartupVolume
|
||||
// Also into the always-on events log (not gated on the logging toggle): one line per
|
||||
// qualifying start, so "did it fire?" is answerable without turning full logging on.
|
||||
ServiceStore.AppendServiceEvent(outcome);
|
||||
// Marker only on success: a boot-time failure (audio stack not up yet) leaves the next
|
||||
// same-boot restart eligible to retry rather than silently never applying.
|
||||
if (ok) ServiceStore.SaveStartupVolumeBootMarker(boot);
|
||||
// Markers only on success: a boot-time failure (audio stack not up yet) leaves the next
|
||||
// restart eligible to retry. The last-applied stamp drives the burst guard for both modes.
|
||||
if (ok)
|
||||
{
|
||||
ServiceStore.SaveStartupVolumeBootMarker(boot);
|
||||
ServiceStore.SaveStartupVolumeLastAppliedUtc(nowUtc);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user