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:
+5
-1
@@ -1047,6 +1047,8 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
|
||||
|
||||
<p>The feature is symmetric: both computers can both send and accept. If you set up hotkeys on both ends and tick “Accept remote volume commands” on both ends, either side can adjust the other's volume. There's no fixed “controller” and “controlled” computer.</p>
|
||||
|
||||
<p><strong>Security:</strong> remote commands are locked with the profile password, the same way the audio is. A command only works when both machines share the password, so nobody on the network can fake one and mute your machine — important when the machine's sound is also your screen reader. Both ends need version 5.6 or newer for remote volume to work; a command from an older version is ignored (the log records it as rejected).</p>
|
||||
|
||||
<h3>What it does not touch</h3>
|
||||
|
||||
<ul>
|
||||
@@ -1184,6 +1186,8 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
|
||||
<tr><td><strong>Then check every</strong> (drop-down)</td><td>Alt+U</td><td>How often RemSound checks for a newer version in the background <em>after</em> launch. Choices: <em>Never</em>, <em>Every hour</em>, <em>Every 6 hours</em>, <em>Every 24 hours</em>. The default is <em>Every 24 hours</em>. Your choice is remembered between launches; if you set it to <em>Never</em> and you've also unticked the startup check, the only way an update arrives is through the manual button below.</td></tr>
|
||||
<tr><td><strong>Check for updates now</strong> (button)</td><td>Alt+N</td><td>Checks for a newer version straight away. If you're already up to date you get a small popup saying so. If there's a newer version, you get a confirmation dialog with the release notes and a Yes / No to install. The same button is in the Help menu (Alt+H, C).</td></tr>
|
||||
<tr><td><strong>Silently install updates when available</strong> (checkbox)</td><td>Alt+I</td><td>When ticked, the background and startup checks install any available update without asking — RemSound downloads it, closes briefly, swaps the files, and reopens itself. Off by default. The startup check shows a brief notice first so you can see what's about to happen (see below). The <em>manual</em> “Check for updates now” button always asks first, no matter how this checkbox is set.</td></tr>
|
||||
<tr><td><strong>Only install updates within this time range</strong> (checkbox)</td><td>Alt+T</td><td>When ticked, <em>automatic</em> installs (the startup check and the background timer) only go ahead inside the daily time range you set below — so an update never closes RemSound and kills your sound while you're mid-session. If an update is found outside the range, RemSound notes it and quietly retries the moment the range opens. Off by default. The manual “Check for updates now” button ignores the range — if you ask by hand, you get it straight away.</td></tr>
|
||||
<tr><td><strong>Start time</strong> / <strong>End time</strong> (drop-downs)</td><td>Alt+A / Alt+D</td><td>The daily range for the checkbox above, in 15-minute steps from 00:00 to 23:45. They only light up when the range is enabled. The default is 01:00 to 06:00 — overnight. A range whose end is at or before its start runs past midnight: 22:00 to 06:00 covers late evening straight through to early morning. The start minute counts as inside the range; the end minute is outside it.</td></tr>
|
||||
<tr><td><strong>Show what's new after each update</strong> (checkbox)</td><td>Alt+H</td><td>When ticked, the first time RemSound opens after an update has installed, it pops up the About box — which starts with the notes for the version you just got — so you can see what changed. Off by default. It only happens once per update, never on an ordinary restart, and never on a fresh install.</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -1411,7 +1415,7 @@ RemSound.exe --connect 192.168.1.42
|
||||
<h3>Setting it up</h3>
|
||||
<p>Everything lives in the <strong>Service</strong> menu on the menu bar:</p>
|
||||
<ol>
|
||||
<li><strong>Configure service profile…</strong> — opens a small window with two tabs (Connectivity and Audio send) where you choose who to send to (plus a password) and what to send. On the Audio send tab, the first output choice is <strong>Use Windows default audio device, follows Windows changes</strong> — tick that to send whatever this machine is currently playing and keep following the Windows default if it later changes, rather than pinning one named card. (You can still pick specific devices, or a specific application, exactly as in the normal app.) There is no “send my audio” switch because the service always sends, and there is no audio-quality tab to fiddle with: the service always uses the settings that work best for live streaming (the Opus live-latency codec, small packets, locked to the audio clock), so it just sounds right. This is a separate profile from your normal ones and does not appear in the usual profile list. The <strong>Additional options</strong> button lets you turn the service's own log on or off. (The service itself never plays sounds — it streams silently in the background — so there are no cue options here.)</li>
|
||||
<li><strong>Configure service profile…</strong> — opens a small window with two tabs (Connectivity and Audio send) where you choose who to send to (plus a password) and what to send. On the Audio send tab, the first output choice is <strong>Use Windows default audio device, follows Windows changes</strong> — tick that to send whatever this machine is currently playing and keep following the Windows default if it later changes, rather than pinning one named card. (You can still pick specific devices, or a specific application, exactly as in the normal app.) There is no “send my audio” switch because the service always sends, and there is no audio-quality tab to fiddle with: the service always uses the settings that work best for live streaming (the Opus live-latency codec, small packets, locked to the audio clock), so it just sounds right. This is a separate profile from your normal ones and does not appear in the usual profile list. The <strong>Additional options</strong> button holds two extras. First, a switch for the service's own log. Second, <strong>Set the machine's volume when the service starts</strong>: tick it, pick a volume percent, and the service unmutes the machine and sets its Windows volume to that level when it starts — handy for an unattended PC that booted muted or was left turned down, so it's audible again with nobody at the keyboard. The <strong>When</strong> list chooses between <em>Only the first start after each boot</em> (the default — a mid-day service restart then leaves the volume alone while someone's using the machine) and <em>Every time the service starts</em>. Changes take effect from the service's next start, no reinstall needed. (The service itself never plays sounds — it streams silently in the background — so there are no cue options here.)</li>
|
||||
<li><strong>Install service</strong> — registers it with Windows so it starts automatically at every boot. Windows asks for administrator permission (one prompt). Do this once. Straight after installing, RemSound asks whether you'd like to <strong>start it now</strong> (otherwise it waits until the next reboot). (When you first install RemSound on a PC, the app installer also offers to set the service up — and start it — for you, so you may have done this already.)</li>
|
||||
<li><strong>Start service</strong> / <strong>Stop service</strong> — run or halt it now without waiting for a reboot.</li>
|
||||
<li><strong>Uninstall service</strong> — removes it entirely.</li>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -291,6 +291,28 @@ internal sealed class PreferencesDialog : Form
|
||||
AutoSize = true,
|
||||
};
|
||||
|
||||
// "Only install updates within this time range" (2026-07-26 feature): automatic installs are
|
||||
// deferred to a daily window — e.g. overnight — so an update never kills the sound while
|
||||
// someone's mid-session. Ticking enables the two time lists (hours in 15-minute steps).
|
||||
private readonly AccessibleCheckBox updateWindowBox = new()
|
||||
{
|
||||
Text = "Only install updates within this &time range",
|
||||
AccessibleName = "Only install updates within this time range",
|
||||
AutoSize = true,
|
||||
};
|
||||
private readonly ComboBox updateWindowStartBox = new()
|
||||
{
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
Width = 100,
|
||||
AccessibleName = "Update window start time",
|
||||
};
|
||||
private readonly ComboBox updateWindowEndBox = new()
|
||||
{
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
Width = 100,
|
||||
AccessibleName = "Update window end time",
|
||||
};
|
||||
|
||||
// After an update installs and RemSound restarts, opening the About box once lets the user
|
||||
// see what changed. Off by default (opt-in). 'h' mnemonic — 'w' is taken by "Write logs now".
|
||||
private readonly AccessibleCheckBox showWhatsNewAfterUpdateBox = new()
|
||||
@@ -719,6 +741,36 @@ internal sealed class PreferencesDialog : Form
|
||||
cfg.SilentlyInstallUpdates = silentlyInstallUpdatesBox.Checked;
|
||||
try { cfg.Save(); } catch { /* harmless */ }
|
||||
};
|
||||
|
||||
// Update install window: 96 quarter-hour slots in each list ("00:00" … "23:45"). The lists
|
||||
// only light up when the range is enabled; every change persists immediately like the
|
||||
// neighbouring update options. End at-or-before start wraps past midnight (22:00–06:00).
|
||||
for (var slot = 0; slot < UpdateWindow.SlotsPerDay; slot++)
|
||||
{
|
||||
var text = UpdateWindow.FormatMinutes(slot * UpdateWindow.SlotMinutes);
|
||||
updateWindowStartBox.Items.Add(text);
|
||||
updateWindowEndBox.Items.Add(text);
|
||||
}
|
||||
updateWindowBox.Checked = cfgForLoad.UpdateWindowEnabled;
|
||||
updateWindowStartBox.SelectedIndex = Math.Clamp(cfgForLoad.UpdateWindowStartMinutes / UpdateWindow.SlotMinutes, 0, UpdateWindow.SlotsPerDay - 1);
|
||||
updateWindowEndBox.SelectedIndex = Math.Clamp(cfgForLoad.UpdateWindowEndMinutes / UpdateWindow.SlotMinutes, 0, UpdateWindow.SlotsPerDay - 1);
|
||||
void SyncUpdateWindowEnabled()
|
||||
{
|
||||
updateWindowStartBox.Enabled = updateWindowBox.Checked;
|
||||
updateWindowEndBox.Enabled = updateWindowBox.Checked;
|
||||
}
|
||||
SyncUpdateWindowEnabled();
|
||||
void SaveUpdateWindow()
|
||||
{
|
||||
var cfg = AppConfig.Load();
|
||||
cfg.UpdateWindowEnabled = updateWindowBox.Checked;
|
||||
cfg.UpdateWindowStartMinutes = Math.Max(0, updateWindowStartBox.SelectedIndex) * UpdateWindow.SlotMinutes;
|
||||
cfg.UpdateWindowEndMinutes = Math.Max(0, updateWindowEndBox.SelectedIndex) * UpdateWindow.SlotMinutes;
|
||||
try { cfg.Save(); } catch { /* harmless */ }
|
||||
}
|
||||
updateWindowBox.CheckedChanged += (_, _) => { SyncUpdateWindowEnabled(); SaveUpdateWindow(); };
|
||||
updateWindowStartBox.SelectedIndexChanged += (_, _) => SaveUpdateWindow();
|
||||
updateWindowEndBox.SelectedIndexChanged += (_, _) => SaveUpdateWindow();
|
||||
showWhatsNewAfterUpdateBox.Checked = cfgForLoad.ShowWhatsNewAfterUpdate;
|
||||
showWhatsNewAfterUpdateBox.CheckedChanged += (_, _) =>
|
||||
{
|
||||
@@ -937,6 +989,23 @@ internal sealed class PreferencesDialog : Form
|
||||
freqRow.Controls.Add(updateFrequencyLabel);
|
||||
freqRow.Controls.Add(updateFrequencyBox);
|
||||
|
||||
// The install-window time range: start + end lists on one row, under the checkbox that
|
||||
// enables them. Labels carry the mnemonics and focus their list.
|
||||
var updateWindowRangeRow = new FlowLayoutPanel
|
||||
{
|
||||
AutoSize = true,
|
||||
Dock = DockStyle.Fill,
|
||||
FlowDirection = FlowDirection.LeftToRight,
|
||||
WrapContents = false,
|
||||
Padding = new Padding(0, 4, 0, 0),
|
||||
};
|
||||
var updateWindowStartLabel = new MnemonicLabel { Text = "St&art time:", MnemonicTarget = updateWindowStartBox, AutoSize = true, Padding = new Padding(0, 6, 8, 0) };
|
||||
var updateWindowEndLabel = new MnemonicLabel { Text = "En&d time:", MnemonicTarget = updateWindowEndBox, AutoSize = true, Padding = new Padding(12, 6, 8, 0) };
|
||||
updateWindowRangeRow.Controls.Add(updateWindowStartLabel);
|
||||
updateWindowRangeRow.Controls.Add(updateWindowStartBox);
|
||||
updateWindowRangeRow.Controls.Add(updateWindowEndLabel);
|
||||
updateWindowRangeRow.Controls.Add(updateWindowEndBox);
|
||||
|
||||
// Group the cue label + list + the two action buttons into a single panel that
|
||||
// occupies one row in the outer layout. The action buttons sit side-by-side under
|
||||
// the list so they read as "buttons that act on the list above" without taking up
|
||||
@@ -1035,7 +1104,7 @@ internal sealed class PreferencesDialog : Form
|
||||
tabs.TabPages.Add(MakeTab("Startup behaviour",
|
||||
startMinimisedBox, startWithUserBox, startWithProfileBox, startupListPanel));
|
||||
tabs.TabPages.Add(MakeTab("Update settings",
|
||||
checkForUpdatesOnStartupBox, freqRow, checkForUpdatesNowButton, silentlyInstallUpdatesBox, showWhatsNewAfterUpdateBox));
|
||||
checkForUpdatesOnStartupBox, freqRow, checkForUpdatesNowButton, silentlyInstallUpdatesBox, updateWindowBox, updateWindowRangeRow, showWhatsNewAfterUpdateBox));
|
||||
tabs.TabPages.Add(MakeTab("Logging",
|
||||
loggingBox, writeLogsNowButton,
|
||||
warnIfLogsExceedBox, logsSizeRow,
|
||||
|
||||
@@ -116,6 +116,7 @@ internal static class SelfTest
|
||||
RunStep(results, "Service folder lockdown args (cross-user LPE hardening)", ServiceDirHardeningArgs);
|
||||
RunStep(results, "Long-run hygiene (log rotation, crash-report cap, priority-mode scope)", LongRunHygiene);
|
||||
RunStep(results, "Service startup volume (boot-once decision + settings round-trip)", ServiceStartupVolume);
|
||||
RunStep(results, "Update install window (same-day, wraparound, retry timing)", UpdateInstallWindow);
|
||||
|
||||
var failed = results.Count(r => r.Status == "FAIL");
|
||||
var skipped = results.Count(r => r.Status == "SKIP");
|
||||
@@ -2375,6 +2376,39 @@ internal static class SelfTest
|
||||
return "sealed + replay/stale/wrong-key/plaintext all rejected; skew tolerated; nonces counter-based";
|
||||
}
|
||||
|
||||
/// <summary>The "only install updates within this time range" gate (2026-07-26 feature):
|
||||
/// same-day windows, past-midnight wraparound, boundary semantics (start in, end out),
|
||||
/// the empty-selection rule, the deferred-retry arithmetic, and the list text format.</summary>
|
||||
private static string? UpdateInstallWindow()
|
||||
{
|
||||
static TimeSpan At(int h, int m) => new(h, m, 0);
|
||||
const int t0100 = 60, t0600 = 360, t2200 = 1320;
|
||||
|
||||
// Same-day window 01:00–06:00.
|
||||
Check(UpdateWindow.IsWithin(At(3, 0), t0100, t0600), "03:00 is inside 01:00-06:00");
|
||||
Check(UpdateWindow.IsWithin(At(1, 0), t0100, t0600), "the start minute is INSIDE (inclusive)");
|
||||
Check(!UpdateWindow.IsWithin(At(6, 0), t0100, t0600), "the end minute is OUTSIDE (exclusive)");
|
||||
Check(!UpdateWindow.IsWithin(At(14, 30), t0100, t0600), "mid-afternoon is outside an overnight window");
|
||||
|
||||
// Wraparound 22:00–06:00 (end before start = spans midnight).
|
||||
Check(UpdateWindow.IsWithin(At(23, 30), t2200, t0600), "23:30 is inside 22:00-06:00 (wraps midnight)");
|
||||
Check(UpdateWindow.IsWithin(At(2, 0), t2200, t0600), "02:00 is inside 22:00-06:00 (the morning side)");
|
||||
Check(!UpdateWindow.IsWithin(At(12, 0), t2200, t0600), "noon is outside 22:00-06:00");
|
||||
|
||||
// start == end → no restriction, never a silent 'updates can never install' trap.
|
||||
Check(UpdateWindow.IsWithin(At(12, 0), t0100, t0100), "an empty range must mean unrestricted, not never");
|
||||
|
||||
// Deferred-retry arithmetic: at 14:30 the 01:00 window opens in 10.5 hours; at 00:30 in 30 min.
|
||||
Check(UpdateWindow.UntilNextStart(At(14, 30), t0100) == TimeSpan.FromMinutes(630), "14:30 → 01:00 is 10.5 h away");
|
||||
Check(UpdateWindow.UntilNextStart(At(0, 30), t0100) == TimeSpan.FromMinutes(30), "00:30 → 01:00 is 30 min away");
|
||||
|
||||
Check(UpdateWindow.FormatMinutes(0) == "00:00" && UpdateWindow.FormatMinutes(1425) == "23:45",
|
||||
"slot text runs 00:00 through 23:45");
|
||||
Check(UpdateWindow.SlotsPerDay == 96, "24 hours in 15-minute steps = 96 list entries");
|
||||
|
||||
return "same-day + wraparound exact; start in, end out; empty = unrestricted; retry timing right";
|
||||
}
|
||||
|
||||
/// <summary>The service's startup-volume option (2026-07-26 feature): the boot-once decision
|
||||
/// core (apply on the FIRST start after each boot, or every start), and the machine-wide
|
||||
/// settings round-trip — including that saving the volume option never clobbers the logging
|
||||
|
||||
@@ -278,6 +278,19 @@ public sealed class AppConfig
|
||||
/// RemSound. Default false — the user gets a confirmation dialog before each install.</summary>
|
||||
public bool SilentlyInstallUpdates { get; set; }
|
||||
|
||||
/// <summary>If true, AUTOMATIC update installs (background poll + startup check) only happen
|
||||
/// inside the daily local-time window below — so an update never kills someone's sound while
|
||||
/// they're using the machine; it lands overnight instead. Manual "Check for updates now" is
|
||||
/// not gated. Default false (house rule: persistence defaults off). See UpdateWindow (Core)
|
||||
/// for the semantics (start inclusive, end exclusive, wraps past midnight).</summary>
|
||||
public bool UpdateWindowEnabled { get; set; }
|
||||
|
||||
/// <summary>Window start, minutes-of-day local time, 15-minute granularity. Default 01:00.</summary>
|
||||
public int UpdateWindowStartMinutes { get; set; } = 60;
|
||||
|
||||
/// <summary>Window end, minutes-of-day local time, 15-minute granularity. Default 06:00.</summary>
|
||||
public int UpdateWindowEndMinutes { get; set; } = 360;
|
||||
|
||||
/// <summary>If true (the default), RemSound runs an update check shortly after launch in
|
||||
/// addition to whatever <see cref="UpdateCheckFrequency"/> drives in the background. The
|
||||
/// startup check is what catches users who quit and re-open the app within the polling
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
namespace RemSound.Core;
|
||||
|
||||
/// <summary>
|
||||
/// The "only install updates within this time range" option (Preferences → updates; feature request
|
||||
/// 2026-07-26). RemSound streams live audio, so an automatic update mid-session kills someone's
|
||||
/// sound; this gates AUTOMATIC installs (background poll + startup check) to a daily window — e.g.
|
||||
/// 01:00 to 06:00 — so updates land overnight. A manual "Check for updates now" is deliberately NOT
|
||||
/// gated: the user asking by hand knows what they're doing. Times are minutes-of-day in local time,
|
||||
/// picked from 15-minute slots; a window whose end is at or before its start wraps past midnight
|
||||
/// (22:00 → 06:00 covers late evening THROUGH early morning).
|
||||
/// </summary>
|
||||
public static class UpdateWindow
|
||||
{
|
||||
public const int SlotMinutes = 15;
|
||||
public const int SlotsPerDay = 24 * 60 / SlotMinutes; // 96
|
||||
|
||||
/// <summary>"HH:mm" for a minutes-of-day value (the Preferences list text).</summary>
|
||||
public static string FormatMinutes(int minutesOfDay) => $"{minutesOfDay / 60:D2}:{minutesOfDay % 60:D2}";
|
||||
|
||||
/// <summary>Is this local time-of-day inside the window? Start inclusive, end exclusive.
|
||||
/// start == end means an empty selection — treated as "no restriction" rather than "never",
|
||||
/// because a window that silently blocks every update forever is a support trap.</summary>
|
||||
public static bool IsWithin(TimeSpan timeOfDay, int startMinutes, int endMinutes)
|
||||
{
|
||||
var m = (int)timeOfDay.TotalMinutes;
|
||||
if (startMinutes == endMinutes) return true;
|
||||
return startMinutes < endMinutes
|
||||
? m >= startMinutes && m < endMinutes // same-day window (01:00–06:00)
|
||||
: m >= startMinutes || m < endMinutes; // wraps midnight (22:00–06:00)
|
||||
}
|
||||
|
||||
/// <summary>How long from this time-of-day until the window next OPENS — drives the deferred
|
||||
/// retry timer, so an update found outside the window installs when the window starts instead
|
||||
/// of waiting for the next (possibly 24-hourly) poll to happen to land inside it.</summary>
|
||||
public static TimeSpan UntilNextStart(TimeSpan timeOfDay, int startMinutes)
|
||||
{
|
||||
var mins = startMinutes - timeOfDay.TotalMinutes;
|
||||
if (mins <= 0) mins += 24 * 60;
|
||||
return TimeSpan.FromMinutes(mins);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user