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
69 changes: 49 additions & 20 deletions Services/PersistentProcessRuleJsonStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public sealed class PersistentProcessRuleJsonStore : IPersistentProcessRuleStore

private readonly Func<string> filePathProvider;
private readonly ILogger<PersistentProcessRuleJsonStore>? logger;
private readonly SemaphoreSlim cacheLock = new(1, 1);
private volatile IReadOnlyList<PersistentProcessRule>? cachedRules;

public PersistentProcessRuleJsonStore(ILogger<PersistentProcessRuleJsonStore>? logger = null)
: this(() => StoragePaths.PersistentRulesFilePath, logger)
Expand All @@ -33,44 +35,71 @@ 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))
if (this.cachedRules != null)
{
this.logger?.LogDebug("Persistent process rules file does not exist at {FilePath}", filePath);
return [];
return this.cachedRules;
}

await this.cacheLock.WaitAsync().ConfigureAwait(false);
try
{
var json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
var rules = JsonSerializer.Deserialize<List<PersistentProcessRule>>(json, JsonOptions) ?? [];
this.logger?.LogDebug("Loaded {RuleCount} persistent process rules from {FilePath}", rules.Count, filePath);
return rules;
if (this.cachedRules != null)
{
return this.cachedRules;
}

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 this.cachedRules = [];
}

try
{
var json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
var rules = JsonSerializer.Deserialize<List<PersistentProcessRule>>(json, JsonOptions) ?? [];
this.logger?.LogDebug("Loaded {RuleCount} persistent process rules from {FilePath}", rules.Count, filePath);
return this.cachedRules = rules.ToArray();
}
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 this.cachedRules = [];
}
}
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
finally
{
this.logger?.LogWarning(ex, "Could not load persistent process rules from {FilePath}", filePath);
return [];
this.cacheLock.Release();
}
}

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

var filePath = this.filePathProvider();
this.logger?.LogDebug("Saving {RuleCount} persistent process rules to {FilePath}", rules.Count, filePath);
await this.cacheLock.WaitAsync().ConfigureAwait(false);
try
{
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);
var filePath = this.filePathProvider();
this.logger?.LogDebug("Saving {RuleCount} persistent process rules to {FilePath}", rules.Count, filePath);
try
{
var json = JsonSerializer.Serialize(rules, JsonOptions);
await AtomicFileWriter.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
this.cachedRules = rules.ToArray();
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;
}
}
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
finally
{
this.logger?.LogError(ex, "Could not save {RuleCount} persistent process rules to {FilePath}", rules.Count, filePath);
throw;
this.cacheLock.Release();
}
}
}
Expand Down
54 changes: 49 additions & 5 deletions Services/PersistentRuleAutoApplyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace ThreadPilot.Services
{
using System.Collections.Concurrent;
using System.IO;
using Microsoft.Extensions.Logging;
using ThreadPilot.Models;

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

Expand All @@ -111,7 +114,8 @@ public PersistentRuleAutoApplyService(
Func<DateTimeOffset> nowProvider,
TimeSpan cooldown,
IActivityAuditService? activityAuditService = null,
IPersistentRulePriorityVerificationService? priorityVerificationService = null)
IPersistentRulePriorityVerificationService? priorityVerificationService = null,
IProcessService? processService = null)
{
this.ruleStore = ruleStore ?? throw new ArgumentNullException(nameof(ruleStore));
this.matcher = matcher ?? throw new ArgumentNullException(nameof(matcher));
Expand All @@ -122,6 +126,7 @@ public PersistentRuleAutoApplyService(
this.cooldown = cooldown <= TimeSpan.Zero ? DefaultCooldown : cooldown;
this.activityAuditService = activityAuditService;
this.priorityVerificationService = priorityVerificationService;
this.processService = processService;
}

public async Task<IReadOnlyList<PersistentRuleAutoApplyResult>> ApplyForDiscoveredProcessesAsync(
Expand Down Expand Up @@ -190,8 +195,35 @@ private async Task<IReadOnlyList<PersistentRuleAutoApplyResult>> ApplyForProcess
CancellationToken cancellationToken)
{
var now = this.nowProvider();
var candidates = rules
.Where(rule => rule.IsEnabled && this.matcher.IsMatch(rule, process))
var potentialRules = rules
.Where(rule => rule.IsEnabled && IsPotentialNameMatch(rule, process.Name))
.ToList();

if (potentialRules.Count == 0)
{
return Array.Empty<PersistentRuleAutoApplyResult>();
}

if (this.processService != null &&
string.IsNullOrWhiteSpace(process.ExecutablePath) &&
potentialRules.Any(rule => !string.IsNullOrWhiteSpace(rule.ExecutablePath)))
{
try
{
await this.processService.RefreshProcessInfo(process).ConfigureAwait(false);
}
catch (Exception ex)
{
this.logger.LogDebug(
ex,
"Could not enrich process {ProcessName} (PID: {ProcessId}) for path-based persistent rule matching",
process.Name,
process.ProcessId);
}
}

var candidates = potentialRules
.Where(rule => this.matcher.IsMatch(rule, process))
.ToList();

if (candidates.Count == 0)
Expand Down Expand Up @@ -393,6 +425,18 @@ private static string ResolveActivityMessage(PersistentRuleAutoApplyResult resul
private bool IsEnabled() =>
this.settingsService.Settings.ApplyPersistentRulesOnProcessStart;

private static bool IsPotentialNameMatch(PersistentProcessRule rule, string processName)
{
var candidateName = !string.IsNullOrWhiteSpace(rule.ProcessName)
? rule.ProcessName
: Path.GetFileNameWithoutExtension(rule.ExecutablePath);
return !string.IsNullOrWhiteSpace(candidateName) &&
string.Equals(
Path.GetFileNameWithoutExtension(candidateName.Trim()),
Path.GetFileNameWithoutExtension(processName.Trim()),
StringComparison.OrdinalIgnoreCase);
}

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

Expand Down
34 changes: 27 additions & 7 deletions Services/ProcessMonitorManagerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -346,13 +346,34 @@ private async Task OnProcessStartedAsync(ProcessEventArgs e)

try
{
await this.enhancedLogger.LogProcessMonitoringEventAsync(
LogEventTypes.ProcessMonitoring.Started,
e.Process.Name, e.Process.ProcessId, "Process started and detected by monitoring");
this.logger.LogDebug(
"Process started: {ProcessName} (PID: {ProcessId})",
e.Process.Name,
e.Process.ProcessId);

await this.ApplyPersistentRulesForProcessStartAsync(e.Process);

var association = this.configuration.FindMatchingAssociation(e.Process);
var associationCandidates = this.configuration
.GetEnabledAssociations()
.Where(association => association.MatchesExecutable(e.Process.Name))
.ToList();
if (associationCandidates.Count > 0)
{
try
{
await this.processService.RefreshProcessInfo(e.Process);
}
catch (Exception ex)
{
this.logger.LogDebug(
ex,
"Could not enrich process {ProcessName} (PID: {ProcessId}) for association matching",
e.Process.Name,
e.Process.ProcessId);
}
}

var association = associationCandidates.FirstOrDefault(candidate => candidate.MatchesProcess(e.Process));
if (association != null)
{
this.runningAssociatedProcesses[e.Process.ProcessId] = e.Process;
Expand Down Expand Up @@ -411,12 +432,11 @@ private async Task OnProcessStoppedAsync(ProcessEventArgs e)
try
{
this.persistentRuleAutoApplyService.MarkProcessExited(e.Process.ProcessId);
this.coreMaskService.UnregisterMaskApplication(e.Process.ProcessId);
this.processService.UntrackProcess(e.Process.ProcessId);

if (this.runningAssociatedProcesses.TryRemove(e.Process.ProcessId, out _))
{
this.coreMaskService.UnregisterMaskApplication(e.Process.ProcessId);
this.processService.UntrackProcess(e.Process.ProcessId);

// Check if there are any other associated processes still running
var remainingProcesses = this.runningAssociatedProcesses.Values.ToList();
await this.DeterminePowerPlanAsync(remainingProcesses);
Expand Down
50 changes: 12 additions & 38 deletions Services/ProcessMonitorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ namespace ThreadPilot.Services
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management;
Expand Down Expand Up @@ -371,23 +370,26 @@ private void OnProcessStarted(object sender, EventArrivedEventArgs e)
});
}

private async Task HandleProcessStartedAsync(EventArrivedEventArgs e)
private Task HandleProcessStartedAsync(EventArrivedEventArgs e)
{
if (!this.isMonitoring || this.IsDisposed)
{
return;
return Task.CompletedTask;
}

try
{
var processId = Convert.ToInt32(e.NewEvent["ProcessID"]);
var processName = e.NewEvent["ProcessName"]?.ToString() ?? string.Empty;

// Get detailed process information
var process = await this.CreateProcessModelFromId(processId, processName).ConfigureAwait(false)
?? (!string.IsNullOrWhiteSpace(processName)
? new ProcessModel { ProcessId = processId, Name = NormalizeProcessName(processName) }
: null);
var process = !string.IsNullOrWhiteSpace(processName)
? new ProcessModel
{
ProcessId = processId,
Name = NormalizeProcessName(processName),
Classification = ProcessClassification.Unknown,
}
: null;

if (process != null)
{
Expand All @@ -399,6 +401,8 @@ private async Task HandleProcessStartedAsync(EventArrivedEventArgs e)
{
this.OnMonitoringStatusChanged($"Error handling process start event: {ex.Message}", ex);
}

return Task.CompletedTask;
}

private void OnProcessStopped(object sender, EventArrivedEventArgs e)
Expand Down Expand Up @@ -581,36 +585,6 @@ private async Task TryRecoverWmiMonitoringAsync()
}
}

private async Task<ProcessModel?> CreateProcessModelFromId(int processId, string processName)
{
try
{
using var process = Process.GetProcessById(processId);
var normalizedName = !string.IsNullOrWhiteSpace(processName)
? NormalizeProcessName(processName)
: NormalizeProcessName(process.ProcessName);

return new ProcessModel
{
ProcessId = process.Id,
Name = normalizedName,
CpuUsage = 0,
MemoryUsage = process.PrivateMemorySize64,
Priority = process.PriorityClass,
ProcessorAffinity = (long)process.ProcessorAffinity,
MainWindowHandle = process.MainWindowHandle,
MainWindowTitle = process.MainWindowTitle ?? string.Empty,
HasVisibleWindow = process.MainWindowHandle != IntPtr.Zero && !string.IsNullOrWhiteSpace(process.MainWindowTitle),
ExecutablePath = process.MainModule?.FileName ?? string.Empty,
};
}
catch (Exception ex)
{
this.logger?.LogDebug(ex, "Process {ProcessId} terminated before access", processId);
return null;
}
}

private void OnMonitoringStatusChanged(string? message = null, Exception? error = null)
{
this.MonitoringStatusChanged?.Invoke(this, new MonitoringStatusEventArgs(
Expand Down
Loading
Loading