From 78c5043a7fa31f6be6cebfbb537d94ab5fabf6f6 Mon Sep 17 00:00:00 2001 From: PrimeBuild-pc Date: Sat, 1 Aug 2026 13:38:31 +0200 Subject: [PATCH] Polish Windows 11 tweaks and add one-click updates --- Installer/setup.iss | 11 +- Locales/de-DE.xaml | 4 +- Locales/en-US.xaml | 4 +- Locales/es-ES.xaml | 4 +- Locales/fr-FR.xaml | 4 +- Locales/it-IT.xaml | 4 +- Locales/ru-RU.xaml | 4 +- Locales/zh-CN.xaml | 4 +- MainWindow.Behaviors.partial.cs | 43 +- MainWindow.xaml | 5 +- Models/ApplicationSettingsModel.cs | 9 - Models/Core/IModel.cs | 47 -- Services/ApplicationSettingsService.cs | 10 - Services/Core/BaseSystemService.cs | 105 ----- Services/Core/ISystemService.cs | 29 -- Services/ISystemTweaksService.cs | 9 - Services/SystemTweaksService.cs | 431 +++++++----------- Services/UpdateInstallerService.cs | 12 +- Services/UpdateService.cs | 17 +- .../ApplicationSettingsModelTests.cs | 1 - .../PowerPlanViewXamlTests.cs | 6 +- .../ProcessViewXamlBindingTests.cs | 18 +- .../SystemTweaksServiceTests.cs | 7 +- .../SystemTweaksStartupAndStyleTests.cs | 79 ++++ .../SystemTweaksViewModelTests.cs | 38 +- .../ThemeDictionaryPolicyTests.cs | 5 +- .../UpdateServiceTests.cs | 60 ++- Themes/FluentDark.xaml | 10 +- Themes/FluentLight.xaml | 10 +- ThreadPilot.csproj | 13 +- ViewModels/SettingsViewModel.cs | 2 +- ViewModels/SystemTweaksViewModel.cs | 188 ++++---- Views/MasksView.xaml | 4 +- Views/PowerPlanView.xaml | 10 +- Views/ProcessView.xaml | 4 +- Views/SettingsView.xaml | 19 - Views/SystemTweaksView.xaml | 100 +--- Views/SystemTweaksView.xaml.cs | 20 +- 38 files changed, 536 insertions(+), 814 deletions(-) delete mode 100644 Services/Core/BaseSystemService.cs delete mode 100644 Services/Core/ISystemService.cs create mode 100644 Tests/ThreadPilot.Core.Tests/SystemTweaksStartupAndStyleTests.cs diff --git a/Installer/setup.iss b/Installer/setup.iss index 2300ee8..3d7ef3a 100644 --- a/Installer/setup.iss +++ b/Installer/setup.iss @@ -61,7 +61,11 @@ Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon -; Intentionally do not auto-launch after setup to keep package-manager installs unattended. +; Normal/package-manager installs stay unattended. In-app updates opt in through +; /THREADPILOTUPDATE=1 and relaunch ThreadPilot after the replacement completes. +[Run] +Filename: "{app}\{#MyAppExeName}"; Flags: nowait; Check: IsThreadPilotUpdate + ; ThreadPilot user data is preserved during install/update. Inno removes installed ; files and shortcuts automatically only when the generated uninstaller runs. ; Per-user AppData cleanup is limited to the account context used by uninstall. @@ -108,3 +112,8 @@ begin DeleteLegacyBetaUninstallEntry(HKCU); Result := True; end; + +function IsThreadPilotUpdate(): Boolean; +begin + Result := CompareText(ExpandConstant('{param:THREADPILOTUPDATE|0}'), '1') = 0; +end; diff --git a/Locales/de-DE.xaml b/Locales/de-DE.xaml index 2a35991..d8c2875 100644 --- a/Locales/de-DE.xaml +++ b/Locales/de-DE.xaml @@ -489,7 +489,7 @@ Nicht überprüft Niemals Installieren Sie das ThreadPilot-Update - ThreadPilot lädt Version {0} herunter und überprüft sie. Anschließend fordert Windows die Berechtigung zum Starten des Installationsprogramms an. Fortfahren? + ThreadPilot lädt Version {0} herunter, überprüft und installiert sie und startet anschließend automatisch neu. Fortfahren? Update abgebrochen. Update wird heruntergeladen und überprüft... Update-Installer gestartet. @@ -530,8 +530,6 @@ Steuert CPU core parking für die Energieverwaltung C-States Steuert CPU C-States für die Energieverwaltung - HPET - Hochpräziser Ereigniszeitgeber für die Systemzeitmessung SysMain-Dienst Windows Superfetch/SysMain Dienst für die Speicherverwaltung Prefetch diff --git a/Locales/en-US.xaml b/Locales/en-US.xaml index 14b5b36..aed61d7 100644 --- a/Locales/en-US.xaml +++ b/Locales/en-US.xaml @@ -489,7 +489,7 @@ Not checked Never Install ThreadPilot update - ThreadPilot will download and verify version {0}, then ask Windows for permission to run the installer. Continue? + ThreadPilot will download, verify and install version {0}, then restart automatically. Continue? Update canceled. Downloading and verifying update... Update installer started. @@ -530,8 +530,6 @@ Controls CPU core parking for power management C-States Controls CPU C-States for power management - HPET - High Precision Event Timer for system timing SysMain Service Windows Superfetch/SysMain service for memory management Prefetch diff --git a/Locales/es-ES.xaml b/Locales/es-ES.xaml index 184a2d4..b90315f 100644 --- a/Locales/es-ES.xaml +++ b/Locales/es-ES.xaml @@ -489,7 +489,7 @@ No comprobado nunca Instalar la actualización ThreadPilot - ThreadPilot descargará y verificará la versión {0}, y luego solicitará permiso a Windows para ejecutar el instalador. ¿Continuar? + ThreadPilot descargará, verificará e instalará la versión {0} y se reiniciará automáticamente. ¿Continuar? Actualización cancelada. Descargando y verificando actualización... Se inició el instalador de la actualización. @@ -530,8 +530,6 @@ Controles CPU core parking para administración de energía C-States Controles CPU C-States para administración de energía - HPET - Temporizador de eventos de alta precisión para la temporización del sistema SysMain Servicio Windows Superfetch/SysMain servicio para gestión de memoria Prefetch diff --git a/Locales/fr-FR.xaml b/Locales/fr-FR.xaml index ce430f6..3327c95 100644 --- a/Locales/fr-FR.xaml +++ b/Locales/fr-FR.xaml @@ -489,7 +489,7 @@ Non vérifié Jamais Installer la mise à jour ThreadPilot - ThreadPilot téléchargera et vérifiera la version {0}, puis demandera à Windows l'autorisation d'exécuter le programme d'installation. Continuer ? + ThreadPilot téléchargera, vérifiera et installera la version {0}, puis redémarrera automatiquement. Continuer ? Mise à jour annulée. Téléchargement et vérification de la mise à jour... Le programme d'installation de la mise à jour a démarré. @@ -530,8 +530,6 @@ Contrôle CPU core parking pour la gestion de l'alimentation C-States Contrôle CPU C-States pour la gestion de l'alimentation - HPET - Minuteur d'événements haute précision pour la synchronisation du système Service SysMain Service Windows Superfetch/SysMain pour la gestion de la mémoire Prefetch diff --git a/Locales/it-IT.xaml b/Locales/it-IT.xaml index 31df58b..721469c 100644 --- a/Locales/it-IT.xaml +++ b/Locales/it-IT.xaml @@ -489,7 +489,7 @@ Non controllato Mai Installa l'aggiornamento ThreadPilot - ThreadPilot scaricherà e verificherà la versione {0}, quindi chiederà a Windows l'autorizzazione per eseguire il programma di installazione. Continuare? + ThreadPilot scaricherà, verificherà e installerà la versione {0}, quindi si riavvierà automaticamente. Continuare? Aggiornamento annullato. Download e verifica dell'aggiornamento in corso... Avviato il programma di installazione dell'aggiornamento. @@ -530,8 +530,6 @@ Controlla CPU core parking per la gestione dell'alimentazione C-States Controlla CPU C-States per la gestione dell'alimentazione - HPET - Timer eventi ad alta precisione per la temporizzazione del sistema SysMain Servizio Windows Superfetch/SysMain servizio per la gestione della memoria Prefetch diff --git a/Locales/ru-RU.xaml b/Locales/ru-RU.xaml index 2edcd9e..8d554a3 100644 --- a/Locales/ru-RU.xaml +++ b/Locales/ru-RU.xaml @@ -489,7 +489,7 @@ Не проверено Никогда Установите обновление ThreadPilot - ThreadPilot загрузит и проверит версию {0}, затем запросит у Windows разрешение на запуск установщика. Продолжить? + ThreadPilot загрузит, проверит и установит версию {0}, а затем автоматически перезапустится. Продолжить? Обновление отменено. Загрузка и проверка обновления... Установщик обновлений запустился. @@ -530,8 +530,6 @@ Элементы управления CPU core parking для управления питанием C-States Элементы управления CPU C-States для управления питанием - HPET - Высокоточный таймер событий для системной синхронизации SysMain Сервис Windows Superfetch/SysMain сервис управления памятью Prefetch diff --git a/Locales/zh-CN.xaml b/Locales/zh-CN.xaml index b5ae622..b3170fa 100644 --- a/Locales/zh-CN.xaml +++ b/Locales/zh-CN.xaml @@ -489,7 +489,7 @@ 尚未检查 从未检查 安装 ThreadPilot 更新 - ThreadPilot 将下载并验证版本 {0},然后请求 Windows 权限运行安装程序。是否继续? + ThreadPilot 将下载、验证并安装版本 {0},然后自动重启。是否继续? 更新已取消。 正在下载并验证更新... 更新安装程序已启动。 @@ -530,8 +530,6 @@ 控制 CPU 核心休眠以进行电源管理 C-States 状态 控制 CPU C-States 以进行节能电源管理 - HPET 定时器 - 高精度事件计时器,用于系统精准定时 SysMain 服务 Windows Superfetch/SysMain 服务,用于内存管理 预取 (Prefetch) diff --git a/MainWindow.Behaviors.partial.cs b/MainWindow.Behaviors.partial.cs index 259683c..5416363 100644 --- a/MainWindow.Behaviors.partial.cs +++ b/MainWindow.Behaviors.partial.cs @@ -156,10 +156,40 @@ private async Task CheckForUpdatesAtStartupAsync() return; } - await this.notificationService.ShowNotificationAsync( - "Update available", - $"ThreadPilot {result.Release.Version} is available. Open Settings to download and install it.", - NotificationType.Information); + var title = Application.Current.TryFindResource("Settings_UpdateConfirmTitle") as string + ?? "Install ThreadPilot update"; + var messageFormat = Application.Current.TryFindResource("Settings_UpdateConfirmMessageFormat") as string + ?? "ThreadPilot will download, verify and install version {0}, then restart automatically. Continue?"; + var confirmation = MessageBox.Show( + this, + string.Format(messageFormat, result.Release.Version), + title, + MessageBoxButton.YesNo, + MessageBoxImage.Information); + + if (confirmation != MessageBoxResult.Yes) + { + this.LogDebug($"Startup update declined: {result.Release.Version}"); + return; + } + + var downloadingMessage = Application.Current.TryFindResource("Settings_StatusDownloadingUpdate") as string + ?? "Downloading and verifying update..."; + await this.notificationService.ShowNotificationAsync(title, downloadingMessage, NotificationType.Information); + + var installResult = await updateService.DownloadAndInstallAsync(result.Release); + if (installResult.Status != UpdateInstallStatus.Started) + { + var failureFormat = Application.Current.TryFindResource("Settings_StatusUpdateInstallFailedFormat") as string + ?? "Update install failed: {0}"; + MessageBox.Show( + this, + string.Format(failureFormat, installResult.Message), + title, + MessageBoxButton.OK, + MessageBoxImage.Error); + } + this.LogDebug($"Startup update check found update: installed {result.CurrentVersion}, latest {result.Release.Version}"); } catch (Exception ex) @@ -402,6 +432,11 @@ private async Task LoadViewModelsAsync() this.LogDebug($"ProcessViewModel fallback (LoadProcesses only) completed after exception, process count: {this.processViewModel.Processes?.Count ?? 0}, filtered count: {this.processViewModel.FilteredProcesses?.Count ?? 0}"); } + this.LogDebug("Detecting persisted system tweak state..."); + await this.systemTweaksViewModel.LoadAsync(); + this.initializedSections.Add("Tweaks"); + this.LogDebug("System tweak state detected successfully"); + this.LogDebug("Skipping inactive view initialization until each page is opened."); // Initialize keyboard shortcuts after window is loaded diff --git a/MainWindow.xaml b/MainWindow.xaml index dbd7e8a..5190960 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -98,13 +98,10 @@ - - - - + diff --git a/Models/ApplicationSettingsModel.cs b/Models/ApplicationSettingsModel.cs index c3fb5d0..7a84e38 100644 --- a/Models/ApplicationSettingsModel.cs +++ b/Models/ApplicationSettingsModel.cs @@ -137,9 +137,6 @@ public partial class ApplicationSettingsModel : ObservableObject, IModel [ObservableProperty] private DateTimeOffset? lastUpdateCheckUtc = null; - [ObservableProperty] - private int updateCheckIntervalDays = 7; - [ObservableProperty] private bool includePrereleaseUpdates = false; @@ -238,7 +235,6 @@ public void CopyFrom(ApplicationSettingsModel other) this.Language = LocalizationService.NormalizeLanguage(other.Language); this.EnableAutomaticUpdateChecks = other.EnableAutomaticUpdateChecks; this.LastUpdateCheckUtc = other.LastUpdateCheckUtc; - this.UpdateCheckIntervalDays = other.UpdateCheckIntervalDays; this.IncludePrereleaseUpdates = other.IncludePrereleaseUpdates; // Monitoring Settings @@ -285,11 +281,6 @@ public ValidationResult Validate() errors.Add("Fallback polling interval must be between 1 and 60 seconds"); } - if (this.UpdateCheckIntervalDays < 1 || this.UpdateCheckIntervalDays > 365) - { - errors.Add("Update check interval must be between 1 and 365 days"); - } - return errors.Count == 0 ? ValidationResult.Success() : ValidationResult.Failure(errors.ToArray()); } diff --git a/Models/Core/IModel.cs b/Models/Core/IModel.cs index d74a6ea..5055004 100644 --- a/Models/Core/IModel.cs +++ b/Models/Core/IModel.cs @@ -32,52 +32,5 @@ public ValidationResult(bool isValid, params string[] errors) public static ValidationResult Failure(params string[] errors) => new(false, errors); } - - public abstract class BaseModel : IModel - { - public string Id { get; protected set; } - - public DateTime CreatedAt { get; protected set; } - - public DateTime UpdatedAt { get; protected set; } - - public event PropertyChangedEventHandler? PropertyChanged; - - protected BaseModel() - { - this.Id = Guid.NewGuid().ToString(); - this.CreatedAt = DateTime.UtcNow; - this.UpdatedAt = DateTime.UtcNow; - } - - protected BaseModel(string id) - { - this.Id = id ?? throw new ArgumentNullException(nameof(id)); - this.CreatedAt = DateTime.UtcNow; - this.UpdatedAt = DateTime.UtcNow; - } - - protected virtual void OnPropertyChanged(string propertyName) - { - this.UpdatedAt = DateTime.UtcNow; - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - protected bool SetProperty(ref T field, T value, string propertyName) - { - if (Equals(field, value)) - { - return false; - } - - field = value; - this.OnPropertyChanged(propertyName); - return true; - } - - public abstract ValidationResult Validate(); - - public abstract IModel Clone(); - } } diff --git a/Services/ApplicationSettingsService.cs b/Services/ApplicationSettingsService.cs index e590084..5a3fd8d 100644 --- a/Services/ApplicationSettingsService.cs +++ b/Services/ApplicationSettingsService.cs @@ -273,16 +273,6 @@ public void ValidateAndFixSettings() this.settings.Language = LocalizationService.ResolveLanguagePreference( this.settings.Language, this.systemUiCultureProvider()); - - if (this.settings.UpdateCheckIntervalDays < 1) - { - this.settings.UpdateCheckIntervalDays = 1; - } - - if (this.settings.UpdateCheckIntervalDays > 365) - { - this.settings.UpdateCheckIntervalDays = 365; - } } public async Task ExportSettingsAsync(string filePath) diff --git a/Services/Core/BaseSystemService.cs b/Services/Core/BaseSystemService.cs deleted file mode 100644 index 1614f8b..0000000 --- a/Services/Core/BaseSystemService.cs +++ /dev/null @@ -1,105 +0,0 @@ -namespace ThreadPilot.Services.Core -{ - using System; - using System.Threading.Tasks; - using Microsoft.Extensions.Logging; - - public abstract class BaseSystemService : ISystemService, IDisposable - { - protected readonly ILogger Logger; - private bool isAvailable; - private bool disposed; - - public bool IsAvailable - { - get => this.isAvailable; - protected set - { - if (this.isAvailable != value) - { - this.isAvailable = value; - this.OnAvailabilityChanged(value); - } - } - } - - public event EventHandler? AvailabilityChanged; - - protected BaseSystemService(ILogger logger) - { - this.Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public virtual async Task InitializeAsync() - { - try - { - this.Logger.LogInformation("Initializing {ServiceType}", this.GetType().Name); - await this.InitializeServiceAsync(); - this.IsAvailable = true; - this.Logger.LogInformation("{ServiceType} initialized successfully", this.GetType().Name); - } - catch (Exception ex) - { - this.Logger.LogError(ex, "Failed to initialize {ServiceType}", this.GetType().Name); - this.IsAvailable = false; - throw; - } - } - - public virtual async Task DisposeAsync() - { - if (this.disposed) - { - return; - } - - try - { - this.Logger.LogInformation("Disposing {ServiceType}", this.GetType().Name); - await this.DisposeServiceAsync(); - this.IsAvailable = false; - } - catch (Exception ex) - { - this.Logger.LogError(ex, "Error disposing {ServiceType}", this.GetType().Name); - } - finally - { - this.disposed = true; - } - } - - protected abstract Task InitializeServiceAsync(); - - protected abstract Task DisposeServiceAsync(); - - protected virtual void OnAvailabilityChanged(bool isAvailable, string? reason = null) - { - this.AvailabilityChanged?.Invoke(this, new ServiceAvailabilityChangedEventArgs(isAvailable, reason)); - } - - protected void ThrowIfDisposed() - { - if (this.disposed) - { - throw new ObjectDisposedException(this.GetType().Name); - } - } - - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!this.disposed && disposing) - { - _ = Task.Run(async () => await this.DisposeAsync()); - } - } - } -} - diff --git a/Services/Core/ISystemService.cs b/Services/Core/ISystemService.cs deleted file mode 100644 index 4875784..0000000 --- a/Services/Core/ISystemService.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace ThreadPilot.Services.Core -{ - using System; - - public interface ISystemService - { - bool IsAvailable { get; } - - event EventHandler? AvailabilityChanged; - - Task InitializeAsync(); - - Task DisposeAsync(); - } - - public class ServiceAvailabilityChangedEventArgs : EventArgs - { - public bool IsAvailable { get; } - - public string? Reason { get; } - - public ServiceAvailabilityChangedEventArgs(bool isAvailable, string? reason = null) - { - this.IsAvailable = isAvailable; - this.Reason = reason; - } - } -} - diff --git a/Services/ISystemTweaksService.cs b/Services/ISystemTweaksService.cs index 2a8c6d3..ca0bdb3 100644 --- a/Services/ISystemTweaksService.cs +++ b/Services/ISystemTweaksService.cs @@ -27,10 +27,6 @@ public interface ISystemTweaksService Task SetPowerThrottlingAsync(bool enabled); - Task GetHpetStatusAsync(); - - Task SetHpetAsync(bool enabled); - Task GetHighSchedulingCategoryStatusAsync(); Task SetHighSchedulingCategoryAsync(bool enabled); @@ -38,8 +34,6 @@ public interface ISystemTweaksService Task GetMenuShowDelayStatusAsync(); Task SetMenuShowDelayAsync(bool enabled); - - Task RefreshAllStatusesAsync(); } public class TweakStatus @@ -49,8 +43,6 @@ public class TweakStatus public bool IsAvailable { get; set; } public string? ErrorMessage { get; set; } - - public string? Description { get; set; } } public class TweakStatusChangedEventArgs : EventArgs @@ -76,7 +68,6 @@ public enum SystemTweak SysMain, Prefetch, PowerThrottling, - Hpet, HighSchedulingCategory, MenuShowDelay, } diff --git a/Services/SystemTweaksService.cs b/Services/SystemTweaksService.cs index 7da99fd..8cd4b01 100644 --- a/Services/SystemTweaksService.cs +++ b/Services/SystemTweaksService.cs @@ -1,10 +1,10 @@ namespace ThreadPilot.Services { using System; - using System.Collections.Generic; + using System.ComponentModel; using System.Diagnostics; using System.IO; - using System.Management; + using System.Runtime.InteropServices; using System.ServiceProcess; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -13,27 +13,16 @@ namespace ThreadPilot.Services public class SystemTweaksService : ISystemTweaksService { - private static readonly string BcdEditExecutablePath = Path.Combine(Environment.SystemDirectory, "bcdedit.exe"); - private static readonly string PowerCfgExecutablePath = Path.Combine(Environment.SystemDirectory, "powercfg.exe"); private static readonly string ScExecutablePath = Path.Combine(Environment.SystemDirectory, "sc.exe"); - private static readonly HashSet AllowedExecutablePaths = new(StringComparer.OrdinalIgnoreCase) - { - Path.GetFullPath(BcdEditExecutablePath), - Path.GetFullPath(PowerCfgExecutablePath), - Path.GetFullPath(ScExecutablePath), - }; - - private static readonly Regex HexValueRegex = new("0x([0-9a-fA-F]+)", RegexOptions.Compiled); private static readonly Regex ServiceNameRegex = new("^[A-Za-z0-9_.-]+$", RegexOptions.Compiled); private static readonly TimeSpan ExternalCommandTimeout = TimeSpan.FromSeconds(20); - private const string ProcessorSubgroupAlias = "SUB_PROCESSOR"; - private const string CoreParkingSettingAlias = "CPMINCORES"; - private const string CStatesSettingAlias = "IDLEDISABLE"; - private const string CoreParkingVisibilityKeyPath = @"SYSTEM\CurrentControlSet\Control\Power\PowerSettings\54533251-82be-4824-96c1-47b60b740d00\0cc5b647-c1df-4637-891a-dec35c318583"; - private const string PriorityControlKeyPath = @"SYSTEM\CurrentControlSet\Control\PriorityControl"; - private const string PrioritySeparationValueName = "Win32PrioritySeparation"; - private const int HighSchedulingCategoryDisabledValue = 2; - internal const int HighSchedulingCategoryEnabledValue = 0x1A; + private static readonly Guid ProcessorSettingsSubgroupGuid = new("54533251-82be-4824-96c1-47b60b740d00"); + private static readonly Guid CoreParkingSettingGuid = new("0cc5b647-c1df-4637-891a-dec35c318583"); + private static readonly Guid ProcessorIdleDisableSettingGuid = new("5d76a2ca-e8c0-402f-a133-2158492d58ad"); + private const string GamesSchedulingKeyPath = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Multimedia\SystemProfile\Tasks\Games"; + private const string SchedulingCategoryValueName = "Scheduling Category"; + internal const string HighSchedulingCategoryEnabledValue = "High"; + private const string HighSchedulingCategoryDisabledValue = "Medium"; private readonly ILogger logger; private readonly IElevationService elevationService; @@ -47,32 +36,26 @@ public SystemTweaksService( this.elevationService = elevationService; } - public async Task GetCoreParkingStatusAsync() + public Task GetCoreParkingStatusAsync() { try { - await this.EnsurePowerSettingVisibleAsync(ProcessorSubgroupAlias, CoreParkingSettingAlias); - - var acValue = await this.GetPowerCfgAcSettingValueAsync(ProcessorSubgroupAlias, CoreParkingSettingAlias); - if (!acValue.HasValue) + if (!TryReadAcPowerSetting(CoreParkingSettingGuid, out var acValue, out var error)) { - return new TweakStatus { IsAvailable = false, ErrorMessage = "Could not query Core Parking value via powercfg" }; + return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = error }); } // ON = disable parking (keep all cores unparked, typically 100) - var isEnabled = acValue.Value >= 100; - - return new TweakStatus + return Task.FromResult(new TweakStatus { - IsEnabled = isEnabled, + IsEnabled = acValue >= 100, IsAvailable = true, - Description = "ON disables core parking (all cores unparked); OFF allows parking", - }; + }); } catch (Exception ex) { this.logger.LogError(ex, "Error getting Core Parking status"); - return new TweakStatus { IsAvailable = false, ErrorMessage = ex.Message }; + return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = ex.Message }); } } @@ -86,41 +69,19 @@ public async Task SetCoreParkingAsync(bool enabled) return false; } - await this.EnsurePowerSettingVisibleAsync(ProcessorSubgroupAlias, CoreParkingSettingAlias); - - var acValue = enabled ? 100 : 10; - var setValueResult = await RunProcessAsync( - PowerCfgExecutablePath, - $"-setacvalueindex SCHEME_CURRENT {ProcessorSubgroupAlias} {CoreParkingSettingAlias} {acValue}"); - if (setValueResult.ExitCode != 0) - { - this.logger.LogError( - "Failed setting Core Parking AC value. ExitCode={ExitCode}, Error={Error}", - setValueResult.ExitCode, setValueResult.StandardError); - return false; - } - - var activateResult = await RunProcessAsync(PowerCfgExecutablePath, "/setactive SCHEME_CURRENT"); - if (activateResult.ExitCode != 0) + var acValue = enabled ? 100u : 10u; + if (!TryWriteAcPowerSetting(CoreParkingSettingGuid, acValue, out var error)) { - this.logger.LogError( - "Failed activating current power scheme after Core Parking change. ExitCode={ExitCode}, Error={Error}", - activateResult.ExitCode, activateResult.StandardError); + this.logger.LogError("Failed setting Core Parking AC value: {Error}", error); return false; } - // Keep setting visible in Windows advanced power UI if the key exists. - using var visibilityKey = Registry.LocalMachine.OpenSubKey(CoreParkingVisibilityKeyPath, true); - if (visibilityKey != null) - { - visibilityKey.SetValue("Attributes", 2, RegistryValueKind.DWord); - } - var status = await this.GetCoreParkingStatusAsync(); this.TweakStatusChanged?.Invoke(this, new TweakStatusChangedEventArgs("CoreParking", status)); - this.logger.LogInformation("Core Parking {Status}", enabled ? "enabled" : "disabled"); - return true; + var persisted = status.IsAvailable && status.IsEnabled == enabled; + this.logger.LogInformation("Core Parking {Status}; persisted={Persisted}", enabled ? "enabled" : "disabled", persisted); + return persisted; } catch (Exception ex) { @@ -129,32 +90,26 @@ public async Task SetCoreParkingAsync(bool enabled) } } - public async Task GetCStatesStatusAsync() + public Task GetCStatesStatusAsync() { try { - await this.EnsurePowerSettingVisibleAsync(ProcessorSubgroupAlias, CStatesSettingAlias); - - var acValue = await this.GetPowerCfgAcSettingValueAsync(ProcessorSubgroupAlias, CStatesSettingAlias); - if (!acValue.HasValue) + if (!TryReadAcPowerSetting(ProcessorIdleDisableSettingGuid, out var acValue, out var error)) { - return new TweakStatus { IsAvailable = false, ErrorMessage = "Could not query C-States value via powercfg" }; + return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = error }); } // ON = enable C-States (IDLEDISABLE=0), OFF = disable C-States (IDLEDISABLE=1) - var isEnabled = acValue.Value == 0; - - return new TweakStatus + return Task.FromResult(new TweakStatus { - IsEnabled = isEnabled, + IsEnabled = acValue == 0, IsAvailable = true, - Description = "ON enables C-States; OFF disables C-States for lower latency", - }; + }); } catch (Exception ex) { this.logger.LogError(ex, "Error getting C-States status"); - return new TweakStatus { IsAvailable = false, ErrorMessage = ex.Message }; + return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = ex.Message }); } } @@ -168,34 +123,19 @@ public async Task SetCStatesAsync(bool enabled) return false; } - await this.EnsurePowerSettingVisibleAsync(ProcessorSubgroupAlias, CStatesSettingAlias); - - var value = enabled ? 0 : 1; - var setValueResult = await RunProcessAsync( - PowerCfgExecutablePath, - $"-setacvalueindex SCHEME_CURRENT {ProcessorSubgroupAlias} {CStatesSettingAlias} {value}"); - if (setValueResult.ExitCode != 0) + var value = enabled ? 0u : 1u; + if (!TryWriteAcPowerSetting(ProcessorIdleDisableSettingGuid, value, out var error)) { - this.logger.LogError( - "Failed setting C-States AC value. ExitCode={ExitCode}, Error={Error}", - setValueResult.ExitCode, setValueResult.StandardError); - return false; - } - - var activateResult = await RunProcessAsync(PowerCfgExecutablePath, "/setactive SCHEME_CURRENT"); - if (activateResult.ExitCode != 0) - { - this.logger.LogError( - "Failed activating current power scheme after C-States change. ExitCode={ExitCode}, Error={Error}", - activateResult.ExitCode, activateResult.StandardError); + this.logger.LogError("Failed setting C-States AC value: {Error}", error); return false; } var status = await this.GetCStatesStatusAsync(); this.TweakStatusChanged?.Invoke(this, new TweakStatusChangedEventArgs("CStates", status)); - this.logger.LogInformation("C-States {Status}", enabled ? "enabled" : "disabled"); - return true; + var persisted = status.IsAvailable && status.IsEnabled == enabled; + this.logger.LogInformation("C-States {Status}; persisted={Persisted}", enabled ? "enabled" : "disabled", persisted); + return persisted; } catch (Exception ex) { @@ -217,7 +157,6 @@ public Task GetSysMainStatusAsync() { IsEnabled = isEnabled, IsAvailable = isAvailable, - Description = "Windows Superfetch/SysMain service for memory management", }); } catch (Exception ex) @@ -260,8 +199,9 @@ public async Task SetSysMainAsync(bool enabled) var status = await this.GetSysMainStatusAsync(); this.TweakStatusChanged?.Invoke(this, new TweakStatusChangedEventArgs("SysMain", status)); - this.logger.LogInformation("SysMain service {Status}", enabled ? "started" : "stopped"); - return true; + var persisted = status.IsAvailable && status.IsEnabled == enabled; + this.logger.LogInformation("SysMain service {Status}; persisted={Persisted}", enabled ? "started" : "stopped", persisted); + return persisted; } catch (Exception ex) { @@ -280,14 +220,16 @@ public Task GetPrefetchStatusAsync() return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = "Prefetch registry key not found" }); } - var enablePrefetcher = key.GetValue("EnablePrefetcher"); - var isEnabled = enablePrefetcher?.ToString() != "0"; // 0 = disabled, 1-3 = enabled + var enablePrefetcher = ReadRegistryIntValue(key, "EnablePrefetcher"); + if (enablePrefetcher is < 0 or > 3 || !enablePrefetcher.HasValue) + { + return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = "Prefetch registry value is missing or invalid" }); + } return Task.FromResult(new TweakStatus { - IsEnabled = isEnabled, + IsEnabled = enablePrefetcher.Value != 0, IsAvailable = true, - Description = "Windows Prefetch feature for faster application loading", }); } catch (Exception ex) @@ -320,8 +262,9 @@ public async Task SetPrefetchAsync(bool enabled) var status = await this.GetPrefetchStatusAsync(); this.TweakStatusChanged?.Invoke(this, new TweakStatusChangedEventArgs("Prefetch", status)); - this.logger.LogInformation("Prefetch {Status}", enabled ? "enabled" : "disabled"); - return true; + var persisted = status.IsAvailable && status.IsEnabled == enabled; + this.logger.LogInformation("Prefetch {Status}; persisted={Persisted}", enabled ? "enabled" : "disabled", persisted); + return persisted; } catch (Exception ex) { @@ -335,20 +278,12 @@ public Task GetPowerThrottlingStatusAsync() try { using var key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\Power\PowerThrottling"); - if (key == null) - { - return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = "Power Throttling not available on this system" }); - } - - var powerThrottlingOff = ReadRegistryIntValue(key, "PowerThrottlingOff"); + var powerThrottlingOff = key == null ? null : ReadRegistryIntValue(key, "PowerThrottlingOff"); // ON = disable throttling (PowerThrottlingOff=1) - var isEnabled = powerThrottlingOff.GetValueOrDefault(0) == 1; - return Task.FromResult(new TweakStatus { - IsEnabled = isEnabled, + IsEnabled = powerThrottlingOff.GetValueOrDefault(0) == 1, IsAvailable = true, - Description = "ON disables Windows Power Throttling for sustained performance", }); } catch (Exception ex) @@ -381,8 +316,9 @@ public async Task SetPowerThrottlingAsync(bool enabled) var status = await this.GetPowerThrottlingStatusAsync(); this.TweakStatusChanged?.Invoke(this, new TweakStatusChangedEventArgs("PowerThrottling", status)); - this.logger.LogInformation("Power Throttling {Status}", enabled ? "enabled" : "disabled"); - return true; + var persisted = status.IsAvailable && status.IsEnabled == enabled; + this.logger.LogInformation("Power Throttling {Status}; persisted={Persisted}", enabled ? "enabled" : "disabled", persisted); + return persisted; } catch (Exception ex) { @@ -391,103 +327,26 @@ public async Task SetPowerThrottlingAsync(bool enabled) } } - public async Task GetHpetStatusAsync() - { - try - { - var result = await RunProcessAsync(BcdEditExecutablePath, "/enum"); - if (result.ExitCode != 0) - { - return new TweakStatus - { - IsAvailable = false, - ErrorMessage = string.IsNullOrWhiteSpace(result.StandardError) - ? "Could not query bcdedit status" - : result.StandardError, - }; - } - - var output = result.StandardOutput; - - var platformClockLine = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) - .FirstOrDefault(l => l.TrimStart().StartsWith("useplatformclock", StringComparison.OrdinalIgnoreCase)); - - // ON = disable HPET (useplatformclock removed/absent) - var isEnabled = true; - if (!string.IsNullOrWhiteSpace(platformClockLine)) - { - isEnabled = !platformClockLine.TrimEnd().EndsWith("Yes", StringComparison.OrdinalIgnoreCase); - } - - return new TweakStatus - { - IsEnabled = isEnabled, - IsAvailable = true, - Description = "High Precision Event Timer for system timing", - }; - } - catch (Exception ex) - { - this.logger.LogError(ex, "Error getting HPET status"); - return new TweakStatus { IsAvailable = false, ErrorMessage = ex.Message }; - } - } - - public async Task SetHpetAsync(bool enabled) - { - try - { - if (!this.elevationService.IsRunningAsAdministrator()) - { - this.logger.LogWarning("Administrator privileges required to modify HPET"); - return false; - } - - // ON = disable HPET (/deletevalue), OFF = force HPET (/set true) - var arguments = enabled ? "/deletevalue useplatformclock" : "/set useplatformclock true"; - var commandResult = await RunProcessAsync(BcdEditExecutablePath, arguments); - var success = commandResult.ExitCode == 0; - - if (success) - { - var status = await this.GetHpetStatusAsync(); - this.TweakStatusChanged?.Invoke(this, new TweakStatusChangedEventArgs("Hpet", status)); - this.logger.LogInformation("HPET {Status}", enabled ? "enabled" : "disabled"); - } - else - { - this.logger.LogWarning( - "Failed to set HPET. ExitCode={ExitCode}, Error={Error}", - commandResult.ExitCode, commandResult.StandardError); - } - - return success; - } - catch (Exception ex) - { - this.logger.LogError(ex, "Error setting HPET to {Enabled}", enabled); - return false; - } - } - public Task GetHighSchedulingCategoryStatusAsync() { try { - using var key = Registry.LocalMachine.OpenSubKey(PriorityControlKeyPath); + using var key = Registry.LocalMachine.OpenSubKey(GamesSchedulingKeyPath); if (key == null) { - return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = "PriorityControl registry key not found" }); + return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = "MMCSS Games registry key not found" }); } - var rawValue = ReadRegistryIntValue(key, PrioritySeparationValueName); - var isEnabled = rawValue.GetValueOrDefault(HighSchedulingCategoryDisabledValue) == HighSchedulingCategoryEnabledValue; + var category = key.GetValue(SchedulingCategoryValueName) as string; + if (string.IsNullOrWhiteSpace(category)) + { + return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = "MMCSS Games scheduling category is missing" }); + } return Task.FromResult(new TweakStatus { - IsEnabled = isEnabled, + IsEnabled = string.Equals(category, HighSchedulingCategoryEnabledValue, StringComparison.OrdinalIgnoreCase), IsAvailable = true, - Description = "ON applies high foreground boost (Win32PrioritySeparation=26 / 0x1A)", }); } catch (Exception ex) @@ -507,21 +366,21 @@ public async Task SetHighSchedulingCategoryAsync(bool enabled) return false; } - using var key = Registry.LocalMachine.OpenSubKey(PriorityControlKeyPath, true); + using var key = Registry.LocalMachine.OpenSubKey(GamesSchedulingKeyPath, true); if (key == null) { - this.logger.LogError("PriorityControl registry key not found"); + this.logger.LogError("MMCSS Games registry key not found"); return false; } - // ON = 26 / 0x1A, OFF = 2 (default/minimal boost) - key.SetValue(PrioritySeparationValueName, GetHighSchedulingCategoryRegistryValue(enabled), RegistryValueKind.DWord); + key.SetValue(SchedulingCategoryValueName, GetHighSchedulingCategoryRegistryValue(enabled), RegistryValueKind.String); var status = await this.GetHighSchedulingCategoryStatusAsync(); this.TweakStatusChanged?.Invoke(this, new TweakStatusChangedEventArgs("HighSchedulingCategory", status)); - this.logger.LogInformation("High Scheduling Category {Status}", enabled ? "enabled" : "disabled"); - return true; + var persisted = status.IsAvailable && status.IsEnabled == enabled; + this.logger.LogInformation("High Scheduling Category {Status}; persisted={Persisted}", enabled ? "enabled" : "disabled", persisted); + return persisted; } catch (Exception ex) { @@ -558,55 +417,85 @@ private async Task SetServiceStartModeAsync(string serviceName, ServiceSta return true; } - internal static int GetHighSchedulingCategoryRegistryValue(bool enabled) => + internal static string GetHighSchedulingCategoryRegistryValue(bool enabled) => enabled ? HighSchedulingCategoryEnabledValue : HighSchedulingCategoryDisabledValue; - private async Task EnsurePowerSettingVisibleAsync(string subgroupAlias, string settingAlias) + private static bool TryReadAcPowerSetting(Guid settingGuid, out uint value, out string? error) { - var attributesResult = await RunProcessAsync( - PowerCfgExecutablePath, - $"-attributes {subgroupAlias} {settingAlias} -ATTRIB_HIDE"); + value = 0; + if (!TryGetActivePowerScheme(out var schemeGuid, out error)) + { + return false; + } - if (attributesResult.ExitCode != 0) + var subgroupGuid = ProcessorSettingsSubgroupGuid; + var result = NativeMethods.PowerReadAcValueIndex( + IntPtr.Zero, + ref schemeGuid, + ref subgroupGuid, + ref settingGuid, + out value); + if (result != 0) { - this.logger.LogDebug( - "Could not unhide power setting {Subgroup}/{Setting}. ExitCode={ExitCode}, Error={Error}", - subgroupAlias, settingAlias, attributesResult.ExitCode, attributesResult.StandardError); + error = new Win32Exception((int)result).Message; + return false; } + + error = null; + return true; } - private async Task GetPowerCfgAcSettingValueAsync(string subgroupAlias, string settingAlias) + private static bool TryWriteAcPowerSetting(Guid settingGuid, uint value, out string? error) { - var queryResult = await RunProcessAsync( - PowerCfgExecutablePath, - $"-query SCHEME_CURRENT {subgroupAlias} {settingAlias}"); + if (!TryGetActivePowerScheme(out var schemeGuid, out error)) + { + return false; + } - if (queryResult.ExitCode != 0) + var subgroupGuid = ProcessorSettingsSubgroupGuid; + var writeResult = NativeMethods.PowerWriteAcValueIndex( + IntPtr.Zero, + ref schemeGuid, + ref subgroupGuid, + ref settingGuid, + value); + if (writeResult != 0) { - this.logger.LogWarning( - "powercfg query failed for {Subgroup}/{Setting}. ExitCode={ExitCode}, Error={Error}", - subgroupAlias, settingAlias, queryResult.ExitCode, queryResult.StandardError); - return null; + error = new Win32Exception((int)writeResult).Message; + return false; } - var line = queryResult.StandardOutput - .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) - .FirstOrDefault(l => l.Contains("Current AC Power Setting Index", StringComparison.OrdinalIgnoreCase)); - if (string.IsNullOrWhiteSpace(line)) + var activateResult = NativeMethods.PowerSetActiveScheme(IntPtr.Zero, ref schemeGuid); + if (activateResult != 0) { - return null; + error = new Win32Exception((int)activateResult).Message; + return false; } - var match = HexValueRegex.Match(line); - if (!match.Success) + error = null; + return true; + } + + private static bool TryGetActivePowerScheme(out Guid schemeGuid, out string? error) + { + schemeGuid = Guid.Empty; + var result = NativeMethods.PowerGetActiveScheme(IntPtr.Zero, out var schemePointer); + if (result != 0 || schemePointer == IntPtr.Zero) { - return null; + error = new Win32Exception((int)result).Message; + return false; } - return int.TryParse(match.Groups[1].Value, System.Globalization.NumberStyles.HexNumber, - System.Globalization.CultureInfo.InvariantCulture, out var parsed) - ? parsed - : null; + try + { + schemeGuid = Marshal.PtrToStructure(schemePointer); + error = null; + return true; + } + finally + { + _ = NativeMethods.LocalFree(schemePointer); + } } private static async Task RunProcessAsync(string fileName, string arguments) @@ -663,7 +552,8 @@ private static bool IsAllowedExecutable(string fileName) } var fullPath = Path.GetFullPath(fileName); - return AllowedExecutablePaths.Contains(fullPath) && File.Exists(fullPath); + return string.Equals(fullPath, Path.GetFullPath(ScExecutablePath), StringComparison.OrdinalIgnoreCase) + && File.Exists(fullPath); } private static int? ReadRegistryIntValue(RegistryKey key, string valueName) @@ -705,14 +595,17 @@ public Task GetMenuShowDelayStatusAsync() return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = "Desktop registry key not found" }); } - var menuShowDelay = key.GetValue("MenuShowDelay"); - var isEnabled = menuShowDelay?.ToString() != "0"; // 0 = no delay, >0 = delay enabled + var rawDelay = key.GetValue("MenuShowDelay")?.ToString(); + var parsedDelay = 400; + if (rawDelay != null && (!int.TryParse(rawDelay, out parsedDelay) || parsedDelay < 0)) + { + return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = "MenuShowDelay registry value is invalid" }); + } return Task.FromResult(new TweakStatus { - IsEnabled = isEnabled, + IsEnabled = parsedDelay > 0, IsAvailable = true, - Description = "Delay before showing context menus", }); } catch (Exception ex) @@ -739,8 +632,9 @@ public async Task SetMenuShowDelayAsync(bool enabled) var status = await this.GetMenuShowDelayStatusAsync(); this.TweakStatusChanged?.Invoke(this, new TweakStatusChangedEventArgs("MenuShowDelay", status)); - this.logger.LogInformation("Menu Show Delay {Status}", enabled ? "enabled" : "disabled"); - return true; + var persisted = status.IsAvailable && status.IsEnabled == enabled; + this.logger.LogInformation("Menu Show Delay {Status}; persisted={Persisted}", enabled ? "enabled" : "disabled", persisted); + return persisted; } catch (Exception ex) { @@ -749,31 +643,32 @@ public async Task SetMenuShowDelayAsync(bool enabled) } } - public async Task RefreshAllStatusesAsync() + private static class NativeMethods { - try - { - this.logger.LogInformation("Refreshing all system tweak statuses"); - - var tasks = new[] - { - this.GetCoreParkingStatusAsync(), - this.GetCStatesStatusAsync(), - this.GetSysMainStatusAsync(), - this.GetPrefetchStatusAsync(), - this.GetPowerThrottlingStatusAsync(), - this.GetHpetStatusAsync(), - this.GetHighSchedulingCategoryStatusAsync(), - this.GetMenuShowDelayStatusAsync(), - }; - - await Task.WhenAll(tasks); - this.logger.LogInformation("All system tweak statuses refreshed"); - } - catch (Exception ex) - { - this.logger.LogError(ex, "Error refreshing system tweak statuses"); - } + [DllImport("PowrProf.dll", EntryPoint = "PowerGetActiveScheme")] + internal static extern uint PowerGetActiveScheme(IntPtr userRootPowerKey, out IntPtr activePolicyGuid); + + [DllImport("PowrProf.dll", EntryPoint = "PowerReadACValueIndex")] + internal static extern uint PowerReadAcValueIndex( + IntPtr rootPowerKey, + ref Guid schemeGuid, + ref Guid subgroupGuid, + ref Guid settingGuid, + out uint acValueIndex); + + [DllImport("PowrProf.dll", EntryPoint = "PowerWriteACValueIndex")] + internal static extern uint PowerWriteAcValueIndex( + IntPtr rootPowerKey, + ref Guid schemeGuid, + ref Guid subgroupGuid, + ref Guid settingGuid, + uint acValueIndex); + + [DllImport("PowrProf.dll", EntryPoint = "PowerSetActiveScheme")] + internal static extern uint PowerSetActiveScheme(IntPtr userRootPowerKey, ref Guid schemeGuid); + + [DllImport("kernel32.dll")] + internal static extern IntPtr LocalFree(IntPtr memory); } } } diff --git a/Services/UpdateInstallerService.cs b/Services/UpdateInstallerService.cs index 4331106..19dfd70 100644 --- a/Services/UpdateInstallerService.cs +++ b/Services/UpdateInstallerService.cs @@ -12,6 +12,16 @@ namespace ThreadPilot.Services public sealed class UpdateInstallerService : IUpdateInstallerService { + private static readonly string[] InstallerArguments = + { + "/VERYSILENT", + "/SUPPRESSMSGBOXES", + "/NORESTART", + "/CLOSEAPPLICATIONS", + "/RESTARTAPPLICATIONS", + "/THREADPILOTUPDATE=1", + }; + private readonly IUpdateTempDirectoryProvider tempDirectoryProvider; private readonly IUpdateProcessLauncher processLauncher; @@ -36,7 +46,7 @@ public Task LaunchInstallerElevatedAsync(string installerPath, CancellationToken throw new InvalidOperationException("Update installer path is not trusted."); } - return this.processLauncher.LaunchElevatedAsync(installerPath, Array.Empty(), cancellationToken); + return this.processLauncher.LaunchElevatedAsync(installerPath, InstallerArguments, cancellationToken); } } diff --git a/Services/UpdateService.cs b/Services/UpdateService.cs index ba9d2ea..01b7846 100644 --- a/Services/UpdateService.cs +++ b/Services/UpdateService.cs @@ -58,13 +58,6 @@ public async Task CheckForUpdatesAsync(UpdateCheckRequest req { return new UpdateCheckResult(UpdateCheckStatus.Skipped, currentVersion, null, "Automatic update checks are disabled."); } - - var intervalDays = Math.Max(1, settings.UpdateCheckIntervalDays); - if (settings.LastUpdateCheckUtc.HasValue && - this.clock.UtcNow - settings.LastUpdateCheckUtc.Value < TimeSpan.FromDays(intervalDays)) - { - return new UpdateCheckResult(UpdateCheckStatus.Skipped, currentVersion, null, "Startup update check throttled."); - } } await this.checkGate.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -121,17 +114,17 @@ public async Task DownloadAndInstallAsync(UpdateReleaseInfo return new UpdateInstallResult(UpdateInstallStatus.Started, "Update installer started."); } catch (Exception ex) when (ex is not OperationCanceledException) - { - this.logger.LogWarning(ex, "ThreadPilot update install failed"); - return new UpdateInstallResult(UpdateInstallStatus.Failed, ex.Message); - } - finally { if (download != null) { this.TryCleanup(download.TempDirectory); } + this.logger.LogWarning(ex, "ThreadPilot update install failed"); + return new UpdateInstallResult(UpdateInstallStatus.Failed, ex.Message); + } + finally + { this.installGate.Release(); } } diff --git a/Tests/ThreadPilot.Core.Tests/ApplicationSettingsModelTests.cs b/Tests/ThreadPilot.Core.Tests/ApplicationSettingsModelTests.cs index df1d848..c4687c1 100644 --- a/Tests/ThreadPilot.Core.Tests/ApplicationSettingsModelTests.cs +++ b/Tests/ThreadPilot.Core.Tests/ApplicationSettingsModelTests.cs @@ -15,7 +15,6 @@ public void Constructor_StartMinimizedDefaultsFalse_ForManualLaunchVisibility() Assert.False(settings.HasSeenStartupMinimizedSuggestion); Assert.Equal("en-US", settings.Language); Assert.True(settings.EnableAutomaticUpdateChecks); - Assert.Equal(7, settings.UpdateCheckIntervalDays); Assert.False(settings.IncludePrereleaseUpdates); Assert.Null(settings.LastUpdateCheckUtc); } diff --git a/Tests/ThreadPilot.Core.Tests/PowerPlanViewXamlTests.cs b/Tests/ThreadPilot.Core.Tests/PowerPlanViewXamlTests.cs index 81fdddd..92cbbaa 100644 --- a/Tests/ThreadPilot.Core.Tests/PowerPlanViewXamlTests.cs +++ b/Tests/ThreadPilot.Core.Tests/PowerPlanViewXamlTests.cs @@ -45,14 +45,16 @@ public void HeaderInstructionText_WrapsToAvoidButtonOverlap() } [Fact] - public void ActivePowerPlanTemplate_ContainsActiveBadgeAndAccentBorder() + public void ActivePowerPlanTemplate_ContainsNeutralActiveBadgeAndBorder() { var document = XDocument.Load(PowerPlanViewPath, LoadOptions.PreserveWhitespace); var serialized = document.ToString(SaveOptions.DisableFormatting); Assert.Contains("PowerPlanView_Active", serialized, StringComparison.Ordinal); Assert.Contains("IsActive", serialized, StringComparison.Ordinal); - Assert.Contains("Accent", serialized, StringComparison.Ordinal); + Assert.Contains("SoftSelectionBorderBrush", serialized, StringComparison.Ordinal); + Assert.Contains("StatusPillBackgroundBrush", serialized, StringComparison.Ordinal); + Assert.DoesNotContain("BorderThickness=\"4,1,1,1\"", serialized, StringComparison.Ordinal); } } } diff --git a/Tests/ThreadPilot.Core.Tests/ProcessViewXamlBindingTests.cs b/Tests/ThreadPilot.Core.Tests/ProcessViewXamlBindingTests.cs index 39861df..540ae83 100644 --- a/Tests/ThreadPilot.Core.Tests/ProcessViewXamlBindingTests.cs +++ b/Tests/ThreadPilot.Core.Tests/ProcessViewXamlBindingTests.cs @@ -65,14 +65,15 @@ public void SelectedProcessSummaryBindings_AreNotUsedByEditableControls() } [Fact] - public void ProcessGridRowStyle_HighlightsSelectedRowsWithAccentTheme() + public void ProcessGridRowStyle_HighlightsSelectedRowsWithNeutralTheme() { var document = XDocument.Load(ProcessViewPath, LoadOptions.PreserveWhitespace); var serialized = document.ToString(SaveOptions.DisableFormatting); Assert.Contains("IsSelected", serialized, StringComparison.Ordinal); - Assert.Contains("Accent", serialized, StringComparison.Ordinal); - Assert.Contains("BorderThickness", serialized, StringComparison.Ordinal); + Assert.Contains("SoftSelectionBackgroundBrush", serialized, StringComparison.Ordinal); + Assert.Contains("SoftSelectionBorderBrush", serialized, StringComparison.Ordinal); + Assert.DoesNotContain("Property=\"BorderBrush\" Value=\"{DynamicResource AccentBrush}\"", serialized, StringComparison.Ordinal); } [Fact] @@ -204,7 +205,7 @@ public void MasksView_SelectedCpuTilesUseSubtleMaskSelectionResources() Assert.Contains("MaskSelectedBackgroundBrush", serialized, StringComparison.Ordinal); Assert.Contains("MaskSelectedBorderBrush", serialized, StringComparison.Ordinal); - Assert.Contains("BorderThickness\" Value=\"2\"", serialized, StringComparison.Ordinal); + Assert.Contains("BorderThickness\" Value=\"1\"", serialized, StringComparison.Ordinal); Assert.DoesNotContain("SoftSelectionBackgroundBrush", serialized, StringComparison.Ordinal); } @@ -223,7 +224,7 @@ public void MasksView_SelectedMaskListItemUsesReadableFluentSelection() Assert.Contains("ItemContainerStyle=\"{StaticResource MaskListItemStyle}\"", serialized, StringComparison.Ordinal); Assert.Contains("MaskSelectedListBackgroundBrush", serialized, StringComparison.Ordinal); Assert.Contains("MaskSelectedBorderBrush", serialized, StringComparison.Ordinal); - Assert.Contains("BorderThickness\" Value=\"2\"", serialized, StringComparison.Ordinal); + Assert.Contains("BorderThickness\" Value=\"1\"", serialized, StringComparison.Ordinal); Assert.Contains("Foreground\" Value=\"{DynamicResource TextFillColorPrimaryBrush}\"", serialized, StringComparison.Ordinal); Assert.DoesNotContain("TextOnAccentFillColorPrimaryBrush", serialized, StringComparison.Ordinal); Assert.DoesNotContain("AccentFillColorDefaultBrush", serialized, StringComparison.Ordinal); @@ -248,16 +249,17 @@ public void MainWindow_QueuesStartupUpdateCheckOnceWithoutBlockingStartup() var mainWindowBehaviorPath = Path.Combine(GetRepositoryRoot(), "MainWindow.Behaviors.partial.cs"); var source = File.ReadAllText(mainWindowBehaviorPath); var updateCheckSection = source[ - source.IndexOf("private void QueueStartupUpdateCheck()", StringComparison.Ordinal).. - source.IndexOf("private void UpdateLoadingStatus", StringComparison.Ordinal)]; + source.IndexOf("private void QueueStartupUpdateCheck()", StringComparison.Ordinal)..source.IndexOf("private void UpdateLoadingStatus", StringComparison.Ordinal)]; Assert.Contains("QueueStartupUpdateCheck();", source, StringComparison.Ordinal); Assert.Contains("Interlocked.Exchange(ref this.startupUpdateCheckStarted, 1)", updateCheckSection, StringComparison.Ordinal); Assert.Contains("TaskSafety.FireAndForget(this.CheckForUpdatesAtStartupAsync()", updateCheckSection, StringComparison.Ordinal); Assert.Contains("GetRequiredService()", updateCheckSection, StringComparison.Ordinal); Assert.Contains("CheckForUpdatesAsync(new UpdateCheckRequest(UpdateCheckTrigger.Startup))", updateCheckSection, StringComparison.Ordinal); + Assert.Contains("MessageBox.Show", updateCheckSection, StringComparison.Ordinal); + Assert.Contains("MessageBoxButton.YesNo", updateCheckSection, StringComparison.Ordinal); + Assert.Contains("DownloadAndInstallAsync(result.Release)", updateCheckSection, StringComparison.Ordinal); Assert.Contains("Startup update check ignored failure", updateCheckSection, StringComparison.Ordinal); - Assert.DoesNotContain("System.Windows.MessageBox.Show", updateCheckSection, StringComparison.Ordinal); } [Fact] diff --git a/Tests/ThreadPilot.Core.Tests/SystemTweaksServiceTests.cs b/Tests/ThreadPilot.Core.Tests/SystemTweaksServiceTests.cs index 83b5c4d..7764297 100644 --- a/Tests/ThreadPilot.Core.Tests/SystemTweaksServiceTests.cs +++ b/Tests/ThreadPilot.Core.Tests/SystemTweaksServiceTests.cs @@ -5,12 +5,11 @@ namespace ThreadPilot.Core.Tests public sealed class SystemTweaksServiceTests { [Fact] - public void GetHighSchedulingCategoryRegistryValue_WhenEnabled_ReturnsWin32PrioritySeparation26() + public void GetHighSchedulingCategoryRegistryValue_WhenEnabled_ReturnsHigh() { var value = SystemTweaksService.GetHighSchedulingCategoryRegistryValue(enabled: true); - Assert.Equal(26, value); - Assert.Equal(0x1A, value); + Assert.Equal("High", value); } [Fact] @@ -18,7 +17,7 @@ public void GetHighSchedulingCategoryRegistryValue_WhenDisabled_KeepsDefaultReve { var value = SystemTweaksService.GetHighSchedulingCategoryRegistryValue(enabled: false); - Assert.Equal(2, value); + Assert.Equal("Medium", value); } } } diff --git a/Tests/ThreadPilot.Core.Tests/SystemTweaksStartupAndStyleTests.cs b/Tests/ThreadPilot.Core.Tests/SystemTweaksStartupAndStyleTests.cs new file mode 100644 index 0000000..4131934 --- /dev/null +++ b/Tests/ThreadPilot.Core.Tests/SystemTweaksStartupAndStyleTests.cs @@ -0,0 +1,79 @@ +namespace ThreadPilot.Core.Tests +{ + using System.Xml.Linq; + + public sealed class SystemTweaksStartupAndStyleTests + { + [Fact] + public void Startup_LoadsPersistedTweakStateBeforePageNavigation() + { + var source = File.ReadAllText(GetRepositoryFilePath("MainWindow.Behaviors.partial.cs")); + var start = source.IndexOf("private async Task LoadViewModelsAsync()", StringComparison.Ordinal); + var end = source.IndexOf("private async Task InitializeServicesAsync()", start, StringComparison.Ordinal); + var startupSection = source[start..end]; + + Assert.Contains("await this.systemTweaksViewModel.LoadAsync();", startupSection, StringComparison.Ordinal); + Assert.Contains("this.initializedSections.Add(\"Tweaks\");", startupSection, StringComparison.Ordinal); + } + + [Fact] + public void TweaksView_UsesNativeFluentToggleSwitch() + { + var document = XDocument.Load(GetRepositoryFilePath("Views", "SystemTweaksView.xaml")); + var serialized = document.ToString(SaveOptions.DisableFormatting); + + Assert.Contains("ToggleSwitch", serialized, StringComparison.Ordinal); + Assert.Contains("AutomationProperties.Name=\"{Binding Name}\"", serialized, StringComparison.Ordinal); + Assert.DoesNotContain("PillToggleButtonStyle", serialized, StringComparison.Ordinal); + } + + [Fact] + public void TweaksService_UsesNativePowerApiAndNoDebugClockOverride() + { + var source = File.ReadAllText(GetRepositoryFilePath("Services", "SystemTweaksService.cs")); + + Assert.Contains("PowerReadACValueIndex", source, StringComparison.Ordinal); + Assert.Contains("PowerWriteACValueIndex", source, StringComparison.Ordinal); + Assert.DoesNotContain("useplatformclock", source, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("bcdedit", source, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void MainNavigation_UsesNeutralSelectionWithoutAccentBorder() + { + var document = XDocument.Load(GetRepositoryFilePath("MainWindow.xaml")); + var activeTrigger = document + .Descendants() + .Single(element => + element.Name.LocalName == "Trigger" + && element.Attribute("Property")?.Value == "IsActive"); + var setters = activeTrigger + .Elements() + .Where(element => element.Name.LocalName == "Setter") + .ToDictionary( + element => element.Attribute("Property")!.Value, + element => element.Attribute("Value")!.Value, + StringComparer.Ordinal); + + Assert.Equal("{DynamicResource SoftSelectionBackgroundBrush}", setters["Background"]); + Assert.Equal("0", setters["BorderThickness"]); + Assert.DoesNotContain(setters.Values, value => value.Contains("Accent", StringComparison.Ordinal)); + } + + private static string GetRepositoryFilePath(params string[] pathParts) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null && !File.Exists(Path.Combine(directory.FullName, "ThreadPilot.csproj"))) + { + directory = directory.Parent; + } + + if (directory == null) + { + throw new InvalidOperationException("Repository root was not found."); + } + + return Path.Combine(new[] { directory.FullName }.Concat(pathParts).ToArray()); + } + } +} diff --git a/Tests/ThreadPilot.Core.Tests/SystemTweaksViewModelTests.cs b/Tests/ThreadPilot.Core.Tests/SystemTweaksViewModelTests.cs index f9b0bc4..4d41392 100644 --- a/Tests/ThreadPilot.Core.Tests/SystemTweaksViewModelTests.cs +++ b/Tests/ThreadPilot.Core.Tests/SystemTweaksViewModelTests.cs @@ -7,13 +7,39 @@ namespace ThreadPilot.Core.Tests public sealed class SystemTweaksViewModelTests { + [Fact] + public async Task LoadAsync_QueriesEverySupportedTweakExactlyOnce() + { + var harness = new Harness(); + var status = new TweakStatus { IsEnabled = true, IsAvailable = true }; + harness.Tweaks.Setup(service => service.GetCoreParkingStatusAsync()).ReturnsAsync(status); + harness.Tweaks.Setup(service => service.GetCStatesStatusAsync()).ReturnsAsync(status); + harness.Tweaks.Setup(service => service.GetSysMainStatusAsync()).ReturnsAsync(status); + harness.Tweaks.Setup(service => service.GetPrefetchStatusAsync()).ReturnsAsync(status); + harness.Tweaks.Setup(service => service.GetPowerThrottlingStatusAsync()).ReturnsAsync(status); + harness.Tweaks.Setup(service => service.GetHighSchedulingCategoryStatusAsync()).ReturnsAsync(status); + harness.Tweaks.Setup(service => service.GetMenuShowDelayStatusAsync()).ReturnsAsync(status); + var viewModel = harness.CreateViewModel(); + + await viewModel.LoadAsync(); + + Assert.Equal(Enum.GetValues().Length, viewModel.TweakItems.Count); + Assert.All(viewModel.TweakItems, item => Assert.True(item.IsEnabled)); + harness.Tweaks.Verify(service => service.GetCoreParkingStatusAsync(), Times.Once); + harness.Tweaks.Verify(service => service.GetCStatesStatusAsync(), Times.Once); + harness.Tweaks.Verify(service => service.GetSysMainStatusAsync(), Times.Once); + harness.Tweaks.Verify(service => service.GetPrefetchStatusAsync(), Times.Once); + harness.Tweaks.Verify(service => service.GetPowerThrottlingStatusAsync(), Times.Once); + harness.Tweaks.Verify(service => service.GetHighSchedulingCategoryStatusAsync(), Times.Once); + harness.Tweaks.Verify(service => service.GetMenuShowDelayStatusAsync(), Times.Once); + } + [Theory] [InlineData(SystemTweak.CoreParking, "Core Parking")] [InlineData(SystemTweak.CStates, "C-States")] [InlineData(SystemTweak.SysMain, "SysMain Service")] [InlineData(SystemTweak.Prefetch, "Prefetch")] [InlineData(SystemTweak.PowerThrottling, "Power Throttling")] - [InlineData(SystemTweak.Hpet, "HPET")] [InlineData(SystemTweak.HighSchedulingCategory, "High Scheduling Category")] [InlineData(SystemTweak.MenuShowDelay, "Menu Show Delay")] public async Task ToggleTweakCommand_CallsExpectedServiceAndLogsSuccess(SystemTweak tweakType, string name) @@ -75,6 +101,8 @@ private sealed class Harness public Mock Logging { get; } = new(MockBehavior.Loose); + public Mock Localization { get; } = new(MockBehavior.Loose); + public ActivityAuditService Audit { get; } = new(NullLogger.Instance); public void SetupTweak(SystemTweak tweakType, bool setResult) @@ -101,10 +129,6 @@ public void SetupTweak(SystemTweak tweakType, bool setResult) this.Tweaks.Setup(service => service.SetPowerThrottlingAsync(true)).ReturnsAsync(setResult); this.Tweaks.Setup(service => service.GetPowerThrottlingStatusAsync()).ReturnsAsync(CreateEnabledStatus()); break; - case SystemTweak.Hpet: - this.Tweaks.Setup(service => service.SetHpetAsync(true)).ReturnsAsync(setResult); - this.Tweaks.Setup(service => service.GetHpetStatusAsync()).ReturnsAsync(CreateEnabledStatus()); - break; case SystemTweak.HighSchedulingCategory: this.Tweaks.Setup(service => service.SetHighSchedulingCategoryAsync(true)).ReturnsAsync(setResult); this.Tweaks.Setup(service => service.GetHighSchedulingCategoryStatusAsync()).ReturnsAsync(CreateEnabledStatus()); @@ -137,9 +161,6 @@ public void VerifySetCalled(SystemTweak tweakType) case SystemTweak.PowerThrottling: this.Tweaks.Verify(service => service.SetPowerThrottlingAsync(true), Times.Once); break; - case SystemTweak.Hpet: - this.Tweaks.Verify(service => service.SetHpetAsync(true), Times.Once); - break; case SystemTweak.HighSchedulingCategory: this.Tweaks.Verify(service => service.SetHighSchedulingCategoryAsync(true), Times.Once); break; @@ -155,6 +176,7 @@ public SystemTweaksViewModel CreateViewModel() => new( this.Tweaks.Object, this.Notifications.Object, + this.Localization.Object, NullLogger.Instance, this.Logging.Object, this.Audit); diff --git a/Tests/ThreadPilot.Core.Tests/ThemeDictionaryPolicyTests.cs b/Tests/ThreadPilot.Core.Tests/ThemeDictionaryPolicyTests.cs index 5a07992..e52567e 100644 --- a/Tests/ThreadPilot.Core.Tests/ThemeDictionaryPolicyTests.cs +++ b/Tests/ThreadPilot.Core.Tests/ThemeDictionaryPolicyTests.cs @@ -90,13 +90,14 @@ public void ThemeDictionaries_DefineSharedVisualResourceKeys(string themePath) } [Fact] - public void DarkTheme_MaskListSelectionUsesSubtleTintWithoutAccentForeground() + public void DarkTheme_SelectionUsesNeutralWindowsElevenSurface() { var themeText = File.ReadAllText(GetRepositoryFilePath("Themes/FluentDark.xaml")); Assert.Contains("x:Key=\"MaskSelectedListBackgroundBrush\"", themeText, StringComparison.Ordinal); - Assert.Contains("Opacity=\"0.05\"", themeText, StringComparison.Ordinal); + Assert.Contains("Color=\"#FF2D2D2D\"", themeText, StringComparison.Ordinal); Assert.Contains("x:Key=\"MaskSelectedBorderBrush\"", themeText, StringComparison.Ordinal); + Assert.Contains("x:Key=\"SoftSelectionBackgroundBrush\" Color=\"#FF2D2D2D\"", themeText, StringComparison.Ordinal); Assert.DoesNotContain( "x:Key=\"MaskSelectedListBackgroundBrush\" Color=\"{StaticResource AccentFillColorDefault}\"", themeText, diff --git a/Tests/ThreadPilot.Core.Tests/UpdateServiceTests.cs b/Tests/ThreadPilot.Core.Tests/UpdateServiceTests.cs index b3be706..461b181 100644 --- a/Tests/ThreadPilot.Core.Tests/UpdateServiceTests.cs +++ b/Tests/ThreadPilot.Core.Tests/UpdateServiceTests.cs @@ -41,15 +41,15 @@ public async Task GitHubUpdateChecker_ExcludesPrereleasesByDefault() } [Fact] - public async Task CheckForUpdatesAsync_StartupSkipsWhenLastCheckInsideInterval() + public async Task CheckForUpdatesAsync_StartupChecksOnEveryLaunch() { var harness = new Harness(); harness.Settings.LastUpdateCheckUtc = harness.Clock.UtcNow.AddDays(-2); var result = await harness.Service.CheckForUpdatesAsync(new UpdateCheckRequest(UpdateCheckTrigger.Startup)); - Assert.Equal(UpdateCheckStatus.Skipped, result.Status); - Assert.False(harness.ReleaseClient.RequestedReleases); + Assert.Equal(UpdateCheckStatus.UpdateAvailable, result.Status); + Assert.True(harness.ReleaseClient.RequestedReleases); } [Fact] @@ -147,6 +147,42 @@ public async Task DownloadAndInstallAsync_StartsInstallerAndRequestsShutdown() Assert.Equal(UpdateInstallStatus.Started, result.Status); harness.Installer.Verify(service => service.LaunchInstallerElevatedAsync(It.IsAny(), It.IsAny()), Times.Once); harness.Shutdown.Verify(service => service.RequestShutdownForUpdate(), Times.Once); + harness.TempProvider.Verify(provider => provider.Cleanup(It.IsAny()), Times.Never); + } + + [Fact] + public async Task UpdateInstallerService_UsesSilentRestartingUpdateMode() + { + using var tempRoot = new TempDirectory(); + var tempProvider = new UpdateTempDirectoryProvider(tempRoot.Path); + var updateDirectory = tempProvider.CreateUpdateTempDirectory(new SemanticVersion(1, 5, 0)); + var installerPath = Path.Combine(updateDirectory, "ThreadPilot_v1.5.0_Setup.exe"); + File.WriteAllText(installerPath, "installer"); + IReadOnlyList? launchedArguments = null; + var launcher = new Mock(); + launcher + .Setup(service => service.LaunchElevatedAsync(installerPath, It.IsAny>(), It.IsAny())) + .Callback, CancellationToken>((_, arguments, _) => launchedArguments = arguments) + .Returns(Task.CompletedTask); + var service = new UpdateInstallerService(tempProvider, launcher.Object); + + await service.LaunchInstallerElevatedAsync(installerPath); + + Assert.NotNull(launchedArguments); + Assert.Contains("/VERYSILENT", launchedArguments); + Assert.Contains("/CLOSEAPPLICATIONS", launchedArguments); + Assert.Contains("/RESTARTAPPLICATIONS", launchedArguments); + Assert.Contains("/THREADPILOTUPDATE=1", launchedArguments); + } + + [Fact] + public void InstallerScript_RestartsOnlyInAppUpdates() + { + var setupScript = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "Installer", "setup.iss")); + + Assert.Contains("[Run]", setupScript, StringComparison.Ordinal); + Assert.Contains("Check: IsThreadPilotUpdate", setupScript, StringComparison.Ordinal); + Assert.Contains("{param:THREADPILOTUPDATE|0}", setupScript, StringComparison.Ordinal); } private static UpdateDownloadService CreateDownloadService(string tempRoot, IUpdateDownloadClient client) @@ -183,6 +219,17 @@ private static string ComputeSha256(byte[] bytes) return Convert.ToHexString(hash); } + private static string GetRepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null && !File.Exists(Path.Combine(directory.FullName, "ThreadPilot.csproj"))) + { + directory = directory.Parent; + } + + return directory?.FullName ?? throw new InvalidOperationException("Repository root was not found."); + } + private sealed class Harness { public ApplicationSettingsModel Settings { get; } = new(); @@ -213,6 +260,8 @@ private sealed class Harness public Mock Shutdown { get; } = new(MockBehavior.Strict); + public Mock TempProvider { get; } = new(MockBehavior.Strict); + public string TempDirectory { get; } public UpdateService Service { get; } @@ -235,8 +284,7 @@ public Harness() versionProvider.SetupGet(provider => provider.CurrentVersion).Returns(new SemanticVersion(1, 3, 1)); versionProvider.SetupGet(provider => provider.DisplayVersion).Returns("v1.3.1"); - var tempProvider = new Mock(); - tempProvider.Setup(provider => provider.Cleanup(It.IsAny())); + this.TempProvider.Setup(provider => provider.Cleanup(It.IsAny())); this.Shutdown.Setup(service => service.RequestShutdownForUpdate()); this.Installer @@ -249,7 +297,7 @@ public Harness() versionProvider.Object, this.Download.Object, this.Installer.Object, - tempProvider.Object, + this.TempProvider.Object, this.Shutdown.Object, this.Clock, NullLogger.Instance); diff --git a/Themes/FluentDark.xaml b/Themes/FluentDark.xaml index 33d43e0..8633f65 100644 --- a/Themes/FluentDark.xaml +++ b/Themes/FluentDark.xaml @@ -93,11 +93,11 @@ - - - - - + + + + + diff --git a/Themes/FluentLight.xaml b/Themes/FluentLight.xaml index bdcc12a..5849f95 100644 --- a/Themes/FluentLight.xaml +++ b/Themes/FluentLight.xaml @@ -93,11 +93,11 @@ - - - - - + + + + + diff --git a/ThreadPilot.csproj b/ThreadPilot.csproj index 6b9b3ba..bfc65ae 100644 --- a/ThreadPilot.csproj +++ b/ThreadPilot.csproj @@ -52,15 +52,10 @@ - - - - - - - - - + + + + diff --git a/ViewModels/SettingsViewModel.cs b/ViewModels/SettingsViewModel.cs index 8fd769d..33198b4 100644 --- a/ViewModels/SettingsViewModel.cs +++ b/ViewModels/SettingsViewModel.cs @@ -730,7 +730,7 @@ private async Task DownloadAndInstallUpdateAsync() var message = this.GetLocalizedString( "Settings_UpdateConfirmMessageFormat", - "ThreadPilot will download and verify version {0}, then ask Windows for permission to run the installer. Continue?", + "ThreadPilot will download, verify and install version {0}, then restart automatically. Continue?", this.availableUpdate.Version); var confirmation = System.Windows.MessageBox.Show( message, diff --git a/ViewModels/SystemTweaksViewModel.cs b/ViewModels/SystemTweaksViewModel.cs index e7ebe54..086ce88 100644 --- a/ViewModels/SystemTweaksViewModel.cs +++ b/ViewModels/SystemTweaksViewModel.cs @@ -2,6 +2,7 @@ namespace ThreadPilot.ViewModels { using System; using System.Collections.ObjectModel; + using System.Globalization; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -13,6 +14,7 @@ public partial class SystemTweaksViewModel : BaseViewModel { private readonly ISystemTweaksService systemTweaksService; private readonly INotificationService notificationService; + private readonly ILocalizationService localizationService; [ObservableProperty] private ObservableCollection tweakItems = new(); @@ -26,6 +28,7 @@ public partial class SystemTweaksViewModel : BaseViewModel public SystemTweaksViewModel( ISystemTweaksService systemTweaksService, INotificationService notificationService, + ILocalizationService localizationService, ILogger logger, IEnhancedLoggingService? enhancedLoggingService = null, IActivityAuditService? activityAuditService = null) @@ -33,100 +36,29 @@ public SystemTweaksViewModel( { this.systemTweaksService = systemTweaksService; this.notificationService = notificationService; + this.localizationService = localizationService; - // Subscribe to tweak status changes this.systemTweaksService.TweakStatusChanged += this.OnTweakStatusChanged; + this.localizationService.LanguageChanged += this.OnLanguageChanged; this.InitializeTweakItems(); } private void InitializeTweakItems() { - this.TweakItems = new ObservableCollection - { - new SystemTweakItem - { - Name = "Core Parking", - Description = "Controls CPU core parking for power management", - TweakType = SystemTweak.CoreParking, - IsEnabled = false, - IsAvailable = true, - ToggleCommand = new AsyncRelayCommand(this.ToggleTweakAsync), - }, - new SystemTweakItem - { - Name = "C-States", - Description = "Controls CPU C-States for power management", - TweakType = SystemTweak.CStates, - IsEnabled = false, - IsAvailable = true, - ToggleCommand = new AsyncRelayCommand(this.ToggleTweakAsync), - }, - new SystemTweakItem + this.TweakItems = new ObservableCollection( + Enum.GetValues().Select(type => new SystemTweakItem { - Name = "SysMain Service", - Description = "Windows Superfetch/SysMain service for memory management", - TweakType = SystemTweak.SysMain, - IsEnabled = false, - IsAvailable = true, + TweakType = type, ToggleCommand = new AsyncRelayCommand(this.ToggleTweakAsync), - }, - new SystemTweakItem - { - Name = "Prefetch", - Description = "Windows Prefetch feature for faster application loading", - TweakType = SystemTweak.Prefetch, - IsEnabled = false, - IsAvailable = true, - ToggleCommand = new AsyncRelayCommand(this.ToggleTweakAsync), - }, - new SystemTweakItem - { - Name = "Power Throttling", - Description = "Windows Power Throttling for energy efficiency", - TweakType = SystemTweak.PowerThrottling, - IsEnabled = false, - IsAvailable = true, - ToggleCommand = new AsyncRelayCommand(this.ToggleTweakAsync), - }, - new SystemTweakItem - { - Name = "HPET", - Description = "High Precision Event Timer for system timing", - TweakType = SystemTweak.Hpet, - IsEnabled = false, - IsAvailable = true, - ToggleCommand = new AsyncRelayCommand(this.ToggleTweakAsync), - }, - new SystemTweakItem - { - Name = "High Scheduling Category", - Description = "High scheduling priority for gaming applications", - TweakType = SystemTweak.HighSchedulingCategory, - IsEnabled = false, - IsAvailable = true, - ToggleCommand = new AsyncRelayCommand(this.ToggleTweakAsync), - }, - new SystemTweakItem - { - Name = "Menu Show Delay", - Description = "Delay before showing context menus", - TweakType = SystemTweak.MenuShowDelay, - IsEnabled = false, - IsAvailable = true, - ToggleCommand = new AsyncRelayCommand(this.ToggleTweakAsync) - }, - }; + })); + this.ApplyLocalizedTweakText(); } [RelayCommand] public async Task LoadAsync() { - await this.ExecuteAsync( - async () => - { - await this.RefreshAllTweaksAsync(); - }, "Loading system tweaks...", "System tweaks loaded successfully"); + await this.RefreshAllTweaksAsync(); } [RelayCommand] @@ -134,39 +66,33 @@ public async Task RefreshAllTweaksAsync() { try { - // Marshal UI updates to the UI thread to prevent cross-thread access exceptions - await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => + await InvokeOnUiAsync(() => { this.IsRefreshing = true; - this.RefreshStatusText = "Refreshing system tweaks..."; + this.RefreshStatusText = this.Localize("SystemTweaks_StatusRefreshing", "Refreshing system tweaks..."); }); - await this.systemTweaksService.RefreshAllStatusesAsync(); - - // Update each tweak item with current status - foreach (var item in this.TweakItems) - { - await this.UpdateTweakItemStatusAsync(item); - } + await Task.WhenAll(this.TweakItems.Select(this.UpdateTweakItemStatusAsync)); - // Marshal UI updates to the UI thread to prevent cross-thread access exceptions - await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => + await InvokeOnUiAsync(() => { - this.RefreshStatusText = $"Last refreshed: {DateTime.Now:HH:mm:ss}"; + this.RefreshStatusText = this.Localize( + "SystemTweaks_StatusLastRefreshedFormat", + "Last refreshed: {0}", + DateTime.Now.ToString("T")); }); } catch (Exception ex) { - await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => + await InvokeOnUiAsync(() => { - this.SetError("Failed to refresh system tweaks", ex); - this.RefreshStatusText = "Refresh failed"; + this.SetError(this.Localize("SystemTweaks_StatusRefreshFailed", "Failed to refresh system tweaks"), ex); + this.RefreshStatusText = this.Localize("SystemTweaks_StatusRefreshFailedShort", "Refresh failed"); }); } finally { - // Marshal UI updates to the UI thread to prevent cross-thread access exceptions - await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => + await InvokeOnUiAsync(() => { this.IsRefreshing = false; }); @@ -184,7 +110,6 @@ private async Task UpdateTweakItemStatusAsync(SystemTweakItem item) SystemTweak.SysMain => await this.systemTweaksService.GetSysMainStatusAsync(), SystemTweak.Prefetch => await this.systemTweaksService.GetPrefetchStatusAsync(), SystemTweak.PowerThrottling => await this.systemTweaksService.GetPowerThrottlingStatusAsync(), - SystemTweak.Hpet => await this.systemTweaksService.GetHpetStatusAsync(), SystemTweak.HighSchedulingCategory => await this.systemTweaksService.GetHighSchedulingCategoryStatusAsync(), SystemTweak.MenuShowDelay => await this.systemTweaksService.GetMenuShowDelayStatusAsync(), _ => new TweakStatus { IsAvailable = false, ErrorMessage = "Unknown tweak type" }, @@ -193,10 +118,6 @@ private async Task UpdateTweakItemStatusAsync(SystemTweakItem item) item.IsEnabled = status.IsEnabled; item.IsAvailable = status.IsAvailable; item.ErrorMessage = status.ErrorMessage; - if (!string.IsNullOrEmpty(status.Description)) - { - item.Description = status.Description; - } } catch (Exception ex) { @@ -217,7 +138,7 @@ private async Task ToggleTweakAsync(SystemTweakItem? item) { await InvokeOnUiAsync(() => { - this.SetStatus($"Toggling {item.Name}..."); + this.SetStatus(this.Localize("SystemTweaks_StatusTogglingFormat", "Toggling {0}...", item.Name)); }); var newState = !item.IsEnabled; @@ -228,7 +149,6 @@ await InvokeOnUiAsync(() => SystemTweak.SysMain => await this.systemTweaksService.SetSysMainAsync(newState), SystemTweak.Prefetch => await this.systemTweaksService.SetPrefetchAsync(newState), SystemTweak.PowerThrottling => await this.systemTweaksService.SetPowerThrottlingAsync(newState), - SystemTweak.Hpet => await this.systemTweaksService.SetHpetAsync(newState), SystemTweak.HighSchedulingCategory => await this.systemTweaksService.SetHighSchedulingCategoryAsync(newState), SystemTweak.MenuShowDelay => await this.systemTweaksService.SetMenuShowDelayAsync(newState), _ => false, @@ -239,12 +159,18 @@ await InvokeOnUiAsync(() => await this.UpdateTweakItemStatusAsync(item); await InvokeOnUiAsync(() => { - this.SetStatus($"{item.Name} {(newState ? "enabled" : "disabled")} successfully"); + this.SetStatus(this.Localize( + newState ? "SystemTweaks_StatusEnabledFormat" : "SystemTweaks_StatusDisabledFormat", + newState ? "{0} enabled successfully" : "{0} disabled successfully", + item.Name)); }); await this.notificationService.ShowSuccessNotificationAsync( - "System Tweak Updated", - $"{item.Name} has been {(newState ? "enabled" : "disabled")}"); + this.Localize("SystemTweaks_NotifyUpdatedTitle", "System Tweak Updated"), + this.Localize( + newState ? "SystemTweaks_NotifyEnabledFormat" : "SystemTweaks_NotifyDisabledFormat", + newState ? "{0} has been enabled" : "{0} has been disabled", + item.Name)); await this.LogUserActionAsync( "SystemTweakApplied", $"{item.Name} {(newState ? "enabled" : "disabled")}", @@ -254,12 +180,15 @@ await this.LogUserActionAsync( { await InvokeOnUiAsync(() => { - this.SetError($"Failed to toggle {item.Name}", null); + this.SetError(this.Localize("SystemTweaks_StatusToggleFailedFormat", "Failed to toggle {0}", item.Name), null); }); await this.notificationService.ShowErrorNotificationAsync( - "System Tweak Failed", - $"Failed to {(newState ? "enable" : "disable")} {item.Name}"); + this.Localize("SystemTweaks_NotifyFailedTitle", "System Tweak Failed"), + this.Localize( + newState ? "SystemTweaks_StatusEnableFailedFormat" : "SystemTweaks_StatusDisableFailedFormat", + newState ? "Failed to enable {0}" : "Failed to disable {0}", + item.Name)); await this.LogUserActionAsync( "SystemTweakFailed", $"Failed to {(newState ? "enable" : "disable")} {item.Name}", @@ -270,7 +199,7 @@ await this.LogUserActionAsync( { await InvokeOnUiAsync(() => { - this.SetError($"Error toggling {item.Name}", ex); + this.SetError(this.Localize("SystemTweaks_StatusErrorTogglingFormat", "Error toggling {0}", item.Name), ex); }); this.Logger.LogError(ex, "Error toggling tweak {TweakName}", item.Name); await this.LogUserActionAsync( @@ -292,6 +221,42 @@ private static Task InvokeOnUiAsync(Action action) return dispatcher.InvokeAsync(action).Task; } + private void ApplyLocalizedTweakText() + { + foreach (var item in this.TweakItems) + { + var text = item.TweakType switch + { + SystemTweak.CoreParking => ("SystemTweak_CoreParking_Name", "Core Parking", "SystemTweak_CoreParking_Desc", "Controls CPU core parking for power management"), + SystemTweak.CStates => ("SystemTweak_CStates_Name", "C-States", "SystemTweak_CStates_Desc", "Controls CPU C-States for power management"), + SystemTweak.SysMain => ("SystemTweak_SysMain_Name", "SysMain Service", "SystemTweak_SysMain_Desc", "Windows SysMain service for memory management"), + SystemTweak.Prefetch => ("SystemTweak_Prefetch_Name", "Prefetch", "SystemTweak_Prefetch_Desc", "Windows Prefetch for faster application loading"), + SystemTweak.PowerThrottling => ("SystemTweak_PowerThrottling_Name", "Power Throttling", "SystemTweak_PowerThrottling_Desc", "Turns off Windows Power Throttling for sustained performance"), + SystemTweak.HighSchedulingCategory => ("SystemTweak_HighSchedulingCategory_Name", "High Scheduling Category", "SystemTweak_HighSchedulingCategory_Desc", "Uses the high MMCSS scheduling category for games"), + SystemTweak.MenuShowDelay => ("SystemTweak_MenuShowDelay_Name", "Menu Show Delay", "SystemTweak_MenuShowDelay_Desc", "Delay before showing context menus"), + _ => throw new ArgumentOutOfRangeException(nameof(item.TweakType), item.TweakType, null), + }; + + item.Name = this.Localize(text.Item1, text.Item2); + item.Description = this.Localize(text.Item3, text.Item4); + } + + this.RefreshStatusText = this.Localize("SystemTweaks_StatusReady", "Ready"); + } + + private string Localize(string key, string fallback, params object[] arguments) + { + var localized = this.localizationService.GetString(key); + var format = string.IsNullOrWhiteSpace(localized) || string.Equals(localized, key, StringComparison.Ordinal) + ? fallback + : localized; + return arguments.Length == 0 + ? format + : string.Format(CultureInfo.CurrentCulture, format, arguments); + } + + private void OnLanguageChanged(object? sender, string language) => this.ApplyLocalizedTweakText(); + private void OnTweakStatusChanged(object? sender, TweakStatusChangedEventArgs e) { try @@ -313,6 +278,7 @@ private void OnTweakStatusChanged(object? sender, TweakStatusChangedEventArgs e) protected override void OnDispose() { this.systemTweaksService.TweakStatusChanged -= this.OnTweakStatusChanged; + this.localizationService.LanguageChanged -= this.OnLanguageChanged; base.OnDispose(); } } diff --git a/Views/MasksView.xaml b/Views/MasksView.xaml index 31c72d4..ded8197 100644 --- a/Views/MasksView.xaml +++ b/Views/MasksView.xaml @@ -40,7 +40,7 @@ - + @@ -195,7 +195,7 @@ - + diff --git a/Views/PowerPlanView.xaml b/Views/PowerPlanView.xaml index c8d4bfd..612613e 100644 --- a/Views/PowerPlanView.xaml +++ b/Views/PowerPlanView.xaml @@ -85,7 +85,7 @@ @@ -115,7 +115,9 @@ TextTrimming="CharacterEllipsis"/> + Foreground="{DynamicResource TextFillColorPrimaryBrush}"/> diff --git a/Views/ProcessView.xaml b/Views/ProcessView.xaml index 6a734ff..fd133ef 100644 --- a/Views/ProcessView.xaml +++ b/Views/ProcessView.xaml @@ -372,7 +372,7 @@ - + @@ -380,7 +380,7 @@ - + diff --git a/Views/SettingsView.xaml b/Views/SettingsView.xaml index 4a5f7e5..60cf7c1 100644 --- a/Views/SettingsView.xaml +++ b/Views/SettingsView.xaml @@ -359,25 +359,6 @@ IsChecked="{Binding Settings.EnableAutomaticUpdateChecks}" Margin="0,0,0,6"/> - - - - - - - - - - - - diff --git a/Views/SystemTweaksView.xaml b/Views/SystemTweaksView.xaml index def8a93..f99d512 100644 --- a/Views/SystemTweaksView.xaml +++ b/Views/SystemTweaksView.xaml @@ -3,82 +3,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" mc:Ignorable="d" d:DesignHeight="600" d:DesignWidth="800"> - - - - @@ -204,13 +132,13 @@ VerticalAlignment="Center"> @@ -218,16 +146,16 @@ - + - { - // Ignore non-fatal loading errors to keep the view responsive. - }); - } - - private async Task UserControl_LoadedAsync() - { - if (this.DataContext is SystemTweaksViewModel viewModel) - { - await viewModel.LoadCommand.ExecuteAsync(null); - } - } } }