Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bffbdee

Browse files
CPU topology v2 phase 8: add persistent rules engine foundation (#14)
Add persistent rules engine foundation Introduce the foundation for Apply at process start without adding a background loop, Windows Service, installer changes, registry affinity or anti-cheat bypass behavior. - Add PersistentProcessRule and PersistentRuleApplyResult models - Add process-name and executable-path rule matching - Add PersistentRulesEngine using existing safe affinity and priority services - Add JSON-backed persistent rule store - Add safe Apply at process start user-facing copy - Validate incomplete rules so missing affinity/priority payloads fail safely - Add tests for matching, CpuSelection, legacy affinity, priority, Realtime guardrails, access denied, protected processes, process exit, disabled rules and storage roundtrip No process monitor hook, UI management, Windows Service, registry/IFEO expansion, version bump or tag changes.
1 parent 9f56303 commit bffbdee

11 files changed

Lines changed: 975 additions & 0 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* ThreadPilot - persistent process rule models.
3+
*/
4+
namespace ThreadPilot.Models
5+
{
6+
using System;
7+
using System.Diagnostics;
8+
9+
public sealed record PersistentProcessRule
10+
{
11+
public string Id { get; init; } = Guid.NewGuid().ToString("N");
12+
13+
public string Name { get; init; } = string.Empty;
14+
15+
public bool IsEnabled { get; init; }
16+
17+
public string? ProcessName { get; init; }
18+
19+
public string? ExecutablePath { get; init; }
20+
21+
public CpuSelection? CpuSelection { get; init; }
22+
23+
public long? LegacyAffinityMask { get; init; }
24+
25+
public ProcessPriorityClass? Priority { get; init; }
26+
27+
public bool ApplyAffinityOnStart { get; init; }
28+
29+
public bool ApplyPriorityOnStart { get; init; }
30+
31+
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
32+
33+
public DateTime UpdatedAt { get; init; } = DateTime.UtcNow;
34+
35+
public string? Description { get; init; }
36+
}
37+
38+
public sealed record PersistentRuleApplyResult
39+
{
40+
public bool Success { get; init; }
41+
42+
public string RuleId { get; init; } = string.Empty;
43+
44+
public int ProcessId { get; init; }
45+
46+
public string ProcessName { get; init; } = string.Empty;
47+
48+
public bool AffinityApplied { get; init; }
49+
50+
public bool PriorityApplied { get; init; }
51+
52+
public string? ErrorCode { get; init; }
53+
54+
public string UserMessage { get; init; } = string.Empty;
55+
56+
public string TechnicalMessage { get; init; } = string.Empty;
57+
58+
public bool IsAccessDenied { get; init; }
59+
60+
public bool IsAntiCheatLikely { get; init; }
61+
62+
public bool IsProcessExited { get; init; }
63+
}
64+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* ThreadPilot - persistent process rule store contract.
3+
*/
4+
namespace ThreadPilot.Services
5+
{
6+
using ThreadPilot.Models;
7+
8+
public interface IPersistentProcessRuleStore
9+
{
10+
Task<IReadOnlyList<PersistentProcessRule>> LoadAsync();
11+
12+
Task SaveAsync(IReadOnlyList<PersistentProcessRule> rules);
13+
}
14+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* ThreadPilot - JSON-backed persistent process rule store.
3+
*/
4+
namespace ThreadPilot.Services
5+
{
6+
using System.IO;
7+
using System.Text.Json;
8+
using Microsoft.Extensions.Logging;
9+
using ThreadPilot.Models;
10+
11+
public sealed class PersistentProcessRuleJsonStore : IPersistentProcessRuleStore
12+
{
13+
private static readonly JsonSerializerOptions JsonOptions = new()
14+
{
15+
WriteIndented = true,
16+
};
17+
18+
private readonly Func<string> filePathProvider;
19+
private readonly ILogger<PersistentProcessRuleJsonStore>? logger;
20+
21+
public PersistentProcessRuleJsonStore(ILogger<PersistentProcessRuleJsonStore>? logger = null)
22+
: this(() => StoragePaths.PersistentRulesFilePath, logger)
23+
{
24+
}
25+
26+
internal PersistentProcessRuleJsonStore(
27+
Func<string> filePathProvider,
28+
ILogger<PersistentProcessRuleJsonStore>? logger = null)
29+
{
30+
this.filePathProvider = filePathProvider ?? throw new ArgumentNullException(nameof(filePathProvider));
31+
this.logger = logger;
32+
}
33+
34+
public async Task<IReadOnlyList<PersistentProcessRule>> LoadAsync()
35+
{
36+
var filePath = this.filePathProvider();
37+
if (!File.Exists(filePath))
38+
{
39+
return [];
40+
}
41+
42+
try
43+
{
44+
var json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
45+
return JsonSerializer.Deserialize<List<PersistentProcessRule>>(json, JsonOptions) ?? [];
46+
}
47+
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
48+
{
49+
this.logger?.LogWarning(ex, "Could not load persistent process rules from {FilePath}", filePath);
50+
return [];
51+
}
52+
}
53+
54+
public async Task SaveAsync(IReadOnlyList<PersistentProcessRule> rules)
55+
{
56+
ArgumentNullException.ThrowIfNull(rules);
57+
58+
var filePath = this.filePathProvider();
59+
var directory = Path.GetDirectoryName(filePath);
60+
if (!string.IsNullOrWhiteSpace(directory))
61+
{
62+
Directory.CreateDirectory(directory);
63+
}
64+
65+
var json = JsonSerializer.Serialize(rules, JsonOptions);
66+
await File.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
67+
}
68+
}
69+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* ThreadPilot - persistent process rule matcher.
3+
*/
4+
namespace ThreadPilot.Services
5+
{
6+
using System.IO;
7+
using ThreadPilot.Models;
8+
9+
public interface IPersistentProcessRuleMatcher
10+
{
11+
bool IsMatch(PersistentProcessRule rule, ProcessModel process);
12+
}
13+
14+
public sealed class PersistentProcessRuleMatcher : IPersistentProcessRuleMatcher
15+
{
16+
public bool IsMatch(PersistentProcessRule rule, ProcessModel process)
17+
{
18+
ArgumentNullException.ThrowIfNull(rule);
19+
ArgumentNullException.ThrowIfNull(process);
20+
21+
if (!rule.IsEnabled)
22+
{
23+
return false;
24+
}
25+
26+
var rulePath = NormalizePath(rule.ExecutablePath);
27+
if (!string.IsNullOrWhiteSpace(rulePath))
28+
{
29+
var processPath = NormalizePath(process.ExecutablePath);
30+
return !string.IsNullOrWhiteSpace(processPath) &&
31+
string.Equals(rulePath, processPath, StringComparison.OrdinalIgnoreCase);
32+
}
33+
34+
return !string.IsNullOrWhiteSpace(rule.ProcessName) &&
35+
string.Equals(rule.ProcessName.Trim(), process.Name?.Trim(), StringComparison.OrdinalIgnoreCase);
36+
}
37+
38+
private static string? NormalizePath(string? path)
39+
{
40+
if (string.IsNullOrWhiteSpace(path))
41+
{
42+
return null;
43+
}
44+
45+
var trimmed = path.Trim();
46+
try
47+
{
48+
trimmed = Path.GetFullPath(trimmed);
49+
}
50+
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
51+
{
52+
// Keep matching best-effort for inaccessible or malformed process paths.
53+
}
54+
55+
return trimmed.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)