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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user