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
64 changes: 64 additions & 0 deletions Models/PersistentProcessRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* ThreadPilot - persistent process rule models.
*/
namespace ThreadPilot.Models
{
using System;
using System.Diagnostics;

public sealed record PersistentProcessRule
{
public string Id { get; init; } = Guid.NewGuid().ToString("N");

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

public bool IsEnabled { get; init; }

public string? ProcessName { get; init; }

public string? ExecutablePath { get; init; }

public CpuSelection? CpuSelection { get; init; }

public long? LegacyAffinityMask { get; init; }

public ProcessPriorityClass? Priority { get; init; }

public bool ApplyAffinityOnStart { get; init; }

public bool ApplyPriorityOnStart { get; init; }

public DateTime CreatedAt { get; init; } = DateTime.UtcNow;

public DateTime UpdatedAt { get; init; } = DateTime.UtcNow;

public string? Description { get; init; }
}

public sealed record PersistentRuleApplyResult
{
public bool Success { get; init; }

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

public int ProcessId { get; init; }

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

public bool AffinityApplied { get; init; }

public bool PriorityApplied { get; init; }

public string? ErrorCode { get; init; }

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

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

public bool IsAccessDenied { get; init; }

public bool IsAntiCheatLikely { get; init; }

public bool IsProcessExited { get; init; }
}
}
14 changes: 14 additions & 0 deletions Services/IPersistentProcessRuleStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* ThreadPilot - persistent process rule store contract.
*/
namespace ThreadPilot.Services
{
using ThreadPilot.Models;

public interface IPersistentProcessRuleStore
{
Task<IReadOnlyList<PersistentProcessRule>> LoadAsync();

Task SaveAsync(IReadOnlyList<PersistentProcessRule> rules);
}
}
69 changes: 69 additions & 0 deletions Services/PersistentProcessRuleJsonStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* ThreadPilot - JSON-backed persistent process rule store.
*/
namespace ThreadPilot.Services
{
using System.IO;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using ThreadPilot.Models;

public sealed class PersistentProcessRuleJsonStore : IPersistentProcessRuleStore
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
};

private readonly Func<string> filePathProvider;
private readonly ILogger<PersistentProcessRuleJsonStore>? logger;

public PersistentProcessRuleJsonStore(ILogger<PersistentProcessRuleJsonStore>? logger = null)
: this(() => StoragePaths.PersistentRulesFilePath, logger)
{
}

internal PersistentProcessRuleJsonStore(
Func<string> filePathProvider,
ILogger<PersistentProcessRuleJsonStore>? logger = null)
{
this.filePathProvider = filePathProvider ?? throw new ArgumentNullException(nameof(filePathProvider));
this.logger = logger;
}

public async Task<IReadOnlyList<PersistentProcessRule>> LoadAsync()
{
var filePath = this.filePathProvider();
if (!File.Exists(filePath))
{
return [];
}

try
{
var json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
return JsonSerializer.Deserialize<List<PersistentProcessRule>>(json, JsonOptions) ?? [];
}
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
{
this.logger?.LogWarning(ex, "Could not load persistent process rules from {FilePath}", filePath);
return [];
}
}

public async Task SaveAsync(IReadOnlyList<PersistentProcessRule> rules)
{
ArgumentNullException.ThrowIfNull(rules);

var filePath = this.filePathProvider();
var directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}

var json = JsonSerializer.Serialize(rules, JsonOptions);
await File.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
}
}
}
58 changes: 58 additions & 0 deletions Services/PersistentProcessRuleMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* ThreadPilot - persistent process rule matcher.
*/
namespace ThreadPilot.Services
{
using System.IO;
using ThreadPilot.Models;

public interface IPersistentProcessRuleMatcher
{
bool IsMatch(PersistentProcessRule rule, ProcessModel process);
}

public sealed class PersistentProcessRuleMatcher : IPersistentProcessRuleMatcher
{
public bool IsMatch(PersistentProcessRule rule, ProcessModel process)
{
ArgumentNullException.ThrowIfNull(rule);
ArgumentNullException.ThrowIfNull(process);

if (!rule.IsEnabled)
{
return false;
}

var rulePath = NormalizePath(rule.ExecutablePath);
if (!string.IsNullOrWhiteSpace(rulePath))
{
var processPath = NormalizePath(process.ExecutablePath);
return !string.IsNullOrWhiteSpace(processPath) &&
string.Equals(rulePath, processPath, StringComparison.OrdinalIgnoreCase);
}

return !string.IsNullOrWhiteSpace(rule.ProcessName) &&
string.Equals(rule.ProcessName.Trim(), process.Name?.Trim(), StringComparison.OrdinalIgnoreCase);
}

private static string? NormalizePath(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return null;
}

var trimmed = path.Trim();
try
{
trimmed = Path.GetFullPath(trimmed);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
{
// Keep matching best-effort for inaccessible or malformed process paths.
}

return trimmed.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
}
}
Loading
Loading