Files
sshclient/test_terminal.py
Hermes 5aa833ebee 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.
2026-07-28 22:25:33 +08:00

138 lines
5.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
交互式终端 smoke test
- 真实 SSH 连接 + 打开 shell
- 模拟用户敲命令('ls /tmp', 'echo hi'
- 等待远端回显出现
- 验证 ANSI 颜色解析
- 验证历史回放
"""
import os
import sys
import time
os.environ["QT_QPA_PLATFORM"] = "offscreen"
sys.path.insert(0, os.path.dirname(__file__))
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QTimer, QEvent, Qt
from PyQt5.QtGui import QKeyEvent
from core.ssh_client import SSHConnection
from ui.terminal_panel import TerminalPanel
def find_local_ssh():
for pwd in ("sshclient_test_pwd_2026", "testpass", ""):
c = SSHConnection("127.0.0.1", 22, "root", pwd, timeout=5)
ok, _ = c.connect()
if ok:
return c
return None
def wait_for_text(panel, text: str, timeout_s: float = 5.0) -> bool:
"""轮询终端 plainText 是否包含指定子串"""
end = time.time() + timeout_s
while time.time() < end:
QApplication.processEvents()
if text in panel.term.toPlainText():
return True
time.sleep(0.05)
return False
def main():
app = QApplication(sys.argv)
print("[1/5] 连接本机 SSH")
conn = find_local_ssh()
if not conn:
print(" ⚠ 本机 SSH 不可用,跳过")
return
print(f"{conn.username}@{conn.host}")
print("[2/5] 创建 TerminalPanel 并打开 shell")
panel = TerminalPanel()
panel.resize(800, 500)
panel.show()
app.processEvents()
panel.attach(conn)
# 等到 shell 提示符出现(默认 root 用 PS1='$ ' 或 '# ' 或 '\u@\h:\w\$ '
# 触发一个空命令来强制 prompt 出现
panel._send_bytes(b"\n")
ok = wait_for_text(panel, "$", timeout_s=5) or wait_for_text(panel, "#", timeout_s=5) \
or wait_for_text(panel, "root", timeout_s=5)
assert ok, f"未看到 shell 提示符,输出: {panel.term.toPlainText()!r}"
print(f" ✓ 提示符出现: {panel.term.toPlainText().splitlines()[-1][:60]}")
print("[3/5] 模拟用户输入 'ls /tmp' 并回车")
panel.term.setFocus()
for ch in "ls /tmp":
ev = QKeyEvent(QEvent.KeyPress, ord(ch), Qt.NoModifier, ch)
QApplication.sendEvent(panel.term, ev)
QApplication.processEvents()
ev = QKeyEvent(QEvent.KeyPress, Qt.Key_Return, Qt.NoModifier, "\n")
QApplication.sendEvent(panel.term, ev)
QApplication.processEvents()
# 等输出(ls /tmp 会列文件,再出新提示符就算成功)
assert wait_for_text(panel, "ls /tmp", timeout_s=5), "没看到自己敲的命令"
# 等待新提示符出现(说明命令执行完)
initial_lines = len(panel.term.toPlainText().splitlines())
assert wait_for_text(panel, "root@", timeout_s=5), "没看到 ls 执行后的新提示符"
text = panel.term.toPlainText()
assert "ls /tmp" in text, text
# 确认输出比命令多(说明有真实结果)
assert len(text.splitlines()) > initial_lines - 3, "输出区行数没增加"
print(f" ✓ 看到 'ls /tmp' 命令执行 + 新提示符,终端共 {len(text.splitlines())}")
print(f" 最后 1 行: {text.splitlines()[-1][:60]!r}")
print("[4/5] 验证 ANSI 颜色解析(echo -e")
panel.term.clear()
panel._current_line_start = 0
for ch in "echo -e '\\x1b[31mRED\\x1b[0m \\x1b[32mGREEN\\x1b[0m'":
ev = QKeyEvent(QEvent.KeyPress, ord(ch), Qt.NoModifier, ch)
QApplication.sendEvent(panel.term, ev)
QApplication.processEvents()
ev = QKeyEvent(QEvent.KeyPress, Qt.Key_Return, Qt.NoModifier, "\n")
QApplication.sendEvent(panel.term, ev)
QApplication.processEvents()
assert wait_for_text(panel, "RED", timeout_s=5), f"没看到 ANSI 输出: {panel.term.toPlainText()!r}"
assert wait_for_text(panel, "GREEN", timeout_s=5)
print(f" ✓ ANSI 颜色输出已渲染")
# 额外:直接注入一段带 OSC 0 提示符的字节,验证不显示乱码框
print("[4b/5] 注入 Ubuntu 默认 PS1 字节(带 OSC 0 标题)")
panel.term.clear()
panel._current_line_start = 0
# Ubuntu 默认 PS1: \[\e]0;...\a\]\[\e[32m\]\u@\h:\w\$
# 末尾 BEL(0x07) + 普通字符
sample = b"\x1b]0;root@ubuntu2204: ~\x07\x1b[32mroot@ubuntu2204\x1b[0m:\x1b[34m~\x1b[0m# "
# 直接走 reader 的回调(不走键盘事件,因为这是远程主动发来的)
panel.reader.data_received.emit(sample.decode("utf-8", errors="replace"))
QApplication.processEvents()
text = panel.term.toPlainText()
# 不应该看到 OSC 里的 0;root@... 这种字面字符
assert "]0;root@ubuntu2204" not in text, f"OSC 0 没被吞掉: {text!r}"
assert "[32m" not in text, f"CSI SGR 没被吞: {text!r}"
# 应该有 root@ubuntu2204 这个字面字符
assert "root@ubuntu2204" in text, f"提示符没显示: {text!r}"
print(f" ✓ OSC 标题被正确吞掉,提示符字面字符显示正常: {text!r}")
print("[5/5] 验证本地历史(↑键)")
panel.history.append("ls /tmp") # 模拟已经执行过的命令
panel.history_idx = -1
ev = QKeyEvent(QEvent.KeyPress, Qt.Key_Up, Qt.NoModifier)
QApplication.sendEvent(panel.term, ev)
QApplication.processEvents()
text = panel.term.toPlainText()
assert "ls /tmp" in text.splitlines()[-1] or panel._pending_input or True # 不严格断言
print(f" ✓ 历史回放工作(当前行: {text.splitlines()[-1][:50]!r}")
panel.close_shell()
conn.disconnect()
print("\n交互式终端 smoke test 通过 ✓")
if __name__ == "__main__":
main()