Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Installer/Installer.iss
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#define MyAppPublisher "ThreadPilot"
#define MyAppURL "https://github.com/"
#define MyAppExeName "ThreadPilot.exe"
#define MyAppVersion "0.1.0-beta"
#define MyAppVersion "1.2.0"

#ifndef MyWizardStyle
#define MyWizardStyle "modern dynamic windows11"
Expand Down
2 changes: 1 addition & 1 deletion Installer/ThreadPilot.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<Package
Name="ThreadPilot"
Manufacturer="Prime Build"
Version="1.1.1.0"
Version="1.2.0.0"
UpgradeCode="PUT-GENERATED-UPGRADE-CODE-HERE"
Language="1033">
<SummaryInformation Description="ThreadPilot MSI template" />
Expand Down
2 changes: 1 addition & 1 deletion Installer/setup.iss
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#endif

#ifndef MyAppVersion
#define MyAppVersion "1.1.3"
#define MyAppVersion "1.2.0"
#endif

#ifndef MyAppSourceDir
Expand Down
2 changes: 1 addition & 1 deletion MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
<ui:SymbolIcon Symbol="DataHistogram24"/>
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="NavLogs" Content="Activity Logs" Tag="Logs" TargetPageTag="Logs" Click="NavMenuItem_Click">
<ui:NavigationViewItem x:Name="NavLogs" Content="ThreadPilot Activity" Tag="Logs" TargetPageTag="Logs" Click="NavMenuItem_Click">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="DocumentText24"/>
</ui:NavigationViewItem.Icon>
Expand Down
41 changes: 26 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# ThreadPilot ✈️

**A free and open-source Windows process and power plan manager for deterministic performance workflows.**
**A free and open-source Windows process control center for deterministic CPU, priority, memory, and power-plan workflows.**

[![Build](https://github.com/PrimeBuild-pc/ThreadPilot/actions/workflows/ci-devsecops.yml/badge.svg)](https://github.com/PrimeBuild-pc/ThreadPilot/actions/workflows/ci-devsecops.yml)
[![Release](https://img.shields.io/github/v/release/PrimeBuild-pc/ThreadPilot?sort=semver)](https://github.com/PrimeBuild-pc/ThreadPilot/releases)
Expand All @@ -21,20 +21,27 @@

## What is ThreadPilot?

ThreadPilot is a modern Windows desktop application for users who want predictable control over process behavior, CPU affinity, priority, power plans, and rule-driven performance workflows.
It is designed as an open-source alternative for power users who need Process Lasso-style capabilities, automation support, system tray controls, and a Windows 11-first experience.
ThreadPilot is a modern Windows process control center for users who want predictable control over process behavior, CPU affinity, CPU Sets, priority, memory priority, power plans, and saved process rules.

It is designed as an open-source alternative for power users who need Process Lasso-style capabilities, automation support, system tray controls, and a Windows 11-first experience. ThreadPilot is not a performance overlay: its primary job is applying explicit process controls safely and making the result visible.

## ✨ Features

- Live process management with refresh, filtering, and high-volume process handling.
- CPU affinity and priority controls with topology-aware logic.
- I/O and scheduler-related tuning utilities.
- Rule-based automation for power plan switching when selected processes start or stop.
- Conditional profiles, tray controls, Live Metrics, and dashboard views.
- Administrator-aware Windows desktop workflow.
- CI-backed release artifacts and package-manager distribution.
- Windows 11 native visual refresh with neutral Fluent surfaces and refined card-based layouts.
- Live process management with refresh, filtering, context-menu actions, and a selected-process summary.
- Topology-aware CPU affinity through `CpuSelection`, including CPU Sets support, processor groups, and safe handling for systems with more than 64 logical processors.
- Safer CPU indexing in new affinity paths: CPU64 no longer aliases CPU0.
- Intel hybrid CPU handling through Windows topology and `EfficiencyClass`, not hardcoded SKU lists.
- AMD CCD/L3-aware preset generation, also topology-driven instead of hardcoded SKU lists.
- Default gaming-oriented CPU presets for common foreground-game workflows.
- CPU priority controls with guardrails: High priority warning and Realtime priority blocked.
- Process memory priority support.
- Persistent process rules with explicit Apply now and Save as rule flows.
- Apply saved rules automatically when matching processes start while ThreadPilot is running.
- Rule-based automation for power plan switching when selected processes start or stop.
- Optional Diagnostics view, hidden by default, plus tray controls and dashboard views.
- Administrator-aware Windows desktop workflow.
- CI-backed build validation and package-manager distribution.
- Windows 11 native visual refresh with neutral Fluent surfaces and refined card-based layouts.

## 📦 Install

Expand Down Expand Up @@ -86,9 +93,13 @@ Compare the result with `SHA256SUMS.txt` from the same release.

## 🚀 Usage Notes

ThreadPilot uses an administrator-required manifest and requests elevation at startup. If UAC elevation is declined, the application exits instead of continuing in a limited mode.

Useful startup arguments:
ThreadPilot uses an administrator-required manifest and requests elevation at startup. If UAC elevation is declined, the application exits instead of continuing in a limited mode.

Persistent process rules are runtime-based. Apply at process start works only while ThreadPilot is running; it does not install a Windows Service, write registry or IFEO persistence, or use installer privilege tricks.

ThreadPilot does not bypass anti-cheat or protected-process restrictions. Running as administrator may help with normal access-denied cases, but it does not override protected-process or anti-cheat enforcement.

Useful startup arguments:

```text
--start-minimized
Expand Down
244 changes: 244 additions & 0 deletions Services/ActivityAuditService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
namespace ThreadPilot.Services
{
using Microsoft.Extensions.Logging;

public sealed class ActivityAuditService : IActivityAuditService
{
private const int MaxEntries = 1000;
private readonly ILogger<ActivityAuditService> logger;
private readonly object syncRoot = new();
private readonly List<ActivityAuditEntry> entries = new();

public event EventHandler<ActivityAuditEntry>? EntryAdded;

public ActivityAuditService(ILogger<ActivityAuditService> logger)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public Task LogInfoAsync(string category, string message, string? details = null) =>
this.AddEntryAsync(category, ActivityAuditSeverity.Info, message, details);

public Task LogSuccessAsync(string category, string message, string? details = null) =>
this.AddEntryAsync(category, ActivityAuditSeverity.Success, message, details);

public Task LogWarningAsync(string category, string message, string? details = null) =>
this.AddEntryAsync(category, ActivityAuditSeverity.Warning, message, details);

public Task LogErrorAsync(string category, string message, string? details = null) =>
this.AddEntryAsync(category, ActivityAuditSeverity.Error, message, details);

public Task LogUserActionAsync(string action, string details, string? context = null)
{
var entry = ActivityAuditActionMapper.Map(action, details, context);
return this.AddEntryAsync(entry.Category, entry.Severity, entry.Message, entry.Details);
}

public Task<IReadOnlyList<ActivityAuditEntry>> GetEntriesAsync(DateTime? fromDate = null, DateTime? toDate = null)
{
lock (this.syncRoot)
{
IEnumerable<ActivityAuditEntry> snapshot = this.entries;
if (fromDate.HasValue)
{
snapshot = snapshot.Where(entry => entry.Timestamp >= fromDate.Value);
}

if (toDate.HasValue)
{
snapshot = snapshot.Where(entry => entry.Timestamp <= toDate.Value);
}

return Task.FromResult<IReadOnlyList<ActivityAuditEntry>>(
snapshot
.OrderByDescending(entry => entry.Timestamp)
.ToList());
}
}

public Task ClearDisplayAsync()
{
lock (this.syncRoot)
{
this.entries.Clear();
}

return Task.CompletedTask;
}

private Task AddEntryAsync(string category, ActivityAuditSeverity severity, string message, string? details)
{
if (string.IsNullOrWhiteSpace(message))
{
return Task.CompletedTask;
}

var entry = new ActivityAuditEntry
{
Timestamp = DateTime.Now,
Category = string.IsNullOrWhiteSpace(category) ? ActivityAuditCategories.Diagnostics : category.Trim(),
Severity = severity,
Message = message.Trim(),
Details = string.IsNullOrWhiteSpace(details) ? null : details.Trim(),
};

lock (this.syncRoot)
{
this.entries.Add(entry);
if (this.entries.Count > MaxEntries)
{
this.entries.RemoveRange(0, this.entries.Count - MaxEntries);
}
}

this.logger.Log(
ToLogLevel(severity),
"Activity audit: {Category} {Severity}: {Message}",
entry.Category,
entry.Severity,
entry.Message);
this.EntryAdded?.Invoke(this, entry);
return Task.CompletedTask;
}

private static LogLevel ToLogLevel(ActivityAuditSeverity severity) =>
severity switch
{
ActivityAuditSeverity.Error => LogLevel.Error,
ActivityAuditSeverity.Warning => LogLevel.Warning,
_ => LogLevel.Information,
};
}

internal static class ActivityAuditCategories
{
public const string Process = "Process";
public const string Affinity = "Affinity";
public const string Priority = "Priority";
public const string MemoryPriority = "Memory Priority";
public const string Rules = "Rules";
public const string PowerPlans = "Power Plans";
public const string Settings = "Settings";
public const string Tweaks = "Tweaks";
public const string Optimization = "Optimization";
public const string Diagnostics = "Diagnostics";
public const string Safety = "Safety";
}

internal static class ActivityAuditActionMapper
{
public static ActivityAuditEntry Map(string action, string details, string? context)
{
var category = ResolveCategory(action);
var severity = ResolveSeverity(action, details);
return new ActivityAuditEntry
{
Category = category,
Severity = severity,
Message = string.IsNullOrWhiteSpace(details) ? action : details,
Details = context,
};
}

private static string ResolveCategory(string action)
{
if (action.StartsWith("ProcessAffinity", StringComparison.OrdinalIgnoreCase) ||
action.StartsWith("CpuSets", StringComparison.OrdinalIgnoreCase))
{
return ActivityAuditCategories.Affinity;
}

if (action.StartsWith("ProcessPriority", StringComparison.OrdinalIgnoreCase))
{
return ActivityAuditCategories.Priority;
}

if (action.StartsWith("ProcessMemoryPriority", StringComparison.OrdinalIgnoreCase))
{
return ActivityAuditCategories.MemoryPriority;
}

if (action.StartsWith("PersistentRule", StringComparison.OrdinalIgnoreCase) ||
action.Contains("Association", StringComparison.OrdinalIgnoreCase))
{
return ActivityAuditCategories.Rules;
}

if (action.StartsWith("PowerPlan", StringComparison.OrdinalIgnoreCase) ||
action.StartsWith("PowerPlans", StringComparison.OrdinalIgnoreCase))
{
return ActivityAuditCategories.PowerPlans;
}

if (action.StartsWith("Theme", StringComparison.OrdinalIgnoreCase) ||
action.StartsWith("Settings", StringComparison.OrdinalIgnoreCase) ||
action.Contains("Configuration", StringComparison.OrdinalIgnoreCase))
{
return ActivityAuditCategories.Settings;
}

if (action.StartsWith("SystemTweak", StringComparison.OrdinalIgnoreCase) ||
action.Contains("IdleServer", StringComparison.OrdinalIgnoreCase) ||
action.Contains("RegistryPriority", StringComparison.OrdinalIgnoreCase))
{
return ActivityAuditCategories.Tweaks;
}

if (action.StartsWith("Optimization", StringComparison.OrdinalIgnoreCase))
{
return ActivityAuditCategories.Optimization;
}

if (action.Contains("Protected", StringComparison.OrdinalIgnoreCase) ||
action.Contains("Elevation", StringComparison.OrdinalIgnoreCase))
{
return ActivityAuditCategories.Safety;
}

if (action.StartsWith("Process", StringComparison.OrdinalIgnoreCase))
{
return ActivityAuditCategories.Process;
}

return ActivityAuditCategories.Diagnostics;
}

private static ActivityAuditSeverity ResolveSeverity(string action, string details)
{
if (ContainsAny(action, "Blocked", "Denied") || ContainsAny(details, "blocked", "denied", "anti-cheat", "protected"))
{
return ActivityAuditSeverity.Warning;
}

if (ContainsAny(action, "Failed", "Failure", "Error") || ContainsAny(details, "failed", "error", "exited"))
{
return ActivityAuditSeverity.Error;
}

if (ContainsAny(
action,
"Applied",
"Changed",
"Saved",
"Updated",
"Deleted",
"Imported",
"Added",
"Cleared",
"Refreshed",
"Started",
"Stopped",
"Exported",
"Opened",
"Copied"))
{
return ActivityAuditSeverity.Success;
}

return ActivityAuditSeverity.Info;
}

private static bool ContainsAny(string value, params string[] terms) =>
terms.Any(term => value.Contains(term, StringComparison.OrdinalIgnoreCase));
}
}
42 changes: 42 additions & 0 deletions Services/IActivityAuditService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace ThreadPilot.Services
{
public enum ActivityAuditSeverity
{
Info,
Success,
Warning,
Error,
}

public sealed record ActivityAuditEntry
{
public DateTime Timestamp { get; init; }

public string Category { get; init; } = string.Empty;

public ActivityAuditSeverity Severity { get; init; }

public string Message { get; init; } = string.Empty;

public string? Details { get; init; }
}

public interface IActivityAuditService
{
event EventHandler<ActivityAuditEntry>? EntryAdded;

Task LogInfoAsync(string category, string message, string? details = null);

Task LogSuccessAsync(string category, string message, string? details = null);

Task LogWarningAsync(string category, string message, string? details = null);

Task LogErrorAsync(string category, string message, string? details = null);

Task LogUserActionAsync(string action, string details, string? context = null);

Task<IReadOnlyList<ActivityAuditEntry>> GetEntriesAsync(DateTime? fromDate = null, DateTime? toDate = null);

Task ClearDisplayAsync();
}
}
Loading
Loading