/*
 * thread_ops.c - 线程操作与 ptrace 基础操作公共模块
 *
 * 包含：目标进程的停/续控制（PTRACE_INTERRUPT / CONT / GETREGSET /
 * SETREGSET）、SIGCHLD 处理与子进程事件、infcall 前的全线程冻结
 * （stop_all_threads / resume_all_threads，经 /proc/<pid>/task 逐个
 * PTRACE_INTERRUPT 兄弟线程），以及线程排障命令 threads / resume <tid>
 *（原独立 threads_opt.c 并入）。
 */

#include "lnxall_trace.h"

volatile sig_atomic_t g_sigchld_ready = 0;

/* ---- SIGCHLD 水平触发通知（self-pipe）-------------------------------
 * 标准 SIGCHLD 不排队：某 tracee 的停止事件到达时，若调试器恰在处理上一
 * 条事件（sigchld_handler 执行窗口 / process_child_events 消费期间），该
 * SIGCHLD 会被内核合并丢弃，g_sigchld_ready 边沿标志保持 0。若 REPL 空闲
 * 只阻塞在 select() 而无 waitpid 被唤醒，该停止事件会滞留在 wait 队列无人
 * 消费（现象：线程长期 State=t，敲回车/空输入无效，直到下次执行命令由
 * waitpid(-1) 循环顺带收走放行，或 detach/退出释放）。
 * 用 self-pipe 提供"水平触发"：信号 handler 只向非阻塞管道写 1 字节，REPL
 * 以 select() 同时监听 stdin 与管道读端——只要还有未消费的停止事件，管道
 * 就保持可读并立即唤醒一次消费循环；事件消费（process_child_events 收光
 * wait 队列）后再读空管道复位。期间新到达的字节留在管道驱动下一次唤醒，
 * 事件不再丢失。
 * 注意（重要陷阱）：内核 do_notify_parent_cldstop() 发送 SIGCHLD 与原因
 * 无关，SIGCHLD 一旦设了 SA_NOCLDSTOP，ptrace-stop 的 CLD_TRAPPED 也一并
 * 被抑制（只剩 __wake_up_parent 唤醒阻塞中的 waitpid）。因此 SIGCHLD 的
 * sigaction 严禁设 SA_NOCLDSTOP（见 main.c 安装处）；REPL 内层 select 另设
 * 200ms 超时轮询 wait 队列兜底，双保险保证迟到/无主 stop 必定被消费放行。
 * ------------------------------------------------------------------ */
static int s_notify_pipe[2] = { -1, -1 };

int sigchld_notify_init(void)
{
    if (pipe(s_notify_pipe) == -1)
        return -1;
    for (int i = 0; i < 2; i++) {
        int fl = fcntl(s_notify_pipe[i], F_GETFL, 0);
        if (fl != -1)
            fcntl(s_notify_pipe[i], F_SETFL, fl | O_NONBLOCK);
    }
    return 0;
}

int sigchld_notify_fd(void)
{
    return s_notify_pipe[0];
}

/* 读空通知管道，复位水平触发。在 process_child_events() 收光 wait 队列后
 * 调用；期间新到达的写入会留在管道中，由下一次 select 唤醒，不丢事件。 */
void sigchld_notify_drain(void)
{
    char buf[64];
    while (read(s_notify_pipe[0], buf, sizeof(buf)) > 0) { }
}

/* ========================================================================
 * Core Ptrace Operations
 * ======================================================================== */

int ptrace_stop_target(pid_t pid)
{
    DBG("ptrace_stop: PTRACE_INTERRUPT pid=%d", pid);
    if (ptrace(PTRACE_INTERRUPT, pid, NULL, NULL) == -1) {
        perror("PTRACE_INTERRUPT"); return -1;
    }
    int status;
    while (1) {
        pid_t w = waitpid(pid, &status, 0);
        if (w == -1) {
            if (errno == EINTR) { process_child_events(); continue; }
            perror("waitpid"); return -1;
        }
        if (WIFSTOPPED(status)) {
            int sig = WSTOPSIG(status);
            DBG("ptrace_stop: pid=%d stopped, sig=%d, status=0x%x",
                pid, sig, status);
            if (sig == SIGTRAP) {
                unsigned int ev = (unsigned int)status >> 16;
                /* PTRACE_EVENT_STOP = our PTRACE_INTERRUPT took effect */
                if (ev == PTRACE_EVENT_STOP) {
                    DBG("ptrace_stop: got PTRACE_EVENT_STOP, target ready");
                    break;
                }
                /* The target may have hit our injected call-return breakpoint
                 * (BRK → SIGTRAP with ev==0) before the INTERRUPT landed.
                 * It is already stopped and usable — do NOT run it again or we
                 * would re-trigger the breakpoint forever. */
                if (g_dbg.call_in_progress && pid == g_dbg.target_pid &&
                    ev == 0) {
                    arch_regs_t cur;
                    if (arch_get_regs(pid, &cur) == 0 &&
                        call_hit_break(&cur, g_dbg.breakpoint_addr)) {
                        DBG("ptrace_stop: caught async call return-breakpoint, "
                            "target already stopped");
                        break;
                    }
                }
                /* Other ptrace event (fork, clone, etc.) — handle and wait again */
                DBG("ptrace_stop: ptrace event 0x%x, handling...", ev);
                handle_child_status(pid, status);
                continue;
            }
            /* Signal delivery — reinject and wait */
            DBG("ptrace_stop: reinjecting signal %d and waiting", sig);
            ptrace(PTRACE_CONT, pid, NULL, (void *)(long)sig);
            continue;
        }
        if (WIFEXITED(status) || WIFSIGNALED(status)) {
            fprintf(stderr, "Error: Target %d exited\n", pid); return -1;
        }
    }
    return 0;
}

int ptrace_continue_target(pid_t pid)
{
    if (ptrace(PTRACE_CONT, pid, NULL, NULL) == -1) {
        perror("PTRACE_CONT"); return -1;
    }
    return 0;
}

/**
 * @brief 冻结目标：停止主线程并暂停全部兄弟线程
 *
 * 各 d/m/call 命令在读取目标内存或寄存器前统一调用，
 * 保证读取期间目标线程状态一致。
 *
 * @return 0 成功，-1 停止主线程失败
 */
int target_freeze(void)
{
    if (ptrace_stop_target(g_dbg.target_pid) != 0) return -1;
    stop_all_threads();
    return 0;
}

/**
 * @brief 解冻目标：恢复全部兄弟线程并继续主线程
 */
void target_thaw(void)
{
    resume_all_threads();
    ptrace_continue_target(g_dbg.target_pid);
}

/**
 * @brief 冻结目标后读取内存，读取完成立即解冻
 *
 * 适用于"仅需一次读内存"的场景，错误路径也保证解冻。
 *
 * @param[in]  addr 目标进程虚拟地址
 * @param[out] buf  读取缓冲区
 * @param[in]  len  读取字节数
 * @return 0 成功，-1 冻结失败或读取失败
 */
int target_read_mem(unsigned long addr, void *buf, size_t len)
{
    if (target_freeze() != 0) return -1;
    int rc = mem_read(g_dbg.target_pid, addr, buf, len);
    target_thaw();
    return rc;
}

/* get_regs/set_regs 已移入 arch_port.c（arch_get_regs/arch_set_regs），
 * 本文件直接使用 arch_ 接口。 */

/* ========================================================================
 * SIGCHLD & Child Process Handling
 * ======================================================================== */

void sigchld_handler(int sig)
{
    (void)sig;
    g_sigchld_ready = 1;
    /* self-pipe 水平触发：写失败（EAGAIN=管道已有数据）无需处理——管道有
     * 数据即足以保证 select 唤醒一次消费循环。 */
    if (s_notify_pipe[1] >= 0) {
        char c = 'S';
        ssize_t n = write(s_notify_pipe[1], &c, 1);
        (void)n;
    }
}

/* 判断 tid 是否仍处于"本次冻结管理列表"（stop_all_threads 登记的
 * thread_list）。仅在 thread_list 内的线程，其 PTRACE_EVENT_STOP /
 * SIGTRAP 停止由随后的 resume_all_threads() 统一恢复，此处不应提前放行。
 * 注意只判断 thread_list、不含 new_threads：
 *   - new_threads 语义是"call 期间保持运行"的线程，本不该被停；它出现
 *     EVENT_STOP/SIGTRAP 说明是迟到 INTERRUPT/信号，必须放行；
 *   - new_threads 有登记残留（常驻线程被登记后只有 exit 或下次
 *     stop_all_threads 才摘除），若把它算作"有管理者"，REPL 空闲期到达的
 *     迟到 stop 会被抑制放行，而空闲期不会运行 resume_all_threads()，
 *     线程将永久冻结在 ptrace-stop（'t'），对应现象：State=t 且 TracerPid
 *     长期保持（threads 命令中 tag 为 new_threads 的卡死线程）。
 *   - stop_all_threads 每次先清空 new_threads（=0）再枚举登记到
 *     thread_list，故本次真正要冻结的集合只存在于 thread_list。 */
static int tid_in_thread_list(pid_t tid)
{
    for (int i = 0; i < g_dbg.thread_count; i++)
        if (g_dbg.thread_list[i] == tid) return 1;
    return 0;
}

void handle_child_status(pid_t pid, int status)
{
    DBG("child_event: pid=%d status=0x%x", pid, status);
    if (!WIFSTOPPED(status)) {
        /* 线程/进程退出终态（WIFEXITED/WIFSIGNALED）。高频线程池场景下
         * 短命线程在 CLONE 时登记进 new_threads，若其在下次 stop_all_threads
         * 整体清空前退出，此处必须同步摘除，否则死 tid 会堆积直至
         * MAX_THREADS 耗尽、后续新线程不再登记而失去统一管理。 */
        if (pid == g_dbg.target_pid) {
            fprintf(stderr,
                    "Error: target process %d has exited (status=0x%x)\n",
                    pid, status);
            g_dbg.is_attached    = 0;
            g_dbg.target_pid     = 0;
            g_dbg.thread_count   = 0;
            g_dbg.new_thread_count = 0;
            return;
        }
        int removed = 0;
        for (int i = 0; i < g_dbg.new_thread_count; i++) {
            if (g_dbg.new_threads[i] == pid) {
                g_dbg.new_threads[i] = g_dbg.new_threads[g_dbg.new_thread_count - 1];
                g_dbg.new_thread_count--;
                removed = 1;
                break;
            }
        }
        for (int i = 0; i < g_dbg.thread_count; i++) {
            if (g_dbg.thread_list[i] == pid) {
                g_dbg.thread_list[i] = g_dbg.thread_list[g_dbg.thread_count - 1];
                g_dbg.thread_count--;
                break;
            }
        }
        DBG("child_event: pid=%d exited (%s; new_threads now %d, "
            "thread_list now %d)", pid,
            removed ? "managed thread" : "unmanaged child",
            g_dbg.new_thread_count, g_dbg.thread_count);
        /* Remove exited child from list (children[] 目前从不登记，保留该
         * 逻辑以兼容未来 fork 子进程的登记) */
        for (int i = 0; i < g_dbg.child_count; i++) {
            if (g_dbg.children[i].pid == pid) {
                g_dbg.children[i] = g_dbg.children[g_dbg.child_count - 1];
                g_dbg.child_count--;
                break;
            }
        }
        return;
    }

    unsigned int event = (unsigned int)status >> 16;
    if (event == PTRACE_EVENT_STOP) {
        /* PTRACE_EVENT_STOP = tracee 被 PTRACE_INTERRUPT 停住。若该事件到达
         * 时线程已不在本次冻结管理列表 thread_list（stop_all_threads 确认
         * 预算耗尽前线程未回到用户态、INTERRUPT 在其后迟到生效，而
         * resume_all_threads 的 CONT 早已对运行中的线程落空为 no-op），
         * 此处是唯一还能恢复它的时机——直接 CONT 放行。leader 或仍在
         * thread_list 中的线程保持停止，由各自的停止管理方 /
         * resume_all_threads() 恢复。new_threads 登记残留不作为抑制依据
         *（见 tid_in_thread_list 注释），否则空闲期到达的迟到 stop 无人
         * 放行，线程永久停在 't'。 */
        if (pid != g_dbg.target_pid && !tid_in_thread_list(pid)) {
            DBG("child_event: releasing unmanaged stop of thread %d "
                "(delayed PTRACE_INTERRUPT)", pid);
            ptrace(PTRACE_CONT, pid, NULL, NULL);
        }
        return;
    }

    /* A plain SIGTRAP stop not tied to any ptrace event.  In the synchronous
     * call design this is never the injected return-breakpoint — that is
     * reaped and completed inside call_wait_for_return().  Any such stop
     * arriving here (e.g. a hardware/software breakpoint of another tracer)
     * must NOT be resumed, or the signal would be lost and the target would
     * spin on the same instruction.  Leave it stopped and report it. */
    if (WSTOPSIG(status) == SIGTRAP && event == 0) {
        arch_regs_t cur;
        if (arch_get_regs(pid, &cur) == 0)
            printf("[+] pid %d stopped on SIGTRAP at pc=0x%llx\n",
                   pid, (unsigned long long)arch_reg_pc(&cur));
        else
            printf("[+] pid %d stopped on SIGTRAP\n", pid);
        fflush(stdout);
        /* 兄弟/新线程且无本次冻结管理者（REPL 空闲）：本工具不设常驻断点，
         * 注入的返回断点仅在 call 进行中存在且由管理方（stop_all_threads/
         * resume_all_threads）负责，能走到这里说明信号已无归属，放行恢复
         * 运行，否则线程停在 't' 无人能救回（new_threads 登记残留同样放行，
         * 见 tid_in_thread_list 注释）。leader 的 SIGTRAP 保持停止，
         * 由 call 管理方 / ptrace_stop_target 负责。 */
        if (pid != g_dbg.target_pid && !tid_in_thread_list(pid))
            ptrace(PTRACE_CONT, pid, NULL, NULL);
        return;   /* leader / managed threads: stays stopped */
    }

    if (event == PTRACE_EVENT_FORK || event == PTRACE_EVENT_VFORK ||
        event == PTRACE_EVENT_CLONE) {
        unsigned long child_pid = 0;
        if (ptrace(PTRACE_GETEVENTMSG, pid, NULL, &child_pid) == 0 && child_pid > 0) {
            if (event == PTRACE_EVENT_CLONE) {
                /* 同进程新线程（pthread_create）：必须保留跟踪——一旦 detach，
                 * 新线程会自由执行共享代码并命中注入的断点（BRK），SIGTRAP
                 * 无处理器直接杀死目标进程（status=0x85）。只保留 fork/clone
                 * 事件选项（刻意不设 TRACEEXEC/TRACEEXIT，防 exec/exit 事件
                 * 堆积），登记到 new_threads 并在 call 期间保持运行。 */
                ptrace(PTRACE_SETOPTIONS, (pid_t)child_pid, NULL,
                       (void *)(PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK |
                                PTRACE_O_TRACECLONE));
                if (g_dbg.new_thread_count < MAX_THREADS) {
                    int known = 0;
                    for (int i = 0; i < g_dbg.new_thread_count; i++) {
                        if (g_dbg.new_threads[i] == (pid_t)child_pid) { known = 1; break; }
                    }
                    if (!known)
                        g_dbg.new_threads[g_dbg.new_thread_count++] = (pid_t)child_pid;
                }
                ptrace(PTRACE_CONT, (pid_t)child_pid, NULL, NULL);
            } else {
                /* 独立子进程（fork/vfork，如 system/popen）：与注入调用无关，
                 * 直接 DETACH 让其自由运行。保留跟踪只会让子进程的每个信号
                 * （SIGCHLD 等）都上报停止事件，call 期间被 waitpid(-1) 逐个
                 * 处理形成事件风暴，且子孙进程无限递归累积（实机每次 call
                 * 都打印 "[+] Child attached" 且 call 明显变慢）。detach 后
                 * 这些事件全部消失，也杜绝 1317 式永久冻结。 */
                DBG("\n[+] Child forked: PID %lu (parent %d) — detached\n",
                       child_pid, pid);
                fflush(stdout);
                ptrace(PTRACE_DETACH, (pid_t)child_pid, NULL, NULL);
            }
        }
        ptrace(PTRACE_CONT, pid, NULL, NULL);
    } 
    else {
        ptrace(PTRACE_CONT, pid, NULL, NULL);
    }
}

/* call_wait_for_return 轮询期间，waitpid 返回兄弟/新线程停止事件时的
 * 统一处理：不丢弃事件，否则 infcall 期间新建的线程（pthread_create）
 * 会永久卡死。
 * 参考 gdb 的 proceed() 语义：infcall 执行期间所有线程保持运行，兄弟
 * 线程的停止事件（普通信号）一律注入恢复运行；命中 SIGTRAP 时保持停止
 * 并报告（不注入，避免丢失断点/陷阱信息）。挂起的兄弟线程会在 call
 * 完成/超时后由 stop_all_threads + resume_all_threads 统一接管。
 * 该函数不消耗 call 超时计数。 */
void handle_side_thread_stop(pid_t tid, int status)
{
    int known = 0;
    for (int i = 0; i < g_dbg.new_thread_count; i++) {
        if (g_dbg.new_threads[i] == tid) { known = 1; break; }
    }
    if (!known && g_dbg.new_thread_count < MAX_THREADS)
        g_dbg.new_threads[g_dbg.new_thread_count++] = tid;

    if (!WIFSTOPPED(status)) {
        DBG("side_thread: tid %d exited/killed, nothing to resume", tid);
        /* 退出线程可能已登记进 new_threads（CLONE 后立刻退出，终态事件
         * 由 stop_all_threads/call 的 waitpid(-1) 循环经此到达）：同步摘除，
         * 避免死 tid 堆积到 MAX_THREADS 后新线程不再登记 */
        for (int i = 0; i < g_dbg.new_thread_count; i++) {
            if (g_dbg.new_threads[i] == tid) {
                g_dbg.new_threads[i] = g_dbg.new_threads[g_dbg.new_thread_count - 1];
                g_dbg.new_thread_count--;
                break;
            }
        }
        return;
    }

    unsigned int ev = (unsigned int)status >> 16;
    if (ev != 0) {
        /* fork/clone/exec/exit 事件：交 handle_child_status 统一处理 */
        handle_child_status(tid, status);
        return;
    }
    if (WSTOPSIG(status) == SIGTRAP) {
        /* 命中 SIGTRAP：保持停止并报告，避免丢失断点信号 */
        arch_regs_t cur;
        if (arch_get_regs(tid, &cur) == 0)
            printf("[+] thread %d stopped on SIGTRAP at pc=0x%llx\n",
                   tid, (unsigned long long)arch_reg_pc(&cur));
        else
            printf("[+] thread %d stopped on SIGTRAP\n", tid);
        fflush(stdout);
        return;
    }

    int sig = WSTOPSIG(status);
    if (sig == SIGSTOP || sig == SIGTSTP || sig == SIGTTIN || sig == SIGTTOU)
        sig = 0;   /* 初始停止/暂停类信号：不注入，否则线程会再次停止 */
    DBG("side_thread: resuming thread %d with signal %d", tid, sig);
    ptrace(PTRACE_CONT, tid, NULL, (void *)(long)sig);
}

void process_child_events(void)
{
    int status;
    pid_t wpid;
    g_sigchld_ready = 0;
    while ((wpid = waitpid(-1, &status, WNOHANG | __WALL)) > 0) {
        DBG("sighandler: child pid=%d ready, status=0x%x", wpid, status);
        handle_child_status(wpid, status);
    }
}

/* ---- Multi-thread support for infcalls -------------------------------
 * gdb proceed() semantics: during an infcall only the leader runs the
 * injected function while every sibling keeps RUNNING — freezing a sibling
 * that holds a lock the callee needs would deadlock, and rewinding its PC
 * on timeout would leave the lock permanently held.  We therefore freeze
 * all siblings (stop_all_threads) only at the moment the return-breakpoint
 * fires or on abort, and resume_all_threads() (or detach) releases them.
 * PTRACE_INTERRUPT only stops the thread it is sent to, so we enumerate
 * /proc/<pid>/task and stop each sibling individually. */

int stop_all_threads(void)
{
    g_dbg.thread_count = 0;
    g_dbg.new_thread_count = 0;   /* 新线程会重新经 /proc/<pid>/task 枚举 */
    char path[64];
    snprintf(path, sizeof(path), "/proc/%d/task", g_dbg.target_pid);
    DIR *d = opendir(path);
    if (!d) {
        DBG("stop_all_threads: cannot open %s (%s)", path, strerror(errno));
        return 0;   /* single-threaded fallback */
    }

    struct dirent *de;
    while ((de = readdir(d)) != NULL) {
        if (de->d_name[0] == '.') continue;
        pid_t tid = (pid_t)atoi(de->d_name);
        if (tid <= 0 || tid == g_dbg.target_pid) continue;  /* leader already stopped */
        if (g_dbg.thread_count >= MAX_THREADS) break;
        if (ptrace(PTRACE_INTERRUPT, tid, NULL, NULL) == -1) {
            if (errno == ESRCH) {
                /* Not traced yet — PTRACE_SEIZE (like do_attach) may not
                 * have attached every sibling.  Attach it now so a running
                 * sibling can never hit our injected BRK (shared lib code)
                 * as an untraced thread — that would deliver SIGTRAP as a
                 * fatal signal and kill the process (the exact failure we
                 * saw: status=0x85). */
                /* 与 do_attach 保持一致：不设 PTRACE_O_TRACEEXIT，避免线程
                 * 退出多一段 EVENT_EXIT event-stop（t 滞留窗口 + 事件风暴） */
                if (ptrace(PTRACE_SEIZE, tid, NULL,
                           (void *)(PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK |
                                    PTRACE_O_TRACECLONE | PTRACE_O_TRACEEXEC)) == 0) {
                    if (ptrace(PTRACE_INTERRUPT, tid, NULL, NULL) == -1) {
                        DBG("stop_all_threads: INTERRUPT tid=%d: %s",
                            tid, strerror(errno));
                        continue;
                    }
                    g_dbg.thread_list[g_dbg.thread_count++] = tid;
                    continue;
                }
                DBG("stop_all_threads: SEIZE tid=%d: %s", tid, strerror(errno));
                continue;
            }
            /* Already stopped (EIO): INTERRUPT only succeeds on running
             * threads; EIO means the sibling is already in a ptrace stop
             * (e.g. a signal-delivery stop).  Register it unconditionally:
             * relying on a WNOHANG waitpid here is racy — the stop event
             * may already have been consumed by handle_side_thread_stop()
             * (via the waitpid(-1) in call_wait_for_return), in which case
             * waitpid() returns 0 and the thread would be dropped from the
             * list and never resumed by resume_all_threads(), freezing it
             * in tracing stop forever.  CONT on an exited/untraced tid
             * merely fails harmlessly with ESRCH/EIO. */
            if (g_dbg.thread_count < MAX_THREADS)
                g_dbg.thread_list[g_dbg.thread_count++] = tid;
            DBG("stop_all_threads: INTERRUPT tid=%d: %s", tid, strerror(errno));
            continue;
        }
        g_dbg.thread_list[g_dbg.thread_count++] = tid;
    }
    closedir(d);

    /* Collect the INTERRUPT stops with a single shared budget instead of a
     * serial per-thread wait.  A sibling blocked in kernel space (futex,
     * ep_poll, IO, nanosleep) only stops once it returns to user space, so
     * a per-thread 5 s poll serializes into 5 s x N — the "call takes 10s+"
     * regression seen on the target after switching to waitpid(-1).  One
     * budget caps the whole freeze; siblings that do not confirm in time are
     * harmless because resume_all_threads() CONTs them (CONT on a running
     * tracee is a no-op, and on a stopped one it just resumes it). */
    int confirmed = 0;
    int budget = 100;                       /* 100 x 10 ms = 1 s total */
    while (confirmed < g_dbg.thread_count && budget-- > 0) {
        int st;
        pid_t w = waitpid(-1, &st, WNOHANG | __WALL);
        if (w == -1) {
            if (errno == EINTR) continue;
            break;
        }
        if (w == 0) {
            usleep(10000);
            continue;
        }
        int known = 0;
        for (int i = 0; i < g_dbg.thread_count; i++) {
            if (g_dbg.thread_list[i] == w) { known = 1; break; }
        }
        if (known) {
            confirmed++;   /* stop event consumed; the thread stays stopped */
            continue;
        }
        /* Event from some other tracee (a stray child): consume it now so it
         * cannot wedge later; benign signals are reinjected, SIGTRAP stays
         * stopped for 'c' to resume. */
        handle_side_thread_stop(w, st);
    }
    DBG("stop_all_threads: stopped %d/%d sibling thread(s)",
        confirmed, g_dbg.thread_count);
    return 0;
}

void resume_all_threads(void)
{
    for (int i = 0; i < g_dbg.thread_count; i++) {
        if (ptrace(PTRACE_CONT, g_dbg.thread_list[i], NULL, NULL) == -1)
            DBG("resume_all_threads: CONT tid=%d: %s",
                g_dbg.thread_list[i], strerror(errno));
    }
    g_dbg.thread_count = 0;
    /* infcall 期间新建线程同样恢复运行 */
    for (int i = 0; i < g_dbg.new_thread_count; i++) {
        if (ptrace(PTRACE_CONT, g_dbg.new_threads[i], NULL, NULL) == -1)
            DBG("resume_all_threads: CONT newtid=%d: %s",
                g_dbg.new_threads[i], strerror(errno));
    }
    g_dbg.new_thread_count = 0;

    /* 枚举式兜底：任何仍处于 ptrace-stop 的兄弟线程都必须恢复。登记列表
     * 可能遗漏已停线程——stop_all_threads 的确认预算（1s）耗尽前线程未
     * 回到用户态时，INTERRUPT 会在其后才生效，而上面的 CONT 对运行中的
     * 线程是 no-op（PTRACE_CONT 对未停止 tracee 返回 EIO），从而漏掉该
     * 线程。此处全量 /proc/<pid>/task 枚举 CONT：对仍在运行的线程无害
     * （no-op），对停住的一律恢复。真正"INTERRUPT 在途、此刻尚未停止"的
     * 线程会在稍后进入 PTRACE_EVENT_STOP，由 handle_child_status 的
     * "无主 stop 放行"分支兜底，故不存在恢复死角。 */
    char path[64];
    snprintf(path, sizeof(path), "/proc/%d/task", g_dbg.target_pid);
    DIR *d = opendir(path);
    if (!d) return;
    struct dirent *de;
    while ((de = readdir(d)) != NULL) {
        if (de->d_name[0] == '.') continue;
        pid_t tid = (pid_t)atoi(de->d_name);
        if (tid <= 0 || tid == g_dbg.target_pid) continue;
        if (ptrace(PTRACE_CONT, tid, NULL, NULL) == -1) {
            if (errno != ESRCH)
                DBG("resume_all_threads: sweep CONT tid=%d: %s",
                    tid, strerror(errno));
        }
    }
    closedir(d);
}

/* ========================================================================
 * 线程排障命令：threads / resume <tid>（并入自原 threads_opt.c）
 *
 * threads：dump tracer 内部线程登记表（thread_list/new_threads）并与
 *          /proc/<pid>/task 实测 State/TracerPid 对照，一眼定位"某 tid
 *          卡在 't' 到底是谁的登记/漏放行"：
 *            - thread_list(frozen)：stop_all_threads 冻结后未 resume（命令
 *              中断/异常路径漏调 resume_all_threads）；
 *            - new_threads：call 期间 clone 登记，call 结束应被统一 CONT；
 *            - untracked：未登记却停住 → 迟到事件无主，若 REPL 空闲应由
 *              handle_child_status 的自动放行分支兜底（预期即恢复运行）。
 * resume <tid>：手动 PTRACE_CONT 放行指定卡死线程（排障兜底），并从冻结
 *               登记表摘除，防止后续 resume_all_threads 二次 CONT（无害，
 *               但摘除后 threads 输出更接近真实状态）。
 * ======================================================================== */

/* 读取 /proc/<tid>/status 的 State 与 TracerPid；文件不存在视为已退出 */
static void proc_tid_state(pid_t tid, char *out_state, int out_len,
                           pid_t *tracer)
{
    char path[64], line[256];
    *tracer = 0;
    snprintf(path, sizeof(path), "/proc/%d/status", tid);
    FILE *f = fopen(path, "r");
    if (!f) {
        snprintf(out_state, out_len, "exited");
        return;
    }
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, "State:", 6) == 0) {
            char *p = line + 6;
            while (*p == ' ' || *p == '\t') p++;
            snprintf(out_state, out_len, "%s", p);
            char *nl = strchr(out_state, '\n');
            if (nl) *nl = '\0';
        } else if (strncmp(line, "TracerPid:", 10) == 0) {
            *tracer = (pid_t)atoi(line + 10);
        }
    }
    fclose(f);
}

int cmd_threads_dump(void)
{
    if (!g_dbg.is_attached) {
        printf("Not attached.\n"); return -1;
    }
    printf("Thread registry (target %d, tracer %d):\n",
           g_dbg.target_pid, getpid());
    printf("  thread_list[%d]:", g_dbg.thread_count);
    if (g_dbg.thread_count == 0) printf(" (none)");
    for (int i = 0; i < g_dbg.thread_count; i++)
        printf(" %d", g_dbg.thread_list[i]);
    printf("\n  new_threads[%d]:", g_dbg.new_thread_count);
    if (g_dbg.new_thread_count == 0) printf(" (none)");
    for (int i = 0; i < g_dbg.new_thread_count; i++)
        printf(" %d", g_dbg.new_threads[i]);
    printf("\n  children[%d]:", g_dbg.child_count);
    if (g_dbg.child_count == 0) printf(" (none)");
    for (int i = 0; i < g_dbg.child_count; i++)
        printf(" %d", g_dbg.children[i].pid);
    printf("\n");

    char path[64];
    snprintf(path, sizeof(path), "/proc/%d/task", g_dbg.target_pid);
    DIR *d = opendir(path);
    if (!d) {
        printf("  (cannot open %s: %s)\n", path, strerror(errno));
        return 0;
    }
    printf("  live threads (State | TracerPid | registry):\n");
    struct dirent *de;
    while ((de = readdir(d)) != NULL) {
        if (de->d_name[0] == '.') continue;
        pid_t tid = (pid_t)atoi(de->d_name);
        if (tid <= 0) continue;
        char st[64];
        pid_t tp;
        proc_tid_state(tid, st, sizeof(st), &tp);
        const char *tag = "untracked";
        if (tid == g_dbg.target_pid)
            tag = "leader";
        else {
            for (int i = 0; i < g_dbg.thread_count; i++)
                if (g_dbg.thread_list[i] == tid) { tag = "thread_list(frozen)"; break; }
            if (tag[0] == 'u')
                for (int i = 0; i < g_dbg.new_thread_count; i++)
                    if (g_dbg.new_threads[i] == tid) { tag = "new_threads"; break; }
        }
        printf("    %-7d %-40s tracer=%-6d %s\n", tid, st, tp, tag);
    }
    closedir(d);
    return 0;
}

int cmd_resume_tid(pid_t tid)
{
    if (!g_dbg.is_attached) {
        printf("Not attached.\n"); return -1;
    }
    if (tid <= 0) {
        printf("Usage: resume <tid>\n"); return -1;
    }
    if (ptrace(PTRACE_CONT, tid, NULL, NULL) == -1) {
        printf("Error: CONT tid %d: %s\n", tid, strerror(errno));
        return -1;
    }
    /* 从冻结登记表摘除，保持 threads 输出与真实状态一致 */
    for (int i = 0; i < g_dbg.thread_count; i++) {
        if (g_dbg.thread_list[i] == tid) {
            g_dbg.thread_list[i] = g_dbg.thread_list[g_dbg.thread_count - 1];
            g_dbg.thread_count--;
            break;
        }
    }
    for (int i = 0; i < g_dbg.new_thread_count; i++) {
        if (g_dbg.new_threads[i] == tid) {
            g_dbg.new_threads[i] = g_dbg.new_threads[g_dbg.new_thread_count - 1];
            g_dbg.new_thread_count--;
            break;
        }
    }
    printf("Resumed tid %d (removed from freeze registry).\n", tid);
    return 0;
}
