Update install window: automatic updates only within a chosen daily time range

Feature (requested 2026-07-26): Preferences' update settings gain 'Only install
updates within this time range' - a checkbox plus Start/End time lists in 15-minute
steps (00:00-23:45, default 01:00-06:00). RemSound streams live audio, so an
automatic update mid-session kills someone's sound; with the range on, the startup
check and the background poll DEFER an available update until the range opens (a
one-shot timer retries right at the range start, so a 24-hourly poll can't keep
missing the window for days). Manual 'Check for updates now' is deliberately never
gated - asking by hand means now. End at-or-before start wraps past midnight
(22:00-06:00); start minute inclusive, end exclusive; an equal start/end means
unrestricted rather than a silent never-install trap. The service inherits the
window for free: its self-update follows the app's install.

readme: the two new Preferences rows, the service startup-volume option, and a
security note on password-locked remote volume (both ends need 5.6).

New gate step pins the window maths (same-day, wraparound, boundaries, empty-range
rule, retry arithmetic). Dialog audit covers the new controls. Gate 66/66.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-26 23:52:21 +01:00
co-authored by Claude Fable 5
parent bb94a56109
commit 9574d9d08b
6 changed files with 203 additions and 2 deletions
+40
View File
@@ -1425,6 +1425,7 @@ public sealed partial class MainForm : Form
deviceRefreshTimer.Stop(); deviceRefreshTimer.Dispose();
continuousTuneTimer.Stop(); continuousTuneTimer.Dispose();
updateCheckTimer.Stop(); updateCheckTimer.Dispose();
deferredUpdateTimer.Stop(); deferredUpdateTimer.Dispose();
asioDriverChangeDebounce.Stop(); asioDriverChangeDebounce.Dispose();
try { sendAppsReconcileTimer?.Stop(); sendAppsReconcileTimer?.Dispose(); } catch { }
DisposeSessionStartWatcher();
@@ -3138,6 +3139,7 @@ public sealed partial class MainForm : Form
// via the updater's Log callback.
if (result is not UpdateAvailable available) return;
var info = available.Info;
if (AutoInstallDeferredByWindow(info.Tag, "background poll")) return;
if (AppConfig.Load().SilentlyInstallUpdates)
{
// Notice the user before the app vanishes and the helper takes over. Hidden from
@@ -3198,6 +3200,7 @@ public sealed partial class MainForm : Form
if (result is not UpdateAvailable available) return;
var info = available.Info;
logFile.Event($"updater: startup check found {info.Tag}");
if (AutoInstallDeferredByWindow(info.Tag, "startup check")) return;
if (AppConfig.Load().SilentlyInstallUpdates)
{
// Heads-up the user before we exit and the helper takes over. The notice is its
@@ -3294,6 +3297,43 @@ public sealed partial class MainForm : Form
return s[..max] + "\n…";
}
// One-shot retry for an update found OUTSIDE the install window: without it, a 24-hourly
// poll could keep landing outside the window and defer the same update for days. Armed by
// AutoInstallDeferredByWindow to fire shortly after the window next opens.
private readonly System.Windows.Forms.Timer deferredUpdateTimer = new();
private bool deferredUpdateTimerWired;
/// <summary>The "only install updates within this time range" gate (Preferences). Applies to
/// AUTOMATIC installs only — the background poll and the startup check; a manual "Check for
/// updates now" is the user asking by hand and is never gated. When the window is closed,
/// logs, arms the retry for when it opens, and returns true (caller bails out).</summary>
private bool AutoInstallDeferredByWindow(string tag, string source)
{
var cfg = AppConfig.Load();
if (!cfg.UpdateWindowEnabled) return false;
var now = DateTime.Now.TimeOfDay;
if (UpdateWindow.IsWithin(now, cfg.UpdateWindowStartMinutes, cfg.UpdateWindowEndMinutes)) return false;
var wait = UpdateWindow.UntilNextStart(now, cfg.UpdateWindowStartMinutes) + TimeSpan.FromMinutes(1);
if (!deferredUpdateTimerWired)
{
deferredUpdateTimerWired = true;
deferredUpdateTimer.Tick += (_, _) =>
{
deferredUpdateTimer.Stop();
logFile.Event("updater: install window opened — re-running the deferred update check");
CheckForUpdatesInBackground();
};
}
deferredUpdateTimer.Stop();
deferredUpdateTimer.Interval = (int)Math.Clamp(wait.TotalMilliseconds, 60_000, int.MaxValue);
deferredUpdateTimer.Start();
logFile.Event($"updater: {source} found {tag}, but it's outside the install window "
+ $"({UpdateWindow.FormatMinutes(cfg.UpdateWindowStartMinutes)}{UpdateWindow.FormatMinutes(cfg.UpdateWindowEndMinutes)}) "
+ $"— deferred; retrying in {wait.TotalMinutes:0} min when the window opens");
return true;
}
/// <summary>Apply (or stop) the background update-poll timer based on
/// <see cref="AppConfig.UpdateCheckFrequency"/>. Called at startup and whenever the user
/// changes the dropdown in Preferences. The first tick fires after one interval — we