Skip to content
Merged
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 "1.4.1"
#define MyAppVersion "1.4.2"

#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.4.1.0"
Version="1.4.2.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.4.1"
#define MyAppVersion "1.4.2"
#endif

#ifndef MyAppSourceDir
Expand Down
10 changes: 10 additions & 0 deletions Models/PersistentProcessRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ public sealed record PersistentRuleApplyResult

public bool PriorityApplied { get; init; }

public ProcessPriorityClass? RequestedPriority { get; init; }

public ProcessPriorityClass? ObservedPriority { get; init; }

public bool PriorityVerified { get; init; }

public bool PriorityVerificationUnavailable { get; init; }

public string? PriorityVerificationPhase { get; init; }

public bool MemoryPriorityApplied { get; init; }

public string? ErrorCode { get; init; }
Expand Down
3 changes: 2 additions & 1 deletion Services/ActivityAuditService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ private static string ResolveCategory(string action)

private static ActivityAuditSeverity ResolveSeverity(string action, string details)
{
if (ContainsAny(action, "Blocked", "Denied") || ContainsAny(details, "blocked", "denied", "anti-cheat", "protected"))
if (ContainsAny(action, "Blocked", "Denied", "NotObserved", "Unavailable", "Reverted") ||
ContainsAny(details, "blocked", "denied", "anti-cheat", "protected", "not observed", "reverted"))
{
return ActivityAuditSeverity.Warning;
}
Expand Down
2 changes: 2 additions & 0 deletions Services/IProcessService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public interface IProcessService

Task SetProcessPriority(ProcessModel process, ProcessPriorityClass priority);

Task SetProcessPriority(ProcessModel process, ProcessPriorityClass priority, ProcessPriorityWriteSource source);

Task<bool> SaveProcessProfile(string profileName, ProcessModel process);

Task<bool> LoadProcessProfile(string profileName, ProcessModel process);
Expand Down
22 changes: 15 additions & 7 deletions Services/PersistentProcessRuleJsonStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,19 @@ internal PersistentProcessRuleJsonStore(
public async Task<IReadOnlyList<PersistentProcessRule>> LoadAsync()
{
var filePath = this.filePathProvider();
this.logger?.LogDebug("Loading persistent process rules from {FilePath}", filePath);
if (!File.Exists(filePath))
{
this.logger?.LogDebug("Persistent process rules file does not exist at {FilePath}", filePath);
return [];
}

try
{
var json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
return JsonSerializer.Deserialize<List<PersistentProcessRule>>(json, JsonOptions) ?? [];
var rules = JsonSerializer.Deserialize<List<PersistentProcessRule>>(json, JsonOptions) ?? [];
this.logger?.LogDebug("Loaded {RuleCount} persistent process rules from {FilePath}", rules.Count, filePath);
return rules;
}
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
{
Expand All @@ -56,14 +60,18 @@ public async Task SaveAsync(IReadOnlyList<PersistentProcessRule> rules)
ArgumentNullException.ThrowIfNull(rules);

var filePath = this.filePathProvider();
var directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrWhiteSpace(directory))
this.logger?.LogDebug("Saving {RuleCount} persistent process rules to {FilePath}", rules.Count, filePath);
try
{
Directory.CreateDirectory(directory);
var json = JsonSerializer.Serialize(rules, JsonOptions);
await AtomicFileWriter.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
this.logger?.LogDebug("Saved {RuleCount} persistent process rules to {FilePath}", rules.Count, filePath);
}
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
{
this.logger?.LogError(ex, "Could not save {RuleCount} persistent process rules to {FilePath}", rules.Count, filePath);
throw;
}

var json = JsonSerializer.Serialize(rules, JsonOptions);
await File.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
}
}
}
133 changes: 103 additions & 30 deletions Services/PersistentRuleAutoApplyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ public sealed record PersistentRuleAutoApplyResult

public bool IsProcessExited { get; init; }

public System.Diagnostics.ProcessPriorityClass? RequestedPriority { get; init; }

public System.Diagnostics.ProcessPriorityClass? ObservedPriority { get; init; }

public bool PriorityVerified { get; init; }

public bool PriorityVerificationUnavailable { get; init; }

public string? PriorityVerificationPhase { get; init; }

public static PersistentRuleAutoApplyResult FromApplyResult(PersistentRuleApplyResult result) =>
new()
{
Expand All @@ -57,6 +67,11 @@ public static PersistentRuleAutoApplyResult FromApplyResult(PersistentRuleApplyR
IsAccessDenied = result.IsAccessDenied,
IsAntiCheatLikely = result.IsAntiCheatLikely,
IsProcessExited = result.IsProcessExited,
RequestedPriority = result.RequestedPriority,
ObservedPriority = result.ObservedPriority,
PriorityVerified = result.PriorityVerified,
PriorityVerificationUnavailable = result.PriorityVerificationUnavailable,
PriorityVerificationPhase = result.PriorityVerificationPhase,
};
}

Expand All @@ -70,6 +85,7 @@ public sealed class PersistentRuleAutoApplyService : IPersistentRuleAutoApplySer
private readonly IApplicationSettingsService settingsService;
private readonly ILogger<PersistentRuleAutoApplyService> logger;
private readonly IActivityAuditService? activityAuditService;
private readonly IPersistentRulePriorityVerificationService? priorityVerificationService;
private readonly Func<DateTimeOffset> nowProvider;
private readonly TimeSpan cooldown;
private readonly ConcurrentDictionary<RuleAttemptKey, DateTimeOffset> recentAttempts = new();
Expand All @@ -80,8 +96,9 @@ public PersistentRuleAutoApplyService(
IPersistentRulesEngine rulesEngine,
IApplicationSettingsService settingsService,
ILogger<PersistentRuleAutoApplyService> logger,
IActivityAuditService? activityAuditService = null)
: this(ruleStore, matcher, rulesEngine, settingsService, logger, () => DateTimeOffset.UtcNow, DefaultCooldown, activityAuditService)
IActivityAuditService? activityAuditService = null,
IPersistentRulePriorityVerificationService? priorityVerificationService = null)
: this(ruleStore, matcher, rulesEngine, settingsService, logger, () => DateTimeOffset.UtcNow, DefaultCooldown, activityAuditService, priorityVerificationService)
{
}

Expand All @@ -93,7 +110,8 @@ public PersistentRuleAutoApplyService(
ILogger<PersistentRuleAutoApplyService> logger,
Func<DateTimeOffset> nowProvider,
TimeSpan cooldown,
IActivityAuditService? activityAuditService = null)
IActivityAuditService? activityAuditService = null,
IPersistentRulePriorityVerificationService? priorityVerificationService = null)
{
this.ruleStore = ruleStore ?? throw new ArgumentNullException(nameof(ruleStore));
this.matcher = matcher ?? throw new ArgumentNullException(nameof(matcher));
Expand All @@ -103,6 +121,7 @@ public PersistentRuleAutoApplyService(
this.nowProvider = nowProvider ?? throw new ArgumentNullException(nameof(nowProvider));
this.cooldown = cooldown <= TimeSpan.Zero ? DefaultCooldown : cooldown;
this.activityAuditService = activityAuditService;
this.priorityVerificationService = priorityVerificationService;
}

public async Task<IReadOnlyList<PersistentRuleAutoApplyResult>> ApplyForDiscoveredProcessesAsync(
Expand Down Expand Up @@ -133,7 +152,7 @@ public async Task<IReadOnlyList<PersistentRuleAutoApplyResult>> ApplyForDiscover
foreach (var process in snapshot)
{
cancellationToken.ThrowIfCancellationRequested();
results.AddRange(await this.ApplyForProcessAsync(process, rules, cancellationToken).ConfigureAwait(false));
results.AddRange(await this.ApplyForProcessAsync(process, rules, "DiscoveredSnapshot", cancellationToken).ConfigureAwait(false));
}

return results;
Expand All @@ -151,7 +170,7 @@ public async Task<IReadOnlyList<PersistentRuleAutoApplyResult>> ApplyForProcessS
}

var rules = await this.ruleStore.LoadAsync().ConfigureAwait(false);
return await this.ApplyForProcessAsync(process, rules, cancellationToken).ConfigureAwait(false);
return await this.ApplyForProcessAsync(process, rules, "ProcessStarted", cancellationToken).ConfigureAwait(false);
}

public void MarkProcessExited(int processId)
Expand All @@ -160,11 +179,14 @@ public void MarkProcessExited(int processId)
{
this.recentAttempts.TryRemove(key, out _);
}

this.priorityVerificationService?.MarkProcessExited(processId);
}

private async Task<IReadOnlyList<PersistentRuleAutoApplyResult>> ApplyForProcessAsync(
ProcessModel process,
IReadOnlyList<PersistentProcessRule> rules,
string source,
CancellationToken cancellationToken)
{
var now = this.nowProvider();
Expand All @@ -191,7 +213,7 @@ private async Task<IReadOnlyList<PersistentRuleAutoApplyResult>> ApplyForProcess
}

var selectedSignatures = selectedRules
.Select(GetRuleSignature)
.Select(PersistentRuleRuntimeKeys.GetRuleSignature)
.ToHashSet(StringComparer.Ordinal);

try
Expand All @@ -201,14 +223,15 @@ private async Task<IReadOnlyList<PersistentRuleAutoApplyResult>> ApplyForProcess
var applyResults = await this.rulesEngine
.ApplyMatchingRulesAsync(
process,
rule => selectedSignatures.Contains(GetRuleSignature(rule)),
rule => selectedSignatures.Contains(PersistentRuleRuntimeKeys.GetRuleSignature(rule)),
cancellationToken)
.ConfigureAwait(false);

var results = applyResults.Select(PersistentRuleAutoApplyResult.FromApplyResult).ToList();
foreach (var result in results)
{
await this.LogResultAsync(result).ConfigureAwait(false);
await this.LogResultAsync(result, process, source).ConfigureAwait(false);
this.priorityVerificationService?.ScheduleDelayedVerification(result, process, source);
}

return results;
Expand Down Expand Up @@ -237,7 +260,7 @@ private async Task<IReadOnlyList<PersistentRuleAutoApplyResult>> ApplyForProcess

private bool TryRecordAttempt(int processId, PersistentProcessRule rule, DateTimeOffset now)
{
var key = new RuleAttemptKey(processId, GetRuleSignature(rule));
var key = new RuleAttemptKey(processId, PersistentRuleRuntimeKeys.GetRuleSignature(rule));
if (this.recentAttempts.TryGetValue(key, out var lastAttempt) &&
now - lastAttempt < this.cooldown)
{
Expand All @@ -256,16 +279,22 @@ private void ClearAttemptsForMissingProcesses(HashSet<int> currentProcessIds)
}
}

private async Task LogResultAsync(PersistentRuleAutoApplyResult result)
private async Task LogResultAsync(PersistentRuleAutoApplyResult result, ProcessModel process, string source)
{
if (result.Success)
{
this.logger.LogInformation(
"Applied saved persistent rule {RuleId} to process {ProcessName} (PID: {ProcessId})",
"Applied saved persistent rule {RuleId} to process {ProcessName} (PID: {ProcessId}). Source: {Source}; path: {ExecutablePath}; requested priority: {RequestedPriority}; observed priority: {ObservedPriority}; priority verified: {PriorityVerified}; phase: {Phase}",
result.RuleId,
result.ProcessName,
result.ProcessId);
await this.LogActivityResultAsync(result).ConfigureAwait(false);
result.ProcessId,
source,
process.ExecutablePath,
result.RequestedPriority,
result.ObservedPriority,
result.PriorityVerified,
result.PriorityVerificationPhase);
await this.LogActivityResultAsync(result, process, source).ConfigureAwait(false);
return;
}

Expand All @@ -274,35 +303,36 @@ private async Task LogResultAsync(PersistentRuleAutoApplyResult result)
: LogLevel.Warning;
this.logger.Log(
logLevel,
"Persistent rule {RuleId} was not applied to process {ProcessName} (PID: {ProcessId}): {Message}",
"Persistent rule {RuleId} was not fully applied to process {ProcessName} (PID: {ProcessId}): {Message}. Source: {Source}; path: {ExecutablePath}; requested priority: {RequestedPriority}; observed priority: {ObservedPriority}; phase: {Phase}",
result.RuleId,
result.ProcessName,
result.ProcessId,
result.UserMessage);
await this.LogActivityResultAsync(result).ConfigureAwait(false);
result.UserMessage,
source,
process.ExecutablePath,
result.RequestedPriority,
result.ObservedPriority,
result.PriorityVerificationPhase);
await this.LogActivityResultAsync(result, process, source).ConfigureAwait(false);
}

private async Task LogActivityResultAsync(PersistentRuleAutoApplyResult result)
private async Task LogActivityResultAsync(PersistentRuleAutoApplyResult result, ProcessModel process, string source)
{
if (this.activityAuditService == null)
{
return;
}

var action = result.Success
? "PersistentRuleAutoApplied"
: "PersistentRuleAutoApplyFailed";
var message = result.Success
? $"Auto-applied saved rule for {result.ProcessName}."
: $"Failed to auto-apply saved rule for {result.ProcessName}: {result.UserMessage}";
var action = ResolveActivityAction(result);
var message = ResolveActivityMessage(result);

try
{
await this.activityAuditService
.LogUserActionAsync(
action,
message,
$"Rule: {result.RuleId}, PID: {result.ProcessId}")
$"Rule: {result.RuleId}, PID: {result.ProcessId}, Source: {source}, Path: {process.ExecutablePath}, RequestedPriority: {result.RequestedPriority?.ToString() ?? "none"}, ObservedPriority: {result.ObservedPriority?.ToString() ?? "unavailable"}, Phase: {result.PriorityVerificationPhase ?? "none"}")
.ConfigureAwait(false);
}
catch (Exception ex)
Expand All @@ -311,18 +341,61 @@ await this.activityAuditService
}
}

private static string ResolveActivityAction(PersistentRuleAutoApplyResult result)
{
if (result.PriorityVerified)
{
return "PersistentRulePriorityVerified";
}

if (result.RequestedPriority.HasValue && result.ObservedPriority.HasValue)
{
return "PersistentRulePriorityNotObserved";
}

if (result.RequestedPriority.HasValue && result.PriorityVerificationUnavailable)
{
return "PersistentRulePriorityVerificationUnavailable";
}

if (result.RequestedPriority.HasValue && !result.Success)
{
return "PersistentRulePriorityApplyFailed";
}

return result.Success
? "PersistentRuleAutoApplied"
: "PersistentRuleAutoApplyFailed";
}

private static string ResolveActivityMessage(PersistentRuleAutoApplyResult result)
{
if (result.PriorityVerified)
{
return $"Saved rule priority verified immediately for {result.ProcessName} as {result.ObservedPriority}.";
}

if (result.RequestedPriority.HasValue && result.ObservedPriority.HasValue)
{
return $"Saved rule priority requested for {result.ProcessName}, but observed {result.ObservedPriority} instead of {result.RequestedPriority}.";
}

if (result.RequestedPriority.HasValue && result.PriorityVerificationUnavailable)
{
return $"Saved rule priority requested for {result.ProcessName}, but verification was unavailable: {result.UserMessage}";
}

return result.Success
? $"Auto-applied saved rule for {result.ProcessName}."
: $"Failed to auto-apply saved rule for {result.ProcessName}: {result.UserMessage}";
}

private bool IsEnabled() =>
this.settingsService.Settings.ApplyPersistentRulesOnProcessStart;

private static bool IsProcessEligible(ProcessModel process) =>
process.ProcessId > 0 && !string.IsNullOrWhiteSpace(process.Name);

private static string GetRuleSignature(PersistentProcessRule rule) =>
string.Join(
"|",
string.IsNullOrWhiteSpace(rule.Id) ? rule.Name : rule.Id,
rule.UpdatedAt.ToUniversalTime().Ticks);

private readonly record struct RuleAttemptKey(int ProcessId, string RuleSignature);
}
}
Loading
Loading