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);
- }
- }
}
}