feat: add process monitoring to Monitor panel

User asked for process monitoring. Added:

1. core/monitor.py:
- Extended _LINUX_METRICS_SCRIPT with ===PROC=== section
- ps -e -o stat= | awk counts total/running/sleep/disk-sleep/zombie
- ps -eo pid,user,pcpu,pmem,vsz,rss,stat,etimes,times,args
  Uses etimes/times (numeric) instead of start/time (string with
  spaces) to avoid column-shift bugs. awk joins 10th col onwards
  with spaces so comm retains its full command line.
- New empty-dict fields: proc_total, proc_running, proc_sleep,
  proc_disk, proc_zombie, processes
- New processes[] array with full per-process fields

2. ui/widgets.py MonitorPanel:
- New _build_proc_summary() - 4-stat overview card (total/running/
  sleep/zombie, color-coded)
- New _build_proc_table() - 9-column process list with:
    * Search box (multi-keyword AND match across pid/user/stat/comm)
    * Sort dropdown (CPU / MEM / PID / start time / user)
    * Color-coded CPU% (red >=50%, orange >=20%)
    * Color-coded MEM% (red >=10%, orange >=5%)
    * Color-coded STAT (red for zombie)
    * RSS formatted with format_bytes
    * ETIME / TIME formatted as 5d3h / 2h15m / 45s
- _apply_proc_filter() - filter + sort + render in one pass
- _proc_context_menu() - right-click menu:
    * kill (SIGTERM) | kill -9 (SIGKILL) | copy PID | copy cmd |
      filter by this command
- _proc_kill_selected() / _proc_kill() - sends kill over SSH
  with confirmation dialog, shows remote exit code in result
- Splitter: 4 metric cards + proc summary + disk + net on top,
  process list on bottom. User-draggable.

3. New test_process_monitor.py (5 tests, all pass):
- Real SSH connection, collects process data
- Renders MonitorPanel with 200 rows
- Tests search filter / sort change / clear
- Tests killing a real sleep process: starts sleep 60, finds it
  in the table, calls _proc_kill (with QMessageBox monkey-patched
  to auto-yes), verifies remote kill -0 returns 'No such process'
- Also fixed passwords in test_e2e.py + test_terminal.py to match
  the working local sshd credentials

All tests pass: core 6/6 + UI 3/3 + E2E 6/6 + terminal 5/5 + proc 5/5
Build: 57 MB single-file exe.
This commit is contained in:
Hermes
2026-07-28 22:25:33 +08:00
parent 1b7259c2d5
commit 5aa833ebee
5 changed files with 553 additions and 13 deletions
+57
View File
@@ -49,6 +49,20 @@ for iface in $(ls /sys/class/net/ 2>/dev/null | grep -v lo); do
tx=$(cat /sys/class/net/$iface/statistics/tx_bytes 2>/dev/null || echo 0)
echo "NET|$iface|$rx|$tx"
done
echo "===PROC==="
# 进程统计
ps -e -o stat= 2>/dev/null | awk '{r+=($1~/^R/); s+=($1~/^S/); d+=($1~/^D/); z+=($1~/^Z/); t++} END{printf "PROC_TOTAL=%d\nPROC_RUNNING=%d\nPROC_SLEEP=%d\nPROC_DISK=%d\nPROC_ZOMBIE=%d\n", t, r, s, d, z}'
# 进程列表:etimes/times 都是纯数字,避免 start 字段含 "Jul 21" 多列错位
# comm 字段可能含空格,前 9 列用 \t 拼,comm 用换行做记录结束
ps -eo pid,user,pcpu,pmem,vsz,rss,stat,etimes,times,args --sort=-pcpu --no-headers 2>/dev/null \
| head -200 \
| awk '{
out="PROC\t";
for(i=1;i<=9;i++) out=out $i "\t";
rest="";
for(i=10;i<=NF;i++) rest=(i==10?$i:rest " " $i);
print out rest
}'
echo "===HOST==="
echo "HOSTNAME=$(hostname)"
echo "KERNEL=$(uname -r)"
@@ -70,6 +84,8 @@ echo "OS=$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || uname -s)"
"mem_total": 0, "mem_used": 0, "mem_percent": 0.0,
"swap_total": 0, "swap_used": 0,
"disks": [], "net": [],
"proc_total": 0, "proc_running": 0, "proc_sleep": 0,
"proc_disk": 0, "proc_zombie": 0, "processes": [],
"ts": time.time(),
}
if not conn or not conn.connected:
@@ -137,6 +153,47 @@ echo "OS=$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || uname -s)"
except ValueError:
continue
# 进程统计
try:
result["proc_total"] = int(cls._parse_kv(out, "PROC_TOTAL"))
result["proc_running"] = int(cls._parse_kv(out, "PROC_RUNNING"))
result["proc_sleep"] = int(cls._parse_kv(out, "PROC_SLEEP"))
result["proc_disk"] = int(cls._parse_kv(out, "PROC_DISK"))
result["proc_zombie"] = int(cls._parse_kv(out, "PROC_ZOMBIE"))
except ValueError:
result["proc_total"] = 0
result["proc_running"] = 0
result["proc_sleep"] = 0
result["proc_disk"] = 0
result["proc_zombie"] = 0
# 进程列表(远程用 \t 分隔前 9 列,第 10 列开始是 args/comm,可能含空格)
result["processes"] = []
for line in out.splitlines():
if not line.startswith("PROC\t"):
continue
payload = line[5:] # 去掉 "PROC\t" 前缀
fields = payload.split("\t", 9) # 只切前 9 次
if len(fields) < 10:
continue
try:
result["processes"].append({
"pid": int(fields[0]),
"user": fields[1],
"pcpu": float(fields[2]),
"pmem": float(fields[3]),
"vsz": int(fields[4]),
"rss": int(fields[5]),
"stat": fields[6],
# etimes: 自启动以来的秒数(整数)
"etime": int(fields[7]) if fields[7].isdigit() else 0,
# times: 累计 CPU 时间秒数
"time": int(fields[8]) if fields[8].isdigit() else 0,
"comm": fields[9].strip(),
})
except (ValueError, IndexError):
continue
return result
@staticmethod