#!/bin/bash # stop-tasks.sh — 随时叫停/重启统一入口 v1.0(2026-08-26) # # 用法: # bash stop-tasks.sh stop # 停止全部(hr1+hr2) # bash stop-tasks.sh stop hr1 # 只停 hr1 # bash stop-tasks.sh restart hr2 # 重启 hr2(先干净停,再启动) # bash stop-tasks.sh status # 查询状态(含卡死检测) # # 三层停止机制: # 1. 优雅:创建 STOP.flag 旗标 → runner/scrape 每轮检查自行退出(最多等45秒) # 2. 强杀:SIGTERM → 3秒 → SIGKILL,父runner和子scrape都杀 # 3. 验证:杀完复查 ps,任何残留即报错;同时清理旗标防止影响下次启动 set -u STATE_DIR=/opt/data/logs/recruitment/state LOG_DIR=/opt/data/logs/recruitment SCRIPTS=/opt/data/skills/recruitment-resume-screener/scripts STOP_FLAG=$STATE_DIR/STOP.flag mkdir -p "$STATE_DIR" # 进程发现:宽匹配(nohup/环境变量/直接参数三种启动方式都能覆盖) find_pids() { # $1=acct 或空=全部 local pat="daily-runner|zhaopin-scrape" [ -n "${1:-}" ] && pat="daily-runner.*$1|ACCOUNT=$1" ps aux | grep -E "$pat" | grep -v -E "grep|stop-tasks" | awk '{print $2}' } kill_wait() { # $@ = pids [ $# -eq 0 ] && return 0 kill "$@" 2>/dev/null || true for i in 1 2 3; do sleep 1; ps -p "$@" >/dev/null 2>&1 || return 0; done kill -9 "$@" 2>/dev/null || true sleep 1 } verify_none() { # $1=acct 或空;返回0=干净 local left left=$(find_pids "${1:-}") if [ -n "$left" ]; then echo "❌ 仍有残留进程: $(echo $left | tr '\n' ' ')" echo "$left" | while read -r p; do ps -p "$p" -o pid,stat,etime,cputime,cmd --no-headers 2>/dev/null done return 1 fi return 0 } do_stop() { local target="${1:-}" echo "🛑 停止筛选任务${target:+($target)}..." touch "$STOP_FLAG" # 第1层:优雅退出信号 local pids; pids=$(find_pids "$target") if [ -n "$pids" ]; then echo " 等待进程优雅退出(最多45秒): $(echo $pids | tr '\n' ' ')" for i in $(seq 1 45); do sleep 1 pids=$(find_pids "$target") [ -z "$pids" ] && break done fi pids=$(find_pids "$target") if [ -n "$pids" ]; then echo " 仍有进程,强制终止: $(echo $pids | tr '\n' ' ')" kill_wait $pids # 第2层:TERM→KILL fi rm -f "$STOP_FLAG" # 清旗标,不影响下次启动 if verify_none "$target"; then # 第3层:验证 echo "✅ 已全部停止,验证无残留" return 0 else echo "⚠️ 建议稍后重跑 status 确认;若仍残留,联系管理员检查" return 1 fi } do_start() { # $1=acct local acct="$1" port label [ "$acct" = "hr1" ] && { port=9223; label=Chrome; } || { port=9225; label=Edge; } # 启动前防重 local existing; existing=$(find_pids "$acct") if [ -n "$existing" ]; then echo "⚠️ $acct 已在运行(PID $(echo $existing | tr '\n' ' ')),先停止再重启" do_stop "$acct" fi # 清掉旧状态,标记新运行 rm -f "$STATE_DIR/state-$acct.json" cd "$SCRIPTS" ACCOUNT=$acct CDP_PORT=$port nohup node daily-runner.js "$acct" "$port" \ > "$LOG_DIR/runner-$acct-$(date +%F-%H%M%S).log" 2>&1 & local pid=$! sleep 3 if ps -p "$pid" >/dev/null 2>&1; then echo "✅ $acct($label) 已启动 PID=$pid 日志=$(ls -t $LOG_DIR/runner-$acct-*.log | head -1)" return 0 else echo "❌ $acct 启动失败,查看日志: $(ls -t $LOG_DIR/runner-$acct-*.log | head -1)" tail -5 "$(ls -t $LOG_DIR/runner-$acct-*.log | head -1)" return 1 fi } do_restart() { do_stop "$1" && do_start "$1" } do_status() { echo "📋 筛选任务状态($(TZ=Asia/Shanghai date '+%F %T'))" local found=0 for acct in hr1 hr2; do local pids; pids=$(find_pids "$acct") if [ -n "$pids" ]; then found=1 echo " $acct: ▶️ 运行中" echo "$pids" | while read -r p; do local info info=$(ps -p "$p" -o pid,stat,etime,cputime,cmd --no-headers 2>/dev/null | cut -c1-120) # 卡死检测:cputime 为 00:00 且 etime>10min local et cpu et=$(echo "$info" | awk '{print $3}'); cpu=$(echo "$info" | awk '{print $4}') local mark="" [ "$cpu" = "00:00:00" ] && case "$et" in *:*:*) mark=" ⚠️疑似卡死(cputime=0)";; esac echo " $info$mark" done local st; st=$(cat "$STATE_DIR/state-$acct.json" 2>/dev/null || echo "无状态文件") echo " 状态: $(echo "$st" | head -c 200)" else echo " $acct: ⏹️ 未运行(状态: $(cat "$STATE_DIR/state-$acct.json" 2>/dev/null | head -c 120 || echo '无'))" fi done [ "$found" = "0" ] && echo " (全部停止)" [ -f "$STOP_FLAG" ] && echo " ⚠️ 存在停止旗标 STOP.flag(新批次将被阻止启动)" } case "${1:-status}" in stop) do_stop "${2:-}" ;; start) do_start "${2:?用法: start hr1|hr2}" ;; restart) do_restart "${2:?用法: restart hr1|hr2}" ;; status) do_status ;; *) echo "用法: $0 stop [hr1|hr2] | start hr1|hr2 | restart hr1|hr2 | status"; exit 1 ;; esac