Skip to content
3 changes: 3 additions & 0 deletions src/Languages/lang_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,7 @@
"Do not automatically install updates when the network connection is metered": "Do not automatically install updates when the network connection is metered",
"Do not automatically install updates when the device runs on battery": "Do not automatically install updates when the device runs on battery",
"Do not automatically install updates when the battery saver is on": "Do not automatically install updates when the battery saver is on",
"Quit UniGetUI once automatic updates have finished, if its window is not open": "Quit UniGetUI once automatic updates have finished, if its window is not open",
"Only show updates that are at least the specified number of days old": "Only show updates that are at least the specified number of days old",
"Warn me when the installer URL host changes between the installed version and the new version (WinGet only)": "Warn me when the installer URL host changes between the installed version and the new version (WinGet only)",
"Change how UniGetUI handles install, update and uninstall operations.": "Change how UniGetUI handles install, update and uninstall operations.",
Expand Down Expand Up @@ -1203,6 +1204,8 @@
"Never": "Never",
"Scheduled maintenance": "Scheduled maintenance",
"Battery and metered connection restrictions, minimum update age, etc.": "Battery and metered connection restrictions, minimum update age, etc.",
"Minimum update age, installer host change warnings, etc.": "Minimum update age, installer host change warnings, etc.",
"Choose when updates are checked and installed, and when automatic installs are skipped or UniGetUI quits": "Choose when updates are checked and installed, and when automatic installs are skipped or UniGetUI quits",
"Backup destination, file name and cloud backup account.": "Backup destination, file name and cloud backup account.",
"Choose the days and hours when updates are checked and installed, and when backups are made": "Choose the days and hours when updates are checked and installed, and when backups are made",
"Choose the days and hours when the backups are performed.": "Choose the days and hours when the backups are performed.",
Expand Down
38 changes: 32 additions & 6 deletions src/UniGetUI.Avalonia/Infrastructure/AvaloniaAutoUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,26 @@ public static async Task UpdateCheckLoopAsync()
}
}

private static int _activeAttempts;
private static volatile bool _awaitingInstallConsent;

internal static bool IsBusy => Volatile.Read(ref _activeAttempts) > 0 && !_awaitingInstallConsent;
Comment thread
GabrielDuf marked this conversation as resolved.
Outdated

// ------------------------------------------------------------------ core logic
internal static async Task<bool> CheckAndInstallUpdatesAsync(bool autoLaunch = false, bool manualCheck = false)
{
Interlocked.Increment(ref _activeAttempts);
try
{
return await CheckAndInstallUpdatesCoreAsync(autoLaunch, manualCheck);
}
finally
{
Interlocked.Decrement(ref _activeAttempts);
}
}

private static async Task<bool> CheckAndInstallUpdatesCoreAsync(bool autoLaunch, bool manualCheck)
{
ResetUpdateLog(manualCheck, autoLaunch);
UpdaterOverrides overrides = LoadUpdaterOverrides();
Expand Down Expand Up @@ -639,15 +657,23 @@ private static async Task<bool> PrepareAndLaunchAsync(
}

// Wait until user requests install, clicks the toast, or the window is being closed
while (!_installRequested && !ReleaseLockForAutoupdate_Window && !ReleaseLockForAutoupdate_Notification)
_awaitingInstallConsent = true;
try
{
if (!manualCheck && Settings.Get(Settings.K.DisableAutoUpdateWingetUI))
while (!_installRequested && !ReleaseLockForAutoupdate_Window && !ReleaseLockForAutoupdate_Notification)
{
LogUpdateWarn("Auto-updater: disabled while waiting for user \u2014 aborting.");
MarkAttemptFinished("aborted - auto-update disabled while waiting");
return true;
if (!manualCheck && Settings.Get(Settings.K.DisableAutoUpdateWingetUI))
{
LogUpdateWarn("Auto-updater: disabled while waiting for user \u2014 aborting.");
MarkAttemptFinished("aborted - auto-update disabled while waiting");
return true;
}
await Task.Delay(500);
}
await Task.Delay(500);
}
finally
{
_awaitingInstallConsent = false;
}

LogUpdateInfo("Installing update \u2014 launching installer.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,7 @@ private static async Task RunPostOperationChecksAsync()
// Let all remaining operations settle before making decisions
await Task.Delay(500);

bool anyStillRunning = Operations.Any(
o => o.Status is OperationStatus.Running or OperationStatus.InQueue);

if (!anyStillRunning)
if (!HasActiveOperations())
{
var unknownShortcuts = GetPendingDesktopShortcuts();

Expand All @@ -337,6 +334,9 @@ private static async Task RunPostOperationChecksAsync()
}
}

public static bool HasActiveOperations()
=> Operations.Any(o => o.Status is OperationStatus.Running or OperationStatus.InQueue);
Comment thread
GabrielDuf marked this conversation as resolved.

public static void PromptPendingShortcutsIfAny()
{
if (!OperatingSystem.IsWindows()) return;
Expand Down
9 changes: 9 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/MaintenanceScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ public static bool IsAutoInstallDue()

public static void MarkAutoInstallHandled() => _pendingInstallSince = null;

public static bool IsAnyTaskRunning
{
get
{
lock (RunningTasks)
return RunningTasks.Count > 0;
}
}

public static bool ShouldRunAtAppStart(MaintenanceTaskKind kind)
{
var schedule = MaintenanceScheduleStore.Get(kind);
Expand Down
81 changes: 81 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/QuitAfterAutomaticUpdates.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using Avalonia.Threading;
using UniGetUI.Avalonia.Views;
using UniGetUI.Core.Logging;
using UniGetUI.Core.SettingsEngine;

namespace UniGetUI.Avalonia.Infrastructure;

internal static class QuitAfterAutomaticUpdates
{
private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(5);
private const int RequiredIdleChecks = 2;

private static int _waiting;

public static void QuitWhenOperationsFinish(Task? launching = null)
{
if (!Settings.Get(Settings.K.QuitAfterAutomaticUpdates))
return;

if (Interlocked.Exchange(ref _waiting, 1) == 1)
return;

_ = WaitAndQuitAsync(launching);
}

private static async Task WaitAndQuitAsync(Task? launching)
{
try
{
if (launching is not null)
await launching;

int idleChecks = 0;
while (idleChecks < RequiredIdleChecks)
{
await Task.Delay(PollInterval);
bool busy = await Dispatcher.UIThread.InvokeAsync(IsBusy);
idleChecks = busy ? 0 : idleChecks + 1;
}

await Dispatcher.UIThread.InvokeAsync(QuitIfStillInBackground);
Comment thread
GabrielDuf marked this conversation as resolved.
Outdated
}
catch (Exception ex)
{
Logger.Error(ex);
}
finally
{
Interlocked.Exchange(ref _waiting, 0);
}
}

private static bool IsBusy()
=> AvaloniaOperationRegistry.HasActiveOperations()
|| MaintenanceScheduler.IsAnyTaskRunning
Comment thread
GabrielDuf marked this conversation as resolved.
|| AvaloniaAutoUpdater.IsBusy;

private static void QuitIfStillInBackground()
{
if (!Settings.Get(Settings.K.QuitAfterAutomaticUpdates))
return;

if (MainWindow.Instance is not { } window || window.IsQuitting)
return;

if (window.IsVisible)
{
Logger.Info("Automatic updates have finished, but UniGetUI will not quit because its window is open");
return;
}

if (window.HasUnsavedWork)
{
Logger.Info("Automatic updates have finished, but UniGetUI will not quit because there is unsaved work");
return;
}

Logger.ImportantInfo("Automatic updates have finished, quitting UniGetUI as configured");
window.QuitApplication();
Comment thread
GabrielDuf marked this conversation as resolved.
Outdated
}
}
7 changes: 4 additions & 3 deletions src/UniGetUI.Avalonia/Infrastructure/SettingsSearchIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@ private sealed record Entry(
new("Check for package updates periodically", ["check for updates", "periodically"], typeof(Scheduler), null),
new("Check for updates every:", ["update frequency", "update interval"], typeof(Scheduler), null),
new("Install available updates automatically", ["automatic updates", "auto install updates"], typeof(Scheduler), null),
new("Do not automatically install updates when the network connection is metered", ["metered connection"], typeof(Updates), "AUPMeteredCard"),
new("Do not automatically install updates when the device runs on battery", ["battery"], typeof(Updates), "AUPBatteryCard"),
new("Do not automatically install updates when the battery saver is on", ["battery saver"], typeof(Updates), "AUPBatterySaverCard"),
new("Do not automatically install updates when the network connection is metered", ["metered connection"], typeof(Scheduler), "AUPMeteredCard"),
new("Do not automatically install updates when the device runs on battery", ["battery"], typeof(Scheduler), "AUPBatteryCard"),
new("Do not automatically install updates when the battery saver is on", ["battery saver"], typeof(Scheduler), "AUPBatterySaverCard"),
new("Quit UniGetUI once automatic updates have finished, if its window is not open", ["quit", "close", "exit after updates"], typeof(Scheduler), "QuitAfterAutomaticUpdatesCard"),
new("Minimum age for updates", ["minimum age", "update security"], typeof(Updates), "MinimumUpdateAgeSelector"),
new("Custom minimum age (days)", ["custom age"], typeof(Updates), "MinimumUpdateAgeCustomInput"),
new("Warn me when the installer URL host changes between the installed version and the new version (WinGet only)", ["installer host", "url host"], typeof(Updates), "InstallerHostWarningCard"),
Expand Down
4 changes: 4 additions & 0 deletions src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,16 @@
private void ToggleOperationsPanel() => OperationsPanelExpanded = !OperationsPanelExpanded;

[RelayCommand]
private void RetryFailedOperations() => AvaloniaOperationRegistry.RetryFailed();

Check warning on line 168 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'RetryFailedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

[RelayCommand]
private void ClearSuccessfulOperations() => AvaloniaOperationRegistry.ClearSuccessful();

Check warning on line 171 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearSuccessfulOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

[RelayCommand]
private void ClearFinishedOperations() => AvaloniaOperationRegistry.ClearFinished();

Check warning on line 174 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ClearFinishedOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

[RelayCommand]
private void CancelAllOperations() => AvaloniaOperationRegistry.CancelAll();

Check warning on line 177 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'CancelAllOperations' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

// ─── Sidebar ─────────────────────────────────────────────────────────────
public SidebarViewModel Sidebar { get; } = new();
Expand Down Expand Up @@ -923,6 +923,10 @@
return true;
}

public bool HasUnsavedWork
=> CurrentPageContent is IAsyncLeaveGuard { HasUnsavedWork: true }
|| CatalogEditor is { IsBusy: true } or { IsDirty: true };
Comment thread
GabrielDuf marked this conversation as resolved.
Outdated

public async Task<bool> CanShutdownAsync(CancellationToken cancellationToken = default)
{
await _navigationSemaphore.WaitAsync(cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using CommunityToolkit.Mvvm.Input;
using UniGetUI.Avalonia.Views.Pages.SettingsPages;
using UniGetUI.Core.Tools;
using UniGetUI.Core.Tools.Scheduling;
using UniGetUI.PackageEngine;
using UniGetUI.PackageEngine.ManagerClasses.Manager;
using CoreSettings = UniGetUI.Core.SettingsEngine.Settings;
Expand All @@ -18,7 +17,6 @@ public partial class UpdatesViewModel : ViewModelBase
{
public event EventHandler<Type>? NavigationRequested;

[ObservableProperty] private bool _isAutomaticUpdatesEnabled;
[ObservableProperty] private bool _isCustomAgeSelected;

/// <summary>Items for the minimum update age ComboboxCard, in display/value pairs.</summary>
Expand All @@ -40,7 +38,6 @@ public UpdatesViewModel()

public void RefreshState()
{
IsAutomaticUpdatesEnabled = MaintenanceScheduleStore.IsEnabled(MaintenanceTaskKind.InstallUpdates);
IsCustomAgeSelected = CoreSettings.GetValue(CoreSettings.K.MinimumUpdateAge) == "custom";
}

Expand Down
2 changes: 2 additions & 0 deletions src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2274,6 +2274,8 @@ public void ShowFromTray()

public bool IsQuitting => _shutdownCoordinator.IsQuitting;

internal bool HasUnsavedWork => ViewModel.HasUnsavedWork;

public void QuitApplication() => _ = RequestQuitApplicationAsync();

internal Task<bool> RequestQuitApplicationAsync(Action? onAuthorized = null) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ private async void OnOpenPolicyEditorRequested(object? sender, PolicyEditorLaunc
_ = _viewModel.RefreshPageCommand.ExecuteAsync(null);
}

public bool HasUnsavedWork => _activeEditorSession is not null;

public async Task<bool> CanLeaveAsync(PageLeaveReason reason, CancellationToken cancellationToken = default)
{
if (_activeEditorSession is null) return true;
Expand Down
34 changes: 33 additions & 1 deletion src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Scheduler.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,38 @@
</ItemsControl.ItemTemplate>
</ItemsControl>

<settings:TranslatedTextBlock Text="Automatic update restrictions"
FontSize="14"
FontWeight="SemiBold"
Margin="44,32,4,8"
automation:AutomationProperties.HeadingLevel="2"/>

<settings:CheckboxCard x:Name="AUPMeteredCard"
SettingName="DisableAUPOnMeteredConnections"
ForceInversion="True"
Text="{t:Translate Do not automatically install updates when the network connection is metered}"
CornerRadius="8,8,0,0"/>

<settings:CheckboxCard x:Name="AUPBatteryCard"
SettingName="DisableAUPOnBattery"
ForceInversion="True"
Text="{t:Translate Do not automatically install updates when the device runs on battery}"
BorderThickness="1,0,1,1"
CornerRadius="0"/>

<settings:CheckboxCard x:Name="AUPBatterySaverCard"
SettingName="DisableAUPOnBatterySaver"
ForceInversion="True"
Text="{t:Translate Do not automatically install updates when the battery saver is on}"
BorderThickness="1,0,1,1"
CornerRadius="0"/>

<settings:CheckboxCard x:Name="QuitAfterAutomaticUpdatesCard"
SettingName="QuitAfterAutomaticUpdates"
Text="{t:Translate Text='Quit UniGetUI once automatic updates have finished, if its window is not open'}"
BorderThickness="1,0,1,1"
CornerRadius="0,0,8,8"/>

<settings:TranslatedTextBlock Text="Related settings"
FontSize="14"
FontWeight="SemiBold"
Expand All @@ -314,7 +346,7 @@
IsClickEnabled="True"
Command="{Binding NavigateToUpdatesCommand}"
Header="{t:Translate Package update preferences}"
Description="{t:Translate Text='Battery and metered connection restrictions, minimum update age, etc.'}"/>
Description="{t:Translate Text='Minimum update age, installer host change warnings, etc.'}"/>

<settings:SettingsCard CornerRadius="0,0,8,8"
IsClickEnabled="True"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ private static void DisposePage(UserControl? page)
private MainWindowViewModel? GetMainWindowViewModel() =>
(TopLevel.GetTopLevel(this) is Window { DataContext: MainWindowViewModel vm }) ? vm : null;

public bool HasUnsavedWork => _currentContent is IAsyncLeaveGuard { HasUnsavedWork: true };

public async Task<bool> CanLeaveAsync(
PageLeaveReason reason,
CancellationToken cancellationToken = default)
Expand Down
31 changes: 1 addition & 30 deletions src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Updates.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,9 @@
CornerRadius="8"
Icon="SandClock"
Text="{t:Translate Scheduled maintenance}"
UnderText="{t:Translate Text='Choose the days and hours when updates are checked and installed'}"
UnderText="{t:Translate Text='Choose when updates are checked and installed, and when automatic installs are skipped or UniGetUI quits'}"
Command="{Binding NavigateToSchedulerCommand}"/>

<settings:TranslatedTextBlock Text="Automatic update restrictions"
FontSize="14"
FontWeight="SemiBold"
Margin="44,32,4,8"
automation:AutomationProperties.HeadingLevel="2"/>

<settings:CheckboxCard x:Name="AUPMeteredCard"
SettingName="DisableAUPOnMeteredConnections"
ForceInversion="True"
Text="{t:Translate Do not automatically install updates when the network connection is metered}"
CornerRadius="8,8,0,0"
IsEnabled="{Binding IsAutomaticUpdatesEnabled}"/>

<settings:CheckboxCard x:Name="AUPBatteryCard"
SettingName="DisableAUPOnBattery"
ForceInversion="True"
Text="{t:Translate Do not automatically install updates when the device runs on battery}"
BorderThickness="1,0,1,1"
CornerRadius="0"
IsEnabled="{Binding IsAutomaticUpdatesEnabled}"/>

<settings:CheckboxCard x:Name="AUPBatterySaverCard"
SettingName="DisableAUPOnBatterySaver"
ForceInversion="True"
Text="{t:Translate Do not automatically install updates when the battery saver is on}"
BorderThickness="1,0,1,1"
CornerRadius="0,0,8,8"
IsEnabled="{Binding IsAutomaticUpdatesEnabled}"/>

<settings:TranslatedTextBlock Text="Update security"
FontSize="14"
FontWeight="SemiBold"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public enum PageLeaveReason

public interface IAsyncLeaveGuard
{
bool HasUnsavedWork { get; }

Task<bool> CanLeaveAsync(
PageLeaveReason reason,
CancellationToken cancellationToken = default);
Expand Down
Loading
Loading