Skip to content
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,4 @@ gitleaks*.tar.gz
*.orig
*.rej
*.xaml.bak
/tmp
20 changes: 20 additions & 0 deletions App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,8 @@ protected override void OnExit(ExitEventArgs e)
this.DispatcherUnhandledException -= this.OnDispatcherUnhandledException;
TaskScheduler.UnobservedTaskException -= this.OnUnobservedTaskException;

this.DisposeServiceProvider();

if (this.singleInstanceMutex != null)
{
try
Expand All @@ -355,6 +357,24 @@ protected override void OnExit(ExitEventArgs e)
base.OnExit(e);
}

private void DisposeServiceProvider()
{
if (this.ServiceProvider is not IDisposable disposableProvider)
{
return;
}

var logger = this.ServiceProvider.GetService<ILogger<App>>();
try
{
disposableProvider.Dispose();
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Failed to dispose the application service provider during shutdown");
}
}

#if DEBUG
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
private static extern bool AllocConsole();
Expand Down
53 changes: 48 additions & 5 deletions MainWindow.Behaviors.partial.cs
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,10 @@ private async Task InitializeSystemTrayAsync()
// Initialize system tray context menu with current data
await this.UpdateSystemTrayContextMenuAsync();

this.trayPowerPlanService ??= this.serviceProvider.GetRequiredService<IPowerPlanService>();
this.trayPowerPlanService.PowerPlanChanged -= this.OnPowerPlanChangedForTray;
this.trayPowerPlanService.PowerPlanChanged += this.OnPowerPlanChangedForTray;

// Start periodic system tray updates
this.StartSystemTrayUpdateTimer();
}
Expand Down Expand Up @@ -1092,14 +1096,24 @@ private async Task UpdateSystemTrayContextMenuAsync()
{
try
{
await this.systemTrayStatusUpdater.UpdateContextMenuAsync(this.systemTrayService);
await this.systemTrayStatusUpdater.UpdateContextMenuAsync(
this.systemTrayService,
action => this.Dispatcher.InvokeAsync(action).Task);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to update system tray context menu: {ex.Message}");
this.LogDebug($"Failed to update system tray context menu: {ex.Message}");
}
}

private void OnPowerPlanChangedForTray(object? sender, PowerPlanChangedEventArgs e)
{
TaskSafety.FireAndForget(this.UpdateSystemTrayContextMenuAsync(), ex =>
{
this.LogDebug($"Failed to refresh tray menu after power plan change: {ex.Message}");
});
}

private void StartSystemTrayUpdateTimer()
{
try
Expand Down Expand Up @@ -1304,8 +1318,15 @@ private string Localize(string key, string fallback) =>

private void OnMonitoringStatusChanged(object? sender, MonitoringStatusEventArgs e)
{
// Update tray icon and status
this.systemTrayService.UpdateMonitoringStatus(e.IsMonitoring, e.IsWmiAvailable);
if (this.Dispatcher.CheckAccess())
{
this.systemTrayService.UpdateMonitoringStatus(e.IsMonitoring, e.IsWmiAvailable);
}
else
{
this.Dispatcher.InvokeAsync(() =>
this.systemTrayService.UpdateMonitoringStatus(e.IsMonitoring, e.IsWmiAvailable));
}

// Show notification if there's an error
if (e.Error != null && this.settingsService.Settings.EnableErrorNotifications)
Expand Down Expand Up @@ -2045,7 +2066,16 @@ protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
}

e.Cancel = true;
_ = this.HandleWindowCloseAsync();

TaskSafety.FireAndForget(this.HandleWindowCloseAsync(), ex =>
{
this.LogDebug($"Window close handling failed: {ex.Message}");
this.Dispatcher.InvokeAsync(() =>
{
this.isPerformingShutdown = true;
System.Windows.Application.Current?.Shutdown();
});
});
}

protected override void OnClosed(EventArgs e)
Expand All @@ -2060,14 +2090,27 @@ protected override void OnClosed(EventArgs e)
this.processMonitorManagerService.ServiceStatusChanged -= this.OnProcessMonitorManagerStatusChanged;
this.keyboardShortcutService.ShortcutActivated -= this.OnShortcutActivated;

if (this.trayPowerPlanService != null)
{
this.trayPowerPlanService.PowerPlanChanged -= this.OnPowerPlanChangedForTray;
this.trayPowerPlanService = null;
}

this.UnsubscribeSystemTrayEvents();

this.systemTrayUpdateTimer?.Stop();
this.systemTrayUpdateTimer?.Dispose();

this.initializationTimeoutTimer?.Stop();
this.initializationTimeoutTimer?.Dispose();

this.performanceViewModel?.Dispose();
this.settingsViewModel.Dispose();
this.powerPlanViewModel.Dispose();
this.associationViewModel.Dispose();
this.logViewerViewModel.Dispose();
this.systemTweaksViewModel.Dispose();
this.mainWindowViewModel.Dispose();

this.selfResourceManagementService.RestoreForegroundMode();
this.navigationBehavior.Dispose();
Expand Down
1 change: 1 addition & 0 deletions MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private TaskCompletionSource<MessageBoxResult>? unsavedSettingsDialogCompletionSource;
private bool isSilentStartupMode;
private bool showStartupMinimizedSuggestionOnReady;
private IPowerPlanService? trayPowerPlanService;

public MainWindow(
ProcessViewModel processViewModel,
Expand Down
20 changes: 15 additions & 5 deletions Platforms/Windows/ProcessCpuSetHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public class ProcessCpuSetHandler : IProcessCpuSetHandler
private static CpuSetMapping staticCpuSetMapping = CpuSetMapping.Empty;
private static readonly object staticInitLock = new object();
private static bool staticInitialized = false;
private static long lastCpuSetMappingFailureTicks = -1;

private static readonly TimeSpan CpuSetMappingRetryInterval = TimeSpan.FromSeconds(30);

private readonly Queue<CpuTimeTimestamp> cpuTimeMovingAverageBuffer = new();
private readonly string executableName;
Expand Down Expand Up @@ -77,19 +80,26 @@ private static CpuSetMapping EnsureStaticInitialization(IProcessCpuSetNativeApi
return staticCpuSetMapping;
}

if (lastCpuSetMappingFailureTicks >= 0 &&
Environment.TickCount64 - lastCpuSetMappingFailureTicks < CpuSetMappingRetryInterval.TotalMilliseconds)
{
return CpuSetMapping.Empty;
}

try
{
staticCpuSetMapping = GetCpuSetMapping(nativeApi);
lastCpuSetMappingFailureTicks = -1;

staticInitialized = true;
return staticCpuSetMapping;
}
catch (Exception)
{
// If we can't get CPU Set mapping, CPU Sets won't be available
// The handler will still work but ApplyCpuSetMask will return false
staticCpuSetMapping = CpuSetMapping.Empty;
lastCpuSetMappingFailureTicks = Environment.TickCount64;
return CpuSetMapping.Empty;
}

staticInitialized = true;
return staticCpuSetMapping;
}
}

Expand Down
23 changes: 9 additions & 14 deletions Services/AutostartService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public partial class AutostartService : IAutostartService
private readonly ILogger<AutostartService> logger;
private readonly IElevationService elevationService;
private readonly IElevatedTaskService elevatedTaskService;
private bool isAutostartEnabled;
private string? autostartPath;
private volatile bool isAutostartEnabled;
private volatile string? autostartPath;

public event EventHandler<AutostartStatusChangedEventArgs>? AutostartStatusChanged;

Expand Down Expand Up @@ -50,9 +50,6 @@ public async Task<bool> EnableAutostartAsync(bool startMinimized = true)
var arguments = this.GetAutostartArguments(startMinimized);
var fullCommand = $"\"{executablePath}\" {arguments}";

// Clean up legacy registry-based startup to keep a single elevated startup mechanism.
this.TryRemoveLegacyRegistryAutostart();

if (!this.elevationService.IsRunningAsAdministrator())
{
LogAutostartRequiresElevation(this.logger);
Expand All @@ -68,6 +65,11 @@ public async Task<bool> EnableAutostartAsync(bool startMinimized = true)
}

var scheduledTaskCreated = await this.elevatedTaskService.EnsureAutostartTaskAsync(executablePath, arguments);
if (scheduledTaskCreated)
{
this.TryRemoveLegacyRegistryAutostart();
}

if (!scheduledTaskCreated)
{
LogAutostartTaskRegistrationFailed(this.logger);
Expand Down Expand Up @@ -113,15 +115,15 @@ public async Task<bool> DisableAutostartAsync()
return false;
}

this.TryRemoveLegacyRegistryAutostart();

var scheduledTaskRemoved = await this.elevatedTaskService.RemoveAutostartTaskAsync();
if (!scheduledTaskRemoved)
{
LogAutostartTaskRemovalFailed(this.logger);
return false;
}

this.TryRemoveLegacyRegistryAutostart();

LogAutostartDisabled(this.logger);

this.isAutostartEnabled = false;
Expand Down Expand Up @@ -169,13 +171,6 @@ public async Task<bool> CheckAutostartStatusAsync()

public async Task<bool> UpdateAutostartAsync(bool startMinimized = true)
{
if (!this.isAutostartEnabled)
{
return await this.EnableAutostartAsync(startMinimized);
}

// Re-enable with new parameters
await this.DisableAutostartAsync();
return await this.EnableAutostartAsync(startMinimized);
}

Expand Down
12 changes: 11 additions & 1 deletion Services/EnhancedLoggingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class EnhancedLoggingService : IEnhancedLoggingService, IDisposable
private int flushScheduled;
private bool isInitialized;
private bool disposed;
private volatile bool isDebugLoggingEnabled;

// PERFORMANCE IMPROVEMENT: Correlation tracking for better debugging
internal readonly AsyncLocal<string?> CorrelationId = new();
Expand All @@ -31,7 +32,7 @@ public class EnhancedLoggingService : IEnhancedLoggingService, IDisposable

public string LogDirectoryPath => this.logDirectory;

public bool IsDebugLoggingEnabled => this.settingsService.Settings.EnableDebugLogging;
public bool IsDebugLoggingEnabled => this.isDebugLoggingEnabled;

public event EventHandler<CriticalErrorEventArgs>? CriticalErrorOccurred;

Expand All @@ -44,9 +45,17 @@ public EnhancedLoggingService(ILogger<EnhancedLoggingService> logger, IApplicati
this.logDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ThreadPilot", "Logs");
this.currentLogFilePath = this.GetCurrentLogFilePath();

this.isDebugLoggingEnabled = settingsService.Settings.EnableDebugLogging;
this.settingsService.SettingsChanged += this.OnSettingsChanged;

this.flushTimer = new System.Threading.Timer(this.FlushLogs, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
}

private void OnSettingsChanged(object? sender, ApplicationSettingsChangedEventArgs e)
{
this.isDebugLoggingEnabled = e.NewSettings.EnableDebugLogging;
}

public async Task InitializeAsync()
{
if (this.isInitialized)
Expand Down Expand Up @@ -517,6 +526,7 @@ public void Dispose()
return;
}

this.settingsService.SettingsChanged -= this.OnSettingsChanged;
this.flushTimer?.Dispose();
this.FlushLogsAsync().Wait(TimeSpan.FromSeconds(5));
this.fileLock?.Dispose();
Expand Down
Loading
Loading