diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d60d726..03f3fec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,13 +66,6 @@ jobs: - name: Publish self-contained single-file run: dotnet publish "ThreadPilot.csproj" --configuration Release -p:PublishProfile=WinX64-SingleFile - - name: Ensure bundled power plans in single-file output - shell: pwsh - run: | - $ErrorActionPreference = "Stop" - New-Item -ItemType Directory -Force -Path "artifacts/release/singlefile/Powerplans" | Out-Null - Copy-Item "assets/Powerplans/*" -Destination "artifacts/release/singlefile/Powerplans" -Recurse -Force - - name: Publish ReadyToRun run: dotnet publish "ThreadPilot.csproj" --configuration Release -p:PublishProfile=WinX64-ReadyToRun @@ -158,6 +151,7 @@ jobs: taskkill /IM "ThreadPilot.exe" /F >nul 2>&1 echo [2/4] Removing startup task and startup registry entry... + schtasks /Delete /TN "ThreadPilot_Launch" /F >nul 2>&1 schtasks /Delete /TN "ThreadPilot_Startup" /F >nul 2>&1 reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "ThreadPilot" /f >nul 2>&1 diff --git a/Installer/Installer.iss b/Installer/Installer.iss index a3b2eaf..785f98c 100644 --- a/Installer/Installer.iss +++ b/Installer/Installer.iss @@ -5,7 +5,7 @@ #define MyAppPublisher "ThreadPilot" #define MyAppURL "https://github.com/" #define MyAppExeName "ThreadPilot.exe" -#define MyAppVersion "1.6.0" +#define MyAppVersion "1.7.0" #ifndef MyWizardStyle #define MyWizardStyle "modern dynamic windows11" @@ -55,6 +55,7 @@ Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChang [UninstallRun] Filename: "taskkill.exe"; Parameters: "/IM ""{#MyAppExeName}"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallKill" +Filename: "schtasks.exe"; Parameters: "/Delete /TN ""ThreadPilot_Launch"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotLaunchTask" Filename: "schtasks.exe"; Parameters: "/Delete /TN ""ThreadPilot_Startup"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotStartupTask" Filename: "reg.exe"; Parameters: "delete ""HKCU\Software\Microsoft\Windows\CurrentVersion\Run"" /v ""ThreadPilot"" /f"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotRunEntry" diff --git a/Installer/ThreadPilot.wxs b/Installer/ThreadPilot.wxs index 0563c60..c277b43 100644 --- a/Installer/ThreadPilot.wxs +++ b/Installer/ThreadPilot.wxs @@ -7,7 +7,7 @@ diff --git a/Installer/setup.iss b/Installer/setup.iss index 0c75239..40c41f2 100644 --- a/Installer/setup.iss +++ b/Installer/setup.iss @@ -11,7 +11,7 @@ #endif #ifndef MyAppVersion - #define MyAppVersion "1.6.0" + #define MyAppVersion "1.7.0" #endif #ifndef MyAppSourceDir @@ -54,7 +54,6 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ Source: "{#MyAppSourceDir}\ThreadPilot.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "{#MyAppSourceDir}\*.dll"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist Source: "{#MyAppSourceDir}\*.pdb"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist -Source: "{#MyAppSourceDir}\Powerplans\*"; DestDir: "{app}\Powerplans"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" @@ -71,6 +70,7 @@ Filename: "{app}\{#MyAppExeName}"; Flags: nowait; Check: IsThreadPilotUpdate ; Per-user AppData cleanup is limited to the account context used by uninstall. [UninstallRun] Filename: "taskkill.exe"; Parameters: "/IM ""{#MyAppExeName}"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallKillThreadPilot" +Filename: "schtasks.exe"; Parameters: "/Delete /TN ""ThreadPilot_Launch"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotLaunchTask" Filename: "schtasks.exe"; Parameters: "/Delete /TN ""ThreadPilot_Startup"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotStartupTask" Filename: "reg.exe"; Parameters: "delete ""HKCU\Software\Microsoft\Windows\CurrentVersion\Run"" /v ""ThreadPilot"" /f"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotRunEntry" diff --git a/Locales/de-DE.xaml b/Locales/de-DE.xaml index 3116589..6575406 100644 --- a/Locales/de-DE.xaml +++ b/Locales/de-DE.xaml @@ -172,6 +172,8 @@ Importieren Sie die ausgewählte benutzerdefinierte .pow-Datei in Windows .pow-Datei hinzufügen Fügen Sie der benutzerdefinierten Bibliothek eine neue benutzerdefinierte Energieplandatei hinzu + Mehr Energiesparpläne? + Weitere Energiesparpläne der Community auf Discord entdecken. Löschen Aktiv @@ -278,8 +280,8 @@ Die Echtzeitpriorität wird von ThreadPilot blockiert, da Windows dadurch instabil werden oder nicht mehr reagieren kann. Echtzeit (blockiert) - Deaktivieren Sie den inaktiven Server - Verhindert, dass das System während der Ausführung dieses Prozesses in den Ruhezustand wechselt. Nützlich für Spiele und leistungskritische Anwendungen. + Ruhezustand während des Prozesses verhindern + Hält das System aktiv, solange der Prozess läuft. Der Bildschirm darf ausgehen und expliziter Ruhezustand wird nicht blockiert. Profil / Regel Geben Sie einen Profilnamen ein, um Einstellungen zu speichern oder zu laden @@ -365,6 +367,9 @@ Regeln Name Legen Sie die Speicherpriorität fest + E/A-Priorität festlegen + E/A-Priorität nicht verfügbar + E/A-Priorität: {0} Sehr niedrig Mittel Prozessinformationen aktualisieren @@ -437,6 +442,11 @@ Aktivieren Sie die Debug-Protokollierung Leistungsindikatoren aktivieren + Stimmt etwas nicht? + Tritt der ThreadPilot-Discord-Community bei, um Hilfe zu erhalten und dich auszutauschen. + Discord öffnen + Melde Fehler oder unerwartetes Verhalten über GitHub Issues. + Problem melden Über Version: Copyright © 2026 PrimeBuild @@ -547,6 +557,28 @@ Hohe Planungspriorität für Gaming-Anwendungen Menü Verzögerung anzeigen Verzögerung vor der Anzeige von Kontextmenüs + Spielmodus + Planungsoptimierungen des Windows-Spielmodus + Speicherintegrität (HVCI) + Öffnet Windows-Sicherheit. Das Deaktivieren verringert die Systemsicherheit und kann einen Neustart erfordern. + Hardwarebeschleunigte GPU-Planung + Öffnet die Windows-Grafikeinstellungen zur Prüfung von HAGS. + Optimierungen für Fensterspiele + Öffnet die Windows-Grafikeinstellungen für Flip-Model-Optimierungen. + Selektives USB-Energiesparen + Steuert selektives USB-Energiesparen im aktiven Netzstromplan. + Zeigerbeschleunigung + Steuert die Windows-Mausbeschleunigung. + Ethernet-Energiesparen deaktivieren + Deaktiviert unterstützte EEE-, Green-Ethernet- und Energiesparoptionen physischer Ethernet-Adapter. + Interrupt-Moderation deaktivieren + Kann die Ethernet-Latenz auf Kosten höherer CPU- und Interrupt-Last senken. + GPU-MSI-Modus + Steuert MSI nur, wenn der Grafiktreiber MSISupported bereits bereitstellt. Neustart erforderlich. + Einstellungen öffnen + Von Windows verwaltet + Windows-Einstellungen konnten nicht geöffnet werden + Einstellungen für {0} geöffnet Bereit diff --git a/Locales/en-US.xaml b/Locales/en-US.xaml index 00e3f0c..6babb75 100644 --- a/Locales/en-US.xaml +++ b/Locales/en-US.xaml @@ -172,6 +172,8 @@ Import the selected custom .pow file into Windows Add .pow File Add a new custom power plan file to the custom library + More plans? + Browse additional community power plans on Discord. Delete Active @@ -278,8 +280,8 @@ Realtime priority is blocked by ThreadPilot because it can make Windows unstable or unresponsive. Realtime (blocked) - Disable Idle Server - Prevents the system from entering idle state while this process is running. Useful for games and performance-critical applications. + Prevent sleep while process runs + Keeps the system awake while this process is running. The display may still turn off, and explicit sleep is not blocked. Profile / Rule Enter a profile name to save or load settings @@ -365,6 +367,9 @@ Rules Name Set Memory Priority + Set I/O Priority + I/O priority unavailable + I/O priority: {0} Very Low Medium Refresh Process Info @@ -437,6 +442,11 @@ Enable debug logging Enable performance counters + Something wrong? + Join the ThreadPilot Discord community for help and discussion. + Open Discord + Report bugs or unexpected behavior through GitHub Issues. + Report an issue About Version: Copyright © 2026 PrimeBuild @@ -547,6 +557,28 @@ High scheduling priority for gaming applications Menu Show Delay Delay before showing context menus + Game Mode + Windows Game Mode scheduling optimizations + Memory Integrity (HVCI) + Opens Windows Security. Disabling this protection reduces system security and may require a restart. + Hardware-accelerated GPU scheduling + Opens Windows graphics settings to let you review HAGS. + Optimizations for windowed games + Opens Windows graphics settings to manage Flip Model optimizations. + USB Selective Suspend + Controls USB selective suspend for the active power plan while plugged in. + Enhance pointer precision + Controls Windows mouse acceleration. + Disable Ethernet power saving + Disables supported EEE, Green Ethernet and power-saving driver properties on physical Ethernet adapters. + Disable interrupt moderation + Can reduce Ethernet latency at the cost of higher CPU and interrupt load. + GPU MSI Mode + Controls MSI only when the display driver already exposes MSISupported. A restart is required. + Open settings + Managed by Windows + Could not open Windows settings + Opened settings for {0} Ready diff --git a/Locales/es-ES.xaml b/Locales/es-ES.xaml index bc1b81f..4fff18e 100644 --- a/Locales/es-ES.xaml +++ b/Locales/es-ES.xaml @@ -172,6 +172,8 @@ Importe el archivo .pow personalizado seleccionado a Windows Agregar archivo .pow Agregue un nuevo archivo de plan de energía personalizado a la biblioteca personalizada + ¿Más planes? + Descubre más planes de energía de la comunidad en Discord. Eliminar Activo @@ -278,8 +280,8 @@ La prioridad en tiempo real está bloqueada por ThreadPilot porque puede hacer que Windows sea inestable o no responda. Tiempo real (bloqueado) - Deshabilitar el servidor inactivo - Evita que el sistema entre en estado inactivo mientras se ejecuta este proceso. Útil para juegos y aplicaciones críticas para el rendimiento. + Evitar suspensión durante el proceso + Mantiene el sistema activo mientras se ejecuta el proceso. La pantalla puede apagarse y no se bloquea una suspensión explícita. Perfil / Regla Ingrese un nombre de perfil para guardar o cargar la configuración @@ -365,6 +367,9 @@ Reglas Nombre Establecer prioridad de memoria + Establecer prioridad de E/S + Prioridad de E/S no disponible + Prioridad de E/S: {0} Muy bajo Medio Actualizar información del proceso @@ -437,6 +442,11 @@ Habilitar el registro de depuración Habilitar contadores de rendimiento + ¿Algo no funciona? + Únete a la comunidad de ThreadPilot en Discord para obtener ayuda y conversar. + Abrir Discord + Informa de errores o comportamientos inesperados mediante GitHub Issues. + Informar de un problema Acerca de Versión: Copyright © 2026 PrimeBuild @@ -547,6 +557,28 @@ Alta prioridad de programación para aplicaciones de juegos Menú Mostrar retraso Retraso antes de mostrar menús contextuales + Modo de juego + Optimizaciones de programación del Modo de juego de Windows + Integridad de memoria (HVCI) + Abre Seguridad de Windows. Desactivarla reduce la seguridad y puede requerir reiniciar. + Programación de GPU acelerada por hardware + Abre la configuración gráfica de Windows para revisar HAGS. + Optimizaciones para juegos en ventana + Abre la configuración gráfica de Windows para administrar Flip Model. + Suspensión selectiva USB + Controla la suspensión selectiva USB del plan activo cuando está conectado. + Mejorar precisión del puntero + Controla la aceleración del ratón de Windows. + Desactivar ahorro de energía Ethernet + Desactiva EEE, Green Ethernet y opciones compatibles en adaptadores Ethernet físicos. + Desactivar moderación de interrupciones + Puede reducir la latencia Ethernet aumentando la carga de CPU e interrupciones. + Modo MSI de GPU + Controla MSI solo si el controlador gráfico ya expone MSISupported. Requiere reiniciar. + Abrir configuración + Administrado por Windows + No se pudo abrir la configuración de Windows + Configuración abierta para {0} Listo diff --git a/Locales/fr-FR.xaml b/Locales/fr-FR.xaml index 0224ff8..c7c777e 100644 --- a/Locales/fr-FR.xaml +++ b/Locales/fr-FR.xaml @@ -172,6 +172,8 @@ Importez le fichier .pow personnalisé sélectionné dans Windows Ajouter un fichier .pow Ajouter un nouveau fichier de plan d'alimentation personnalisé à la bibliothèque personnalisée + Plus de modes ? + Découvrez d'autres modes d'alimentation de la communauté sur Discord. Supprimer Actif @@ -278,8 +280,8 @@ La priorité en temps réel est bloquée par ThreadPilot car elle peut rendre Windows instable ou ne pas répondre. Temps réel (bloqué) - Désactiver le serveur inactif - Empêche le système d'entrer en état d'inactivité pendant l'exécution de ce processus. Utile pour les jeux et les applications critiques en termes de performances. + Empêcher la veille pendant le processus + Maintient le système éveillé pendant l'exécution du processus. L'écran peut s'éteindre et une veille explicite n'est pas bloquée. Profil / Règle Entrez un nom de profil pour enregistrer ou charger les paramètres @@ -365,6 +367,9 @@ Règles Nom Définir la priorité de la mémoire + Définir la priorité E/S + Priorité E/S indisponible + Priorité E/S : {0} Très faible Moyen Actualiser les informations sur le processus @@ -437,6 +442,11 @@ Activer la journalisation du débogage Activer les compteurs de performances + Un problème ? + Rejoignez la communauté Discord de ThreadPilot pour obtenir de l'aide et échanger. + Ouvrir Discord + Signalez les bugs ou comportements inattendus via GitHub Issues. + Signaler un problème À propos Version : Copyright © 2026 PrimeBuild @@ -547,6 +557,28 @@ Priorité de planification élevée pour les applications de jeux Menu Afficher le délai Délai avant l'affichage des menus contextuels + Mode Jeu + Optimisations de planification du Mode Jeu Windows + Intégrité de la mémoire (HVCI) + Ouvre Sécurité Windows. La désactivation réduit la sécurité et peut nécessiter un redémarrage. + Planification GPU à accélération matérielle + Ouvre les paramètres graphiques Windows pour vérifier HAGS. + Optimisations des jeux fenêtrés + Ouvre les paramètres graphiques Windows pour gérer les optimisations Flip Model. + Suspension sélective USB + Contrôle la suspension sélective USB du mode d'alimentation actif sur secteur. + Améliorer la précision du pointeur + Contrôle l'accélération de la souris Windows. + Désactiver l'économie d'énergie Ethernet + Désactive EEE, Green Ethernet et les options d'économie prises en charge sur les adaptateurs Ethernet physiques. + Désactiver la modération des interruptions + Peut réduire la latence Ethernet au prix d'une charge CPU et d'interruptions supérieure. + Mode MSI du GPU + Contrôle MSI uniquement si le pilote graphique expose déjà MSISupported. Redémarrage requis. + Ouvrir les paramètres + Géré par Windows + Impossible d'ouvrir les paramètres Windows + Paramètres ouverts pour {0} Prêt diff --git a/Locales/it-IT.xaml b/Locales/it-IT.xaml index 5c316cc..748e389 100644 --- a/Locales/it-IT.xaml +++ b/Locales/it-IT.xaml @@ -172,6 +172,8 @@ Importa il file .pow personalizzato selezionato in Windows Aggiungi il file .pow Aggiungi un nuovo file di combinazione di risparmio energia personalizzato alla libreria personalizzata + Altri piani? + Scopri altri piani energetici della community su Discord. Elimina Attivo @@ -278,8 +280,8 @@ La priorità in tempo reale è bloccata da ThreadPilot perché può rendere Windows instabile o non rispondere. In tempo reale (bloccato) - Disabilita server inattivo - Impedisce al sistema di entrare nello stato inattivo mentre questo processo è in esecuzione. Utile per giochi e applicazioni critiche per le prestazioni. + Impedisci sospensione durante il processo + Mantiene attivo il sistema finché il processo è in esecuzione. Lo schermo può spegnersi e la sospensione richiesta dall'utente non viene bloccata. Profilo/Regola Inserisci un nome di profilo per salvare o caricare le impostazioni @@ -365,6 +367,9 @@ Regole Nome Imposta la priorità della memoria + Imposta priorità I/O + Priorità I/O non disponibile + Priorità I/O: {0} Molto basso Medio Aggiorna informazioni sul processo @@ -437,6 +442,11 @@ Abilita la registrazione del debug Abilita i contatori delle prestazioni + Qualcosa non va? + Entra nella community Discord di ThreadPilot per assistenza e discussioni. + Apri Discord + Segnala bug o comportamenti anomali tramite GitHub Issues. + Segnala un problema Informazioni Versione: Copyright © 2026 PrimeBuild @@ -547,6 +557,28 @@ Alta priorità di pianificazione per le applicazioni di gioco Menu Mostra ritardo Ritardo prima di mostrare i menu contestuali + Modalità gioco + Ottimizzazioni di pianificazione della Modalità gioco di Windows + Integrità della memoria (HVCI) + Apre Sicurezza di Windows. Disattivare questa protezione riduce la sicurezza e può richiedere il riavvio. + Pianificazione GPU con accelerazione hardware + Apre le impostazioni grafiche di Windows per controllare HAGS. + Ottimizzazioni per giochi in finestra + Apre le impostazioni grafiche di Windows per gestire le ottimizzazioni Flip Model. + Sospensione selettiva USB + Controlla la sospensione selettiva USB del piano attivo quando il PC è collegato alla rete elettrica. + Aumenta precisione puntatore + Controlla l'accelerazione del mouse di Windows. + Disattiva risparmio energetico Ethernet + Disattiva EEE, Green Ethernet e le proprietà di risparmio energetico supportate sugli adattatori Ethernet fisici. + Disattiva moderazione interrupt + Può ridurre la latenza Ethernet aumentando il carico CPU e il numero di interrupt. + Modalità MSI GPU + Controlla MSI solo se il driver video espone già MSISupported. È necessario riavviare. + Apri impostazioni + Gestito da Windows + Impossibile aprire le impostazioni di Windows + Impostazioni aperte per {0} Pronto diff --git a/Locales/ru-RU.xaml b/Locales/ru-RU.xaml index e445183..ed2da51 100644 --- a/Locales/ru-RU.xaml +++ b/Locales/ru-RU.xaml @@ -172,6 +172,8 @@ Импортируйте выбранный пользовательский файл .pow в Windows. Добавить файл .pow Добавьте новый файл пользовательского плана электропитания в пользовательскую библиотеку. + Нужны другие планы? + Найдите дополнительные планы электропитания сообщества в Discord. Удалить Активный @@ -278,8 +280,8 @@ Приоритет реального времени блокируется ThreadPilot, поскольку это может сделать Windows нестабильным или перестать отвечать на запросы. В реальном времени (заблокировано) - Отключить простаивающий сервер - Предотвращает переход системы в состояние ожидания во время выполнения этого процесса. Полезно для игр и приложений, критичных к производительности. + Не переходить в сон во время процесса + Поддерживает работу системы, пока процесс запущен. Экран может выключиться, а явный переход в сон не блокируется. Профиль/Правило Введите имя профиля, чтобы сохранить или загрузить настройки. @@ -365,6 +367,9 @@ Правила Имя Установить приоритет памяти + Установить приоритет ввода-вывода + Приоритет ввода-вывода недоступен + Приоритет ввода-вывода: {0} Очень низкий Средний Обновить информацию о процессе @@ -437,6 +442,11 @@ Включить ведение журнала отладки Включить счетчики производительности + Что-то не работает? + Присоединяйтесь к сообществу ThreadPilot в Discord для помощи и общения. + Открыть Discord + Сообщайте об ошибках и неожиданном поведении через GitHub Issues. + Сообщить о проблеме О Версия: © 2026 PrimeBuild. @@ -547,6 +557,28 @@ Высокий приоритет планирования для игровых приложений Задержка показа меню Задержка перед показом контекстных меню + Игровой режим + Оптимизации планирования игрового режима Windows + Целостность памяти (HVCI) + Открывает Безопасность Windows. Отключение снижает безопасность и может потребовать перезагрузки. + Аппаратное планирование GPU + Открывает параметры графики Windows для проверки HAGS. + Оптимизация оконных игр + Открывает параметры графики Windows для управления Flip Model. + Выборочная приостановка USB + Управляет приостановкой USB в активном плане при питании от сети. + Повышенная точность указателя + Управляет ускорением мыши Windows. + Отключить энергосбережение Ethernet + Отключает поддерживаемые EEE, Green Ethernet и энергосбережение физических Ethernet-адаптеров. + Отключить модерацию прерываний + Может снизить задержку Ethernet ценой повышенной нагрузки CPU и прерываний. + Режим MSI для GPU + Управляет MSI, только если видеодрайвер уже предоставляет MSISupported. Требуется перезагрузка. + Открыть параметры + Управляется Windows + Не удалось открыть параметры Windows + Открыты параметры для {0} Готово diff --git a/Locales/zh-CN.xaml b/Locales/zh-CN.xaml index 7b68060..a457fc7 100644 --- a/Locales/zh-CN.xaml +++ b/Locales/zh-CN.xaml @@ -172,6 +172,8 @@ 将所选的自定义 .pow 文件导入 Windows 添加 .pow 文件 向自定义库添加新的自定义电源计划文件 + 需要更多计划? + 在 Discord 中获取更多社区电源计划。 删除 活动 @@ -278,8 +280,8 @@ 实时优先级已被 ThreadPilot 阻止,因为它可能会使 Windows 不稳定或无响应。 实时 (已阻止) - 禁用空闲服务 - 在此进程运行时阻止系统进入空闲状态。适用于游戏和对性能要求严苛的应用程序。 + 进程运行时阻止睡眠 + 进程运行时保持系统唤醒。显示器仍可关闭,并且不会阻止用户明确请求的睡眠。 配置文件 / 规则 输入配置文件名称以保存或加载设置 @@ -365,6 +367,9 @@ 规则 名称 设置内存优先级 + 设置 I/O 优先级 + I/O 优先级不可用 + I/O 优先级:{0} 极低 刷新进程信息 @@ -437,6 +442,11 @@ 开启调试诊断日志记录 启用内部性能计数器 + 遇到问题? + 加入 ThreadPilot Discord 社区以获取帮助并参与讨论。 + 打开 Discord + 通过 GitHub Issues 报告错误或异常行为。 + 报告问题 关于应用 软件版本: 版权所有 © 2026 PrimeBuild @@ -547,6 +557,28 @@ 为游戏应用程序提供高调度优先级 菜单显示延迟 显示上下文菜单之前的延迟时间 + 游戏模式 + Windows 游戏模式调度优化 + 内存完整性 (HVCI) + 打开 Windows 安全中心。关闭此保护会降低系统安全性,并且可能需要重启。 + 硬件加速 GPU 计划 + 打开 Windows 图形设置以查看 HAGS。 + 窗口化游戏优化 + 打开 Windows 图形设置以管理 Flip Model 优化。 + USB 选择性暂停 + 控制接通电源时当前电源计划的 USB 选择性暂停。 + 提高指针精确度 + 控制 Windows 鼠标加速。 + 禁用以太网节能 + 禁用物理以太网适配器支持的 EEE、Green Ethernet 和节能属性。 + 禁用中断调节 + 可以降低以太网延迟,但会增加 CPU 和中断负载。 + GPU MSI 模式 + 仅当显示驱动已公开 MSISupported 时控制 MSI。需要重启。 + 打开设置 + 由 Windows 管理 + 无法打开 Windows 设置 + 已打开 {0} 的设置 就绪 diff --git a/Models/PersistentProcessRule.cs b/Models/PersistentProcessRule.cs index c7e37aa..65271d3 100644 --- a/Models/PersistentProcessRule.cs +++ b/Models/PersistentProcessRule.cs @@ -20,20 +20,26 @@ public sealed record PersistentProcessRule public CpuSelection? CpuSelection { get; init; } - public long? LegacyAffinityMask { get; init; } - - public CpuAssignmentMode CpuAssignmentMode { get; init; } = CpuAssignmentMode.Automatic; + public long? LegacyAffinityMask { get; init; } + + public CpuAssignmentMode CpuAssignmentMode { get; init; } = CpuAssignmentMode.Automatic; public ProcessPriorityClass? Priority { get; init; } public ProcessMemoryPriority? MemoryPriority { get; init; } + public ProcessIoPriority? IoPriority { get; init; } + public bool ApplyAffinityOnStart { get; init; } public bool ApplyPriorityOnStart { get; init; } public bool ApplyMemoryPriorityOnStart { get; init; } + public bool ApplyIoPriorityOnStart { get; init; } + + public bool PreventSystemSleepWhileRunning { get; init; } + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; public DateTime UpdatedAt { get; init; } = DateTime.UtcNow; @@ -67,6 +73,10 @@ public sealed record PersistentRuleApplyResult public bool MemoryPriorityApplied { get; init; } + public bool IoPriorityApplied { get; init; } + + public bool PreventSleepApplied { get; init; } + public string? ErrorCode { get; init; } public string UserMessage { get; init; } = string.Empty; diff --git a/Models/ProcessMemoryPriority.cs b/Models/ProcessMemoryPriority.cs index 6c83902..645dd8a 100644 --- a/Models/ProcessMemoryPriority.cs +++ b/Models/ProcessMemoryPriority.cs @@ -11,4 +11,12 @@ public enum ProcessMemoryPriority BelowNormal = 4, Normal = 5, } + + public enum ProcessIoPriority + { + VeryLow = 0, + Low = 1, + Normal = 2, + High = 3, + } } diff --git a/Platforms/Windows/IProcessMemoryPriorityNativeApi.cs b/Platforms/Windows/IProcessMemoryPriorityNativeApi.cs index d22b2b8..1511d3c 100644 --- a/Platforms/Windows/IProcessMemoryPriorityNativeApi.cs +++ b/Platforms/Windows/IProcessMemoryPriorityNativeApi.cs @@ -26,6 +26,14 @@ bool SetProcessInformation( uint processInformationSize); int GetLastWin32Error(); + + int QueryIoPriority(SafeProcessHandle process, ref int priority, out uint returnLength) + { + returnLength = 0; + return unchecked((int)0xC0000002); + } + + int SetIoPriority(SafeProcessHandle process, ref int priority) => unchecked((int)0xC0000002); } public sealed class ProcessMemoryPriorityNativeApi : IProcessMemoryPriorityNativeApi @@ -73,6 +81,21 @@ public int GetLastWin32Error() { return Marshal.GetLastWin32Error(); } + + public int QueryIoPriority(SafeProcessHandle process, ref int priority, out uint returnLength) => + ProcessMemoryPriorityNativeMethods.NtQueryInformationProcess( + process, + ProcessMemoryPriorityNativeMethods.ProcessIoPriority, + ref priority, + sizeof(int), + out returnLength); + + public int SetIoPriority(SafeProcessHandle process, ref int priority) => + ProcessMemoryPriorityNativeMethods.NtSetInformationProcess( + process, + ProcessMemoryPriorityNativeMethods.ProcessIoPriority, + ref priority, + sizeof(int)); } public enum ProcessInformationClass diff --git a/Platforms/Windows/ProcessMemoryPriorityNativeMethods.cs b/Platforms/Windows/ProcessMemoryPriorityNativeMethods.cs index 1535bfa..d0ba944 100644 --- a/Platforms/Windows/ProcessMemoryPriorityNativeMethods.cs +++ b/Platforms/Windows/ProcessMemoryPriorityNativeMethods.cs @@ -8,6 +8,8 @@ namespace ThreadPilot.Platforms.Windows internal static partial class ProcessMemoryPriorityNativeMethods { + internal const int ProcessIoPriority = 33; + [LibraryImport("kernel32.dll", SetLastError = true)] public static partial SafeProcessHandle OpenProcess( ProcessAccessFlags access, @@ -29,5 +31,20 @@ public static partial bool SetProcessInformation( ProcessInformationClass processInformationClass, ref MemoryPriorityInformation processInformation, uint processInformationSize); + + [LibraryImport("ntdll.dll")] + public static partial int NtQueryInformationProcess( + SafeProcessHandle process, + int processInformationClass, + ref int processInformation, + int processInformationLength, + out uint returnLength); + + [LibraryImport("ntdll.dll")] + public static partial int NtSetInformationProcess( + SafeProcessHandle process, + int processInformationClass, + ref int processInformation, + int processInformationLength); } } diff --git a/README.md b/README.md index 8d263ed..68c7b10 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,8 @@ Useful startup arguments: In `Power Plans > Custom Power Plans`, use `Add .pow File` to import custom power plans directly from the app. +ThreadPilot does not bundle third-party `.pow` files. The Power Plans page can open the community Discord catalog on request; Settings also links to Discord support and GitHub Issues. Existing plans already imported into Windows are not removed by updates. + ## 🧱 Build from Source ```powershell @@ -161,6 +163,9 @@ Build release artifacts with the project script: - CI validates build, formatting, analyzers, vulnerability checks, and secret scanning. - Security disclosures are handled through private GitHub advisories. See [`docs/SECURITY.md`](docs/SECURITY.md). +- ThreadPilot has no analytics or telemetry. See [`docs/PRIVACY.md`](docs/PRIVACY.md). +- Release signing scope and controls are documented in [`docs/CODE_SIGNING_POLICY.md`](docs/CODE_SIGNING_POLICY.md). +- Free code signing provided by [SignPath.io](https://signpath.io), certificate by [SignPath Foundation](https://signpath.org), once the pending application is approved; current artifacts are unsigned. - Change history is tracked in [`docs/CHANGELOG.md`](docs/CHANGELOG.md). - Coverage focuses on business/application code and excludes generated build artifacts. @@ -170,6 +175,8 @@ Build release artifacts with the project script: - [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) - [`docs/CODE_OF_CONDUCT.md`](docs/CODE_OF_CONDUCT.md) - [`docs/RELEASE_SIGNING.md`](docs/RELEASE_SIGNING.md) +- [`docs/CODE_SIGNING_POLICY.md`](docs/CODE_SIGNING_POLICY.md) +- [`docs/PRIVACY.md`](docs/PRIVACY.md) - [`docs/release/PACKAGING.md`](docs/release/PACKAGING.md) - [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) - [`docs/QUALITY_GATES.md`](docs/QUALITY_GATES.md) diff --git a/Services/GameModeService.cs b/Services/GameModeService.cs index 7550192..229e8f4 100644 --- a/Services/GameModeService.cs +++ b/Services/GameModeService.cs @@ -53,7 +53,7 @@ public async Task SetGameModeAsync(bool enabled) try { - using var key = Registry.CurrentUser.OpenSubKey(GameBarKeyPath, writable: true); + using var key = Registry.CurrentUser.CreateSubKey(GameBarKeyPath, writable: true); if (key == null) { this.logger.LogWarning("GameBar registry key not found, cannot modify Game Mode"); @@ -75,38 +75,6 @@ public async Task SetGameModeAsync(bool enabled) return false; } } - - public async Task DisableGameModeForAffinityAsync() - { - try - { - bool isEnabled = await this.IsGameModeEnabledAsync(); - if (!isEnabled) - { - this.logger.LogDebug("Game Mode already disabled, no action needed"); - return false; - } - - this.logger.LogInformation("Game Mode is enabled, disabling for better CPU affinity control"); - bool success = await this.SetGameModeAsync(false); - - if (success) - { - this.logger.LogInformation("Successfully disabled Windows Game Mode for CPU affinity optimization"); - } - else - { - this.logger.LogWarning("Failed to disable Game Mode, CPU affinity may be affected"); - } - - return success; - } - catch (Exception ex) - { - this.logger.LogError(ex, "Error disabling Game Mode for affinity"); - return false; - } - } } } diff --git a/Services/IGameModeService.cs b/Services/IGameModeService.cs index fcd29ac..c01ffea 100644 --- a/Services/IGameModeService.cs +++ b/Services/IGameModeService.cs @@ -7,8 +7,6 @@ public interface IGameModeService Task IsGameModeEnabledAsync(); Task SetGameModeAsync(bool enabled); - - Task DisableGameModeForAffinityAsync(); } } diff --git a/Services/IProcessMemoryPriorityService.cs b/Services/IProcessMemoryPriorityService.cs index c2e6b34..5769858 100644 --- a/Services/IProcessMemoryPriorityService.cs +++ b/Services/IProcessMemoryPriorityService.cs @@ -10,5 +10,14 @@ public interface IProcessMemoryPriorityService Task GetMemoryPriorityAsync(ProcessModel process); Task SetMemoryPriorityAsync(ProcessModel process, ProcessMemoryPriority priority); + + Task GetIoPriorityAsync(ProcessModel process) => + Task.FromResult(null); + + Task SetIoPriorityAsync(ProcessModel process, ProcessIoPriority priority) => + Task.FromResult(ProcessOperationResult.Failed( + "Unsupported", + "I/O priority is unavailable on this system.", + "The configured priority service does not support I/O priority.")); } } diff --git a/Services/IProcessService.cs b/Services/IProcessService.cs index 1b1a085..940e3f6 100644 --- a/Services/IProcessService.cs +++ b/Services/IProcessService.cs @@ -38,8 +38,6 @@ public interface IProcessService Task IsProcessStillRunning(ProcessModel process); - Task SetIdleServerStateAsync(ProcessModel process, bool enableIdleServer); - Task SetRegistryPriorityAsync(ProcessModel process, bool enable, ProcessPriorityClass priority); void SetUseCpuSets(bool useCpuSets); diff --git a/Services/ISystemTweaksService.cs b/Services/ISystemTweaksService.cs index ca0bdb3..835dde7 100644 --- a/Services/ISystemTweaksService.cs +++ b/Services/ISystemTweaksService.cs @@ -7,6 +7,10 @@ public interface ISystemTweaksService { event EventHandler? TweakStatusChanged; + Task GetGameModeStatusAsync(); + + Task SetGameModeAsync(bool enabled); + Task GetCoreParkingStatusAsync(); Task SetCoreParkingAsync(bool enabled); @@ -34,6 +38,30 @@ public interface ISystemTweaksService Task GetMenuShowDelayStatusAsync(); Task SetMenuShowDelayAsync(bool enabled); + + Task GetUsbSelectiveSuspendStatusAsync(); + + Task SetUsbSelectiveSuspendAsync(bool enabled); + + Task GetPointerPrecisionStatusAsync(); + + Task SetPointerPrecisionAsync(bool enabled); + + Task GetEthernetPowerSavingStatusAsync(); + + Task SetEthernetPowerSavingAsync(bool enabled); + + Task GetInterruptModerationStatusAsync(); + + Task SetInterruptModerationAsync(bool enabled); + + Task GetGpuMsiModeStatusAsync(); + + Task SetGpuMsiModeAsync(bool enabled); + + Task GetMemoryIntegrityStatusAsync(); + + Task OpenSettingsAsync(SystemTweak tweak); } public class TweakStatus @@ -43,6 +71,8 @@ public class TweakStatus public bool IsAvailable { get; set; } public string? ErrorMessage { get; set; } + + public string? StatusText { get; set; } } public class TweakStatusChangedEventArgs : EventArgs @@ -63,8 +93,17 @@ public TweakStatusChangedEventArgs(string tweakName, TweakStatus status) public enum SystemTweak { + GameMode, CoreParking, CStates, + MemoryIntegrity, + Hags, + WindowedOptimizations, + UsbSelectiveSuspend, + PointerPrecision, + EthernetPowerSaving, + InterruptModeration, + GpuMsiMode, SysMain, Prefetch, PowerThrottling, diff --git a/Services/PersistentRulesEngine.cs b/Services/PersistentRulesEngine.cs index 849bccf..c21fce8 100644 --- a/Services/PersistentRulesEngine.cs +++ b/Services/PersistentRulesEngine.cs @@ -32,6 +32,7 @@ public sealed class PersistentRulesEngine : IPersistentRulesEngine private readonly IAffinityApplyService affinityApplyService; private readonly IProcessService processService; private readonly IProcessMemoryPriorityService memoryPriorityService; + private readonly ProcessPowerRequestService? powerRequestService; private readonly ILogger logger; public PersistentRulesEngine( @@ -40,7 +41,8 @@ public PersistentRulesEngine( IAffinityApplyService affinityApplyService, IProcessService processService, IProcessMemoryPriorityService memoryPriorityService, - ILogger logger) + ILogger logger, + ProcessPowerRequestService? powerRequestService = null) { this.ruleStore = ruleStore ?? throw new ArgumentNullException(nameof(ruleStore)); this.matcher = matcher ?? throw new ArgumentNullException(nameof(matcher)); @@ -48,6 +50,7 @@ public PersistentRulesEngine( this.processService = processService ?? throw new ArgumentNullException(nameof(processService)); this.memoryPriorityService = memoryPriorityService ?? throw new ArgumentNullException(nameof(memoryPriorityService)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.powerRequestService = powerRequestService; } public async Task> ApplyMatchingRulesAsync( @@ -79,7 +82,11 @@ private async Task ApplyRuleAsync( var result = CreateSuccessResult(rule, process); var success = true; - if (!rule.ApplyAffinityOnStart && !rule.ApplyPriorityOnStart && !rule.ApplyMemoryPriorityOnStart) + if (!rule.ApplyAffinityOnStart && + !rule.ApplyPriorityOnStart && + !rule.ApplyMemoryPriorityOnStart && + !rule.ApplyIoPriorityOnStart && + !rule.PreventSystemSleepWhileRunning) { return MarkRuleConfigurationFailure( result, @@ -172,6 +179,50 @@ private async Task ApplyRuleAsync( } } + if (rule.ApplyIoPriorityOnStart && !result.IsProcessExited) + { + if (!rule.IoPriority.HasValue) + { + success = false; + result = MarkRuleConfigurationFailure( + result, + rule, + "PersistentRuleMissingIoPriority", + "This saved rule has no I/O priority value to apply."); + } + else + { + var ioResult = await this.memoryPriorityService + .SetIoPriorityAsync(process, rule.IoPriority.Value) + .ConfigureAwait(false); + if (ioResult.Success) + { + result = result with { IoPriorityApplied = true }; + } + else + { + success = false; + result = this.MergeMemoryPriorityFailure(result, ioResult); + } + } + } + + if (rule.PreventSystemSleepWhileRunning && !result.IsProcessExited) + { + var applied = this.powerRequestService != null && + await this.powerRequestService.SetPreventSleepAsync(process, enabled: true).ConfigureAwait(false); + result = result with { PreventSleepApplied = applied }; + success = success && applied; + if (!applied) + { + result = result with + { + ErrorCode = "PowerRequestFailed", + UserMessage = "ThreadPilot could not prevent system sleep for this process.", + }; + } + } + return result with { Success = success, @@ -291,18 +342,18 @@ private async Task VerifyPriorityAsync( private Task ApplyAffinityAsync(PersistentProcessRule rule, ProcessModel process) { - if (rule.CpuSelection != null) - { - return rule.CpuAssignmentMode == CpuAssignmentMode.Automatic - ? this.affinityApplyService.ApplyAsync(process, rule.CpuSelection) - : this.affinityApplyService.ApplyAsync(process, rule.CpuSelection, rule.CpuAssignmentMode); - } + if (rule.CpuSelection != null) + { + return rule.CpuAssignmentMode == CpuAssignmentMode.Automatic + ? this.affinityApplyService.ApplyAsync(process, rule.CpuSelection) + : this.affinityApplyService.ApplyAsync(process, rule.CpuSelection, rule.CpuAssignmentMode); + } if (rule.LegacyAffinityMask.HasValue) { - return rule.CpuAssignmentMode == CpuAssignmentMode.Automatic - ? this.affinityApplyService.ApplyAsync(process, rule.LegacyAffinityMask.Value) - : this.affinityApplyService.ApplyAsync(process, rule.LegacyAffinityMask.Value, rule.CpuAssignmentMode); + return rule.CpuAssignmentMode == CpuAssignmentMode.Automatic + ? this.affinityApplyService.ApplyAsync(process, rule.LegacyAffinityMask.Value) + : this.affinityApplyService.ApplyAsync(process, rule.LegacyAffinityMask.Value, rule.CpuAssignmentMode); } return Task.FromResult(AffinityApplyResult.Succeeded(0, process.ProcessorAffinity)); diff --git a/Services/ProcessMemoryPriorityService.cs b/Services/ProcessMemoryPriorityService.cs index 33805ec..592d9bd 100644 --- a/Services/ProcessMemoryPriorityService.cs +++ b/Services/ProcessMemoryPriorityService.cs @@ -183,6 +183,119 @@ public Task SetMemoryPriorityAsync(ProcessModel process, } } + public Task GetIoPriorityAsync(ProcessModel process) + { + if (!this.nativeApi.IsSupported || !IsValidProcess(process)) + { + return Task.FromResult(null); + } + + try + { + using var handle = this.nativeApi.OpenProcess( + ProcessAccessFlags.PROCESS_QUERY_LIMITED_INFORMATION, + inheritHandle: false, + (uint)process.ProcessId); + if (handle.IsInvalid) + { + return Task.FromResult(null); + } + + var value = 0; + var status = this.nativeApi.QueryIoPriority(handle, ref value, out _); + return Task.FromResult(status == 0 && Enum.IsDefined(typeof(ProcessIoPriority), value) + ? (ProcessIoPriority?)value + : null); + } + catch (Exception ex) + { + this.logger.LogDebug(ex, "Could not read I/O priority for process {ProcessId}", process.ProcessId); + return Task.FromResult(null); + } + } + + public Task SetIoPriorityAsync(ProcessModel process, ProcessIoPriority priority) + { + if (!IsValidProcess(process)) + { + return Task.FromResult(ProcessOperationResult.Failed( + InvalidProcessErrorCode, + ProcessOperationUserMessages.ProcessExited, + "Process is null or has an invalid PID.")); + } + + if (!Enum.IsDefined(priority)) + { + return Task.FromResult(ProcessOperationResult.Failed( + "InvalidIoPriority", + "This I/O priority value is not supported.", + $"I/O priority value '{priority}' is not supported.")); + } + + try + { + using var handle = this.nativeApi.OpenProcess( + ProcessAccessFlags.PROCESS_SET_INFORMATION, + inheritHandle: false, + (uint)process.ProcessId); + if (handle.IsInvalid) + { + return Task.FromResult(this.FromLastError("OpenProcess failed before setting I/O priority.")); + } + + var value = (int)priority; + var status = this.nativeApi.SetIoPriority(handle, ref value); + if (status == 0) + { + return Task.FromResult(ProcessOperationResult.Succeeded( + "I/O priority applied.", + $"Process {process.Name} (PID: {process.ProcessId}) I/O priority set to {priority}.")); + } + + return Task.FromResult(status switch + { + unchecked((int)0xC0000022) => ProcessOperationResult.Failed( + AffinityApplyErrorCodes.AccessDenied, + ProcessOperationUserMessages.AccessDenied, + $"NtSetInformationProcess failed with NTSTATUS 0x{status:X8}.", + isAccessDenied: true), + unchecked((int)0xC0000008) => ProcessOperationResult.Failed( + AffinityApplyErrorCodes.ProcessExited, + ProcessOperationUserMessages.ProcessExited, + $"NtSetInformationProcess failed with NTSTATUS 0x{status:X8}.", + isProcessExited: true), + _ => ProcessOperationResult.Failed( + AffinityApplyErrorCodes.NativeApplyFailed, + "ThreadPilot could not apply the I/O priority change.", + $"NtSetInformationProcess failed with NTSTATUS 0x{status:X8}."), + }); + } + catch (Exception ex) when (AffinityApplyExceptionClassifier.IsProcessExited(ex)) + { + return Task.FromResult(ProcessOperationResult.Failed( + AffinityApplyErrorCodes.ProcessExited, + ProcessOperationUserMessages.ProcessExited, + ex.Message, + isProcessExited: true)); + } + catch (Exception ex) when (AffinityApplyExceptionClassifier.IsAccessDenied(ex)) + { + return Task.FromResult(ProcessOperationResult.Failed( + AffinityApplyErrorCodes.AccessDenied, + ProcessOperationUserMessages.AccessDenied, + ex.Message, + isAccessDenied: true)); + } + catch (Exception ex) + { + this.logger.LogWarning(ex, "I/O priority apply failed for process {ProcessId}", process.ProcessId); + return Task.FromResult(ProcessOperationResult.Failed( + AffinityApplyErrorCodes.NativeApplyFailed, + "ThreadPilot could not apply the I/O priority change.", + ex.Message)); + } + } + private static bool IsValidProcess(ProcessModel? process) => process != null && process.ProcessId > 0; diff --git a/Services/ProcessPowerRequestService.cs b/Services/ProcessPowerRequestService.cs new file mode 100644 index 0000000..2028e90 --- /dev/null +++ b/Services/ProcessPowerRequestService.cs @@ -0,0 +1,160 @@ +namespace ThreadPilot.Services +{ + using System.Diagnostics; + using System.Runtime.InteropServices; + using Microsoft.Extensions.Logging; + using Microsoft.Win32.SafeHandles; + using ThreadPilot.Models; + + public sealed partial class ProcessPowerRequestService : IDisposable + { + private readonly Dictionary requests = []; + private readonly object sync = new(); + private readonly ILogger logger; + private bool disposed; + + public ProcessPowerRequestService(ILogger logger) + { + this.logger = logger; + } + + public Task SetPreventSleepAsync(ProcessModel process, bool enabled) + { + ArgumentNullException.ThrowIfNull(process); + if (!enabled) + { + this.Release(process.ProcessId); + return Task.FromResult(true); + } + + lock (this.sync) + { + ObjectDisposedException.ThrowIf(this.disposed, this); + if (this.requests.ContainsKey(process.ProcessId)) + { + return Task.FromResult(true); + } + } + + try + { + var target = Process.GetProcessById(process.ProcessId); + var reasonPointer = Marshal.StringToHGlobalUni($"ThreadPilot: {process.Name} is running"); + SafeFileHandle request; + try + { + var context = new ReasonContext { Version = 0, Flags = 1, SimpleReasonString = reasonPointer }; + request = NativeMethods.PowerCreateRequest(ref context); + } + finally + { + Marshal.FreeHGlobal(reasonPointer); + } + + if (request.IsInvalid || !NativeMethods.PowerSetRequest(request, PowerRequestType.SystemRequired)) + { + request.Dispose(); + target.Dispose(); + return Task.FromResult(false); + } + + EventHandler exited = (_, _) => this.Release(process.ProcessId); + target.EnableRaisingEvents = true; + target.Exited += exited; + lock (this.sync) + { + if (this.disposed || this.requests.ContainsKey(process.ProcessId)) + { + target.Exited -= exited; + _ = NativeMethods.PowerClearRequest(request, PowerRequestType.SystemRequired); + request.Dispose(); + target.Dispose(); + return Task.FromResult(!this.disposed); + } + + this.requests[process.ProcessId] = new RequestEntry(target, request, exited); + } + + if (target.HasExited) + { + this.Release(process.ProcessId); + return Task.FromResult(false); + } + + return Task.FromResult(true); + } + catch (Exception ex) + { + this.logger.LogDebug(ex, "Could not create power request for PID {ProcessId}", process.ProcessId); + return Task.FromResult(false); + } + } + + public void Release(int processId) + { + RequestEntry? entry; + lock (this.sync) + { + if (!this.requests.Remove(processId, out entry)) + { + return; + } + } + + entry.Process.Exited -= entry.ExitedHandler; + _ = NativeMethods.PowerClearRequest(entry.Request, PowerRequestType.SystemRequired); + entry.Request.Dispose(); + entry.Process.Dispose(); + } + + public void Dispose() + { + int[] processIds; + lock (this.sync) + { + if (this.disposed) + { + return; + } + + this.disposed = true; + processIds = [.. this.requests.Keys]; + } + + foreach (var processId in processIds) + { + this.Release(processId); + } + } + + private sealed record RequestEntry(Process Process, SafeFileHandle Request, EventHandler ExitedHandler); + + private enum PowerRequestType + { + DisplayRequired = 0, + SystemRequired = 1, + } + + [StructLayout(LayoutKind.Sequential)] + private struct ReasonContext + { + public uint Version; + public uint Flags; + public IntPtr SimpleReasonString; + } + + private static partial class NativeMethods + { + [LibraryImport("kernel32.dll", SetLastError = true)] + internal static partial SafeFileHandle PowerCreateRequest(ref ReasonContext context); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool PowerSetRequest(SafeFileHandle request, PowerRequestType requestType); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool PowerClearRequest(SafeFileHandle request, PowerRequestType requestType); + } + } +} diff --git a/Services/ProcessRuleCreationService.cs b/Services/ProcessRuleCreationService.cs index 82cfd4a..ad4c425 100644 --- a/Services/ProcessRuleCreationService.cs +++ b/Services/ProcessRuleCreationService.cs @@ -14,12 +14,14 @@ Task SaveRuleAsync( ProcessRuleCreationPayload payload, CancellationToken cancellationToken = default); - Task SaveCurrentSettingsAsRuleAsync( - ProcessModel process, - IReadOnlyList? currentCoreSelection, - ProcessMemoryPriority? currentMemoryPriority, - CpuAssignmentMode cpuAssignmentMode = CpuAssignmentMode.Automatic, - CancellationToken cancellationToken = default); + Task SaveCurrentSettingsAsRuleAsync( + ProcessModel process, + IReadOnlyList? currentCoreSelection, + ProcessMemoryPriority? currentMemoryPriority, + CpuAssignmentMode cpuAssignmentMode = CpuAssignmentMode.Automatic, + ProcessIoPriority? currentIoPriority = null, + bool preventSystemSleepWhileRunning = false, + CancellationToken cancellationToken = default); } public sealed record ProcessRuleCreationPayload @@ -30,9 +32,13 @@ public sealed record ProcessRuleCreationPayload public ProcessPriorityClass? Priority { get; init; } - public ProcessMemoryPriority? MemoryPriority { get; init; } - - public CpuAssignmentMode CpuAssignmentMode { get; init; } = CpuAssignmentMode.Automatic; + public ProcessMemoryPriority? MemoryPriority { get; init; } + + public ProcessIoPriority? IoPriority { get; init; } + + public bool PreventSystemSleepWhileRunning { get; init; } + + public CpuAssignmentMode CpuAssignmentMode { get; init; } = CpuAssignmentMode.Automatic; } public sealed record ProcessRuleCreationResult @@ -85,12 +91,14 @@ public ProcessRuleCreationService( this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public async Task SaveCurrentSettingsAsRuleAsync( - ProcessModel process, - IReadOnlyList? currentCoreSelection, - ProcessMemoryPriority? currentMemoryPriority, - CpuAssignmentMode cpuAssignmentMode = CpuAssignmentMode.Automatic, - CancellationToken cancellationToken = default) + public async Task SaveCurrentSettingsAsRuleAsync( + ProcessModel process, + IReadOnlyList? currentCoreSelection, + ProcessMemoryPriority? currentMemoryPriority, + CpuAssignmentMode cpuAssignmentMode = CpuAssignmentMode.Automatic, + ProcessIoPriority? currentIoPriority = null, + bool preventSystemSleepWhileRunning = false, + CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(process); @@ -99,8 +107,10 @@ public async Task SaveCurrentSettingsAsRuleAsync( Priority = ShouldCaptureCurrentCpuPriority(process.Priority) ? process.Priority : null, - MemoryPriority = currentMemoryPriority, - CpuAssignmentMode = cpuAssignmentMode, + MemoryPriority = currentMemoryPriority, + IoPriority = currentIoPriority, + PreventSystemSleepWhileRunning = preventSystemSleepWhileRunning, + CpuAssignmentMode = cpuAssignmentMode, }; var affinityPayload = currentCoreSelection == null @@ -123,8 +133,8 @@ public async Task SaveCurrentSettingsAsRuleAsync( payload = payload with { CpuSelection = affinityPayload.Payload.CpuSelection, - LegacyAffinityMask = affinityPayload.Payload.LegacyAffinityMask, - CpuAssignmentMode = cpuAssignmentMode, + LegacyAffinityMask = affinityPayload.Payload.LegacyAffinityMask, + CpuAssignmentMode = cpuAssignmentMode, }; } @@ -170,13 +180,16 @@ public async Task SaveRuleAsync( ProcessName = processName, ExecutablePath = executablePath, CpuSelection = payload.CpuSelection, - LegacyAffinityMask = HasSelectionPayload(payload.CpuSelection) ? null : payload.LegacyAffinityMask, - CpuAssignmentMode = payload.CpuAssignmentMode, + LegacyAffinityMask = HasSelectionPayload(payload.CpuSelection) ? null : payload.LegacyAffinityMask, + CpuAssignmentMode = payload.CpuAssignmentMode, Priority = payload.Priority, MemoryPriority = payload.MemoryPriority, + IoPriority = payload.IoPriority, ApplyAffinityOnStart = HasSelectionPayload(payload.CpuSelection) || payload.LegacyAffinityMask.HasValue, ApplyPriorityOnStart = payload.Priority.HasValue, ApplyMemoryPriorityOnStart = payload.MemoryPriority.HasValue, + ApplyIoPriorityOnStart = payload.IoPriority.HasValue, + PreventSystemSleepWhileRunning = payload.PreventSystemSleepWhileRunning, CreatedAt = existing?.CreatedAt ?? now, UpdatedAt = now, Description = RuleDescription, @@ -241,21 +254,21 @@ private static PayloadSanitizationResult SanitizePayload(ProcessRuleCreationPayl ProcessOperationUserMessages.RealtimePriorityBlocked); } - var hasCpuSelection = HasSelectionPayload(payload.CpuSelection); + var hasCpuSelection = HasSelectionPayload(payload.CpuSelection); var legacyMask = hasCpuSelection ? null : payload.LegacyAffinityMask; if (legacyMask.HasValue && legacyMask.Value == 0) { legacyMask = null; } - return PayloadSanitizationResult.Succeeded(payload with - { - CpuSelection = hasCpuSelection ? payload.CpuSelection : null, - LegacyAffinityMask = legacyMask, - CpuAssignmentMode = Enum.IsDefined(payload.CpuAssignmentMode) - ? payload.CpuAssignmentMode - : CpuAssignmentMode.Automatic, - }); + return PayloadSanitizationResult.Succeeded(payload with + { + CpuSelection = hasCpuSelection ? payload.CpuSelection : null, + LegacyAffinityMask = legacyMask, + CpuAssignmentMode = Enum.IsDefined(payload.CpuAssignmentMode) + ? payload.CpuAssignmentMode + : CpuAssignmentMode.Automatic, + }); } private static bool ShouldCaptureCurrentCpuPriority(ProcessPriorityClass priority) => @@ -268,7 +281,9 @@ private static bool HasActionablePayload(ProcessRuleCreationPayload payload) => HasSelectionPayload(payload.CpuSelection) || payload.LegacyAffinityMask.HasValue || payload.Priority.HasValue || - payload.MemoryPriority.HasValue; + payload.MemoryPriority.HasValue || + payload.IoPriority.HasValue || + payload.PreventSystemSleepWhileRunning; private static bool HasSelectionPayload(CpuSelection? selection) => selection != null && diff --git a/Services/ProcessService.cs b/Services/ProcessService.cs index 140484f..1002ebe 100644 --- a/Services/ProcessService.cs +++ b/Services/ProcessService.cs @@ -1106,43 +1106,6 @@ public async Task IsProcessStillRunning(ProcessModel process) }).ConfigureAwait(false); } - public async Task SetIdleServerStateAsync(ProcessModel process, bool enableIdleServer) - { - return await Task.Run(() => - { - try - { - // Get the actual process - var actualProcess = Process.GetProcessById(process.ProcessId); - - // Use Windows API to set execution state for the process - // This prevents the system from entering idle state while the process is running - if (!enableIdleServer) - { - // Disable idle server by setting ES_CONTINUOUS | ES_SYSTEM_REQUIRED - // This keeps the system awake while the process is running - var result = NativeMethods.SetThreadExecutionState( - NativeMethods.EXECUTION_STATE.ES_CONTINUOUS | - NativeMethods.EXECUTION_STATE.ES_SYSTEM_REQUIRED); - - return result != 0; - } - else - { - // Re-enable idle server by clearing the execution state - var result = NativeMethods.SetThreadExecutionState( - NativeMethods.EXECUTION_STATE.ES_CONTINUOUS); - - return result != 0; - } - } - catch (Exception) - { - return false; - } - }).ConfigureAwait(false); - } - public async Task SetRegistryPriorityAsync(ProcessModel process, bool enable, ProcessPriorityClass priority) { ArgumentNullException.ThrowIfNull(process); @@ -1443,19 +1406,4 @@ private void MigrateLegacyProfilesIfNeeded() } } } - - internal static class NativeMethods - { - [System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, SetLastError = true)] - public static extern uint SetThreadExecutionState(EXECUTION_STATE esFlags); - - [System.Flags] - public enum EXECUTION_STATE : uint - { - ES_AWAYMODE_REQUIRED = 0x00000040, - ES_CONTINUOUS = 0x80000000, - ES_DISPLAY_REQUIRED = 0x00000002, - ES_SYSTEM_REQUIRED = 0x00000001, - } - } } diff --git a/Services/ServiceConfiguration.cs b/Services/ServiceConfiguration.cs index 0697742..50ec95d 100644 --- a/Services/ServiceConfiguration.cs +++ b/Services/ServiceConfiguration.cs @@ -75,6 +75,7 @@ private static IServiceCollection ConfigureCoreSystemServices(this IServiceColle services.AddSingleton(); services.AddSingleton(ProcessMemoryPriorityNativeApi.Instance); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Services/StoragePaths.cs b/Services/StoragePaths.cs index 286eb34..2a37897 100644 --- a/Services/StoragePaths.cs +++ b/Services/StoragePaths.cs @@ -19,6 +19,8 @@ internal static class StoragePaths public static string PersistentRulesFilePath => Path.Combine(AppDataRoot, "persistent_rules.json"); + public static string DeviceTweakBackupsFilePath => Path.Combine(AppDataRoot, "device_tweak_backups.json"); + public static string PowerPlansDirectory => Path.Combine(AppDataRoot, "Powerplans"); public static void EnsureAppDataDirectories() diff --git a/Services/SystemTweaksService.cs b/Services/SystemTweaksService.cs index 8cd4b01..50e0520 100644 --- a/Services/SystemTweaksService.cs +++ b/Services/SystemTweaksService.cs @@ -1,11 +1,16 @@ namespace ThreadPilot.Services { using System; + using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; + using System.Globalization; using System.IO; + using System.Linq; + using System.Management; using System.Runtime.InteropServices; using System.ServiceProcess; + using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -19,21 +24,247 @@ public class SystemTweaksService : ISystemTweaksService 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 static readonly Guid UsbSettingsSubgroupGuid = new("2a737441-1930-4402-8d77-b2bebba308a3"); + private static readonly Guid UsbSelectiveSuspendSettingGuid = new("48e6b7a6-50f5-4782-a5d4-53bb8f07e226"); + private static readonly string[] EthernetPowerSavingValueNames = ["*EEE", "AdvancedEEE", "EnableGreenEthernet", "PowerSavingMode"]; + private static readonly string[] InterruptModerationValueNames = ["*InterruptModeration"]; + private const string NetworkAdapterClassPath = @"SYSTEM\CurrentControlSet\Control\Class\{4d36e972-e325-11ce-bfc1-08002be10318}"; + private const string MsiPropertiesSuffix = @"Device Parameters\Interrupt Management\MessageSignaledInterruptProperties"; 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; + private readonly IGameModeService gameModeService; public event EventHandler? TweakStatusChanged; public SystemTweaksService( ILogger logger, - IElevationService elevationService) + IElevationService elevationService, + IGameModeService gameModeService) { this.logger = logger; this.elevationService = elevationService; + this.gameModeService = gameModeService; + } + + public async Task GetGameModeStatusAsync() => new() + { + IsEnabled = await this.gameModeService.IsGameModeEnabledAsync(), + IsAvailable = true, + }; + + public async Task SetGameModeAsync(bool enabled) + { + var success = await this.gameModeService.SetGameModeAsync(enabled); + if (success) + { + this.RaiseStatusChanged(nameof(SystemTweak.GameMode), await this.GetGameModeStatusAsync()); + } + + return success; + } + + public Task GetUsbSelectiveSuspendStatusAsync() + { + var available = TryReadAcPowerSetting( + UsbSettingsSubgroupGuid, + UsbSelectiveSuspendSettingGuid, + out var value, + out var error); + return Task.FromResult(new TweakStatus + { + IsEnabled = value != 0, + IsAvailable = available, + ErrorMessage = error, + }); + } + + public async Task SetUsbSelectiveSuspendAsync(bool enabled) + { + string? error = "Administrator privileges are required."; + if (!this.elevationService.IsRunningAsAdministrator() || + !TryWriteAcPowerSetting( + UsbSettingsSubgroupGuid, + UsbSelectiveSuspendSettingGuid, + enabled ? 1u : 0u, + out error)) + { + this.logger.LogWarning("Could not update USB selective suspend: {Error}", error); + return false; + } + + var status = await this.GetUsbSelectiveSuspendStatusAsync(); + this.RaiseStatusChanged(nameof(SystemTweak.UsbSelectiveSuspend), status); + return status.IsAvailable && status.IsEnabled == enabled; + } + + public Task GetPointerPrecisionStatusAsync() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Mouse"); + var mouseSpeed = key?.GetValue("MouseSpeed")?.ToString(); + if (int.TryParse(mouseSpeed, NumberStyles.Integer, CultureInfo.InvariantCulture, out var speed)) + { + return Task.FromResult(new TweakStatus { IsEnabled = speed != 0, IsAvailable = true }); + } + + var values = new int[3]; + var available = NativeMethods.SystemParametersInfo(NativeMethods.SpiGetMouse, 0, values, 0); + return Task.FromResult(new TweakStatus + { + IsEnabled = available && values[2] != 0, + IsAvailable = available, + ErrorMessage = available ? null : new Win32Exception(Marshal.GetLastWin32Error()).Message, + }); + } + catch (Exception ex) + { + return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = ex.Message }); + } + } + + public async Task SetPointerPrecisionAsync(bool enabled) + { + var values = new int[3]; + if (!NativeMethods.SystemParametersInfo(NativeMethods.SpiGetMouse, 0, values, 0)) + { + return false; + } + + values = GetPointerPrecisionValues(enabled, values); + var notified = NativeMethods.SystemParametersInfo( + NativeMethods.SpiSetMouse, + 0, + values, + NativeMethods.SpifUpdateIniFile | NativeMethods.SpifSendChange); + if (!notified) + { + this.logger.LogWarning( + "Windows did not apply pointer precision immediately: {Error}", + new Win32Exception(Marshal.GetLastWin32Error()).Message); + } + + try + { + using var key = Registry.CurrentUser.CreateSubKey(@"Control Panel\Mouse", writable: true); + key.SetValue("MouseThreshold1", values[0].ToString(CultureInfo.InvariantCulture), RegistryValueKind.String); + key.SetValue("MouseThreshold2", values[1].ToString(CultureInfo.InvariantCulture), RegistryValueKind.String); + key.SetValue("MouseSpeed", values[2].ToString(CultureInfo.InvariantCulture), RegistryValueKind.String); + } + catch (Exception ex) + { + this.logger.LogWarning(ex, "Could not persist pointer precision settings."); + return false; + } + + var status = await this.GetPointerPrecisionStatusAsync(); + this.RaiseStatusChanged(nameof(SystemTweak.PointerPrecision), status); + return status.IsAvailable && status.IsEnabled == enabled; + } + + internal static int[] GetPointerPrecisionValues(bool enabled, IReadOnlyList current) + { + if (current.Count < 3) + { + throw new ArgumentException("Three mouse parameters are required.", nameof(current)); + } + + if (!enabled) + { + // Windows may normalize acceleration back to enabled when non-zero + // thresholds are submitted with acceleration disabled. + return [0, 0, 0]; + } + + return + [ + current[0] > 0 ? current[0] : 6, + current[1] > 0 ? current[1] : 10, + current[2] > 0 ? current[2] : 1, + ]; + } + + public Task GetEthernetPowerSavingStatusAsync() => + Task.FromResult(GetRegistryTargetsStatus(FindEthernetTargets(EthernetPowerSavingValueNames), enabledValue: 0)); + + public Task SetEthernetPowerSavingAsync(bool enabled) => + this.SetRegistryTargetsAsync( + SystemTweak.EthernetPowerSaving, + FindEthernetTargets(EthernetPowerSavingValueNames), + enabled, + enabledValue: 0, + disabledValue: 1); + + public Task GetInterruptModerationStatusAsync() => + Task.FromResult(GetRegistryTargetsStatus(FindEthernetTargets(InterruptModerationValueNames), enabledValue: 0)); + + public Task SetInterruptModerationAsync(bool enabled) => + this.SetRegistryTargetsAsync( + SystemTweak.InterruptModeration, + FindEthernetTargets(InterruptModerationValueNames), + enabled, + enabledValue: 0, + disabledValue: 1); + + public Task GetGpuMsiModeStatusAsync() => + Task.FromResult(GetRegistryTargetsStatus(FindGpuMsiTargets(), enabledValue: 1)); + + public Task SetGpuMsiModeAsync(bool enabled) => + this.SetRegistryTargetsAsync( + SystemTweak.GpuMsiMode, + FindGpuMsiTargets(), + enabled, + enabledValue: 1, + disabledValue: 0, + restoreOriginalWhenDisabled: false); + + public Task GetMemoryIntegrityStatusAsync() + { + try + { + using var key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\CI\State"); + var value = key == null ? null : ReadRegistryIntValue(key, "HVCIEnabled"); + return Task.FromResult(new TweakStatus + { + IsEnabled = value == 1, + IsAvailable = value.HasValue, + ErrorMessage = value.HasValue ? null : "Memory Integrity state is not exposed by Windows.", + }); + } + catch (Exception ex) + { + return Task.FromResult(new TweakStatus { IsAvailable = false, ErrorMessage = ex.Message }); + } + } + + public Task OpenSettingsAsync(SystemTweak tweak) + { + var uri = tweak switch + { + SystemTweak.MemoryIntegrity => "windowsdefender://coreisolation", + SystemTweak.Hags or SystemTweak.WindowedOptimizations => "ms-settings:display-advancedgraphics-default", + _ => null, + }; + + if (uri == null) + { + return Task.FromResult(false); + } + + try + { + Process.Start(new ProcessStartInfo(uri) { UseShellExecute = true }); + return Task.FromResult(true); + } + catch (Exception ex) + { + this.logger.LogWarning(ex, "Could not open Windows settings for {Tweak}", tweak); + return Task.FromResult(false); + } } public Task GetCoreParkingStatusAsync() @@ -420,7 +651,233 @@ private async Task SetServiceStartModeAsync(string serviceName, ServiceSta internal static string GetHighSchedulingCategoryRegistryValue(bool enabled) => enabled ? HighSchedulingCategoryEnabledValue : HighSchedulingCategoryDisabledValue; + private void RaiseStatusChanged(string tweakName, TweakStatus status) => + this.TweakStatusChanged?.Invoke(this, new TweakStatusChangedEventArgs(tweakName, status)); + + private static List FindEthernetTargets(IEnumerable valueNames) + { + var targets = new List(); + using var adapters = Registry.LocalMachine.OpenSubKey(NetworkAdapterClassPath); + if (adapters == null) + { + return targets; + } + + foreach (var subKeyName in adapters.GetSubKeyNames()) + { + var path = $@"{NetworkAdapterClassPath}\{subKeyName}"; + using var adapter = Registry.LocalMachine.OpenSubKey(path); + var characteristics = adapter == null ? null : ReadRegistryIntValue(adapter, "Characteristics"); + if (adapter == null || + ReadRegistryIntValue(adapter, "*IfType") != 6 || + !characteristics.HasValue || + (characteristics.Value & 0x4) == 0) + { + continue; + } + + foreach (var valueName in valueNames) + { + AddRegistryTarget(targets, adapter, path, valueName); + } + } + + return targets; + } + + private static List FindGpuMsiTargets() + { + var targets = new List(); + try + { + using var searcher = new ManagementObjectSearcher("SELECT PNPDeviceID FROM Win32_VideoController"); + using var results = searcher.Get(); + foreach (ManagementObject device in results) + { + using (device) + { + var pnpDeviceId = device["PNPDeviceID"]?.ToString(); + if (string.IsNullOrWhiteSpace(pnpDeviceId)) + { + continue; + } + + var path = $@"SYSTEM\CurrentControlSet\Enum\{pnpDeviceId}\{MsiPropertiesSuffix}"; + using var key = Registry.LocalMachine.OpenSubKey(path); + if (key != null) + { + AddRegistryTarget(targets, key, path, "MSISupported"); + } + } + } + } + catch (ManagementException) + { + // No supported display adapter information is available. + } + + return targets; + } + + private static void AddRegistryTarget( + ICollection targets, + RegistryKey key, + string path, + string valueName) + { + var value = ReadRegistryIntValue(key, valueName); + if (!value.HasValue) + { + return; + } + + var kind = key.GetValueKind(valueName); + if (kind is RegistryValueKind.DWord or RegistryValueKind.QWord or RegistryValueKind.String) + { + targets.Add(new RegistryTarget(path, valueName, value.Value, kind)); + } + } + + private static TweakStatus GetRegistryTargetsStatus(IReadOnlyCollection targets, int enabledValue) => new() + { + IsAvailable = targets.Count > 0, + IsEnabled = targets.Count > 0 && targets.All(target => target.Value == enabledValue), + ErrorMessage = targets.Count == 0 ? "No supported driver property was found." : null, + }; + + private async Task SetRegistryTargetsAsync( + SystemTweak tweak, + IReadOnlyCollection targets, + bool enabled, + int enabledValue, + int disabledValue, + bool restoreOriginalWhenDisabled = true) + { + if (!this.elevationService.IsRunningAsAdministrator() || targets.Count == 0) + { + return false; + } + + var backups = this.LoadRegistryBackups(); + var changedBackups = false; + var changedTargets = new List(); + try + { + foreach (var target in targets) + { + var backupKey = target.BackupKey; + if (!backups.ContainsKey(backupKey)) + { + backups[backupKey] = new RegistryValueBackup(target.Value, target.Kind); + changedBackups = true; + } + } + + if (changedBackups) + { + this.SaveRegistryBackups(backups); + } + + foreach (var target in targets) + { + var backupKey = target.BackupKey; + var desiredValue = enabled + ? enabledValue + : restoreOriginalWhenDisabled && backups.TryGetValue(backupKey, out var backup) + ? backup.Value + : disabledValue; + using var key = Registry.LocalMachine.OpenSubKey(target.Path, writable: true); + if (key == null) + { + return false; + } + + key.SetValue( + target.ValueName, + target.Kind == RegistryValueKind.String + ? desiredValue.ToString(CultureInfo.InvariantCulture) + : desiredValue, + target.Kind); + changedTargets.Add(target); + } + + var status = tweak switch + { + SystemTweak.EthernetPowerSaving => await this.GetEthernetPowerSavingStatusAsync(), + SystemTweak.InterruptModeration => await this.GetInterruptModerationStatusAsync(), + SystemTweak.GpuMsiMode => await this.GetGpuMsiModeStatusAsync(), + _ => new TweakStatus { IsAvailable = false }, + }; + this.RaiseStatusChanged(tweak.ToString(), status); + return status.IsAvailable && status.IsEnabled == enabled; + } + catch (Exception ex) + { + this.logger.LogWarning(ex, "Could not update driver tweak {Tweak}", tweak); + foreach (var target in changedTargets) + { + try + { + using var key = Registry.LocalMachine.OpenSubKey(target.Path, writable: true); + key?.SetValue( + target.ValueName, + target.Kind == RegistryValueKind.String + ? target.Value.ToString(CultureInfo.InvariantCulture) + : target.Value, + target.Kind); + } + catch (Exception rollbackException) + { + this.logger.LogWarning( + rollbackException, + "Could not roll back {Path}\\{ValueName}", + target.Path, + target.ValueName); + } + } + + return false; + } + } + + private Dictionary LoadRegistryBackups() + { + try + { + if (!File.Exists(StoragePaths.DeviceTweakBackupsFilePath)) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + var stored = JsonSerializer.Deserialize>( + File.ReadAllText(StoragePaths.DeviceTweakBackupsFilePath)); + return stored == null + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : new Dictionary(stored, StringComparer.OrdinalIgnoreCase); + } + catch (Exception ex) + { + this.logger.LogWarning(ex, "Could not load device tweak backups"); + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } + + private void SaveRegistryBackups(Dictionary backups) + { + StoragePaths.EnsureAppDataDirectories(); + File.WriteAllText( + StoragePaths.DeviceTweakBackupsFilePath, + JsonSerializer.Serialize(backups, new JsonSerializerOptions { WriteIndented = true })); + } + private static bool TryReadAcPowerSetting(Guid settingGuid, out uint value, out string? error) + => TryReadAcPowerSetting(ProcessorSettingsSubgroupGuid, settingGuid, out value, out error); + + private static bool TryReadAcPowerSetting( + Guid subgroupGuid, + Guid settingGuid, + out uint value, + out string? error) { value = 0; if (!TryGetActivePowerScheme(out var schemeGuid, out error)) @@ -428,7 +885,6 @@ private static bool TryReadAcPowerSetting(Guid settingGuid, out uint value, out return false; } - var subgroupGuid = ProcessorSettingsSubgroupGuid; var result = NativeMethods.PowerReadAcValueIndex( IntPtr.Zero, ref schemeGuid, @@ -446,13 +902,19 @@ private static bool TryReadAcPowerSetting(Guid settingGuid, out uint value, out } private static bool TryWriteAcPowerSetting(Guid settingGuid, uint value, out string? error) + => TryWriteAcPowerSetting(ProcessorSettingsSubgroupGuid, settingGuid, value, out error); + + private static bool TryWriteAcPowerSetting( + Guid subgroupGuid, + Guid settingGuid, + uint value, + out string? error) { if (!TryGetActivePowerScheme(out var schemeGuid, out error)) { return false; } - var subgroupGuid = ProcessorSettingsSubgroupGuid; var writeResult = NativeMethods.PowerWriteAcValueIndex( IntPtr.Zero, ref schemeGuid, @@ -585,6 +1047,17 @@ public ProcessResult(int exitCode, string standardOutput, string standardError) public string StandardError { get; } } + private sealed record RegistryTarget( + string Path, + string ValueName, + int Value, + RegistryValueKind Kind) + { + public string BackupKey => $"{this.Path}|{this.ValueName}"; + } + + private sealed record RegistryValueBackup(int Value, RegistryValueKind Kind); + public Task GetMenuShowDelayStatusAsync() { try @@ -645,6 +1118,15 @@ public async Task SetMenuShowDelayAsync(bool enabled) private static class NativeMethods { + internal const uint SpiGetMouse = 0x0003; + internal const uint SpiSetMouse = 0x0004; + internal const uint SpifUpdateIniFile = 0x0001; + internal const uint SpifSendChange = 0x0002; + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool SystemParametersInfo(uint action, uint parameter, [In, Out] int[] values, uint flags); + [DllImport("PowrProf.dll", EntryPoint = "PowerGetActiveScheme")] internal static extern uint PowerGetActiveScheme(IntPtr userRootPowerKey, out IntPtr activePolicyGuid); diff --git a/Tests/ThreadPilot.Core.Tests/BundledPowerPlanAssetTests.cs b/Tests/ThreadPilot.Core.Tests/BundledPowerPlanAssetTests.cs index a9de865..5aea8ed 100644 --- a/Tests/ThreadPilot.Core.Tests/BundledPowerPlanAssetTests.cs +++ b/Tests/ThreadPilot.Core.Tests/BundledPowerPlanAssetTests.cs @@ -1,127 +1,41 @@ -namespace ThreadPilot.Core.Tests -{ - using System.Security.Cryptography; - using System.Text; - using Microsoft.Extensions.Logging.Abstractions; - using Moq; - using ThreadPilot.Services; - using ThreadPilot.Services.Abstractions; - - public sealed class BundledPowerPlanAssetTests - { - private static readonly string[] NewPlanFiles = - [ - "0 Synez_Public_Power.pow", - "arsenza low latency (INTEL FIX THREAD-DIRECTOR).pow", - "arsenza low latency.pow", - "AutoOS.pow", - "BEYOND-PERFORMANCE-AMD+INTEL.pow", - "Bitsum Highest Performance.pow", - "cactusOS.pow", - "FPSHEAVEN2026.pow", - "GALA's ultimate performance(AMD).pow", - "Gavot Performance.pow", - "GTweaks Power Plan V3.pow", - "imribiy2026.pow", - "IrisFixed.pow", - "J o k r O S P o w e r P l a n.pow", - "Jackpot2026.pow", - "Kizzimo's Extreme Low Latency.pow", - "KSOS11.pow", - "melody LowestLatency.pow", - "Microsoft High performance.pow", - "Microsoft Ultimate Performance.pow", - "Mitstas IDLE ENABLED.pow", - "n1kobg GPU_Booster_Power_Plan.pow", - "Prodazin Power Plan.pow", - "Reticle v2.pow", - "RevisionPowerPlanV2.8.pow", - "RIP Tweaks Power Plan.pow", - "Rosca Tweaks v2.pow", - "Velo's Power Plan.pow", - "VTRL Optimized.pow", - "XNRL Pro Plan.pow", - ]; - - [Fact] - public void BundledPlans_AreValidUniqueRegistryHives() - { - var directory = GetPowerPlansDirectory(); - var files = Directory.GetFiles(directory, "*", SearchOption.TopDirectoryOnly); - - Assert.All(files, file => Assert.Equal(".pow", Path.GetExtension(file), ignoreCase: true)); - Assert.Equal(files.Length, files.Select(Path.GetFileNameWithoutExtension).Distinct(StringComparer.OrdinalIgnoreCase).Count()); - - foreach (var file in files) - { - var info = new FileInfo(file); - Assert.InRange(info.Length, 4, 10 * 1024 * 1024); - Assert.Equal("regf", Encoding.ASCII.GetString(File.ReadAllBytes(file), 0, 4)); - Assert.DoesNotContain(Path.GetFileName(file), character => Path.GetInvalidFileNameChars().Contains(character)); - Assert.DoesNotContain("#U", Path.GetFileName(file), StringComparison.OrdinalIgnoreCase); - } - } - - [Fact] - public async Task NewAndExistingPlans_ArePresentDiscoverableAndNotBinaryDuplicates() - { - var directory = GetPowerPlansDirectory(); - var files = Directory.GetFiles(directory, "*.pow", SearchOption.TopDirectoryOnly); - var newPaths = NewPlanFiles.Select(file => Path.Combine(directory, file)).ToArray(); - - Assert.All(newPaths, path => Assert.True(File.Exists(path), $"Missing bundled power plan: {Path.GetFileName(path)}")); - Assert.All(["Amit.pow", "Beyond.pow", "L1 Final Version.pow"], file => Assert.True(File.Exists(Path.Combine(directory, file)))); - - var newHashes = newPaths.ToDictionary(path => path, GetSha256); - Assert.Equal(newHashes.Count, newHashes.Values.Distinct(StringComparer.Ordinal).Count()); - Assert.All( - newHashes, - entry => Assert.DoesNotContain(files, file => !newPaths.Contains(file, StringComparer.OrdinalIgnoreCase) && GetSha256(file) == entry.Value)); - - var logger = new Mock(MockBehavior.Loose); - var runner = new Mock(MockBehavior.Strict); - var service = new PowerPlanService( - NullLogger.Instance, - logger.Object, - runner.Object, - () => directory); - - var discovered = await service.GetCustomPowerPlansAsync(); - Assert.All(NewPlanFiles, file => Assert.Contains(discovered, plan => plan.Name == Path.GetFileNameWithoutExtension(file))); - Assert.Equal(files.Length, discovered.Count); - } - - [Fact] - public void Project_ConfiguresAllPowerPlansForBuildAndPublish() - { - var project = File.ReadAllText(Path.Combine(FindRepositoryRoot(), "ThreadPilot.csproj")); - - Assert.Contains(@"assets\Powerplans\**\*.pow", project, StringComparison.Ordinal); - Assert.Contains(@"Powerplans\%(RecursiveDir)%(Filename)%(Extension)", project, StringComparison.Ordinal); - Assert.Contains("PreserveNewest", project, StringComparison.Ordinal); - Assert.Contains("PreserveNewest", project, StringComparison.Ordinal); - } - - private static string GetPowerPlansDirectory() => - Path.Combine(FindRepositoryRoot(), "assets", "Powerplans"); - - private static string GetSha256(string path) => - Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))); - - private static string FindRepositoryRoot() - { - var directory = new DirectoryInfo(AppContext.BaseDirectory); - while (directory != null) - { - if (File.Exists(Path.Combine(directory.FullName, "ThreadPilot.csproj"))) - { - return directory.FullName; - } - - directory = directory.Parent; - } - - throw new InvalidOperationException("Repository root could not be located."); - } - } -} +namespace ThreadPilot.Core.Tests +{ + public sealed class BundledPowerPlanAssetTests + { + [Fact] + public void RepositoryAndReleasePipeline_DoNotBundlePowerPlanFiles() + { + var root = FindRepositoryRoot(); + var assetsDirectory = Path.Combine(root, "assets", "Powerplans"); + var project = File.ReadAllText(Path.Combine(root, "ThreadPilot.csproj")); + var installer = File.ReadAllText(Path.Combine(root, "Installer", "setup.iss")); + var workflow = File.ReadAllText(Path.Combine(root, ".github", "workflows", "release.yml")); + + Assert.Empty(Directory.Exists(assetsDirectory) + ? Directory.GetFiles(assetsDirectory, "*.pow", SearchOption.AllDirectories) + : []); + Assert.DoesNotContain(@"assets\Powerplans", project, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain(@"\Powerplans\*", installer, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("assets/Powerplans", workflow, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void CommunityAndSupportLinks_AreTheRequestedDestinations() + { + Assert.Equal("https://discord.gg/EmPfb57Kfr", ThreadPilot.ViewModels.PowerPlanViewModel.MorePlansUrl); + Assert.Equal("https://discord.gg/VYwkCbr4vH", ThreadPilot.ViewModels.SettingsViewModel.DiscordUrl); + Assert.Equal("https://github.com/PrimeBuild-pc/ThreadPilot/issues", ThreadPilot.ViewModels.SettingsViewModel.IssuesUrl); + } + + private static string FindRepositoryRoot() + { + 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."); + } + } +} diff --git a/Tests/ThreadPilot.Core.Tests/PackagingMetadataTests.cs b/Tests/ThreadPilot.Core.Tests/PackagingMetadataTests.cs index 37f96d9..1d0c915 100644 --- a/Tests/ThreadPilot.Core.Tests/PackagingMetadataTests.cs +++ b/Tests/ThreadPilot.Core.Tests/PackagingMetadataTests.cs @@ -4,8 +4,8 @@ namespace ThreadPilot.Core.Tests public sealed partial class PackagingMetadataTests { - private const string ReleaseVersion = "1.6.0"; - private const string ReleaseAssemblyVersion = "1.6.0.0"; + private const string ReleaseVersion = "1.7.0"; + private const string ReleaseAssemblyVersion = "1.7.0.0"; [Fact] public void InnoInstallers_UseStableDisplayNameAndSeparateVersionMetadata() @@ -38,10 +38,31 @@ public void PrimaryInstaller_RemovesThreadPilotOwnedDataOnlyDuringUninstall() Assert.Contains("[UninstallDelete]", script, StringComparison.Ordinal); Assert.Contains("Name: \"{userappdata}\\ThreadPilot\"", script, StringComparison.Ordinal); Assert.Contains("ThreadPilot user data is preserved during install/update", script, StringComparison.Ordinal); + Assert.Contains("ThreadPilot_Launch", script, StringComparison.Ordinal); + Assert.Contains("ThreadPilot_Startup", script, StringComparison.Ordinal); Assert.DoesNotContain("[InstallDelete]", script, StringComparison.Ordinal); Assert.DoesNotContain("Name: \"{userappdata}\"", script, StringComparison.Ordinal); } + [Fact] + public void PortableUninstallers_RemoveCurrentAndLegacyStartupTasks() + { + var root = FindRepositoryRoot(); + var paths = new[] + { + Path.Combine(root, "build", "build-release.ps1"), + Path.Combine(root, "build", "package-release-zips.ps1"), + Path.Combine(root, ".github", "workflows", "release.yml"), + }; + + foreach (var path in paths) + { + var script = File.ReadAllText(path); + Assert.Contains("ThreadPilot_Launch", script, StringComparison.Ordinal); + Assert.Contains("ThreadPilot_Startup", script, StringComparison.Ordinal); + } + } + [Fact] public void PrimaryInstaller_CleansOnlyRecognizedLegacyBetaUninstallRegistryEntries() { @@ -99,7 +120,7 @@ private static string FindRepositoryRoot() throw new InvalidOperationException("Repository root could not be located."); } - [GeneratedRegex("#define MyAppVersion \"1\\.6\\.0\"", RegexOptions.CultureInvariant)] + [GeneratedRegex("#define MyAppVersion \"1\\.7\\.0\"", RegexOptions.CultureInvariant)] private static partial Regex MyAppVersionRegex(); } } diff --git a/Tests/ThreadPilot.Core.Tests/PersistentProcessRuleJsonStoreTests.cs b/Tests/ThreadPilot.Core.Tests/PersistentProcessRuleJsonStoreTests.cs index 3ce5313..36aa945 100644 --- a/Tests/ThreadPilot.Core.Tests/PersistentProcessRuleJsonStoreTests.cs +++ b/Tests/ThreadPilot.Core.Tests/PersistentProcessRuleJsonStoreTests.cs @@ -36,13 +36,16 @@ public async Task SaveAndLoadAsync_RoundTripsCpuSelectionAndLegacyAffinityMask() LogicalProcessors = [new ProcessorRef(0, 0, 0)], GlobalLogicalProcessorIndexes = [0], }, - LegacyAffinityMask = 3, - CpuAssignmentMode = CpuAssignmentMode.IdealProcessor, + LegacyAffinityMask = 3, + CpuAssignmentMode = CpuAssignmentMode.IdealProcessor, Priority = ProcessPriorityClass.AboveNormal, MemoryPriority = ProcessMemoryPriority.BelowNormal, + IoPriority = ProcessIoPriority.Low, ApplyAffinityOnStart = true, ApplyPriorityOnStart = true, ApplyMemoryPriorityOnStart = true, + ApplyIoPriorityOnStart = true, + PreventSystemSleepWhileRunning = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, Description = ProcessOperationUserMessages.PersistentRulesDescription, @@ -56,11 +59,14 @@ public async Task SaveAndLoadAsync_RoundTripsCpuSelectionAndLegacyAffinityMask() var loadedRule = Assert.Single(loaded); Assert.Equal("rule-a", loadedRule.Id); - Assert.Equal(3, loadedRule.LegacyAffinityMask); - Assert.Equal(CpuAssignmentMode.IdealProcessor, loadedRule.CpuAssignmentMode); + Assert.Equal(3, loadedRule.LegacyAffinityMask); + Assert.Equal(CpuAssignmentMode.IdealProcessor, loadedRule.CpuAssignmentMode); Assert.Equal(ProcessPriorityClass.AboveNormal, loadedRule.Priority); Assert.Equal(ProcessMemoryPriority.BelowNormal, loadedRule.MemoryPriority); Assert.True(loadedRule.ApplyMemoryPriorityOnStart); + Assert.Equal(ProcessIoPriority.Low, loadedRule.IoPriority); + Assert.True(loadedRule.ApplyIoPriorityOnStart); + Assert.True(loadedRule.PreventSystemSleepWhileRunning); Assert.NotNull(loadedRule.CpuSelection); Assert.Equal(0, loadedRule.CpuSelection.GlobalLogicalProcessorIndexes.Single()); } @@ -68,30 +74,30 @@ public async Task SaveAndLoadAsync_RoundTripsCpuSelectionAndLegacyAffinityMask() { DeleteFile(filePath); } - } - - [Fact] - public async Task LoadAsync_OldRuleWithoutCpuAssignmentMode_DefaultsToAutomatic() - { - var filePath = CreateTemporaryFilePath(); - Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); - await File.WriteAllTextAsync( - filePath, - """ - [{"Id":"legacy","Name":"Legacy","IsEnabled":true,"ProcessName":"game.exe","LegacyAffinityMask":3,"ApplyAffinityOnStart":true}] - """); - - try - { - var rule = Assert.Single(await new PersistentProcessRuleJsonStore(() => filePath).LoadAsync()); - Assert.Equal(CpuAssignmentMode.Automatic, rule.CpuAssignmentMode); - Assert.Equal(3, rule.LegacyAffinityMask); - } - finally - { - DeleteFile(filePath); - } - } + } + + [Fact] + public async Task LoadAsync_OldRuleWithoutCpuAssignmentMode_DefaultsToAutomatic() + { + var filePath = CreateTemporaryFilePath(); + Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); + await File.WriteAllTextAsync( + filePath, + """ + [{"Id":"legacy","Name":"Legacy","IsEnabled":true,"ProcessName":"game.exe","LegacyAffinityMask":3,"ApplyAffinityOnStart":true}] + """); + + try + { + var rule = Assert.Single(await new PersistentProcessRuleJsonStore(() => filePath).LoadAsync()); + Assert.Equal(CpuAssignmentMode.Automatic, rule.CpuAssignmentMode); + Assert.Equal(3, rule.LegacyAffinityMask); + } + finally + { + DeleteFile(filePath); + } + } [Fact] public async Task MultipleRules_SaveThenReload_DoesNotLoseEntries() diff --git a/Tests/ThreadPilot.Core.Tests/PowerPlanViewXamlTests.cs b/Tests/ThreadPilot.Core.Tests/PowerPlanViewXamlTests.cs index 92cbbaa..97a6173 100644 --- a/Tests/ThreadPilot.Core.Tests/PowerPlanViewXamlTests.cs +++ b/Tests/ThreadPilot.Core.Tests/PowerPlanViewXamlTests.cs @@ -56,5 +56,15 @@ public void ActivePowerPlanTemplate_ContainsNeutralActiveBadgeAndBorder() Assert.Contains("StatusPillBackgroundBrush", serialized, StringComparison.Ordinal); Assert.DoesNotContain("BorderThickness=\"4,1,1,1\"", serialized, StringComparison.Ordinal); } + + [Fact] + public void Header_ExposesLocalizedCommunityPlansLink() + { + var document = XDocument.Load(PowerPlanViewPath, LoadOptions.PreserveWhitespace); + var serialized = document.ToString(SaveOptions.DisableFormatting); + + Assert.Contains("OpenMorePlansCommand", serialized, StringComparison.Ordinal); + Assert.Contains("PowerPlanView_MorePlans", serialized, StringComparison.Ordinal); + } } } diff --git a/Tests/ThreadPilot.Core.Tests/ProcessMemoryPriorityServiceTests.cs b/Tests/ThreadPilot.Core.Tests/ProcessMemoryPriorityServiceTests.cs index f1012fa..13414cb 100644 --- a/Tests/ThreadPilot.Core.Tests/ProcessMemoryPriorityServiceTests.cs +++ b/Tests/ThreadPilot.Core.Tests/ProcessMemoryPriorityServiceTests.cs @@ -42,6 +42,24 @@ public async Task GetMemoryPriorityAsync_WithValidProcess_ReadsNativeApi() Assert.Equal(ProcessAccessFlags.PROCESS_QUERY_LIMITED_INFORMATION, nativeApi.LastOpenAccess); } + [Fact] + public async Task IoPriority_WithValidProcess_RoundTripsThroughNativeApi() + { + var nativeApi = new FakeProcessMemoryPriorityNativeApi + { + IoPriorityToReturn = ProcessIoPriority.Low, + }; + var service = CreateService(nativeApi); + var process = CreateProcess(); + + var observed = await service.GetIoPriorityAsync(process); + var result = await service.SetIoPriorityAsync(process, ProcessIoPriority.High); + + Assert.Equal(ProcessIoPriority.Low, observed); + Assert.True(result.Success); + Assert.Equal(ProcessIoPriority.High, nativeApi.LastSetIoPriority); + } + [Theory] [InlineData(1, ProcessMemoryPriority.VeryLow)] [InlineData(2, ProcessMemoryPriority.Low)] @@ -198,6 +216,10 @@ private sealed class FakeProcessMemoryPriorityNativeApi : IProcessMemoryPriority public ProcessMemoryPriority PriorityToReturn { get; init; } = ProcessMemoryPriority.Normal; + public ProcessIoPriority IoPriorityToReturn { get; init; } = ProcessIoPriority.Normal; + + public ProcessIoPriority? LastSetIoPriority { get; private set; } + public Exception? OpenException { get; init; } public Exception? SetException { get; init; } @@ -243,6 +265,19 @@ public bool SetProcessInformation( } public int GetLastWin32Error() => this.LastError; + + public int QueryIoPriority(SafeProcessHandle process, ref int priority, out uint returnLength) + { + priority = (int)this.IoPriorityToReturn; + returnLength = sizeof(int); + return 0; + } + + public int SetIoPriority(SafeProcessHandle process, ref int priority) + { + this.LastSetIoPriority = (ProcessIoPriority)priority; + return 0; + } } } } diff --git a/Tests/ThreadPilot.Core.Tests/ProcessViewModelAffinityTests.cs b/Tests/ThreadPilot.Core.Tests/ProcessViewModelAffinityTests.cs index 2f89e81..f187380 100644 --- a/Tests/ThreadPilot.Core.Tests/ProcessViewModelAffinityTests.cs +++ b/Tests/ThreadPilot.Core.Tests/ProcessViewModelAffinityTests.cs @@ -13,9 +13,6 @@ public async Task SelectingCoreMask_DoesNotApplyProcessorAffinity() { var processService = new Mock(MockBehavior.Loose); var gameModeService = new Mock(MockBehavior.Loose); - gameModeService - .Setup(service => service.DisableGameModeForAffinityAsync()) - .ReturnsAsync(false); var viewModel = CreateViewModel(processService.Object, gameModeService.Object); viewModel.SelectedProcess = new ProcessModel @@ -58,7 +55,7 @@ public async Task SelectingCoreMask_ReportsPendingAffinityWithoutChangingCurrent Assert.True(viewModel.HasPendingAffinityEdits); Assert.Equal("Current OS affinity: 0x3", viewModel.CurrentAffinityText); Assert.Equal("Pending core mask: 0x1", viewModel.PendingAffinityText); - Assert.Equal("CPU selection staged. Use Apply CPU Assignment to apply it.", viewModel.AffinityEditStateText); + Assert.Equal("CPU selection staged. Use Apply CPU Assignment to apply it.", viewModel.AffinityEditStateText); } [Fact] diff --git a/Tests/ThreadPilot.Core.Tests/SettingsViewModelThemeTests.cs b/Tests/ThreadPilot.Core.Tests/SettingsViewModelThemeTests.cs index 698d845..6117814 100644 --- a/Tests/ThreadPilot.Core.Tests/SettingsViewModelThemeTests.cs +++ b/Tests/ThreadPilot.Core.Tests/SettingsViewModelThemeTests.cs @@ -197,7 +197,7 @@ public async Task NavigationPrompt_SavePersistsPendingSettingsBeforeNavigating() } [Fact] - public void SettingsView_ExposesPersistentRuleAutoApplyToggle() + public void SettingsView_ExposesPersistentRuleAutoApplyToggle() { var settingsViewPath = Path.Combine( AppContext.BaseDirectory, @@ -214,28 +214,28 @@ public void SettingsView_ExposesPersistentRuleAutoApplyToggle() Assert.Contains("Text=\"{DynamicResource SettingsView_ApplyOnStart}\"", serialized, StringComparison.Ordinal); Assert.Contains("TextWrapping=\"Wrap\"", serialized, StringComparison.Ordinal); Assert.Contains("IsChecked=\"{Binding Settings.ApplyPersistentRulesOnProcessStart}\"", serialized, StringComparison.Ordinal); - Assert.Contains("Text=\"{DynamicResource SettingsView_ApplyOnStartDescription}\"", serialized, StringComparison.Ordinal); - } - - [Fact] - public void SettingsView_ExposesDefaultCpuAssignmentAndReadOnlyReservedState() - { - var settingsViewPath = Path.Combine( - AppContext.BaseDirectory, - "..", - "..", - "..", - "..", - "..", - "Views", - "SettingsView.xaml"); - var serialized = File.ReadAllText(settingsViewPath); - - Assert.Contains("SelectedValue=\"{Binding Settings.DefaultCpuAssignmentMode}\"", serialized, StringComparison.Ordinal); - Assert.Contains("Text=\"{Binding ReservedCpuSetsStatus}\"", serialized, StringComparison.Ordinal); - Assert.Contains("AutomationProperties.HelpText=\"{DynamicResource SettingsView_ReservedCpuSetsTooltip}\"", serialized, StringComparison.Ordinal); - Assert.DoesNotContain("ReservedCpuSetsStatus, Mode=TwoWay", serialized, StringComparison.Ordinal); - } + Assert.Contains("Text=\"{DynamicResource SettingsView_ApplyOnStartDescription}\"", serialized, StringComparison.Ordinal); + } + + [Fact] + public void SettingsView_ExposesDefaultCpuAssignmentAndReadOnlyReservedState() + { + var settingsViewPath = Path.Combine( + AppContext.BaseDirectory, + "..", + "..", + "..", + "..", + "..", + "Views", + "SettingsView.xaml"); + var serialized = File.ReadAllText(settingsViewPath); + + Assert.Contains("SelectedValue=\"{Binding Settings.DefaultCpuAssignmentMode}\"", serialized, StringComparison.Ordinal); + Assert.Contains("Text=\"{Binding ReservedCpuSetsStatus}\"", serialized, StringComparison.Ordinal); + Assert.Contains("AutomationProperties.HelpText=\"{DynamicResource SettingsView_ReservedCpuSetsTooltip}\"", serialized, StringComparison.Ordinal); + Assert.DoesNotContain("ReservedCpuSetsStatus, Mode=TwoWay", serialized, StringComparison.Ordinal); + } [Fact] public void SettingsView_ExposesAllSupportedLanguageSelections() @@ -258,6 +258,26 @@ public void SettingsView_ExposesAllSupportedLanguageSelections() } } + [Fact] + public void SettingsView_ExposesLocalizedDiscordAndIssueActions() + { + var settingsViewPath = Path.Combine( + AppContext.BaseDirectory, + "..", + "..", + "..", + "..", + "..", + "Views", + "SettingsView.xaml"); + var serialized = File.ReadAllText(settingsViewPath); + + Assert.Contains("SettingsView_SomethingWrong", serialized, StringComparison.Ordinal); + Assert.Contains("OpenDiscordCommand", serialized, StringComparison.Ordinal); + Assert.Contains("ReportIssueCommand", serialized, StringComparison.Ordinal); + Assert.Contains("SettingsView_ReportIssue", serialized, StringComparison.Ordinal); + } + private sealed class Harness { private readonly ApplicationSettingsModel settings; diff --git a/Tests/ThreadPilot.Core.Tests/SystemTweaksServiceTests.cs b/Tests/ThreadPilot.Core.Tests/SystemTweaksServiceTests.cs index 7764297..3405504 100644 --- a/Tests/ThreadPilot.Core.Tests/SystemTweaksServiceTests.cs +++ b/Tests/ThreadPilot.Core.Tests/SystemTweaksServiceTests.cs @@ -19,5 +19,29 @@ public void GetHighSchedulingCategoryRegistryValue_WhenDisabled_KeepsDefaultReve Assert.Equal("Medium", value); } + + [Fact] + public void GetPointerPrecisionValues_WhenDisabled_ClearsAccelerationAndThresholds() + { + var values = SystemTweaksService.GetPointerPrecisionValues(enabled: false, [6, 10, 1]); + + Assert.Equal([0, 0, 0], values); + } + + [Fact] + public void GetPointerPrecisionValues_WhenEnabled_RestoresWindowsDefaultsAfterDisable() + { + var values = SystemTweaksService.GetPointerPrecisionValues(enabled: true, [0, 0, 0]); + + Assert.Equal([6, 10, 1], values); + } + + [Fact] + public void GetPointerPrecisionValues_WhenEnabled_PreservesExistingNonZeroValues() + { + var values = SystemTweaksService.GetPointerPrecisionValues(enabled: true, [4, 8, 2]); + + Assert.Equal([4, 8, 2], values); + } } } diff --git a/Tests/ThreadPilot.Core.Tests/SystemTweaksViewModelTests.cs b/Tests/ThreadPilot.Core.Tests/SystemTweaksViewModelTests.cs index 4d41392..cb55c3a 100644 --- a/Tests/ThreadPilot.Core.Tests/SystemTweaksViewModelTests.cs +++ b/Tests/ThreadPilot.Core.Tests/SystemTweaksViewModelTests.cs @@ -12,8 +12,15 @@ public async Task LoadAsync_QueriesEverySupportedTweakExactlyOnce() { var harness = new Harness(); var status = new TweakStatus { IsEnabled = true, IsAvailable = true }; + harness.Tweaks.Setup(service => service.GetGameModeStatusAsync()).ReturnsAsync(status); harness.Tweaks.Setup(service => service.GetCoreParkingStatusAsync()).ReturnsAsync(status); harness.Tweaks.Setup(service => service.GetCStatesStatusAsync()).ReturnsAsync(status); + harness.Tweaks.Setup(service => service.GetMemoryIntegrityStatusAsync()).ReturnsAsync(status); + harness.Tweaks.Setup(service => service.GetUsbSelectiveSuspendStatusAsync()).ReturnsAsync(status); + harness.Tweaks.Setup(service => service.GetPointerPrecisionStatusAsync()).ReturnsAsync(status); + harness.Tweaks.Setup(service => service.GetEthernetPowerSavingStatusAsync()).ReturnsAsync(status); + harness.Tweaks.Setup(service => service.GetInterruptModerationStatusAsync()).ReturnsAsync(status); + harness.Tweaks.Setup(service => service.GetGpuMsiModeStatusAsync()).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); @@ -24,9 +31,17 @@ public async Task LoadAsync_QueriesEverySupportedTweakExactlyOnce() await viewModel.LoadAsync(); Assert.Equal(Enum.GetValues().Length, viewModel.TweakItems.Count); - Assert.All(viewModel.TweakItems, item => Assert.True(item.IsEnabled)); + Assert.All(viewModel.TweakItems.Where(item => !item.IsGuidedAction), item => Assert.True(item.IsEnabled)); + Assert.All(viewModel.TweakItems, item => Assert.True(item.IsAvailable)); + harness.Tweaks.Verify(service => service.GetGameModeStatusAsync(), Times.Once); harness.Tweaks.Verify(service => service.GetCoreParkingStatusAsync(), Times.Once); harness.Tweaks.Verify(service => service.GetCStatesStatusAsync(), Times.Once); + harness.Tweaks.Verify(service => service.GetMemoryIntegrityStatusAsync(), Times.Once); + harness.Tweaks.Verify(service => service.GetUsbSelectiveSuspendStatusAsync(), Times.Once); + harness.Tweaks.Verify(service => service.GetPointerPrecisionStatusAsync(), Times.Once); + harness.Tweaks.Verify(service => service.GetEthernetPowerSavingStatusAsync(), Times.Once); + harness.Tweaks.Verify(service => service.GetInterruptModerationStatusAsync(), Times.Once); + harness.Tweaks.Verify(service => service.GetGpuMsiModeStatusAsync(), 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); @@ -35,8 +50,14 @@ public async Task LoadAsync_QueriesEverySupportedTweakExactlyOnce() } [Theory] + [InlineData(SystemTweak.GameMode, "Game Mode")] [InlineData(SystemTweak.CoreParking, "Core Parking")] [InlineData(SystemTweak.CStates, "C-States")] + [InlineData(SystemTweak.UsbSelectiveSuspend, "USB Selective Suspend")] + [InlineData(SystemTweak.PointerPrecision, "Enhance pointer precision")] + [InlineData(SystemTweak.EthernetPowerSaving, "Disable Ethernet power saving")] + [InlineData(SystemTweak.InterruptModeration, "Disable interrupt moderation")] + [InlineData(SystemTweak.GpuMsiMode, "GPU MSI Mode")] [InlineData(SystemTweak.SysMain, "SysMain Service")] [InlineData(SystemTweak.Prefetch, "Prefetch")] [InlineData(SystemTweak.PowerThrottling, "Power Throttling")] @@ -109,6 +130,10 @@ public void SetupTweak(SystemTweak tweakType, bool setResult) { switch (tweakType) { + case SystemTweak.GameMode: + this.Tweaks.Setup(service => service.SetGameModeAsync(true)).ReturnsAsync(setResult); + this.Tweaks.Setup(service => service.GetGameModeStatusAsync()).ReturnsAsync(CreateEnabledStatus()); + break; case SystemTweak.CoreParking: this.Tweaks.Setup(service => service.SetCoreParkingAsync(true)).ReturnsAsync(setResult); this.Tweaks.Setup(service => service.GetCoreParkingStatusAsync()).ReturnsAsync(CreateEnabledStatus()); @@ -117,6 +142,26 @@ public void SetupTweak(SystemTweak tweakType, bool setResult) this.Tweaks.Setup(service => service.SetCStatesAsync(true)).ReturnsAsync(setResult); this.Tweaks.Setup(service => service.GetCStatesStatusAsync()).ReturnsAsync(CreateEnabledStatus()); break; + case SystemTweak.UsbSelectiveSuspend: + this.Tweaks.Setup(service => service.SetUsbSelectiveSuspendAsync(true)).ReturnsAsync(setResult); + this.Tweaks.Setup(service => service.GetUsbSelectiveSuspendStatusAsync()).ReturnsAsync(CreateEnabledStatus()); + break; + case SystemTweak.PointerPrecision: + this.Tweaks.Setup(service => service.SetPointerPrecisionAsync(true)).ReturnsAsync(setResult); + this.Tweaks.Setup(service => service.GetPointerPrecisionStatusAsync()).ReturnsAsync(CreateEnabledStatus()); + break; + case SystemTweak.EthernetPowerSaving: + this.Tweaks.Setup(service => service.SetEthernetPowerSavingAsync(true)).ReturnsAsync(setResult); + this.Tweaks.Setup(service => service.GetEthernetPowerSavingStatusAsync()).ReturnsAsync(CreateEnabledStatus()); + break; + case SystemTweak.InterruptModeration: + this.Tweaks.Setup(service => service.SetInterruptModerationAsync(true)).ReturnsAsync(setResult); + this.Tweaks.Setup(service => service.GetInterruptModerationStatusAsync()).ReturnsAsync(CreateEnabledStatus()); + break; + case SystemTweak.GpuMsiMode: + this.Tweaks.Setup(service => service.SetGpuMsiModeAsync(true)).ReturnsAsync(setResult); + this.Tweaks.Setup(service => service.GetGpuMsiModeStatusAsync()).ReturnsAsync(CreateEnabledStatus()); + break; case SystemTweak.SysMain: this.Tweaks.Setup(service => service.SetSysMainAsync(true)).ReturnsAsync(setResult); this.Tweaks.Setup(service => service.GetSysMainStatusAsync()).ReturnsAsync(CreateEnabledStatus()); @@ -146,12 +191,30 @@ public void VerifySetCalled(SystemTweak tweakType) { switch (tweakType) { + case SystemTweak.GameMode: + this.Tweaks.Verify(service => service.SetGameModeAsync(true), Times.Once); + break; case SystemTweak.CoreParking: this.Tweaks.Verify(service => service.SetCoreParkingAsync(true), Times.Once); break; case SystemTweak.CStates: this.Tweaks.Verify(service => service.SetCStatesAsync(true), Times.Once); break; + case SystemTweak.UsbSelectiveSuspend: + this.Tweaks.Verify(service => service.SetUsbSelectiveSuspendAsync(true), Times.Once); + break; + case SystemTweak.PointerPrecision: + this.Tweaks.Verify(service => service.SetPointerPrecisionAsync(true), Times.Once); + break; + case SystemTweak.EthernetPowerSaving: + this.Tweaks.Verify(service => service.SetEthernetPowerSavingAsync(true), Times.Once); + break; + case SystemTweak.InterruptModeration: + this.Tweaks.Verify(service => service.SetInterruptModerationAsync(true), Times.Once); + break; + case SystemTweak.GpuMsiMode: + this.Tweaks.Verify(service => service.SetGpuMsiModeAsync(true), Times.Once); + break; case SystemTweak.SysMain: this.Tweaks.Verify(service => service.SetSysMainAsync(true), Times.Once); break; diff --git a/ThreadPilot.csproj b/ThreadPilot.csproj index 7179973..da1d696 100644 --- a/ThreadPilot.csproj +++ b/ThreadPilot.csproj @@ -16,10 +16,10 @@ link true CS1998;CS0067;CS0414;WFAC010;IL3000;MVVMTK0034 - 1.6.0 - 1.6.0.0 - 1.6.0.0 - 1.6.0 + 1.7.0 + 1.7.0.0 + 1.7.0.0 + 1.7.0 @@ -36,15 +36,6 @@ - - - - Powerplans\%(RecursiveDir)%(Filename)%(Extension) - PreserveNewest - PreserveNewest - - - diff --git a/ViewModels/BaseViewModel.cs b/ViewModels/BaseViewModel.cs index ccf6ccd..3a6e267 100644 --- a/ViewModels/BaseViewModel.cs +++ b/ViewModels/BaseViewModel.cs @@ -1,6 +1,7 @@ namespace ThreadPilot.ViewModels { using System; + using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -104,6 +105,18 @@ protected void ClearError() this.HasError = false; } + protected void OpenExternalLink(string url) + { + try + { + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } + catch (Exception ex) + { + this.SetError("Unable to open the link.", ex); + } + } + protected async Task ExecuteAsync(Func operation, string? statusMessage = null, string? successMessage = null) { try diff --git a/ViewModels/PowerPlanViewModel.cs b/ViewModels/PowerPlanViewModel.cs index d727e82..d8a8e69 100644 --- a/ViewModels/PowerPlanViewModel.cs +++ b/ViewModels/PowerPlanViewModel.cs @@ -16,6 +16,7 @@ namespace ThreadPilot.ViewModels public partial class PowerPlanViewModel : BaseViewModel { + internal const string MorePlansUrl = "https://discord.gg/EmPfb57Kfr"; private readonly IPowerPlanService powerPlanService; private System.Timers.Timer? refreshTimer; private bool isAutoRefreshPaused = true; @@ -133,6 +134,9 @@ public void ResumeAutoRefresh(bool refreshImmediately = true) }); } + [RelayCommand] + private void OpenMorePlans() => this.OpenExternalLink(MorePlansUrl); + [RelayCommand] public async Task LoadPowerPlans() { diff --git a/ViewModels/ProcessViewModel.Behaviors.partial.cs b/ViewModels/ProcessViewModel.Behaviors.partial.cs index 64e2a8b..b2a357c 100644 --- a/ViewModels/ProcessViewModel.Behaviors.partial.cs +++ b/ViewModels/ProcessViewModel.Behaviors.partial.cs @@ -145,24 +145,24 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => } } - partial void OnSelectedProcessChanged(ProcessModel? value) - { - this.SetCpuAssignmentMode( - this.settingsService?.Settings.DefaultCpuAssignmentMode ?? CpuAssignmentMode.Automatic, - overriddenByRule: false); - this.UpdateSelectedProcessSummary(value); + partial void OnSelectedProcessChanged(ProcessModel? value) + { + this.SetCpuAssignmentMode( + this.settingsService?.Settings.DefaultCpuAssignmentMode ?? CpuAssignmentMode.Automatic, + overriddenByRule: false); + this.UpdateSelectedProcessSummary(value); if (value != null && CpuTopology != null) { this.HasPendingAffinityEdits = false; this.UpdateAffinityDisplayState(); // Immediately fetch and display real-time process information - TaskSafety.FireAndForget(HandleSelectedProcessChangedAsync(value), ex => + TaskSafety.FireAndForget(HandleSelectedProcessChangedAsync(value), ex => { this.Logger.LogWarning(ex, "Failed while handling selected process change for {ProcessName}", value.Name); - }); - TaskSafety.FireAndForget(this.ResolveCpuAssignmentModeAsync(value), ex => - this.Logger.LogDebug(ex, "Could not resolve saved CPU assignment mode for {ProcessName}", value.Name)); + }); + TaskSafety.FireAndForget(this.ResolveCpuAssignmentModeAsync(value), ex => + this.Logger.LogDebug(ex, "Could not resolve saved CPU assignment mode for {ProcessName}", value.Name)); } else if (value == null) { @@ -172,39 +172,39 @@ partial void OnSelectedProcessChanged(ProcessModel? value) // Update system tray context menu this.systemTrayService.UpdateContextMenu(value?.Name, value != null); - } - - partial void OnSelectedCpuAssignmentModeChanged(CpuAssignmentMode value) - { - if (!this.suppressCpuAssignmentModeChanges && this.SelectedProcess != null) - { - this.HasPendingAffinityEdits = true; - this.cpuAssignmentModeOverriddenByRule = true; - } - } - - private async Task ResolveCpuAssignmentModeAsync(ProcessModel process) - { - if (this.persistentRuleStore == null || this.persistentRuleMatcher == null) - { - return; - } - - var rules = await this.persistentRuleStore.LoadAsync().ConfigureAwait(false); - var rule = rules.FirstOrDefault(candidate => candidate.IsEnabled && this.persistentRuleMatcher.IsMatch(candidate, process)); - if (rule != null && this.SelectedProcess?.ProcessId == process.ProcessId) - { - await InvokeOnUiAsync(() => this.SetCpuAssignmentMode(rule.CpuAssignmentMode, overriddenByRule: true)); - } - } - - private void SetCpuAssignmentMode(CpuAssignmentMode mode, bool overriddenByRule) - { - this.suppressCpuAssignmentModeChanges = true; - this.SelectedCpuAssignmentMode = Enum.IsDefined(mode) ? mode : CpuAssignmentMode.Automatic; - this.suppressCpuAssignmentModeChanges = false; - this.cpuAssignmentModeOverriddenByRule = overriddenByRule; - } + } + + partial void OnSelectedCpuAssignmentModeChanged(CpuAssignmentMode value) + { + if (!this.suppressCpuAssignmentModeChanges && this.SelectedProcess != null) + { + this.HasPendingAffinityEdits = true; + this.cpuAssignmentModeOverriddenByRule = true; + } + } + + private async Task ResolveCpuAssignmentModeAsync(ProcessModel process) + { + if (this.persistentRuleStore == null || this.persistentRuleMatcher == null) + { + return; + } + + var rules = await this.persistentRuleStore.LoadAsync().ConfigureAwait(false); + var rule = rules.FirstOrDefault(candidate => candidate.IsEnabled && this.persistentRuleMatcher.IsMatch(candidate, process)); + if (rule != null && this.SelectedProcess?.ProcessId == process.ProcessId) + { + await InvokeOnUiAsync(() => this.SetCpuAssignmentMode(rule.CpuAssignmentMode, overriddenByRule: true)); + } + } + + private void SetCpuAssignmentMode(CpuAssignmentMode mode, bool overriddenByRule) + { + this.suppressCpuAssignmentModeChanges = true; + this.SelectedCpuAssignmentMode = Enum.IsDefined(mode) ? mode : CpuAssignmentMode.Automatic; + this.suppressCpuAssignmentModeChanges = false; + this.cpuAssignmentModeOverriddenByRule = overriddenByRule; + } private void UpdateSelectedProcessSummary(ProcessModel? process) { @@ -546,7 +546,7 @@ private void UpdateAffinityDisplayState() this.PendingAffinityText = pendingMask > 0 ? $"Pending core mask: 0x{pendingMask:X}" : "Pending core mask: no cores selected"; - this.AffinityEditStateText = "CPU selection staged. Use Apply CPU Assignment to apply it."; + this.AffinityEditStateText = "CPU selection staged. Use Apply CPU Assignment to apply it."; } [RelayCommand] @@ -813,11 +813,13 @@ private async Task SaveCurrentSettingsAsRule(ProcessModel? process) var currentCoreSelection = this.HasPendingAffinityEdits && this.CpuCores.Count > 0 ? this.GetPendingCoreSelectionMask() : null; - var result = await this.processRuleCreationService.SaveCurrentSettingsAsRuleAsync( - targetProcess, - currentCoreSelection, - this.SelectedProcessSummary.MemoryPriority, - this.SelectedCpuAssignmentMode); + var result = await this.processRuleCreationService.SaveCurrentSettingsAsRuleAsync( + targetProcess, + currentCoreSelection, + this.SelectedProcessSummary.MemoryPriority, + this.SelectedCpuAssignmentMode, + this.SelectedProcessSummary.IoPriority, + this.IsIdleServerDisabled); this.ApplyRuleCreationResultStatus(result); await this.LogUserActionAsync( @@ -848,24 +850,24 @@ private async Task ApplyAffinityAndSaveAsRule(ProcessModel? process) } var pendingSelection = this.GetPendingCoreSelectionMask(); - var applyResult = await this.processAffinityApplyCoordinator.ApplyCoreSelectionAsync( - process, - pendingSelection, - "Manual Process tab context menu CPU selection", - this.SelectedCpuAssignmentMode); + var applyResult = await this.processAffinityApplyCoordinator.ApplyCoreSelectionAsync( + process, + pendingSelection, + "Manual Process tab context menu CPU selection", + this.SelectedCpuAssignmentMode); if (!applyResult.Success) { this.SetContextError(applyResult.Message); await this.LogUserActionAsync( - "ProcessCpuAssignmentFailed", - applyResult.Message, - $"Process: {process.Name}, PID: {process.ProcessId}, RequestedMode: {applyResult.RequestedMode}, EffectiveMode: {applyResult.EffectiveMode}, RequestedMask: 0x{applyResult.RequestedMask:X}"); + "ProcessCpuAssignmentFailed", + applyResult.Message, + $"Process: {process.Name}, PID: {process.ProcessId}, RequestedMode: {applyResult.RequestedMode}, EffectiveMode: {applyResult.EffectiveMode}, RequestedMask: 0x{applyResult.RequestedMask:X}"); await this.UpdateSelectedProcessSummaryAsync(process); return; } - if (applyResult.EffectiveMode == CpuAssignmentMode.AffinityMask) + if (applyResult.EffectiveMode == CpuAssignmentMode.AffinityMask) { this.UpdateCoreSelections(process.ProcessorAffinity, true); } @@ -875,11 +877,13 @@ await this.LogUserActionAsync( this.HasPendingAffinityEdits = false; this.UpdateAffinityDisplayState(); - var saveResult = await this.processRuleCreationService.SaveCurrentSettingsAsRuleAsync( - process, - pendingSelection, - currentMemoryPriority: null, - cpuAssignmentMode: this.SelectedCpuAssignmentMode); + var saveResult = await this.processRuleCreationService.SaveCurrentSettingsAsRuleAsync( + process, + pendingSelection, + currentMemoryPriority: null, + cpuAssignmentMode: this.SelectedCpuAssignmentMode, + currentIoPriority: null, + preventSystemSleepWhileRunning: this.IsIdleServerDisabled); this.ApplyRuleCreationResultStatus(saveResult); await this.LogUserActionAsync( @@ -913,15 +917,15 @@ await InvokeOnUiAsync(() => this.SetStatus($"Setting affinity for {selectedProcess.Name}..."); }); - var result = await this.processAffinityApplyCoordinator.ApplyCoreSelectionAsync( - selectedProcess, - pendingSelection, - selectionReason, - this.SelectedCpuAssignmentMode); + var result = await this.processAffinityApplyCoordinator.ApplyCoreSelectionAsync( + selectedProcess, + pendingSelection, + selectionReason, + this.SelectedCpuAssignmentMode); await InvokeOnUiAsync(() => { - if (result.EffectiveMode == CpuAssignmentMode.AffinityMask) + if (result.EffectiveMode == CpuAssignmentMode.AffinityMask) { this.UpdateCoreSelections(selectedProcess.ProcessorAffinity, true); } @@ -933,58 +937,58 @@ await InvokeOnUiAsync(() => { this.HasPendingAffinityEdits = false; this.UpdateAffinityDisplayState(); - this.SetStatus($"CPU assignment applied successfully to {selectedProcess.Name} using {result.EffectiveMode}.", false); - _ = this.notificationService.ShowNotificationAsync("CPU assignment applied", $"{selectedProcess.Name}: {result.EffectiveMode}", NotificationType.Success); + this.SetStatus($"CPU assignment applied successfully to {selectedProcess.Name} using {result.EffectiveMode}.", false); + _ = this.notificationService.ShowNotificationAsync("CPU assignment applied", $"{selectedProcess.Name}: {result.EffectiveMode}", NotificationType.Success); } else if (result.FailureReason == AffinityApplyFailureReason.VerificationMismatch) { this.HasPendingAffinityEdits = false; this.UpdateAffinityDisplayState(); this.SetStatus(result.Message, false); - _ = this.notificationService.ShowNotificationAsync("CPU assignment adjusted", result.Message, NotificationType.Warning); + _ = this.notificationService.ShowNotificationAsync("CPU assignment adjusted", result.Message, NotificationType.Warning); } else if (result.FailureReason == AffinityApplyFailureReason.ProcessTerminated) { this.SelectedProcess = null; this.ClearProcessSelection(); this.SetCriticalStatus(result.Message); - _ = this.notificationService.ShowNotificationAsync("CPU assignment failed", result.Message, NotificationType.Warning); + _ = this.notificationService.ShowNotificationAsync("CPU assignment failed", result.Message, NotificationType.Warning); } else if (result.FailureReason == AffinityApplyFailureReason.AccessDenied) { this.SetCriticalStatus(result.Message); - _ = this.notificationService.ShowNotificationAsync("CPU assignment blocked", result.Message, NotificationType.Warning); + _ = this.notificationService.ShowNotificationAsync("CPU assignment blocked", result.Message, NotificationType.Warning); } else if (result.IsInvalidTopology || result.IsLegacyFallbackBlocked) { this.SetCriticalStatus(result.Message); - _ = this.notificationService.ShowNotificationAsync("CPU assignment blocked", result.Message, NotificationType.Warning); + _ = this.notificationService.ShowNotificationAsync("CPU assignment blocked", result.Message, NotificationType.Warning); } else { this.SetStatus(result.Message, false); - _ = this.notificationService.ShowNotificationAsync("CPU assignment error", result.Message, NotificationType.Error); + _ = this.notificationService.ShowNotificationAsync("CPU assignment error", result.Message, NotificationType.Error); } }); await this.LogUserActionAsync( - result.Success ? "ProcessCpuAssignmentApplied" : "ProcessCpuAssignmentFailed", - result.Message, - $"Process: {selectedProcess.Name}, PID: {selectedProcess.ProcessId}, RequestedMode: {result.RequestedMode}, EffectiveMode: {result.EffectiveMode}, Threads: {result.AppliedThreadCount}, RequestedMask: 0x{result.RequestedMask:X}, VerifiedMask: 0x{result.VerifiedMask:X}"); + result.Success ? "ProcessCpuAssignmentApplied" : "ProcessCpuAssignmentFailed", + result.Message, + $"Process: {selectedProcess.Name}, PID: {selectedProcess.ProcessId}, RequestedMode: {result.RequestedMode}, EffectiveMode: {result.EffectiveMode}, Threads: {result.AppliedThreadCount}, RequestedMask: 0x{result.RequestedMask:X}, VerifiedMask: 0x{result.VerifiedMask:X}"); await this.UpdateSelectedProcessSummaryAsync(selectedProcess); } catch (Exception ex) { var friendly = ex.Message; - _ = this.notificationService.ShowNotificationAsync("CPU assignment error", friendly, NotificationType.Error); + _ = this.notificationService.ShowNotificationAsync("CPU assignment error", friendly, NotificationType.Error); await this.LogUserActionAsync( - "ProcessCpuAssignmentFailed", - friendly, - $"Process: {selectedProcess.Name}, PID: {selectedProcess.ProcessId}, Mode: {this.SelectedCpuAssignmentMode}"); + "ProcessCpuAssignmentFailed", + friendly, + $"Process: {selectedProcess.Name}, PID: {selectedProcess.ProcessId}, Mode: {this.SelectedCpuAssignmentMode}"); await InvokeOnUiAsync(() => { - this.SetCriticalStatus($"Error applying CPU assignment: {friendly}"); + this.SetCriticalStatus($"Error applying CPU assignment: {friendly}"); }); // Try to refresh process info even if setting failed, to show current state @@ -1148,10 +1152,6 @@ private async Task ApplyCoreMaskToProcessAsync(CoreMask mask) "Applying mask '{MaskName}' to process {ProcessName} (PID: {ProcessId})", mask.Name, selectedProcess.Name, selectedProcess.ProcessId); - // Disable Windows Game Mode for better CPU affinity control - // Game Mode can interfere with CPU Sets, particularly on AMD systems - await this.gameModeService.DisableGameModeForAffinityAsync(); - var result = await this.processAffinityApplyCoordinator.ApplyCoreMaskAsync(selectedProcess, mask); System.Windows.Application.Current.Dispatcher.Invoke(() => @@ -1231,7 +1231,7 @@ private async Task QuickApplyAffinityAndPowerPlan() try { - var affinityModeChangedOsAffinity = false; + var affinityModeChangedOsAffinity = false; await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => { @@ -1242,16 +1242,16 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => var pendingSelection = this.GetPendingCoreSelectionMask(); if (pendingSelection.Any(selected => selected)) { - var result = await this.processAffinityApplyCoordinator.ApplyCoreSelectionAsync( - selectedProcess, - pendingSelection, - "Manual Process tab quick apply CPU selection", - this.SelectedCpuAssignmentMode); + var result = await this.processAffinityApplyCoordinator.ApplyCoreSelectionAsync( + selectedProcess, + pendingSelection, + "Manual Process tab quick apply CPU selection", + this.SelectedCpuAssignmentMode); if (!result.Success) { await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => { - if (result.EffectiveMode == CpuAssignmentMode.AffinityMask) + if (result.EffectiveMode == CpuAssignmentMode.AffinityMask) { this.UpdateCoreSelections(selectedProcess.ProcessorAffinity, true); } @@ -1265,7 +1265,7 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => this.HasPendingAffinityEdits = false; this.UpdateAffinityDisplayState(); - affinityModeChangedOsAffinity = result.EffectiveMode == CpuAssignmentMode.AffinityMask; + affinityModeChangedOsAffinity = result.EffectiveMode == CpuAssignmentMode.AffinityMask; } // Apply power plan if selected @@ -1278,7 +1278,7 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => { - if (affinityModeChangedOsAffinity) + if (affinityModeChangedOsAffinity) { this.UpdateCoreSelections(selectedProcess.ProcessorAffinity, true); } @@ -1531,6 +1531,22 @@ private Task SetContextMemoryPriorityBelowNormal(ProcessModel? process) => private Task SetContextMemoryPriorityNormal(ProcessModel? process) => this.SetContextMemoryPriorityAsync(process, ProcessMemoryPriority.Normal); + [RelayCommand] + private Task SetContextIoPriorityVeryLow(ProcessModel? process) => + this.SetContextIoPriorityAsync(process, ProcessIoPriority.VeryLow); + + [RelayCommand] + private Task SetContextIoPriorityLow(ProcessModel? process) => + this.SetContextIoPriorityAsync(process, ProcessIoPriority.Low); + + [RelayCommand] + private Task SetContextIoPriorityNormal(ProcessModel? process) => + this.SetContextIoPriorityAsync(process, ProcessIoPriority.Normal); + + [RelayCommand] + private Task SetContextIoPriorityHigh(ProcessModel? process) => + this.SetContextIoPriorityAsync(process, ProcessIoPriority.High); + [RelayCommand] private async Task ClearContextCpuSets(ProcessModel? process) { @@ -1665,6 +1681,7 @@ private async Task CopyContextProcessInfo(ProcessModel? process) .AppendLine($"Path: {path}") .AppendLine($"CPU priority: {process.Priority}") .AppendLine($"Memory priority: {this.SelectedProcessSummary.MemoryPriority?.ToString() ?? "unavailable"}") + .AppendLine($"I/O priority: {this.SelectedProcessSummary.IoPriority?.ToString() ?? "unavailable"}") .AppendLine($"Affinity: 0x{process.ProcessorAffinity:X}") .AppendLine($"Rule status: {this.SelectedProcessSummary.RuleStatusText}"); @@ -1794,6 +1811,33 @@ await this.LogUserActionAsync( } } + private async Task SetContextIoPriorityAsync(ProcessModel? process, ProcessIoPriority priority) + { + if (process == null || this.memoryPriorityService == null) + { + return; + } + + var result = await this.memoryPriorityService.SetIoPriorityAsync(process, priority); + if (!result.Success) + { + this.SetContextError(result.UserMessage); + await this.LogUserActionAsync( + "ProcessIoPriorityFailed", + result.UserMessage, + $"Process: {process.Name}, PID: {process.ProcessId}, Priority: {priority}"); + await this.UpdateSelectedProcessSummaryAsync(process); + return; + } + + this.SetStatus($"I/O priority applied successfully to {process.Name}: {priority}.", false); + await this.LogUserActionAsync( + "ProcessIoPriorityChanged", + $"I/O priority changed for {process.Name}: {priority}", + $"PID: {process.ProcessId}"); + await this.UpdateSelectedProcessSummaryAsync(process); + } + private async Task TryRefreshContextProcessSummaryAsync(ProcessModel process) { try @@ -2230,32 +2274,30 @@ private async Task ToggleIdleServerAsync(bool disable) try { - this.SetStatus($"{(disable ? "Disabling" : "Enabling")} idle server for {this.SelectedProcess.Name}..."); - - // Implementation for disabling/enabling idle server - // This typically involves setting process execution state or power management settings - var success = await this.processService.SetIdleServerStateAsync(this.SelectedProcess, !disable); + this.SetStatus($"{(disable ? "Enabling" : "Disabling")} sleep prevention for {this.SelectedProcess.Name}..."); + var success = this.powerRequestService != null && + await this.powerRequestService.SetPreventSleepAsync(this.SelectedProcess, disable); if (success) { this.SelectedProcess.IsIdleServerDisabled = disable; - this.SetStatus($"Idle server {(disable ? "disabled" : "enabled")} for {this.SelectedProcess.Name}"); + this.SetStatus($"Sleep prevention {(disable ? "enabled" : "disabled")} for {this.SelectedProcess.Name}"); await this.LogUserActionAsync( - "IdleServer", - $"Idle server {(disable ? "disabled" : "enabled")} for process {this.SelectedProcess.Name}", + "ProcessSleepPrevention", + $"Sleep prevention {(disable ? "enabled" : "disabled")} for process {this.SelectedProcess.Name}", $"PID: {this.SelectedProcess.ProcessId}"); } else { - this.SetStatus($"Failed to {(disable ? "disable" : "enable")} idle server for {this.SelectedProcess.Name}", false); + this.SetStatus($"Failed to {(disable ? "enable" : "disable")} sleep prevention for {this.SelectedProcess.Name}", false); // Revert the UI state this.IsIdleServerDisabled = !disable; } } catch (Exception ex) { - this.Logger.LogError(ex, "Error toggling idle server for process {ProcessName}", this.SelectedProcess.Name); + this.Logger.LogError(ex, "Error toggling sleep prevention for process {ProcessName}", this.SelectedProcess.Name); this.SetStatus($"Error: {ex.Message}", false); // Revert the UI state this.IsIdleServerDisabled = !disable; diff --git a/ViewModels/ProcessViewModel.cs b/ViewModels/ProcessViewModel.cs index e251658..2b2d393 100644 --- a/ViewModels/ProcessViewModel.cs +++ b/ViewModels/ProcessViewModel.cs @@ -32,10 +32,11 @@ public partial class ProcessViewModel : BaseViewModel private readonly IAffinityApplyService affinityApplyService; private readonly IProcessAffinityApplyCoordinator processAffinityApplyCoordinator; private readonly IProcessMemoryPriorityService? memoryPriorityService; + private readonly ProcessPowerRequestService? powerRequestService; private readonly IProcessRuleCreationService? processRuleCreationService; - private readonly IApplicationSettingsService? settingsService; - private readonly IPersistentProcessRuleStore? persistentRuleStore; - private readonly IPersistentProcessRuleMatcher? persistentRuleMatcher; + private readonly IApplicationSettingsService? settingsService; + private readonly IPersistentProcessRuleStore? persistentRuleStore; + private readonly IPersistentProcessRuleMatcher? persistentRuleMatcher; private readonly Action clipboardSetter; private readonly Action executableLocationOpener; private System.Timers.Timer? refreshTimer; @@ -47,9 +48,9 @@ public partial class ProcessViewModel : BaseViewModel private readonly ThrottledRefreshCoordinator filterRefreshCoordinator; private bool isApplyingFilter; private bool filterRefreshPending; - private bool suppressCoreSelectionEvents; - private bool suppressCpuAssignmentModeChanges; - private bool cpuAssignmentModeOverriddenByRule; + private bool suppressCoreSelectionEvents; + private bool suppressCpuAssignmentModeChanges; + private bool cpuAssignmentModeOverriddenByRule; [ObservableProperty] private bool isAutomationMonitoringEnabled = true; @@ -105,10 +106,10 @@ public partial class ProcessViewModel : BaseViewModel private string topologyStatus = "Detecting CPU topology..."; [ObservableProperty] - private bool areAdvancedFeaturesAvailable = false; - - [ObservableProperty] - private CpuAssignmentMode selectedCpuAssignmentMode = CpuAssignmentMode.Automatic; + private bool areAdvancedFeaturesAvailable = false; + + [ObservableProperty] + private CpuAssignmentMode selectedCpuAssignmentMode = CpuAssignmentMode.Automatic; [ObservableProperty] private PowerPlanModel? selectedPowerPlan; @@ -194,7 +195,8 @@ public ProcessViewModel( Action? clipboardSetter = null, Action? executableLocationOpener = null, ILocalizationService? localizationService = null, - IApplicationSettingsService? settingsService = null) + IApplicationSettingsService? settingsService = null, + ProcessPowerRequestService? powerRequestService = null) : base(logger, enhancedLoggingService, activityAuditService) { this.processService = processService ?? throw new ArgumentNullException(nameof(processService)); @@ -217,10 +219,11 @@ public ProcessViewModel( new CpuSelectionMigrationService(), NullLogger.Instance); this.memoryPriorityService = memoryPriorityService; - this.settingsService = settingsService; - this.persistentRuleStore = persistentRuleStore; - this.persistentRuleMatcher = persistentRuleMatcher; - this.SelectedCpuAssignmentMode = settingsService?.Settings.DefaultCpuAssignmentMode ?? CpuAssignmentMode.Automatic; + this.powerRequestService = powerRequestService; + this.settingsService = settingsService; + this.persistentRuleStore = persistentRuleStore; + this.persistentRuleMatcher = persistentRuleMatcher; + this.SelectedCpuAssignmentMode = settingsService?.Settings.DefaultCpuAssignmentMode ?? CpuAssignmentMode.Automatic; this.IsAutomationMonitoringEnabled = settingsService?.Settings.EnableAutomationMonitoring ?? true; if (settingsService != null) { @@ -265,11 +268,11 @@ public ProcessViewModel( private void OnSettingsChanged(object? sender, ApplicationSettingsChangedEventArgs e) { - this.IsAutomationMonitoringEnabled = e.NewSettings.EnableAutomationMonitoring; - if (!this.cpuAssignmentModeOverriddenByRule) - { - this.SetCpuAssignmentMode(e.NewSettings.DefaultCpuAssignmentMode, overriddenByRule: false); - } + this.IsAutomationMonitoringEnabled = e.NewSettings.EnableAutomationMonitoring; + if (!this.cpuAssignmentModeOverriddenByRule) + { + this.SetCpuAssignmentMode(e.NewSettings.DefaultCpuAssignmentMode, overriddenByRule: false); + } if (!this.IsAutomationMonitoringEnabled) { this.SetUiRefreshEnabled(false, refreshImmediately: false); diff --git a/ViewModels/SelectedProcessSummaryViewModel.cs b/ViewModels/SelectedProcessSummaryViewModel.cs index 2802061..4173dcd 100644 --- a/ViewModels/SelectedProcessSummaryViewModel.cs +++ b/ViewModels/SelectedProcessSummaryViewModel.cs @@ -24,12 +24,14 @@ public sealed class SelectedProcessSummaryViewModel : ObservableObject private ProcessPriorityClass cpuPriority; private long processorAffinity; private ProcessMemoryPriority? memoryPriority; + private ProcessIoPriority? ioPriority; private string processTitle = "No process selected"; private string currentProcessStatusText = "No process selected"; private string cpuUsageText = "CPU: unavailable"; private string memoryUsageText = "Memory: unavailable"; private string cpuPriorityText = "CPU priority: unavailable"; private string memoryPriorityText = "Memory priority unavailable"; + private string ioPriorityText = "I/O priority unavailable"; private string affinityText = "Affinity: unavailable"; private string ruleStatusText = "No saved rule"; private string lastOperationMessage = "No recent ThreadPilot action"; @@ -105,6 +107,12 @@ public ProcessMemoryPriority? MemoryPriority private set => this.SetProperty(ref this.memoryPriority, value); } + public ProcessIoPriority? IoPriority + { + get => this.ioPriority; + private set => this.SetProperty(ref this.ioPriority, value); + } + public string ProcessTitle { get => this.processTitle; @@ -141,6 +149,12 @@ public string MemoryPriorityText private set => this.SetProperty(ref this.memoryPriorityText, value); } + public string IoPriorityText + { + get => this.ioPriorityText; + private set => this.SetProperty(ref this.ioPriorityText, value); + } + public string AffinityText { get => this.affinityText; @@ -214,6 +228,12 @@ public async Task UpdateAsync( return; } + await this.UpdateIoPriorityAsync(process, version); + if (!this.IsCurrentVersion(version)) + { + return; + } + await this.UpdateRuleStatusAsync(process, version); } @@ -244,6 +264,7 @@ private void Clear(int version, string? lastOperationMessage, bool lastOperation this.CpuPriority = default; this.ProcessorAffinity = 0; this.MemoryPriority = null; + this.IoPriority = null; this.IsProtectedOrAccessDenied = false; this.HasThreadPilotRule = false; this.ProcessTitle = this.L("ProcessView_NoProcessSelected", "No process selected"); @@ -252,6 +273,7 @@ private void Clear(int version, string? lastOperationMessage, bool lastOperation this.MemoryUsageText = this.L("ProcessSummary_MemoryUnavailable", "Memory: unavailable"); this.CpuPriorityText = this.L("ProcessSummary_CpuPriorityUnavailable", "CPU priority: unavailable"); this.MemoryPriorityText = this.L("ProcessSummary_MemoryPriorityUnavailable", "Memory priority unavailable"); + this.IoPriorityText = this.L("ProcessSummary_IoPriorityUnavailable", "I/O priority unavailable"); this.AffinityText = this.L("ProcessSummary_AffinityUnavailable", "Affinity: unavailable"); this.RuleStatusText = this.L("ProcessSummary_NoSavedRule", "No saved rule"); this.UpdateLastOperation(lastOperationMessage, lastOperationIsError); @@ -295,6 +317,32 @@ private async Task UpdateMemoryPriorityAsync(ProcessModel process, int version) } } + private async Task UpdateIoPriorityAsync(ProcessModel process, int version) + { + this.IoPriority = null; + this.IoPriorityText = this.L("ProcessSummary_IoPriorityUnavailable", "I/O priority unavailable"); + if (this.memoryPriorityService == null) + { + return; + } + + try + { + var priority = await this.memoryPriorityService.GetIoPriorityAsync(process); + if (!this.IsCurrentVersion(version) || priority == null) + { + return; + } + + this.IoPriority = priority.Value; + this.IoPriorityText = this.L("ProcessSummary_IoPriorityFormat", "I/O priority: {0}", priority.Value); + } + catch (Exception) + { + this.IoPriority = null; + } + } + private async Task UpdateRuleStatusAsync(ProcessModel process, int version) { this.HasThreadPilotRule = false; diff --git a/ViewModels/SettingsViewModel.cs b/ViewModels/SettingsViewModel.cs index 70a96d6..3786562 100644 --- a/ViewModels/SettingsViewModel.cs +++ b/ViewModels/SettingsViewModel.cs @@ -21,6 +21,8 @@ namespace ThreadPilot.ViewModels public partial class SettingsViewModel : BaseViewModel { + internal const string DiscordUrl = "https://discord.gg/VYwkCbr4vH"; + internal const string IssuesUrl = "https://github.com/PrimeBuild-pc/ThreadPilot/issues"; private readonly IApplicationSettingsService settingsService; private readonly INotificationService notificationService; private readonly IAutostartService autostartService; @@ -31,8 +33,8 @@ public partial class SettingsViewModel : BaseViewModel private readonly ISystemTrayService systemTrayService; private readonly IUpdateService updateService; private readonly IApplicationVersionProvider versionProvider; - private readonly ILocalizationService localizationService; - private readonly ICpuTopologyProvider? cpuTopologyProvider; + private readonly ILocalizationService localizationService; + private readonly ICpuTopologyProvider? cpuTopologyProvider; private ApplicationSettingsModel savedSettingsSnapshot; private bool isSyncingFromService = false; private bool? appliedThemePreference; @@ -88,11 +90,11 @@ public partial class SettingsViewModel : BaseViewModel [ObservableProperty] private string lastUpdateCheckText = string.Empty; - [ObservableProperty] - private bool isUpdateAvailable = false; - - [ObservableProperty] - private string reservedCpuSetsStatus = string.Empty; + [ObservableProperty] + private bool isUpdateAvailable = false; + + [ObservableProperty] + private string reservedCpuSetsStatus = string.Empty; public bool CanDownloadAndInstallUpdate => this.IsUpdateAvailable && !this.IsLoading; @@ -108,10 +110,10 @@ public SettingsViewModel( ISystemTrayService systemTrayService, IUpdateService updateService, IApplicationVersionProvider versionProvider, - ILocalizationService localizationService, - IEnhancedLoggingService? enhancedLoggingService = null, - IActivityAuditService? activityAuditService = null, - ICpuTopologyProvider? cpuTopologyProvider = null) + ILocalizationService localizationService, + IEnhancedLoggingService? enhancedLoggingService = null, + IActivityAuditService? activityAuditService = null, + ICpuTopologyProvider? cpuTopologyProvider = null) : base(logger, enhancedLoggingService, activityAuditService) { this.settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); @@ -124,8 +126,8 @@ public SettingsViewModel( this.systemTrayService = systemTrayService ?? throw new ArgumentNullException(nameof(systemTrayService)); this.updateService = updateService ?? throw new ArgumentNullException(nameof(updateService)); this.versionProvider = versionProvider ?? throw new ArgumentNullException(nameof(versionProvider)); - this.localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); - this.cpuTopologyProvider = cpuTopologyProvider; + this.localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + this.cpuTopologyProvider = cpuTopologyProvider; this.ApplicationVersion = this.versionProvider.DisplayVersion; @@ -133,8 +135,8 @@ public SettingsViewModel( this.settings = (ApplicationSettingsModel)this.settingsService.Settings.Clone(); this.savedSettingsSnapshot = (ApplicationSettingsModel)this.settings.Clone(); this.appliedThemePreference = this.settings.UseDarkTheme; - this.LatestUpdateVersion = this.GetLocalizedString("Settings_UpdateNotChecked", "Not checked"); - this.ReservedCpuSetsStatus = this.GetLocalizedString("SettingsView_ReservedCpuSetsUnknown", "Reserved CPU Sets: status unavailable."); + this.LatestUpdateVersion = this.GetLocalizedString("Settings_UpdateNotChecked", "Not checked"); + this.ReservedCpuSetsStatus = this.GetLocalizedString("SettingsView_ReservedCpuSetsUnknown", "Reserved CPU Sets: status unavailable."); this.UpdateLastCheckedText(); // Initialize commands @@ -158,36 +160,42 @@ public SettingsViewModel( this.Logger.LogInformation("Settings ViewModel initialized"); } - public override async Task InitializeAsync() - { - await this.RefreshSettingsAsync(); - await this.RefreshPowerPlansAsync(); - await this.RefreshReservedCpuSetsStatusAsync(); - } - - private async Task RefreshReservedCpuSetsStatusAsync() - { - if (this.cpuTopologyProvider == null) - { - return; - } - - try - { - var topology = await this.cpuTopologyProvider.GetTopologySnapshotAsync(); - this.ReservedCpuSetsStatus = topology.ReservedLogicalProcessorIndexes.Count == 0 - ? this.GetLocalizedString("SettingsView_ReservedCpuSetsNone", "Reserved CPU Sets: none detected.") - : this.GetLocalizedString( - "SettingsView_ReservedCpuSetsDetectedFormat", - "Reserved CPU Sets detected on logical processors: {0}.", - string.Join(", ", topology.ReservedLogicalProcessorIndexes)); - } - catch (Exception ex) - { - this.Logger.LogDebug(ex, "Could not read reserved CPU Set state"); - this.ReservedCpuSetsStatus = this.GetLocalizedString("SettingsView_ReservedCpuSetsUnknown", "Reserved CPU Sets: status unavailable."); - } - } + public override async Task InitializeAsync() + { + await this.RefreshSettingsAsync(); + await this.RefreshPowerPlansAsync(); + await this.RefreshReservedCpuSetsStatusAsync(); + } + + [RelayCommand] + private void OpenDiscord() => this.OpenExternalLink(DiscordUrl); + + [RelayCommand] + private void ReportIssue() => this.OpenExternalLink(IssuesUrl); + + private async Task RefreshReservedCpuSetsStatusAsync() + { + if (this.cpuTopologyProvider == null) + { + return; + } + + try + { + var topology = await this.cpuTopologyProvider.GetTopologySnapshotAsync(); + this.ReservedCpuSetsStatus = topology.ReservedLogicalProcessorIndexes.Count == 0 + ? this.GetLocalizedString("SettingsView_ReservedCpuSetsNone", "Reserved CPU Sets: none detected.") + : this.GetLocalizedString( + "SettingsView_ReservedCpuSetsDetectedFormat", + "Reserved CPU Sets detected on logical processors: {0}.", + string.Join(", ", topology.ReservedLogicalProcessorIndexes)); + } + catch (Exception ex) + { + this.Logger.LogDebug(ex, "Could not read reserved CPU Set state"); + this.ReservedCpuSetsStatus = this.GetLocalizedString("SettingsView_ReservedCpuSetsUnknown", "Reserved CPU Sets: status unavailable."); + } + } private void OnSettingsPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { diff --git a/ViewModels/SystemTweaksViewModel.cs b/ViewModels/SystemTweaksViewModel.cs index 086ce88..36f937a 100644 --- a/ViewModels/SystemTweaksViewModel.cs +++ b/ViewModels/SystemTweaksViewModel.cs @@ -50,7 +50,9 @@ private void InitializeTweakItems() Enum.GetValues().Select(type => new SystemTweakItem { TweakType = type, + IsGuidedAction = type is SystemTweak.MemoryIntegrity or SystemTweak.Hags or SystemTweak.WindowedOptimizations, ToggleCommand = new AsyncRelayCommand(this.ToggleTweakAsync), + ActionCommand = new AsyncRelayCommand(this.OpenTweakSettingsAsync), })); this.ApplyLocalizedTweakText(); } @@ -105,8 +107,20 @@ private async Task UpdateTweakItemStatusAsync(SystemTweakItem item) { TweakStatus status = item.TweakType switch { + SystemTweak.GameMode => await this.systemTweaksService.GetGameModeStatusAsync(), SystemTweak.CoreParking => await this.systemTweaksService.GetCoreParkingStatusAsync(), SystemTweak.CStates => await this.systemTweaksService.GetCStatesStatusAsync(), + SystemTweak.MemoryIntegrity => await this.systemTweaksService.GetMemoryIntegrityStatusAsync(), + SystemTweak.Hags or SystemTweak.WindowedOptimizations => new TweakStatus + { + IsAvailable = true, + StatusText = this.Localize("SystemTweaks_StatusManagedByWindows", "Managed by Windows"), + }, + SystemTweak.UsbSelectiveSuspend => await this.systemTweaksService.GetUsbSelectiveSuspendStatusAsync(), + SystemTweak.PointerPrecision => await this.systemTweaksService.GetPointerPrecisionStatusAsync(), + SystemTweak.EthernetPowerSaving => await this.systemTweaksService.GetEthernetPowerSavingStatusAsync(), + SystemTweak.InterruptModeration => await this.systemTweaksService.GetInterruptModerationStatusAsync(), + SystemTweak.GpuMsiMode => await this.systemTweaksService.GetGpuMsiModeStatusAsync(), SystemTweak.SysMain => await this.systemTweaksService.GetSysMainStatusAsync(), SystemTweak.Prefetch => await this.systemTweaksService.GetPrefetchStatusAsync(), SystemTweak.PowerThrottling => await this.systemTweaksService.GetPowerThrottlingStatusAsync(), @@ -118,6 +132,7 @@ private async Task UpdateTweakItemStatusAsync(SystemTweakItem item) item.IsEnabled = status.IsEnabled; item.IsAvailable = status.IsAvailable; item.ErrorMessage = status.ErrorMessage; + item.StatusText = status.StatusText ?? this.GetStatusText(status); } catch (Exception ex) { @@ -144,8 +159,14 @@ await InvokeOnUiAsync(() => var newState = !item.IsEnabled; bool success = item.TweakType switch { + SystemTweak.GameMode => await this.systemTweaksService.SetGameModeAsync(newState), SystemTweak.CoreParking => await this.systemTweaksService.SetCoreParkingAsync(newState), SystemTweak.CStates => await this.systemTweaksService.SetCStatesAsync(newState), + SystemTweak.UsbSelectiveSuspend => await this.systemTweaksService.SetUsbSelectiveSuspendAsync(newState), + SystemTweak.PointerPrecision => await this.systemTweaksService.SetPointerPrecisionAsync(newState), + SystemTweak.EthernetPowerSaving => await this.systemTweaksService.SetEthernetPowerSavingAsync(newState), + SystemTweak.InterruptModeration => await this.systemTweaksService.SetInterruptModerationAsync(newState), + SystemTweak.GpuMsiMode => await this.systemTweaksService.SetGpuMsiModeAsync(newState), SystemTweak.SysMain => await this.systemTweaksService.SetSysMainAsync(newState), SystemTweak.Prefetch => await this.systemTweaksService.SetPrefetchAsync(newState), SystemTweak.PowerThrottling => await this.systemTweaksService.SetPowerThrottlingAsync(newState), @@ -209,6 +230,18 @@ await this.LogUserActionAsync( } } + private async Task OpenTweakSettingsAsync(SystemTweakItem? item) + { + if (item == null || !await this.systemTweaksService.OpenSettingsAsync(item.TweakType)) + { + this.SetError(this.Localize("SystemTweaks_StatusOpenSettingsFailed", "Could not open Windows settings"), null); + return; + } + + this.SetStatus(this.Localize("SystemTweaks_StatusSettingsOpenedFormat", "Opened settings for {0}", item.Name)); + await this.LogUserActionAsync("SystemTweakSettingsOpened", $"Opened settings for {item.Name}", item.TweakType.ToString()); + } + private static Task InvokeOnUiAsync(Action action) { var dispatcher = System.Windows.Application.Current?.Dispatcher; @@ -227,8 +260,17 @@ private void ApplyLocalizedTweakText() { var text = item.TweakType switch { + SystemTweak.GameMode => ("SystemTweak_GameMode_Name", "Game Mode", "SystemTweak_GameMode_Desc", "Windows Game Mode scheduling optimizations"), 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.MemoryIntegrity => ("SystemTweak_MemoryIntegrity_Name", "Memory Integrity (HVCI)", "SystemTweak_MemoryIntegrity_Desc", "Opens Windows Security. Disabling this protection reduces system security and may require a restart."), + SystemTweak.Hags => ("SystemTweak_Hags_Name", "Hardware-accelerated GPU scheduling", "SystemTweak_Hags_Desc", "Opens Windows graphics settings to let you review HAGS."), + SystemTweak.WindowedOptimizations => ("SystemTweak_WindowedOptimizations_Name", "Optimizations for windowed games", "SystemTweak_WindowedOptimizations_Desc", "Opens Windows graphics settings to manage Flip Model optimizations."), + SystemTweak.UsbSelectiveSuspend => ("SystemTweak_UsbSelectiveSuspend_Name", "USB Selective Suspend", "SystemTweak_UsbSelectiveSuspend_Desc", "Controls USB selective suspend for the active power plan while plugged in."), + SystemTweak.PointerPrecision => ("SystemTweak_PointerPrecision_Name", "Enhance pointer precision", "SystemTweak_PointerPrecision_Desc", "Controls Windows mouse acceleration."), + SystemTweak.EthernetPowerSaving => ("SystemTweak_EthernetPowerSaving_Name", "Disable Ethernet power saving", "SystemTweak_EthernetPowerSaving_Desc", "Disables supported EEE, Green Ethernet and power-saving driver properties on physical Ethernet adapters."), + SystemTweak.InterruptModeration => ("SystemTweak_InterruptModeration_Name", "Disable interrupt moderation", "SystemTweak_InterruptModeration_Desc", "Can reduce Ethernet latency at the cost of higher CPU and interrupt load."), + SystemTweak.GpuMsiMode => ("SystemTweak_GpuMsiMode_Name", "GPU MSI Mode", "SystemTweak_GpuMsiMode_Desc", "Controls MSI only when the display driver already exposes MSISupported. A restart is required."), 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"), @@ -239,6 +281,10 @@ private void ApplyLocalizedTweakText() item.Name = this.Localize(text.Item1, text.Item2); item.Description = this.Localize(text.Item3, text.Item4); + if (!string.IsNullOrEmpty(item.StatusText) && item.TweakType is not SystemTweak.Hags and not SystemTweak.WindowedOptimizations) + { + item.StatusText = this.GetStatusText(new TweakStatus { IsAvailable = item.IsAvailable, IsEnabled = item.IsEnabled }); + } } this.RefreshStatusText = this.Localize("SystemTweaks_StatusReady", "Ready"); @@ -255,6 +301,10 @@ private string Localize(string key, string fallback, params object[] arguments) : string.Format(CultureInfo.CurrentCulture, format, arguments); } + private string GetStatusText(TweakStatus status) => !status.IsAvailable + ? this.Localize("SystemTweaks_StatusNotAvailable", "Not available") + : this.Localize(status.IsEnabled ? "SystemTweaks_StatusEnabled" : "SystemTweaks_StatusDisabled", status.IsEnabled ? "Enabled" : "Disabled"); + private void OnLanguageChanged(object? sender, string language) => this.ApplyLocalizedTweakText(); private void OnTweakStatusChanged(object? sender, TweakStatusChangedEventArgs e) @@ -267,6 +317,7 @@ private void OnTweakStatusChanged(object? sender, TweakStatusChangedEventArgs e) item.IsEnabled = e.Status.IsEnabled; item.IsAvailable = e.Status.IsAvailable; item.ErrorMessage = e.Status.ErrorMessage; + item.StatusText = e.Status.StatusText ?? this.GetStatusText(e.Status); } } catch (Exception ex) @@ -303,6 +354,14 @@ public partial class SystemTweakItem : ObservableObject [ObservableProperty] private string? errorMessage; + [ObservableProperty] + private string statusText = string.Empty; + + [ObservableProperty] + private bool isGuidedAction; + public IAsyncRelayCommand? ToggleCommand { get; set; } + + public IAsyncRelayCommand? ActionCommand { get; set; } } } diff --git a/Views/PowerPlanView.xaml b/Views/PowerPlanView.xaml index 612613e..9aff249 100644 --- a/Views/PowerPlanView.xaml +++ b/Views/PowerPlanView.xaml @@ -15,10 +15,26 @@ - - - - + + + + + + + + + + + diff --git a/Views/ProcessView.xaml b/Views/ProcessView.xaml index 5b479d6..47b336f 100644 --- a/Views/ProcessView.xaml +++ b/Views/ProcessView.xaml @@ -305,6 +305,25 @@ Command="{Binding ApplyAffinityAndSaveAsRuleCommand}" CommandParameter="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/> + + + + + + @@ -460,6 +479,9 @@ + diff --git a/Views/SettingsView.xaml b/Views/SettingsView.xaml index cd89592..02555d2 100644 --- a/Views/SettingsView.xaml +++ b/Views/SettingsView.xaml @@ -6,6 +6,7 @@ xmlns:local="clr-namespace:ThreadPilot.Views" xmlns:models="clr-namespace:ThreadPilot.Models" xmlns:converters="clr-namespace:ThreadPilot.Converters" + xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" mc:Ignorable="d" d:DesignHeight="600" d:DesignWidth="800"> @@ -379,6 +380,41 @@ + + + + + + + + + + + + + + + + diff --git a/Views/SystemTweaksView.xaml b/Views/SystemTweaksView.xaml index f99d512..e796bfe 100644 --- a/Views/SystemTweaksView.xaml +++ b/Views/SystemTweaksView.xaml @@ -127,23 +127,10 @@ - - - - - + VerticalAlignment="Center"/> + ToolTip="{DynamicResource SystemTweaksView_TweakTooltip}"> + + + + + + - + diff --git a/assets/Powerplans/0 Synez_Public_Power.pow b/assets/Powerplans/0 Synez_Public_Power.pow deleted file mode 100644 index 3b6eab3..0000000 Binary files a/assets/Powerplans/0 Synez_Public_Power.pow and /dev/null differ diff --git a/assets/Powerplans/Amit.pow b/assets/Powerplans/Amit.pow deleted file mode 100644 index 9392ab0..0000000 Binary files a/assets/Powerplans/Amit.pow and /dev/null differ diff --git a/assets/Powerplans/Anonymous.pow b/assets/Powerplans/Anonymous.pow deleted file mode 100644 index 7f84bad..0000000 Binary files a/assets/Powerplans/Anonymous.pow and /dev/null differ diff --git a/assets/Powerplans/ApexOS.pow b/assets/Powerplans/ApexOS.pow deleted file mode 100644 index 36e64d1..0000000 Binary files a/assets/Powerplans/ApexOS.pow and /dev/null differ diff --git a/assets/Powerplans/AutoOS.pow b/assets/Powerplans/AutoOS.pow deleted file mode 100644 index 08e1882..0000000 Binary files a/assets/Powerplans/AutoOS.pow and /dev/null differ diff --git a/assets/Powerplans/BEYOND-PERFORMANCE-AMD+INTEL.pow b/assets/Powerplans/BEYOND-PERFORMANCE-AMD+INTEL.pow deleted file mode 100644 index 1a20bbd..0000000 Binary files a/assets/Powerplans/BEYOND-PERFORMANCE-AMD+INTEL.pow and /dev/null differ diff --git a/assets/Powerplans/Beyond.pow b/assets/Powerplans/Beyond.pow deleted file mode 100644 index b06e787..0000000 Binary files a/assets/Powerplans/Beyond.pow and /dev/null differ diff --git a/assets/Powerplans/Bitsum Highest Performance.pow b/assets/Powerplans/Bitsum Highest Performance.pow deleted file mode 100644 index 93e1577..0000000 Binary files a/assets/Powerplans/Bitsum Highest Performance.pow and /dev/null differ diff --git a/assets/Powerplans/BlitzOS.pow b/assets/Powerplans/BlitzOS.pow deleted file mode 100644 index 86218bd..0000000 Binary files a/assets/Powerplans/BlitzOS.pow and /dev/null differ diff --git a/assets/Powerplans/CalyptoPowerPlan.pow b/assets/Powerplans/CalyptoPowerPlan.pow deleted file mode 100644 index d5a697c..0000000 Binary files a/assets/Powerplans/CalyptoPowerPlan.pow and /dev/null differ diff --git a/assets/Powerplans/Delta.pow b/assets/Powerplans/Delta.pow deleted file mode 100644 index d5bba49..0000000 Binary files a/assets/Powerplans/Delta.pow and /dev/null differ diff --git a/assets/Powerplans/EXO.pow b/assets/Powerplans/EXO.pow deleted file mode 100644 index 46a79c1..0000000 Binary files a/assets/Powerplans/EXO.pow and /dev/null differ diff --git a/assets/Powerplans/EnvyOS.pow b/assets/Powerplans/EnvyOS.pow deleted file mode 100644 index 6e51141..0000000 Binary files a/assets/Powerplans/EnvyOS.pow and /dev/null differ diff --git a/assets/Powerplans/FPSHEAVEN2026.pow b/assets/Powerplans/FPSHEAVEN2026.pow deleted file mode 100644 index 87b2642..0000000 Binary files a/assets/Powerplans/FPSHEAVEN2026.pow and /dev/null differ diff --git a/assets/Powerplans/FrameSyncBoost.pow b/assets/Powerplans/FrameSyncBoost.pow deleted file mode 100644 index 597efb0..0000000 Binary files a/assets/Powerplans/FrameSyncBoost.pow and /dev/null differ diff --git a/assets/Powerplans/GALA's ultimate performance(AMD).pow b/assets/Powerplans/GALA's ultimate performance(AMD).pow deleted file mode 100644 index 80e4748..0000000 Binary files a/assets/Powerplans/GALA's ultimate performance(AMD).pow and /dev/null differ diff --git a/assets/Powerplans/GTweaks Power Plan V3.pow b/assets/Powerplans/GTweaks Power Plan V3.pow deleted file mode 100644 index 6865cc5..0000000 Binary files a/assets/Powerplans/GTweaks Power Plan V3.pow and /dev/null differ diff --git a/assets/Powerplans/Gavot Performance.pow b/assets/Powerplans/Gavot Performance.pow deleted file mode 100644 index 27412a3..0000000 Binary files a/assets/Powerplans/Gavot Performance.pow and /dev/null differ diff --git a/assets/Powerplans/HYDRA_PBO.pow b/assets/Powerplans/HYDRA_PBO.pow deleted file mode 100644 index 645454b..0000000 Binary files a/assets/Powerplans/HYDRA_PBO.pow and /dev/null differ diff --git a/assets/Powerplans/IIIEXOIII_LOW_LATENCY.pow b/assets/Powerplans/IIIEXOIII_LOW_LATENCY.pow deleted file mode 100644 index f79f6bb..0000000 Binary files a/assets/Powerplans/IIIEXOIII_LOW_LATENCY.pow and /dev/null differ diff --git a/assets/Powerplans/IrisFixed.pow b/assets/Powerplans/IrisFixed.pow deleted file mode 100644 index 94a195d..0000000 Binary files a/assets/Powerplans/IrisFixed.pow and /dev/null differ diff --git a/assets/Powerplans/IrisOS.pow b/assets/Powerplans/IrisOS.pow deleted file mode 100644 index 31c8195..0000000 Binary files a/assets/Powerplans/IrisOS.pow and /dev/null differ diff --git a/assets/Powerplans/J o k r O S P o w e r P l a n.pow b/assets/Powerplans/J o k r O S P o w e r P l a n.pow deleted file mode 100644 index 7dad1a3..0000000 Binary files a/assets/Powerplans/J o k r O S P o w e r P l a n.pow and /dev/null differ diff --git a/assets/Powerplans/Jackpot2026.pow b/assets/Powerplans/Jackpot2026.pow deleted file mode 100644 index 3b62603..0000000 Binary files a/assets/Powerplans/Jackpot2026.pow and /dev/null differ diff --git a/assets/Powerplans/JackpotOS.pow b/assets/Powerplans/JackpotOS.pow deleted file mode 100644 index e446954..0000000 Binary files a/assets/Powerplans/JackpotOS.pow and /dev/null differ diff --git a/assets/Powerplans/KSOS11.pow b/assets/Powerplans/KSOS11.pow deleted file mode 100644 index 5a81393..0000000 Binary files a/assets/Powerplans/KSOS11.pow and /dev/null differ diff --git a/assets/Powerplans/Kaisen V4.pow b/assets/Powerplans/Kaisen V4.pow deleted file mode 100644 index 3185271..0000000 Binary files a/assets/Powerplans/Kaisen V4.pow and /dev/null differ diff --git a/assets/Powerplans/Kaisen V5 fps.pow b/assets/Powerplans/Kaisen V5 fps.pow deleted file mode 100644 index 9c1df19..0000000 Binary files a/assets/Powerplans/Kaisen V5 fps.pow and /dev/null differ diff --git a/assets/Powerplans/Kaisen V5 lowlatency.pow b/assets/Powerplans/Kaisen V5 lowlatency.pow deleted file mode 100644 index 83401ed..0000000 Binary files a/assets/Powerplans/Kaisen V5 lowlatency.pow and /dev/null differ diff --git a/assets/Powerplans/Kizzimo's Extreme Low Latency.pow b/assets/Powerplans/Kizzimo's Extreme Low Latency.pow deleted file mode 100644 index a41dad9..0000000 Binary files a/assets/Powerplans/Kizzimo's Extreme Low Latency.pow and /dev/null differ diff --git a/assets/Powerplans/L1 Final Version.pow b/assets/Powerplans/L1 Final Version.pow deleted file mode 100644 index e298aa2..0000000 Binary files a/assets/Powerplans/L1 Final Version.pow and /dev/null differ diff --git "a/assets/Powerplans/LLG-C\316\236RT1F1\316\236D-FOR-NON-K-CPUs.pow" "b/assets/Powerplans/LLG-C\316\236RT1F1\316\236D-FOR-NON-K-CPUs.pow" deleted file mode 100644 index 015d8c5..0000000 Binary files "a/assets/Powerplans/LLG-C\316\236RT1F1\316\236D-FOR-NON-K-CPUs.pow" and /dev/null differ diff --git "a/assets/Powerplans/LLG-C\316\236RT1F1\316\236D-FOR-PARKING+E-CORES-FIX.pow" "b/assets/Powerplans/LLG-C\316\236RT1F1\316\236D-FOR-PARKING+E-CORES-FIX.pow" deleted file mode 100644 index 577f301..0000000 Binary files "a/assets/Powerplans/LLG-C\316\236RT1F1\316\236D-FOR-PARKING+E-CORES-FIX.pow" and /dev/null differ diff --git "a/assets/Powerplans/LLG-C\316\236RT1F1\316\236D.pow" "b/assets/Powerplans/LLG-C\316\236RT1F1\316\236D.pow" deleted file mode 100644 index 41cf24a..0000000 Binary files "a/assets/Powerplans/LLG-C\316\236RT1F1\316\236D.pow" and /dev/null differ diff --git a/assets/Powerplans/LLG.pow b/assets/Powerplans/LLG.pow deleted file mode 100644 index 015d8c5..0000000 Binary files a/assets/Powerplans/LLG.pow and /dev/null differ diff --git a/assets/Powerplans/MaxOS Low Latency Powerplan v1.pow b/assets/Powerplans/MaxOS Low Latency Powerplan v1.pow deleted file mode 100644 index 1623907..0000000 Binary files a/assets/Powerplans/MaxOS Low Latency Powerplan v1.pow and /dev/null differ diff --git a/assets/Powerplans/MaxOS Low Latency Powerplan v2.pow b/assets/Powerplans/MaxOS Low Latency Powerplan v2.pow deleted file mode 100644 index 6ed68dc..0000000 Binary files a/assets/Powerplans/MaxOS Low Latency Powerplan v2.pow and /dev/null differ diff --git a/assets/Powerplans/MaxOS Low Latency Powerplan v3.pow b/assets/Powerplans/MaxOS Low Latency Powerplan v3.pow deleted file mode 100644 index 121a9b4..0000000 Binary files a/assets/Powerplans/MaxOS Low Latency Powerplan v3.pow and /dev/null differ diff --git a/assets/Powerplans/MaxPower.pow b/assets/Powerplans/MaxPower.pow deleted file mode 100644 index 197587c..0000000 Binary files a/assets/Powerplans/MaxPower.pow and /dev/null differ diff --git a/assets/Powerplans/Microsoft High performance.pow b/assets/Powerplans/Microsoft High performance.pow deleted file mode 100644 index 6225a5c..0000000 Binary files a/assets/Powerplans/Microsoft High performance.pow and /dev/null differ diff --git a/assets/Powerplans/Microsoft Ultimate Performance.pow b/assets/Powerplans/Microsoft Ultimate Performance.pow deleted file mode 100644 index ed6ebc0..0000000 Binary files a/assets/Powerplans/Microsoft Ultimate Performance.pow and /dev/null differ diff --git a/assets/Powerplans/Mitstas IDLE ENABLED.pow b/assets/Powerplans/Mitstas IDLE ENABLED.pow deleted file mode 100644 index 3556f58..0000000 Binary files a/assets/Powerplans/Mitstas IDLE ENABLED.pow and /dev/null differ diff --git a/assets/Powerplans/NovaOS.pow b/assets/Powerplans/NovaOS.pow deleted file mode 100644 index a03a935..0000000 Binary files a/assets/Powerplans/NovaOS.pow and /dev/null differ diff --git a/assets/Powerplans/Plan.pow b/assets/Powerplans/Plan.pow deleted file mode 100644 index dd2a9cd..0000000 Binary files a/assets/Powerplans/Plan.pow and /dev/null differ diff --git a/assets/Powerplans/Prodazin Power Plan.pow b/assets/Powerplans/Prodazin Power Plan.pow deleted file mode 100644 index b92ec9e..0000000 Binary files a/assets/Powerplans/Prodazin Power Plan.pow and /dev/null differ diff --git a/assets/Powerplans/RIP Tweaks Power Plan.pow b/assets/Powerplans/RIP Tweaks Power Plan.pow deleted file mode 100644 index c7ea89d..0000000 Binary files a/assets/Powerplans/RIP Tweaks Power Plan.pow and /dev/null differ diff --git a/assets/Powerplans/RaxOS.pow b/assets/Powerplans/RaxOS.pow deleted file mode 100644 index 066f57e..0000000 Binary files a/assets/Powerplans/RaxOS.pow and /dev/null differ diff --git a/assets/Powerplans/Reticle v2.pow b/assets/Powerplans/Reticle v2.pow deleted file mode 100644 index 21f71ed..0000000 Binary files a/assets/Powerplans/Reticle v2.pow and /dev/null differ diff --git a/assets/Powerplans/RevisionPowerPlanV2.8.pow b/assets/Powerplans/RevisionPowerPlanV2.8.pow deleted file mode 100644 index f0b6a90..0000000 Binary files a/assets/Powerplans/RevisionPowerPlanV2.8.pow and /dev/null differ diff --git a/assets/Powerplans/Rosca Tweaks v2.pow b/assets/Powerplans/Rosca Tweaks v2.pow deleted file mode 100644 index 61c1aa0..0000000 Binary files a/assets/Powerplans/Rosca Tweaks v2.pow and /dev/null differ diff --git a/assets/Powerplans/Sazinho-Idle-OFF.pow b/assets/Powerplans/Sazinho-Idle-OFF.pow deleted file mode 100644 index d430e1e..0000000 Binary files a/assets/Powerplans/Sazinho-Idle-OFF.pow and /dev/null differ diff --git a/assets/Powerplans/Slower.pow b/assets/Powerplans/Slower.pow deleted file mode 100644 index 132b910..0000000 Binary files a/assets/Powerplans/Slower.pow and /dev/null differ diff --git a/assets/Powerplans/TrinityOS.pow b/assets/Powerplans/TrinityOS.pow deleted file mode 100644 index 5d3d2f3..0000000 Binary files a/assets/Powerplans/TrinityOS.pow and /dev/null differ diff --git a/assets/Powerplans/VTRL Optimized.pow b/assets/Powerplans/VTRL Optimized.pow deleted file mode 100644 index c72f68b..0000000 Binary files a/assets/Powerplans/VTRL Optimized.pow and /dev/null differ diff --git a/assets/Powerplans/VainOS.pow b/assets/Powerplans/VainOS.pow deleted file mode 100644 index 1e33fd0..0000000 Binary files a/assets/Powerplans/VainOS.pow and /dev/null differ diff --git a/assets/Powerplans/VantageOS Power Plan.pow b/assets/Powerplans/VantageOS Power Plan.pow deleted file mode 100644 index 4ce7231..0000000 Binary files a/assets/Powerplans/VantageOS Power Plan.pow and /dev/null differ diff --git a/assets/Powerplans/Velo's Power Plan.pow b/assets/Powerplans/Velo's Power Plan.pow deleted file mode 100644 index 39dcba4..0000000 Binary files a/assets/Powerplans/Velo's Power Plan.pow and /dev/null differ diff --git a/assets/Powerplans/Winhance.pow b/assets/Powerplans/Winhance.pow deleted file mode 100644 index 045753c..0000000 Binary files a/assets/Powerplans/Winhance.pow and /dev/null differ diff --git a/assets/Powerplans/XNRL Pro Plan.pow b/assets/Powerplans/XNRL Pro Plan.pow deleted file mode 100644 index 685453f..0000000 Binary files a/assets/Powerplans/XNRL Pro Plan.pow and /dev/null differ diff --git a/assets/Powerplans/Zoyata.pow b/assets/Powerplans/Zoyata.pow deleted file mode 100644 index e97511c..0000000 Binary files a/assets/Powerplans/Zoyata.pow and /dev/null differ diff --git a/assets/Powerplans/adamx.pow b/assets/Powerplans/adamx.pow deleted file mode 100644 index 97adb6e..0000000 Binary files a/assets/Powerplans/adamx.pow and /dev/null differ diff --git a/assets/Powerplans/ancel.pow b/assets/Powerplans/ancel.pow deleted file mode 100644 index d5c397a..0000000 Binary files a/assets/Powerplans/ancel.pow and /dev/null differ diff --git a/assets/Powerplans/arsenza low latency (INTEL FIX THREAD-DIRECTOR).pow b/assets/Powerplans/arsenza low latency (INTEL FIX THREAD-DIRECTOR).pow deleted file mode 100644 index 1f9b9b2..0000000 Binary files a/assets/Powerplans/arsenza low latency (INTEL FIX THREAD-DIRECTOR).pow and /dev/null differ diff --git a/assets/Powerplans/arsenza low latency.pow b/assets/Powerplans/arsenza low latency.pow deleted file mode 100644 index c11d5b0..0000000 Binary files a/assets/Powerplans/arsenza low latency.pow and /dev/null differ diff --git a/assets/Powerplans/atlas.pow b/assets/Powerplans/atlas.pow deleted file mode 100644 index 58b61f1..0000000 Binary files a/assets/Powerplans/atlas.pow and /dev/null differ diff --git a/assets/Powerplans/bitsum.pow b/assets/Powerplans/bitsum.pow deleted file mode 100644 index c66f040..0000000 Binary files a/assets/Powerplans/bitsum.pow and /dev/null differ diff --git a/assets/Powerplans/cactusOS.pow b/assets/Powerplans/cactusOS.pow deleted file mode 100644 index 956373b..0000000 Binary files a/assets/Powerplans/cactusOS.pow and /dev/null differ diff --git a/assets/Powerplans/calypto.pow b/assets/Powerplans/calypto.pow deleted file mode 100644 index cddc1aa..0000000 Binary files a/assets/Powerplans/calypto.pow and /dev/null differ diff --git a/assets/Powerplans/core.pow b/assets/Powerplans/core.pow deleted file mode 100644 index ba2cc6c..0000000 Binary files a/assets/Powerplans/core.pow and /dev/null differ diff --git a/assets/Powerplans/exmfree.pow b/assets/Powerplans/exmfree.pow deleted file mode 100644 index d9c4a5d..0000000 Binary files a/assets/Powerplans/exmfree.pow and /dev/null differ diff --git a/assets/Powerplans/gora.pow b/assets/Powerplans/gora.pow deleted file mode 100644 index adee821..0000000 Binary files a/assets/Powerplans/gora.pow and /dev/null differ diff --git a/assets/Powerplans/hybred.pow b/assets/Powerplans/hybred.pow deleted file mode 100644 index 9168250..0000000 Binary files a/assets/Powerplans/hybred.pow and /dev/null differ diff --git a/assets/Powerplans/icon.pow b/assets/Powerplans/icon.pow deleted file mode 100644 index c3670bc..0000000 Binary files a/assets/Powerplans/icon.pow and /dev/null differ diff --git a/assets/Powerplans/imribiy2026.pow b/assets/Powerplans/imribiy2026.pow deleted file mode 100644 index c3ed95c..0000000 Binary files a/assets/Powerplans/imribiy2026.pow and /dev/null differ diff --git a/assets/Powerplans/jiNN GoKu.pow b/assets/Powerplans/jiNN GoKu.pow deleted file mode 100644 index c851202..0000000 Binary files a/assets/Powerplans/jiNN GoKu.pow and /dev/null differ diff --git a/assets/Powerplans/kaisen.pow b/assets/Powerplans/kaisen.pow deleted file mode 100644 index 83401ed..0000000 Binary files a/assets/Powerplans/kaisen.pow and /dev/null differ diff --git a/assets/Powerplans/khorvie.pow b/assets/Powerplans/khorvie.pow deleted file mode 100644 index d53490d..0000000 Binary files a/assets/Powerplans/khorvie.pow and /dev/null differ diff --git a/assets/Powerplans/kirby.pow b/assets/Powerplans/kirby.pow deleted file mode 100644 index fda534f..0000000 Binary files a/assets/Powerplans/kirby.pow and /dev/null differ diff --git a/assets/Powerplans/kizzimo.pow b/assets/Powerplans/kizzimo.pow deleted file mode 100644 index b99d3ab..0000000 Binary files a/assets/Powerplans/kizzimo.pow and /dev/null differ diff --git a/assets/Powerplans/lawliet.pow b/assets/Powerplans/lawliet.pow deleted file mode 100644 index 2eb13fc..0000000 Binary files a/assets/Powerplans/lawliet.pow and /dev/null differ diff --git a/assets/Powerplans/melody LowestLatency.pow b/assets/Powerplans/melody LowestLatency.pow deleted file mode 100644 index 1614e0a..0000000 Binary files a/assets/Powerplans/melody LowestLatency.pow and /dev/null differ diff --git a/assets/Powerplans/n1kobg GPU_Booster_Power_Plan.pow b/assets/Powerplans/n1kobg GPU_Booster_Power_Plan.pow deleted file mode 100644 index d3814ac..0000000 Binary files a/assets/Powerplans/n1kobg GPU_Booster_Power_Plan.pow and /dev/null differ diff --git a/assets/Powerplans/nexus.pow b/assets/Powerplans/nexus.pow deleted file mode 100644 index 3dc19fd..0000000 Binary files a/assets/Powerplans/nexus.pow and /dev/null differ diff --git a/assets/Powerplans/powerx.pow b/assets/Powerplans/powerx.pow deleted file mode 100644 index a3735ac..0000000 Binary files a/assets/Powerplans/powerx.pow and /dev/null differ diff --git a/assets/Powerplans/sapphire.pow b/assets/Powerplans/sapphire.pow deleted file mode 100644 index c2a0909..0000000 Binary files a/assets/Powerplans/sapphire.pow and /dev/null differ diff --git a/assets/Powerplans/void-v1.pow b/assets/Powerplans/void-v1.pow deleted file mode 100644 index 3b0c6c3..0000000 Binary files a/assets/Powerplans/void-v1.pow and /dev/null differ diff --git a/assets/Powerplans/vtrl.pow b/assets/Powerplans/vtrl.pow deleted file mode 100644 index 91bd6a6..0000000 Binary files a/assets/Powerplans/vtrl.pow and /dev/null differ diff --git a/assets/Powerplans/xilly.pow b/assets/Powerplans/xilly.pow deleted file mode 100644 index 1c04dc7..0000000 Binary files a/assets/Powerplans/xilly.pow and /dev/null differ diff --git a/assets/Powerplans/xos.pow b/assets/Powerplans/xos.pow deleted file mode 100644 index 3420af3..0000000 Binary files a/assets/Powerplans/xos.pow and /dev/null differ diff --git a/build/build-installer.ps1 b/build/build-installer.ps1 index 21b400d..41967cb 100644 --- a/build/build-installer.ps1 +++ b/build/build-installer.ps1 @@ -1,5 +1,5 @@ param( - [string]$Version = "1.6.0", + [string]$Version = "1.7.0", [string]$Configuration = "Release", [switch]$SkipPublish ) diff --git a/build/build-release.ps1 b/build/build-release.ps1 index aa50b73..1b63660 100644 --- a/build/build-release.ps1 +++ b/build/build-release.ps1 @@ -1,5 +1,5 @@ param( - [string]$Version = "1.6.0", + [string]$Version = "1.7.0", [string]$Configuration = "Release", [string]$Runtime = "win-x64" ) @@ -62,6 +62,7 @@ echo [1/4] Closing running ThreadPilot processes... taskkill /IM "ThreadPilot.exe" /F >nul 2>&1 echo [2/4] Removing startup task and startup registry entry... +schtasks /Delete /TN "ThreadPilot_Launch" /F >nul 2>&1 schtasks /Delete /TN "ThreadPilot_Startup" /F >nul 2>&1 reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "ThreadPilot" /f >nul 2>&1 diff --git a/build/package-release-zips.ps1 b/build/package-release-zips.ps1 index 1b69016..2aa575a 100644 --- a/build/package-release-zips.ps1 +++ b/build/package-release-zips.ps1 @@ -1,5 +1,5 @@ param( - [string]$Version = "1.6.0" + [string]$Version = "1.7.0" ) $ErrorActionPreference = "Stop" @@ -35,6 +35,7 @@ echo [1/4] Closing running ThreadPilot processes... taskkill /IM "ThreadPilot.exe" /F >nul 2>&1 echo [2/4] Removing startup task and startup registry entry... +schtasks /Delete /TN "ThreadPilot_Launch" /F >nul 2>&1 schtasks /Delete /TN "ThreadPilot_Startup" /F >nul 2>&1 reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "ThreadPilot" /f >nul 2>&1 diff --git a/chocolatey/threadpilot.nuspec b/chocolatey/threadpilot.nuspec index 45336c6..f1388b7 100644 --- a/chocolatey/threadpilot.nuspec +++ b/chocolatey/threadpilot.nuspec @@ -2,7 +2,7 @@ threadpilot - 1.6.0 + 1.7.0 ThreadPilot Prime Build https://github.com/PrimeBuild-pc/ThreadPilot @@ -15,7 +15,7 @@ false Advanced Windows process and power plan manager with rules automation and performance controls. ThreadPilot process and power plan manager. - https://github.com/PrimeBuild-pc/ThreadPilot/releases/tag/v1.6.0 + https://github.com/PrimeBuild-pc/ThreadPilot/releases/tag/v1.7.0 threadpilot process powerplan performance windows diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4de73e1..cff1927 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to this project are documented in this file. +## v1.7.0 - Gaming and device tuning + +### Added + +- Added Game Mode, USB selective suspend, pointer precision, Ethernet power saving, interrupt moderation, and supported GPU MSI Mode controls. +- Added guided links for Memory Integrity, hardware-accelerated GPU scheduling, and windowed-game optimizations so Windows retains control over security-sensitive settings. +- Added per-process I/O priority and persistent-rule support for memory priority, I/O priority, and preventing system sleep while a process is running. +- Added device-driver registry backups before supported Ethernet or GPU properties are changed. + +### Changed + +- Replaced the previous process idle-server action with a real Windows power request that is released automatically when the process exits or ThreadPilot closes. +- Game Mode is now an explicit system tweak and is no longer changed implicitly when affinity is applied. +- Removed unverified third-party `.pow` files from the repository and release packages while preserving local imports and previously installed Windows plans. +- Added user-initiated Discord links for community power plans and support, plus a direct GitHub Issues link. + +### Safety and compatibility + +- ThreadPilot changes only driver properties already exposed by the installed Ethernet or display driver; it does not create unsupported properties. +- Device-level tweaks require administrator privileges. Driver and firmware behavior varies, and some changes require a restart. +- Disabling Memory Integrity is intentionally left to the user in Windows Security because it reduces system protection. + ## v1.6.0 - CPU assignment strategies ### Added diff --git a/docs/CODE_SIGNING_POLICY.md b/docs/CODE_SIGNING_POLICY.md new file mode 100644 index 0000000..3770320 --- /dev/null +++ b/docs/CODE_SIGNING_POLICY.md @@ -0,0 +1,47 @@ +# Code-Signing Policy + +Free code signing provided by [SignPath.io](https://signpath.io), certificate by [SignPath Foundation](https://signpath.org). + +ThreadPilot releases remain unsigned until the SignPath Foundation application is approved and the integration is enabled. + +## Scope + +Only official ThreadPilot release executables and installers produced from the public GitHub repository may be signed. Pull-request builds, local builds, forks, scripts, and third-party binaries are not signed by the project. + +## Authorized releases + +- A signed build must originate from a protected release tag in the official repository. +- The commit must pass the repository's required build, test, analyzer, and packaging checks. +- Signing requests must be created by the GitHub release workflow through the approved SignPath integration. +- Maintainer or SignPath approval is required wherever configured by the signing policy. +- Checksums are generated only after signing and are published with the release. + +## Team roles and approval + +The current repository team consists of one maintainer: + +- **Authors:** `@PrimeBuild-pc` may contribute directly to the official repository. +- **Reviewers:** `@PrimeBuild-pc` reviews contributions from people who are not Authors. +- **Approvers:** `@PrimeBuild-pc` manually approves or rejects every SignPath signing request. + +No signing request is auto-approved. If another maintainer is added, this section and `.github/CODEOWNERS` must be updated before granting repository or SignPath access. A person must not approve an unreviewed artifact whose source they cannot trace to the approved release commit. + +## Account security + +Every Author, Reviewer, and Approver must enable multi-factor authentication on both GitHub and SignPath before access is granted and must keep it enabled. MFA status is verified manually when access is created and during each release-access review; it is not inferred from repository API output. Accounts that no longer satisfy this requirement are removed from the project until MFA is restored. + +## Key protection + +ThreadPilot maintainers do not download, export, or commit the signing private key. SignPath controls the signing key. Integration credentials are restricted to a protected GitHub environment and are never exposed to pull requests from forks. + +## Verification and transparency + +Release notes identify whether artifacts are signed. Users can inspect the Authenticode signature with `Get-AuthenticodeSignature` and compare the published SHA-256 checksums. The source revision used for a release is represented by its public Git tag. + +## Revocation and incidents + +If credentials are exposed, an unauthorized signing request is suspected, or a signed artifact differs from the published source build, maintainers will disable release signing, revoke or rotate affected credentials, withdraw the artifact, publish an incident notice, and coordinate certificate revocation with SignPath when necessary. + +## Policy changes + +Changes to signing scope, identity, or approval requirements are reviewed as repository changes before taking effect. diff --git a/docs/POWER_PLAN_LICENSING.md b/docs/POWER_PLAN_LICENSING.md new file mode 100644 index 0000000..c076151 --- /dev/null +++ b/docs/POWER_PLAN_LICENSING.md @@ -0,0 +1,36 @@ +# Bundled Power Plan Licensing Audit + +## Result + +**Resolved for 1.7.0:** the 89 unverified `.pow` files were removed from the repository and release artifacts. + +The files were obtained from public YouTube descriptions or forum posts and were available at no charge. Public availability and zero price do not grant redistribution rights and do not make a component open source. File names that refer to Microsoft or third-party projects are not evidence of authorship, origin, or license. + +Under the SignPath Foundation terms, every component of the project must be distributed under an OSI-approved license and the project must not contain proprietary components. ThreadPilot therefore no longer bundles third-party power-plan files. + +## Evidence reviewed + +- All 89 `.pow` files under `assets/Powerplans`. +- Repository license, notices, documentation, and file history. +- The pull request and commits that introduced or expanded the bundle. +- Build, publish, installer, and release-workflow paths that copy every `.pow` file into release artifacts. + +No per-file source URL, author permission, license text, SPDX identifier, or provenance record was found. Every removed file remains classified as **unverified** unless contrary evidence is recorded. + +## Implemented remediation + +- ThreadPilot retains its local `.pow` import feature. +- Windows plans that users previously imported or activated are not deleted or changed. +- Upgrade installs do not delete old local `.pow` files from users' machines. +- The Power Plans page offers an explicit user-initiated link to the ThreadPilot Discord community for additional plans; no third-party plan is downloaded automatically or included in signed artifacts. + +An informal statement such as “free to download/use,” a public post, a video description without license terms, or attribution alone is insufficient. + +## Admission checklist for future power plans + +- Original source URL and author/rights holder identified. +- Exact OSI-approved license identified and preserved. +- Redistribution and modification are permitted. +- SHA-256 hash recorded for the reviewed file. +- Any attribution or notice requirements are included in the release. +- The file is reviewed before being added to any release asset directory. diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md new file mode 100644 index 0000000..6531831 --- /dev/null +++ b/docs/PRIVACY.md @@ -0,0 +1,13 @@ +# Privacy + +ThreadPilot runs locally and does not include advertising, analytics, telemetry, user accounts, or automatic log uploads. + +The application reads local process, CPU, power, device, and Windows configuration information to provide its features. Settings, rules, tweak backups, activity history, and diagnostic logs are stored locally under the current Windows user profile. Users can inspect or remove those files locally. + +When update checks are enabled, ThreadPilot contacts the GitHub API for the official repository's release metadata. If the user chooses to download an update, the release artifact is downloaded from GitHub. GitHub receives ordinary connection information such as the user's IP address and user agent under GitHub's own privacy terms. + +ThreadPilot does not transmit the local process list, executable paths, hardware inventory, settings, rules, or logs to the project maintainers. Information is shared only when a user chooses to include it in a GitHub issue or another support request. + +The Power Plans and Settings pages contain links to Discord and GitHub Issues. ThreadPilot opens these links only after an explicit user click; the destination then receives ordinary connection information under its own privacy terms. ThreadPilot does not automatically download community power plans. + +This policy describes ThreadPilot 1.7.0. Material changes to data collection or network behavior will be documented before release. diff --git a/docs/README.md b/docs/README.md index 6d1c8e8..094ae98 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,7 +18,11 @@ This folder contains release and engineering documentation for ThreadPilot. - Packaging guide: release/PACKAGING.md - Release runbook: release/RELEASE_RUNBOOK.md - Release notes template: release/RELEASE_NOTES_TEMPLATE.md -- Go/No-Go matrix: release/GO_NO_GO_MATRIX.md +- Go/No-Go matrix: release/GO_NO_GO_MATRIX.md +- Release signing guide: RELEASE_SIGNING.md +- Code-signing policy: CODE_SIGNING_POLICY.md +- Bundled power-plan licensing audit: POWER_PLAN_LICENSING.md +- Privacy policy: PRIVACY.md ## Release and Governance - Gitleaks scan report: audits/GITLEAKS_REPORT_2026-04-15.json diff --git a/docs/RELEASE_SIGNING.md b/docs/RELEASE_SIGNING.md index 8d629b8..6d79ed5 100644 --- a/docs/RELEASE_SIGNING.md +++ b/docs/RELEASE_SIGNING.md @@ -1,35 +1,33 @@ # Release Signing Guide -This document describes how to prepare ThreadPilot for signed production releases. +ThreadPilot releases are currently unsigned. An application for SignPath Foundation code signing is pending; the release workflow remains fully automated and unchanged until the project is approved. -## Objectives -- Authenticate official binaries. -- Reduce SmartScreen friction. -- Provide traceability for shipped artifacts. +## Intended signing model -## Recommended Strategy -1. Use an Authenticode code-signing certificate from a trusted CA. -2. Store signing secrets in GitHub Environments with required approvals. -3. Sign binaries only in protected release workflows triggered by tags. +- Sign the published executable and installer with Authenticode through SignPath. +- Use the publisher identity assigned by SignPath Foundation. +- Submit signing requests only from the protected GitHub release workflow. +- Keep SignPath project identifiers and organization secrets in GitHub environment secrets. +- Require an explicit SignPath approval when the selected policy requires it. -## Local Signing Example -```powershell -signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /a "ThreadPilot.exe" -``` +Signing will be inserted between artifact creation and checksum generation. Builds, tests, packaging, GitHub Release creation, and artifact upload remain automated; no private certificate or PFX will be stored in the repository or on a GitHub runner. + +## Status and rollout -## CI Signing Steps -1. Decode PFX from encrypted secret at workflow runtime. -2. Import certificate into current user certificate store. -3. Sign release binaries and installer artifacts. -4. Publish SHA256 checksums and signature verification instructions. +1. Complete the SignPath Foundation application. +2. Wait for approval and the SignPath organization/project invitation. +3. Configure the SignPath project and trusted build policy. +4. Add the identifiers and token supplied by SignPath as protected GitHub secrets. +5. Integrate signing on a separate change and test it with a prerelease tag. +6. Verify the Authenticode publisher, timestamp, checksums, and installer before publishing a stable release. + +Until those steps are complete, release artifacts must be described as unsigned. Do not enable the legacy PFX signing path without a separately acquired certificate and a reviewed secret-storage procedure. ## Verification + ```powershell Get-AuthenticodeSignature .\ThreadPilot.exe Get-FileHash .\ThreadPilot.exe -Algorithm SHA256 ``` -## Operational Notes -- Never commit certificates or private keys. -- Rotate secrets periodically. -- Require maintainer approval for release jobs. +See the public [code-signing policy](CODE_SIGNING_POLICY.md) for scope, approvals, and incident handling. diff --git a/docs/release/RELEASE_NOTES.md b/docs/release/RELEASE_NOTES.md index 777c205..60c7f62 100644 --- a/docs/release/RELEASE_NOTES.md +++ b/docs/release/RELEASE_NOTES.md @@ -1,26 +1,42 @@ -## ThreadPilot v1.6.0 +## ThreadPilot v1.7.0 -ThreadPilot 1.6.0 adds explicit CPU assignment strategies while keeping the established ThreadPilot automatic behavior as the persistent default. +ThreadPilot 1.7.0 expands Windows gaming and device controls, adds persistent per-process I/O and power behavior, and prepares the release pipeline for trusted Authenticode signing. ### Highlights -- Choose between ThreadPilot automatic, Affinity Mask, Ideal Processor, and CPU Sets. -- Save a global default or override the strategy for individual process rules. -- Apply topology-aware affinity across processor groups and processor indexes above 63. -- Distribute Ideal Processor preferences deterministically across existing process threads. -- Inspect reserved CPU Sets without changing global Windows configuration. -- Use the new controls and explanations in all seven supported languages. +- Control Game Mode, CPU Core Parking, supported CPU C-State behavior, USB selective suspend, pointer precision, supported Ethernet power-saving properties, interrupt moderation, and GPU MSI Mode. +- Open the correct Windows pages for Memory Integrity, HAGS, and windowed-game optimizations without silently weakening Windows security. +- Set memory and I/O priority per process and preserve them in persistent rules. +- Prevent system sleep while a selected process is running through a Windows power request that is released automatically. +- Restore backed-up device-driver values when supported tweaks are turned off. +- Open community power-plan and support links on demand, or report a problem directly through GitHub Issues. +- Use every new control and support action in all seven supported languages: English, Italian, German, Spanish, French, Russian, and Simplified Chinese. + +### Power-plan distribution change + +ThreadPilot no longer bundles the 89 third-party `.pow` files previously shipped with the application. Those files were publicly available, but no reliable per-file license or redistribution permission could be verified. Removing them keeps the signed release payload compatible with the SignPath Foundation requirement that distributed components have clear open-source redistribution rights. + +This does not remove functionality or user data: + +- Power plans already imported into Windows remain installed and active. +- An upgrade does not delete `.pow` files left by an earlier ThreadPilot installation. +- Manual `.pow` import remains available. +- The new **More plans?** action opens the ThreadPilot community Discord only when the user clicks it; no plan is downloaded automatically or included in the signed package. ### Compatibility and safety -- Existing settings and rules remain compatible and continue in ThreadPilot automatic mode. -- Explicit CPU Sets and Ideal Processor modes do not silently fall back to affinity. -- ThreadPilot never widens an existing hard affinity restriction; switching to a soft mode can require restarting the target process. -- With automation monitoring disabled, Ideal Processor and multi-group Affinity apply only to threads that exist at apply time. -- Reserved CPU Sets remain read-only in this release. +- Existing rules remain compatible; all new persistent options default to off. +- Ethernet and GPU controls only modify properties already exposed by the installed driver and require administrator privileges. +- Hardware and driver behavior varies. GPU MSI Mode and some Windows graphics or security changes may require a restart. +- Memory Integrity remains a user-controlled Windows Security setting. ### Validation -- 689 automated tests passed in Debug and Release configurations. -- All four strategies passed hardware smoke tests on a Windows 11 Hyper-V guest with four logical processors. -- Multi-group and processor indexes above 63 passed synthetic topology tests. +- 701 automated Release tests pass, including XAML compilation and localization-key coverage for all seven languages. +- The final portable and installer artifacts contain zero `.pow` files. +- A Windows 11 Hyper-V gate passed portable launch, clean install, normal launch, system-tweak apply/restore, upgrade, and uninstall checks without ThreadPilot application errors. +- A real 1.6.0 to 1.7.0 installer upgrade preserved all 89 legacy `.pow` files byte-for-byte and did not change the active Windows power plan. + +### Signing status + +Release artifacts remain unsigned while the SignPath Foundation application is pending. The automated build, test, packaging, checksum, SBOM, and publishing flow remains in place, including its existing optional Authenticode path. The SignPath-specific integration will be added and tested separately after approval. diff --git a/sonar-project.properties b/sonar-project.properties index 49dd148..f8e466f 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,6 +1,6 @@ sonar.projectKey=threadpilot sonar.projectName=ThreadPilot -sonar.projectVersion=1.6.0 +sonar.projectVersion=1.7.0 sonar.sourceEncoding=UTF-8 sonar.sources=.