Service startup volume: unmute + set level on boot or on every service start
Feature (requested 2026-07-26): Additional service options gains 'Set the machine's volume when the service starts' - a checkbox, a percent field (0-100, also unmutes), and a WHEN list: 'Only the first start after each boot' (default) or 'Every time the service starts'. For an unattended machine that boots muted or turned down, the service makes it audible again with nobody at the keyboard; boot-only mode means a mid-day manual service restart never blasts the volume while someone's using the box. Mechanics: settings live machine-wide beside the service logging flag (the service reads them fresh each start - no restart needed to change them); boot identity comes from now-minus-uptime persisted in a marker file, so 'first start after boot' survives same-boot service restarts and re-fires after a real reboot; the marker is only written on a SUCCESSFUL apply, so a boot-time audio-stack race retries on the next qualifying start. Applies via the same endpoint-volume helper the remote-control commands use (master endpoint volume is device-global, so session 0 works). Outcome logged to the always-on service events log. The new round-trip test immediately caught SaveLoggingEnabled clobbering the volume fields in the shared settings file - rewired both savers to load-modify-save. The dialog audit picks up the new controls (mnemonics + names) automatically. Gate 65/65. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
using RemSound.Core;
|
||||
|
||||
namespace RemSound.App;
|
||||
|
||||
/// <summary>
|
||||
/// The service's "unmute the machine and set volume to X% when the service starts" option
|
||||
/// (Additional service options; feature request 2026-07-26). Two timing modes, chosen by a list in
|
||||
/// the dialog: only the FIRST service start after each boot (the default — a mid-day manual
|
||||
/// restart then never blasts the volume back up while someone is using the machine), or EVERY
|
||||
/// service start. Boot identity comes from the system uptime clock, persisted in a marker file, so
|
||||
/// "first start after boot" survives same-boot service restarts and still re-fires after a reboot.
|
||||
/// </summary>
|
||||
internal static class StartupVolume
|
||||
{
|
||||
/// <summary>Two boot instants within this window are the SAME boot. Generous: the instant is
|
||||
/// computed from now-minus-uptime, which drifts a little between computations (timer
|
||||
/// granularity, clock adjustments); a real reboot separates instants by minutes at least.</summary>
|
||||
internal static readonly TimeSpan SameBootTolerance = TimeSpan.FromMinutes(2);
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (!enabled) return false;
|
||||
if (!bootOnly) return true;
|
||||
if (markerBootUtc is null) return true;
|
||||
return (currentBootUtc - markerBootUtc.Value).Duration() > SameBootTolerance;
|
||||
}
|
||||
|
||||
/// <summary>Called from the service's OnStart. Reads the option, decides, applies via the same
|
||||
/// endpoint-volume helper the remote-control commands use, and records the boot marker on a
|
||||
/// successful apply. Never throws — a volume hiccup must not stop the service starting.</summary>
|
||||
public static void ApplyIfConfigured(Action<string>? log)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (enabled, percent, bootOnly) = ServiceStore.LoadStartupVolume();
|
||||
if (!enabled) return;
|
||||
var boot = CurrentBootUtc();
|
||||
if (!ShouldApply(enabled, bootOnly, ServiceStore.LoadStartupVolumeBootMarker(), boot))
|
||||
{
|
||||
log?.Invoke("service: startup volume skipped — already applied this boot (boot-only mode)");
|
||||
return;
|
||||
}
|
||||
var ok = SystemVolumeHelper.TrySetVolumeAndUnmute(percent);
|
||||
var outcome = ok
|
||||
? $"startup volume applied — default output set to {percent}% and unmuted ({(bootOnly ? "first start after boot" : "every service start")})"
|
||||
: "startup volume FAILED — could not reach the default output device (will retry on the next qualifying start)";
|
||||
log?.Invoke($"service: {outcome}");
|
||||
// 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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log?.Invoke($"service: startup volume error {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user