From 5a36a96c51fb3754e031bcf714906faaafcbe5f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Jun 2026 19:10:21 +0000 Subject: [PATCH 1/3] Fix auto-update interval handling --- .../Services/AutoUpdateServiceTests.cs | 27 +++++-- .../Services/Update/AutoUpdateService.cs | 70 +++++++++++++++---- Pointframe/Views/OverlayWindow.xaml.cs | 16 ++++- 3 files changed, 92 insertions(+), 21 deletions(-) diff --git a/Pointframe.Tests/Services/AutoUpdateServiceTests.cs b/Pointframe.Tests/Services/AutoUpdateServiceTests.cs index b499c453..b6287295 100644 --- a/Pointframe.Tests/Services/AutoUpdateServiceTests.cs +++ b/Pointframe.Tests/Services/AutoUpdateServiceTests.cs @@ -1,4 +1,5 @@ using System.Net.Http; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Pointframe.Models; @@ -21,7 +22,8 @@ private static AutoUpdateService CreateService( Mock updateService, Mock userSettings, Mock downloadService, - Mock messageBox) + Mock messageBox, + TimeSpan? settingsPollInterval = null) { return new AutoUpdateService( eventAggregator, @@ -30,7 +32,8 @@ private static AutoUpdateService CreateService( downloadService.Object, messageBox.Object, NullLogger.Instance, - Mock.Of()); + Mock.Of(), + settingsPollInterval); } private static Mock SettingsMock(UpdateCheckInterval interval = UpdateCheckInterval.EveryTwoHours) @@ -308,9 +311,10 @@ public async Task ExecuteAsync_IntervalNever_DoesNotUpdateLastCheckTimestamp() } [Fact] - public async Task ExecuteAsync_RuntimeIntervalChangeToNever_StopsFurtherChecks() + public async Task ExecuteAsync_RuntimeIntervalChangeToNever_StopsPeriodicLoopPromptly() { var callCount = 0; + var startupCheckCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var settings = new UserSettings { AutoUpdateCheckInterval = UpdateCheckInterval.EveryTwoHours, @@ -330,6 +334,7 @@ public async Task ExecuteAsync_RuntimeIntervalChangeToNever_StopsFurtherChecks() if (callCount == 1) { settings.AutoUpdateCheckInterval = UpdateCheckInterval.Never; + startupCheckCompleted.TrySetResult(); } return NoUpdate; @@ -340,16 +345,28 @@ public async Task ExecuteAsync_RuntimeIntervalChangeToNever_StopsFurtherChecks() updateService, settingsMock, new Mock(), - new Mock()); + new Mock(), + TimeSpan.FromMilliseconds(10)); await sut.StartAsync(CancellationToken.None); - await Task.Delay(100); + await startupCheckCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await GetExecuteTask(sut).WaitAsync(TimeSpan.FromSeconds(5)); await sut.StopAsync(CancellationToken.None); Assert.Equal(1, callCount); Assert.Equal(UpdateCheckInterval.Never, settings.AutoUpdateCheckInterval); } + private static Task GetExecuteTask(BackgroundService service) + { + var property = typeof(BackgroundService).GetProperty("ExecuteTask"); + Assert.NotNull(property); + + var executeTask = property.GetValue(service) as Task; + Assert.NotNull(executeTask); + return executeTask; + } + private sealed class UpdateAvailableRecorder { public TaskCompletionSource Received { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/Pointframe/Services/Update/AutoUpdateService.cs b/Pointframe/Services/Update/AutoUpdateService.cs index dd634787..6cc75631 100644 --- a/Pointframe/Services/Update/AutoUpdateService.cs +++ b/Pointframe/Services/Update/AutoUpdateService.cs @@ -5,6 +5,7 @@ namespace Pointframe.Services; public sealed class AutoUpdateService : BackgroundService, IAutoUpdateService { + private static readonly TimeSpan DefaultSettingsPollInterval = TimeSpan.FromSeconds(1); private readonly IEventAggregator _eventAggregator; private readonly IUpdateService _updateService; private readonly IUserSettingsService _userSettings; @@ -12,6 +13,7 @@ public sealed class AutoUpdateService : BackgroundService, IAutoUpdateService private readonly IMessageBoxService _messageBox; private readonly ILogger _logger; private readonly ITelemetryService _telemetry; + private readonly TimeSpan _settingsPollInterval; public AutoUpdateService( IEventAggregator eventAggregator, @@ -20,7 +22,8 @@ public AutoUpdateService( IUpdateDownloadService downloadService, IMessageBoxService messageBox, ILogger logger, - ITelemetryService telemetry) + ITelemetryService telemetry, + TimeSpan? settingsPollInterval = null) { _eventAggregator = eventAggregator; _updateService = updateService; @@ -29,6 +32,7 @@ public AutoUpdateService( _messageBox = messageBox; _logger = logger; _telemetry = telemetry; + _settingsPollInterval = settingsPollInterval.GetValueOrDefault(DefaultSettingsPollInterval); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -52,23 +56,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) UpdateLastCheckedUtc(); - var timerInterval = GetTimerInterval(interval); - _logger.LogInformation( - "Auto-update: periodic loop started (interval = {Interval}, every {IntervalTime})", - interval, - timerInterval); + _logger.LogInformation("Auto-update: periodic loop started"); try { - using var timer = new PeriodicTimer(timerInterval); - while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false)) + while (await WaitForNextCheckAsync(stoppingToken).ConfigureAwait(false)) { - if (_userSettings.Current.AutoUpdateCheckInterval == UpdateCheckInterval.Never) - { - _logger.LogInformation("Auto-update: periodic loop stopped (interval changed to Never)"); - break; - } - _logger.LogInformation("Auto-update: running periodic check"); try { @@ -134,6 +127,55 @@ private void UpdateLastCheckedUtc() }); } + private async Task WaitForNextCheckAsync(CancellationToken stoppingToken) + { + while (true) + { + var interval = _userSettings.Current.AutoUpdateCheckInterval; + if (interval == UpdateCheckInterval.Never) + { + _logger.LogInformation("Auto-update: periodic loop stopped (interval changed to Never)"); + return false; + } + + var timerInterval = GetTimerInterval(interval); + _logger.LogInformation( + "Auto-update: waiting for next check (interval = {Interval}, every {IntervalTime})", + interval, + timerInterval); + + var remaining = timerInterval; + while (remaining > TimeSpan.Zero) + { + var delay = remaining < _settingsPollInterval ? remaining : _settingsPollInterval; + await Task.Delay(delay, stoppingToken).ConfigureAwait(false); + + var currentInterval = _userSettings.Current.AutoUpdateCheckInterval; + if (currentInterval == UpdateCheckInterval.Never) + { + _logger.LogInformation("Auto-update: periodic loop stopped (interval changed to Never)"); + return false; + } + + if (currentInterval != interval) + { + _logger.LogInformation( + "Auto-update: periodic interval changed from {PreviousInterval} to {CurrentInterval}", + interval, + currentInterval); + break; + } + + remaining -= delay; + } + + if (remaining <= TimeSpan.Zero) + { + return true; + } + } + } + private static TimeSpan GetTimerInterval(UpdateCheckInterval interval) => interval switch { diff --git a/Pointframe/Views/OverlayWindow.xaml.cs b/Pointframe/Views/OverlayWindow.xaml.cs index 986a5995..884bab26 100644 --- a/Pointframe/Views/OverlayWindow.xaml.cs +++ b/Pointframe/Views/OverlayWindow.xaml.cs @@ -485,6 +485,12 @@ private async Task DoLassoOcr(Rect lassoRect) private async void ShowOcrToast(string message) { + if (!Dispatcher.CheckAccess()) + { + await Dispatcher.InvokeAsync(() => ShowOcrToast(message)); + return; + } + OcrToastText.Text = message; OcrToast.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); var toastSize = OcrToast.DesiredSize; @@ -502,8 +508,14 @@ private async void ShowOcrToast(string message) OcrToast.Visibility = Visibility.Visible; - await Task.Delay(1500); - OcrToast.Visibility = Visibility.Collapsed; + await Task.Delay(1500).ConfigureAwait(false); + + if (Dispatcher.HasShutdownStarted || Dispatcher.HasShutdownFinished) + { + return; + } + + await Dispatcher.InvokeAsync(() => OcrToast.Visibility = Visibility.Collapsed, DispatcherPriority.Normal); } private void PositionOcrToastNearActionBar(Size toastSize) From fcf3590e0d75225f70a871f7dee0bacb459f86dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Jun 2026 19:12:21 +0000 Subject: [PATCH 2/3] Address review feedback on PR 130 --- .../Services/AutoUpdateServiceTests.cs | 12 +---------- .../Services/Update/AutoUpdateService.cs | 4 +++- Pointframe/Views/OverlayWindow.xaml.cs | 20 ++++++++++++++++++- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Pointframe.Tests/Services/AutoUpdateServiceTests.cs b/Pointframe.Tests/Services/AutoUpdateServiceTests.cs index b6287295..7994d1df 100644 --- a/Pointframe.Tests/Services/AutoUpdateServiceTests.cs +++ b/Pointframe.Tests/Services/AutoUpdateServiceTests.cs @@ -350,23 +350,13 @@ public async Task ExecuteAsync_RuntimeIntervalChangeToNever_StopsPeriodicLoopPro await sut.StartAsync(CancellationToken.None); await startupCheckCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5)); - await GetExecuteTask(sut).WaitAsync(TimeSpan.FromSeconds(5)); + await sut.ExecuteTask!.WaitAsync(TimeSpan.FromSeconds(5)); await sut.StopAsync(CancellationToken.None); Assert.Equal(1, callCount); Assert.Equal(UpdateCheckInterval.Never, settings.AutoUpdateCheckInterval); } - private static Task GetExecuteTask(BackgroundService service) - { - var property = typeof(BackgroundService).GetProperty("ExecuteTask"); - Assert.NotNull(property); - - var executeTask = property.GetValue(service) as Task; - Assert.NotNull(executeTask); - return executeTask; - } - private sealed class UpdateAvailableRecorder { public TaskCompletionSource Received { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/Pointframe/Services/Update/AutoUpdateService.cs b/Pointframe/Services/Update/AutoUpdateService.cs index 6cc75631..3e8ac151 100644 --- a/Pointframe/Services/Update/AutoUpdateService.cs +++ b/Pointframe/Services/Update/AutoUpdateService.cs @@ -129,7 +129,7 @@ private void UpdateLastCheckedUtc() private async Task WaitForNextCheckAsync(CancellationToken stoppingToken) { - while (true) + while (!stoppingToken.IsCancellationRequested) { var interval = _userSettings.Current.AutoUpdateCheckInterval; if (interval == UpdateCheckInterval.Never) @@ -174,6 +174,8 @@ private async Task WaitForNextCheckAsync(CancellationToken stoppingToken) return true; } } + + return false; } private static TimeSpan GetTimerInterval(UpdateCheckInterval interval) => diff --git a/Pointframe/Views/OverlayWindow.xaml.cs b/Pointframe/Views/OverlayWindow.xaml.cs index 884bab26..487ca6e1 100644 --- a/Pointframe/Views/OverlayWindow.xaml.cs +++ b/Pointframe/Views/OverlayWindow.xaml.cs @@ -485,6 +485,11 @@ private async Task DoLassoOcr(Rect lassoRect) private async void ShowOcrToast(string message) { + if (Dispatcher.HasShutdownStarted || Dispatcher.HasShutdownFinished) + { + return; + } + if (!Dispatcher.CheckAccess()) { await Dispatcher.InvokeAsync(() => ShowOcrToast(message)); @@ -506,6 +511,11 @@ private async void ShowOcrToast(string message) Canvas.SetTop(OcrToast, selection.Y + ((selection.Height - toastSize.Height) / 2d)); } + if (Dispatcher.HasShutdownStarted || Dispatcher.HasShutdownFinished) + { + return; + } + OcrToast.Visibility = Visibility.Visible; await Task.Delay(1500).ConfigureAwait(false); @@ -515,7 +525,15 @@ private async void ShowOcrToast(string message) return; } - await Dispatcher.InvokeAsync(() => OcrToast.Visibility = Visibility.Collapsed, DispatcherPriority.Normal); + await Dispatcher.InvokeAsync(() => + { + if (Dispatcher.HasShutdownStarted || Dispatcher.HasShutdownFinished) + { + return; + } + + OcrToast.Visibility = Visibility.Collapsed; + }, DispatcherPriority.Normal); } private void PositionOcrToastNearActionBar(Size toastSize) From d42575ed972fb123280b121dab3a6573031d607f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Jun 2026 19:13:57 +0000 Subject: [PATCH 3/3] Polish PR 130 follow-up fixes --- Pointframe/Services/Update/AutoUpdateService.cs | 2 +- Pointframe/Views/OverlayWindow.xaml.cs | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/Pointframe/Services/Update/AutoUpdateService.cs b/Pointframe/Services/Update/AutoUpdateService.cs index 3e8ac151..76943638 100644 --- a/Pointframe/Services/Update/AutoUpdateService.cs +++ b/Pointframe/Services/Update/AutoUpdateService.cs @@ -147,7 +147,7 @@ private async Task WaitForNextCheckAsync(CancellationToken stoppingToken) var remaining = timerInterval; while (remaining > TimeSpan.Zero) { - var delay = remaining < _settingsPollInterval ? remaining : _settingsPollInterval; + var delay = TimeSpan.FromTicks(Math.Min(remaining.Ticks, _settingsPollInterval.Ticks)); await Task.Delay(delay, stoppingToken).ConfigureAwait(false); var currentInterval = _userSettings.Current.AutoUpdateCheckInterval; diff --git a/Pointframe/Views/OverlayWindow.xaml.cs b/Pointframe/Views/OverlayWindow.xaml.cs index 487ca6e1..0d2a842f 100644 --- a/Pointframe/Views/OverlayWindow.xaml.cs +++ b/Pointframe/Views/OverlayWindow.xaml.cs @@ -525,15 +525,7 @@ private async void ShowOcrToast(string message) return; } - await Dispatcher.InvokeAsync(() => - { - if (Dispatcher.HasShutdownStarted || Dispatcher.HasShutdownFinished) - { - return; - } - - OcrToast.Visibility = Visibility.Collapsed; - }, DispatcherPriority.Normal); + await Dispatcher.InvokeAsync(() => OcrToast.Visibility = Visibility.Collapsed, DispatcherPriority.Normal); } private void PositionOcrToastNearActionBar(Size toastSize)