/* spinner.c - "思考中"指示器实现 */ #include "spinner.h" #include "i18n.h" #include #include #include #include #include #include #define DIM "\033[2m" #define OFF "\033[0m" static pthread_t g_thread; static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t g_cond; static pthread_once_t g_cond_once = PTHREAD_ONCE_INIT; static volatile int g_running; static int g_started; static int g_enabled; static char g_label[64]; /* 当前这段等待的起点. 换标签时重置, 于是秒数反映的是"这个阶段等了多久": * 先看到"等待响应 (1s)", 切成"思考中"后重新从 0 数起. */ static struct timespec g_t0; /* 条件变量必须显式指定用 CLOCK_MONOTONIC. * * pthread_cond_timedwait 默认按 CLOCK_REALTIME 解释超时, 而我们算出来的 * 是 CLOCK_MONOTONIC 的绝对时间. 开机时长 (几十万秒)永远小于 epoch 时间 * (十几亿秒), 于是"超时时刻"永远被当成已经过去 —— timedwait 立刻返回, * worker 变成疯狂重绘的忙循环. */ static void init_cond(void) { pthread_condattr_t attr; pthread_condattr_init(&attr); pthread_condattr_setclock(&attr, CLOCK_MONOTONIC); pthread_cond_init(&g_cond, &attr); pthread_condattr_destroy(&attr); } static void *worker(void *arg) { (void)arg; static const char *dots[] = { ".", "..", "...", "...." }; int i = 0; struct timespec next; clock_gettime(CLOCK_MONOTONIC, &next); for (;;) { struct timespec now; long elapsed; pthread_mutex_lock(&g_lock); if (!g_running) { pthread_mutex_unlock(&g_lock); break; } clock_gettime(CLOCK_MONOTONIC, &now); elapsed = (long)(now.tv_sec - g_t0.tv_sec); fprintf(stderr, "\r\033[K%s%s%s%s", DIM, g_label, dots[i], OFF); /* 等得越久越该说明"还活着", 超过 5 秒就把秒数亮出来 */ if (elapsed >= 5) fprintf(stderr, " %s(%lds)%s", DIM, elapsed, OFF); fflush(stderr); i = (i + 1) % 4; /* 睡到下一帧. 这里必须用条件变量定时等, 不能 nanosleep: * spinner_stop() 要 pthread_join, nanosleep 的话就得干等这一觉 * 睡满 —— 每次收尾白搭最多 250ms, 而这笔账直接加在用户感知的 * 延迟上 (每轮模型往返、每个工具来回都要付一次). */ next.tv_nsec += 250000000L; if (next.tv_nsec >= 1000000000L) { next.tv_sec++; next.tv_nsec -= 1000000000L; } pthread_cond_timedwait(&g_cond, &g_lock, &next); pthread_mutex_unlock(&g_lock); } return NULL; } void spinner_init(int enabled) { g_enabled = enabled ? 1 : 0; } /* 调用方必须已经持有锁. 标签真的变了才重置计时, * 否则每来一片思维链都重置, "思考中 (12s)"永远停在 0. */ static void set_label_locked(const char *label) { const char *want = (label && *label) ? label : t("spinner.thinking"); if (strcmp(g_label, want) == 0) return; snprintf(g_label, sizeof g_label, "%s", want); clock_gettime(CLOCK_MONOTONIC, &g_t0); } void spinner_start(const char *label) { if (!g_enabled) return; pthread_once(&g_cond_once, init_cond); /* 保证 g_cond 用的是单调时钟 */ pthread_mutex_lock(&g_lock); if (!g_started) { /* 先把标签算成**一份副本**再写进 g_label: label 有可能就是 g_label 自己 * —— spinner_notice() 从一条重试提示里恢复动画时正是这么调的. 以前这里 * 先执行 g_label[0] = '\0', 于是要读的那个字符串被自己清空, 标签退化成 * 默认的"思考中": 每次重试退避之后, 屏幕上的"等待响应"都会变成"思考中", * 而这两段等待的含义恰恰是它要区分的. */ char want[sizeof g_label]; snprintf(want, sizeof want, "%s", (label && *label) ? label : t("spinner.thinking")); g_label[0] = '\0'; /* 让 set_label_locked 一定认成"变了" */ set_label_locked(want); /* 传副本, 不传 g_label 自己 */ g_running = 1; g_started = 1; if (pthread_create(&g_thread, NULL, worker, NULL) != 0) { g_started = 0; g_running = 0; } } pthread_mutex_unlock(&g_lock); } void spinner_set_label(const char *label) { if (!g_enabled) return; pthread_mutex_lock(&g_lock); set_label_locked(label); pthread_mutex_unlock(&g_lock); } void spinner_stop(void) { if (!g_enabled) return; pthread_mutex_lock(&g_lock); if (!g_started) { pthread_mutex_unlock(&g_lock); return; } g_running = 0; pthread_cond_signal(&g_cond); /* 立刻叫醒 worker, 别等它睡满一帧 */ pthread_mutex_unlock(&g_lock); pthread_join(g_thread, NULL); pthread_mutex_lock(&g_lock); g_started = 0; fputs("\r\033[K", stderr); /* 把动画那一行擦干净 */ fflush(stderr); pthread_mutex_unlock(&g_lock); } void spinner_notice(const char *fmt, ...) { int was_on; pthread_mutex_lock(&g_lock); was_on = g_started; pthread_mutex_unlock(&g_lock); if (was_on) spinner_stop(); va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputc('\n', stderr); fflush(stderr); if (was_on) spinner_start(g_label); }