Files
team/scripts/tmux-monitor.sh
arno 34346be862
All checks were successful
CI / lint (push) Successful in 6s
配置: 初始化 ISOS Agent Teams 软件研发模板
2026-04-19 21:47:08 +08:00

393 lines
14 KiB
Bash
Executable File
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.
#!/bin/bash
# ==============================================================================
# tmux 实时监控面板 — 管理与显示
#
# 使用方法:
# bash scripts/tmux-monitor.sh # 创建监控面板 (5秒刷新)
# bash scripts/tmux-monitor.sh <秒数> # 创建监控面板 (自定义刷新)
# bash scripts/tmux-monitor.sh -k # 关闭监控面板
# bash scripts/tmux-monitor.sh -r # 重启监控面板
# bash scripts/tmux-monitor.sh -h # 显示帮助
# bash scripts/tmux-monitor.sh --display # (内部) 运行显示循环
# ==============================================================================
SCRIPT_PATH="/workspace/scripts/tmux-monitor.sh"
# ── 参数解析 ──────────────────────────────────────────────
ACTION="create"
INTERVAL=""
while [ $# -gt 0 ]; do
case "$1" in
--display)
ACTION="display"; shift
;;
-k|--kill)
ACTION="kill"; shift
;;
-r|--restart)
ACTION="restart"; shift
;;
-h|--help)
ACTION="help"; shift
;;
-*)
echo "未知选项: $1" >&2; exit 1
;;
*)
INTERVAL="$1"; shift
;;
esac
done
INTERVAL="${INTERVAL:-1}"
# ── 前置检查 ──────────────────────────────────────────────
if ! command -v tmux &>/dev/null; then
echo "错误: tmux 未安装" >&2
exit 1
fi
# ── 管理函数 ──────────────────────────────────────────────
find_monitor_pane() {
tmux list-panes -F '#{pane_index} #{pane_start_command}' 2>/dev/null \
| grep -i 'tmux-monitor' \
| head -1 \
| awk '{print $1}'
}
do_kill() {
local pane_idx
pane_idx=$(find_monitor_pane)
if [ -n "$pane_idx" ]; then
tmux kill-pane -t "$pane_idx"
echo "监控面板 (Pane ${pane_idx}) 已关闭"
return 0
else
echo "未找到运行中的监控面板"
return 1
fi
}
do_create() {
if [ -z "${TMUX:-}" ]; then
echo "错误: 需要在 tmux 会话内运行" >&2
return 1
fi
# 关闭已有监控面板
local pane_idx
pane_idx=$(find_monitor_pane)
if [ -n "$pane_idx" ]; then
tmux kill-pane -t "$pane_idx"
fi
# 右侧监控面板固定宽度 60
local monitor_width=60
tmux split-window -h -l "$monitor_width" \
"TMUX_MONITOR_INTERVAL=${INTERVAL} bash ${SCRIPT_PATH} --display"
echo "监控面板已启动 (${INTERVAL} 秒刷新)"
}
# ── 非显示操作: 执行后退出 ───────────────────────────────
if [ "$ACTION" != "display" ]; then
case "$ACTION" in
help)
echo "用法: bash $(basename "$0") [选项] [刷新间隔]"
echo ""
echo "选项:"
echo " (空) 创建监控面板 (默认 ${INTERVAL} 秒刷新)"
echo " <秒数> 创建监控面板 (自定义刷新间隔)"
echo " -k, --kill 关闭监控面板"
echo " -r, --restart 重启监控面板"
echo " -h, --help 显示帮助信息"
;;
kill)
do_kill
;;
restart)
do_kill 2>/dev/null || true
do_create
;;
create)
do_create
;;
esac
exit $?
fi
# ══════════════════════════════════════════════════════════
# 以下为 display 模式: 运行监控显示循环
# ══════════════════════════════════════════════════════════
# 颜色定义 — 使用 $'...' ANSI-C 引号,赋值时即产生真正的 ESC 字节
readonly R=$'\033[0;31m' # RED
readonly G=$'\033[0;32m' # GREEN
readonly Y=$'\033[0;33m' # YELLOW
readonly B=$'\033[0;34m' # BLUE
readonly P=$'\033[0;35m' # PURPLE
readonly C=$'\033[0;36m' # CYAN
readonly D=$'\033[0;90m' # GRAY/DIM
readonly BD=$'\033[1m' # BOLD
readonly N=$'\033[0m' # RESET
# 刷新间隔(秒)— 由管理模式通过 TMUX_MONITOR_INTERVAL 传入
INTERVAL="${TMUX_MONITOR_INTERVAL:-1}"
# ── 工具函数 ──────────────────────────────────────────────
# 生成指定宽度的分隔线
make_sep() {
local width="${1:-80}"
local sep_char='─'
printf '%*s' "$width" '' | tr ' ' "$sep_char"
}
# ── 显示层级常量 ─────────────────────────────────────────
# 根据终端宽度决定显示详细程度
LVL_MINIMAL=0 # < 50 列:仅显示核心信息
LVL_COMPACT=1 # 50-69 列:省略 PID、路径、子命令
LVL_NORMAL=2 # 70-99 列:省略子命令详情
LVL_FULL=3 # >= 100 列:显示全部
get_display_level() {
local cols="$1"
if [ "$cols" -lt 50 ]; then
echo "$LVL_MINIMAL"
elif [ "$cols" -lt 70 ]; then
echo "$LVL_COMPACT"
elif [ "$cols" -lt 100 ]; then
echo "$LVL_NORMAL"
else
echo "$LVL_FULL"
fi
}
# 记录终端尺寸,用于检测 resize
last_cols=0
last_rows=0
while true; do
# ── 检测终端尺寸变化,清屏防重叠 ──
cur_cols=$(tput cols 2>/dev/null || echo 80)
cur_rows=$(tput lines 2>/dev/null || echo 24)
if [ "$cur_cols" != "$last_cols" ] || [ "$cur_rows" != "$last_rows" ]; then
tput clear
last_cols=$cur_cols
last_rows=$cur_rows
fi
# 当前显示层级
level=$(get_display_level "$cur_cols")
# 动态分隔线
SEP=$(make_sep "$cur_cols")
# ── 构建输出到缓冲区 ──
buf=""
buf="${buf}\n"
buf="${buf}${BD}${SEP}${N}\n"
# 标题行:自适应宽度
title=" tmux 实时监控 $(date '+%H:%M:%S')"
if [ "$level" -ge "$LVL_COMPACT" ]; then
buf="${buf}${C}${BD}${title}${N}\n"
else
buf="${buf}${C}${BD} 监控 $(date '+%H:%M:%S')${N}\n"
fi
buf="${buf}${BD}${SEP}${N}\n"
buf="${buf}\n"
cur_session=$(tmux display-message -p '#{session_name}' 2>/dev/null)
# 一次性获取所有面板信息(含状态字段)
panes_info=$(tmux list-panes -a -F \
'#{session_name}|#{session_created}|#{session_attached}|#{window_index}|#{window_name}|#{window_active}|#{window_width}x#{window_height}|#{pane_index}|#{pane_title}|#{pane_current_command}|#{pane_current_path}|#{pane_active}|#{pane_pid}|#{pane_width}x#{pane_height}|#{pane_dead}|#{pane_dead_status}|#{pane_in_mode}|#{pane_mode}' \
2>/dev/null)
total_sessions=0
total_windows=0
total_panes=0
session_idx=0
if [ -n "$panes_info" ]; then
prev_session=""
prev_window=""
while IFS='|' read -r ssn s_created s_attached widx wname w_active w_size pidx ptitle cmd path active pid p_size p_dead p_dead_status p_in_mode p_mode; do
# ── 新会话 ──
if [ "$ssn" != "$prev_session" ]; then
[ -n "$prev_session" ] && buf="${buf}\n"
total_sessions=$((total_sessions + 1))
session_idx=$((session_idx + 1))
if [ "$ssn" = "$cur_session" ]; then
s_marker="${G}${N}"
s_name="${BD}${G}${ssn}${N}"
else
s_marker="${D}${N}"
s_name="${P}${ssn}${N}"
fi
# Session 状态标签
s_status=""
if [ "$s_attached" = "0" ]; then
s_status=" ${Y}${BD}[detached]${N}"
fi
s_time=""
if [ "$level" -ge "$LVL_NORMAL" ] && [ -n "$s_created" ] && [ "$s_created" -gt 0 ] 2>/dev/null; then
s_time=$(date -d "@$s_created" '+%m-%d %H:%M' 2>/dev/null)
fi
# 根据宽度调整 Session 行
if [ "$level" -ge "$LVL_COMPACT" ]; then
s_line=" ${s_marker} ${BD}Session${N} ${session_idx} ${s_name}${s_status}"
else
s_line=" ${s_marker} ${BD}S${N}${session_idx} ${s_name}${s_status}"
fi
[ -n "$s_time" ] && s_line="${s_line} ${D}[自 ${s_time}]${N}"
buf="${buf}${s_line}\n"
prev_session="$ssn"
prev_window=""
fi
# ── 新窗口 ──
if [ "$widx:$wname" != "$prev_window" ]; then
total_windows=$((total_windows + 1))
if [ "$w_active" = "1" ] && [ "$ssn" = "$cur_session" ]; then
w_marker="${G}${N}"
w_name="${BD}${G}${wname}${N}"
else
w_marker="${D}${N}"
w_name="${Y}${wname}${N}"
fi
# Window 状态标签
w_status=""
if [ "$w_active" = "1" ] && [ "$ssn" = "$cur_session" ]; then
w_status=" ${G}[active]${N}"
fi
# 根据宽度决定是否显示尺寸
if [ "$level" -ge "$LVL_COMPACT" ]; then
w_line=" ${D}├─${N} ${w_marker} ${BD}Win${N} ${widx} ${w_name}${w_status} ${D}[${w_size}]${N}"
else
w_line=" ${D}${N} ${w_marker} ${w_name}${w_status}"
fi
buf="${buf}${w_line}\n"
prev_window="$widx:$wname"
fi
# ── 面板 ──
total_panes=$((total_panes + 1))
if [ "$active" = "1" ] && [ "$ssn" = "$cur_session" ]; then
p_marker="${G}${N}"
else
p_marker="${D}${N}"
fi
case "$cmd" in
bash|zsh|fish|sh) cmd_color="$D" ;;
vim|nvim|vi) cmd_color="$G" ;;
python|python3) cmd_color="$Y" ;;
node) cmd_color="$G" ;;
ssh) cmd_color="$R" ;;
tmux-monitor.sh|tmux_monitor.sh) cmd_color="$C" ;;
*) cmd_color="$N" ;;
esac
# 路径缩写:根据层级决定缩写程度
short_path=""
if [ "$level" -ge "$LVL_NORMAL" ]; then
short_path=$(echo "$path" | sed "s|^$HOME|~|" | awk -F/ '{
if(NF>3) print ".."/$(NF-1)/$NF; else print $0
}')
elif [ "$level" -ge "$LVL_COMPACT" ]; then
short_path=$(echo "$path" | sed "s|^$HOME|~|" | awk -F/ '{
print $NF
}')
fi
# 子命令:仅在 FULL 级别显示
child_cmd=""
if [ "$level" -ge "$LVL_FULL" ] && [ "$(uname)" = "Linux" ]; then
child_pid=$(ps --ppid "$pid" -o pid= 2>/dev/null | head -1 | tr -d ' ')
if [ -n "$child_pid" ]; then
child_cmd=$(ps -p "$child_pid" -o args= 2>/dev/null)
fi
fi
# 格式: 根据层级决定显示内容
p_title="${ptitle}"
[ -z "$p_title" ] && p_title="$cmd"
# Pane 状态标签
p_status=""
if [ "$p_dead" = "1" ]; then
p_status=" ${R}${BD}[dead:${p_dead_status}]${N}"
fi
if [ "$p_in_mode" = "1" ] && [ -n "$p_mode" ]; then
p_status="${p_status} ${Y}[${p_mode}]${N}"
fi
if [ "$active" = "1" ] && [ "$ssn" = "$cur_session" ]; then
p_status="${p_status} ${G}[active]${N}"
fi
if [ "$level" -ge "$LVL_NORMAL" ]; then
# NORMAL / FULL: Pane 编号 + 标题 + 命令 + 尺寸 + 状态
p_line=" ${D}│ ├─${N} ${p_marker} ${BD}Pane${N} ${pidx} ${C}${p_title}${N} ${cmd_color}${cmd}${N} ${D}[${p_size}]${N}${p_status}"
if [ "$level" -ge "$LVL_FULL" ]; then
p_line="${p_line} ${D}PID:${pid}${N}"
fi
elif [ "$level" -ge "$LVL_COMPACT" ]; then
# COMPACT: Pane 编号 + 标题 + 命令
p_line=" ${D}│ ├${N} ${p_marker} ${BD}P${N}${pidx} ${C}${p_title}${N} ${cmd_color}${cmd}${N}"
else
# MINIMAL: 仅标题 + 命令
p_line=" ${D}${N} ${p_marker} ${C}${p_title}${N}"
fi
buf="${buf}${p_line}\n"
if [ -n "$child_cmd" ]; then
c_line=" ${D}│ │ ↳${N} ${D}${child_cmd}${N}"
buf="${buf}${c_line}\n"
fi
if [ -n "$short_path" ]; then
sp_line=" ${D}│ │ ${N}${D}${short_path}${N}"
buf="${buf}${sp_line}\n"
fi
done <<< "$panes_info"
fi
# ── 系统概要 ──
buf="${buf}\n"
buf="${buf}${D}${SEP}${N}\n"
buf="${buf} ${BD}总计:${N} ${total_sessions} 会话 | ${total_windows} 窗口 | ${total_panes} 面板\n"
buf="${buf}${D}${SEP}${N}\n"
buf="${buf} ${D}${INTERVAL} 秒刷新 | Ctrl+C 退出${N}\n"
# ── 双缓冲输出:移到顶部 → 写入 → 清除尾部残留 ──
# 每行末尾添加 \033[K清除到行尾防止短行覆盖长行时残留旧字符
buf="${buf//\\n/\\033[K\\n}"
tput cup 0 0
printf '%b' "$buf"
tput ed
sleep "$INTERVAL"
done