From 2f96b050d61c6557208ed4eeded92061458c3747 Mon Sep 17 00:00:00 2001 From: WHC Date: Thu, 29 Oct 2026 11:41:16 +0800 Subject: [PATCH] perf: faster builds, leaner daemon, smoother install/run (0.9.20) Reuse libcstcore.a for tests with -MMD incremental deps; idle-backoff the parliament seat loop; defer curl init and cache settings; add systemd helper. Co-authored-by: Cursor --- .gitignore | 2 + CMakeLists.txt | 12 ++-- Makefile | 78 ++++++++++++--------- README.md | 13 ++-- docs/parliament.md | 16 +++++ include/cst_version.h | 2 +- scripts/bootstrap_linux.sh | 43 ++++++++++-- scripts/cst-agent-parliament.service.in | 21 ++++++ src/discover.c | 31 +++++++-- src/extensions.c | 7 +- src/main.c | 90 +++++++++++++++---------- src/parliament_daemon.c | 34 ++++++---- src/settings.c | 32 +++++++++ 13 files changed, 278 insertions(+), 103 deletions(-) create mode 100644 scripts/cst-agent-parliament.service.in diff --git a/.gitignore b/.gitignore index 0b2a4b5..69f3019 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.o +*.d cst-agent cst-agent.exe +libcstcore.a tests/test_* !tests/test_*.c session.jsonl diff --git a/CMakeLists.txt b/CMakeLists.txt index 3870a3f..8bb87ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -400,17 +400,13 @@ if(CST_BUILD_WEBUI) list(APPEND _CST_CORE_TESTS test_webui) endif() foreach(t ${_CST_CORE_TESTS}) - add_executable(${t} tests/${t}.c ${CST_CORE_SOURCES}) - # The core now contains src/web_md.c, which needs sashidown's header and - # archive exactly like the Makefile build does. + # Link the shared cstcore archive instead of recompiling every core .c. + add_executable(${t} tests/${t}.c) target_include_directories(${t} PRIVATE include "${CJSON_DIR}" "${SD_DIR}/include") target_compile_definitions(${t} PRIVATE _GNU_SOURCE) - target_link_libraries(${t} PRIVATE - "${SASHIDOWN_A}" CURL::libcurl Threads::Threads) + target_link_libraries(${t} PRIVATE cstcore) add_dependencies(${t} sashidown) - if(NOT WIN32) - target_link_libraries(${t} PRIVATE m) - else() + if(WIN32) target_link_libraries(${t} PRIVATE ws2_32 crypt32 secur32 bcrypt advapi32 user32) endif() cst_win_exe(${t}) diff --git a/Makefile b/Makefile index 34e1f78..2dc6099 100644 --- a/Makefile +++ b/Makefile @@ -134,8 +134,9 @@ OBJ = $(SRC:.c=.o) # the GUI links the same engine the CLI/TUI/WebUI run. No TUI, no main. CORE_OBJ = $(CORE_SRC:.c=.o) CST_CORE_LIB = libcstcore.a -HDRS = $(wildcard include/*.h) $(wildcard src/tools/*.h) $(CJSON_DIR)/cJSON.h \ - .gen/models_builtin.inc .gen/web_assets.inc +# Generated embeds; per-file .d files (from -MMD) track the rest. +GEN_INCS = .gen/models_builtin.inc .gen/web_assets.inc +DEPFILES = $(OBJ:.o=.d) TESTS = \ tests/test_ctl_frontier \ @@ -193,33 +194,29 @@ TEST_LIBS = -L$(SD_DIR) -lsashidown $(CURL_LIBS) $(RPATH_LIBS) $(TEST_SYS_LIBS) # keeps them, so the small tests link the OS layer too. TEST_OS_SRC = src/os_compat.c src/os_win.c src/util.c -.PHONY: all clean check demo sashidown-lib FORCE cmake-gcc cmake-clang asan install uninstall +.PHONY: all clean check demo sashidown-lib cmake-gcc cmake-clang asan install \ + uninstall install-service all: sashidown-lib cst-agent $(CST_CORE_LIB) -$(CST_CORE_LIB): $(CORE_OBJ) FORCE +$(CST_CORE_LIB): $(CORE_OBJ) rm -f $@ $(AR) rcs $@ $(CORE_OBJ) cmake-gcc: cmake -B build-gcc -DCMAKE_C_COMPILER=gcc -DCST_BUILD_TUI=ON - cmake --build build-gcc - ctest --test-dir build-gcc --output-on-failure + cmake --build build-gcc -j + ctest --test-dir build-gcc --output-on-failure -j cmake-clang: cmake -B build-clang -DCMAKE_C_COMPILER=clang -DCST_BUILD_TUI=ON - cmake --build build-clang + cmake --build build-clang -j -sashidown-lib: $(SD_DIR)/libsashidown.a - -# The sub-make decides whether the archive is stale; FORCE only makes sure it -# is asked, and -j builds wait for it before linking. -$(SD_DIR)/libsashidown.a: FORCE +# Always ask the sub-make; it no-ops when libsashidown.a is current. +sashidown-lib: $(MAKE) -C $(SD_DIR) libsashidown.a -FORCE: - -cst-agent: $(OBJ) $(SD_DIR)/libsashidown.a +cst-agent: $(OBJ) sashidown-lib $(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS) # Built-in model data as a C byte list (included by model_registry.c). @@ -232,11 +229,15 @@ cst-agent: $(OBJ) $(SD_DIR)/libsashidown.a @mkdir -p .gen @sh scripts/embed_web.sh $@ $(WEB_DIR) -%.o: %.c $(HDRS) - $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< +# Incremental rebuilds: -MMD tracks real includes instead of every header. +COMPILE_C = $(CC) $(CFLAGS) $(CPPFLAGS) -MMD -MP -c -o $@ $< -src/tui.o: src/tui.c $(HDRS) $(SD_DIR)/include/sashidown.h - $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< +%.o: %.c + $(COMPILE_C) + +src/model_registry.o: .gen/models_builtin.inc +src/webui.o: .gen/web_assets.inc +src/tui.o: $(SD_DIR)/include/sashidown.h tests/test_ctl_frontier: tests/test_ctl_frontier.c src/ctl_parse.c include/ctl.h $(TEST_OS_SRC) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_ctl_frontier.c src/ctl_parse.c $(TEST_OS_SRC) $(TEST_SYS_LIBS) @@ -244,7 +245,7 @@ tests/test_ctl_frontier: tests/test_ctl_frontier.c src/ctl_parse.c include/ctl.h tests/test_ctl_heredoc: tests/test_ctl_heredoc.c src/ctl_parse.c include/ctl.h $(TEST_OS_SRC) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_ctl_heredoc.c src/ctl_parse.c $(TEST_OS_SRC) $(TEST_SYS_LIBS) -tests/test_json_baseline: tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(HDRS) $(TEST_OS_SRC) +tests/test_json_baseline: tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC) $(TEST_SYS_LIBS) tests/test_replay_starvation: tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC) @@ -265,32 +266,47 @@ tests/test_pdf_text: tests/test_pdf_text.c src/pdf_text.c include/cst_pdf.h $(TE tests/test_markdown: tests/test_markdown.c $(SD_DIR)/libsashidown.a $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_markdown.c $(LDFLAGS) -lsashidown -lm -tests/test_%: tests/test_%.c $(CORE_SRC) $(HDRS) - $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< $(CORE_SRC) $(TEST_LIBS) +# Core tests reuse libcstcore.a — avoid recompiling ~35 .c files per binary. +# Do not depend on the phony sashidown-lib target or every test relinks each check. +tests/test_%: tests/test_%.c $(CST_CORE_LIB) $(SD_DIR)/libsashidown.a + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< $(CST_CORE_LIB) $(TEST_LIBS) -$(TESTS): sashidown-lib +$(SD_DIR)/libsashidown.a: sashidown-lib -check: $(TESTS) +# Build test binaries with make -j; run them one-by-one for readable logs. +check: sashidown-lib $(TESTS) @set -e; for t in $(TESTS); do echo "== $$t"; ./$$t; done -# AddressSanitizer + UBSan build of the agent and all tests. -# Under a ptrace-based sandbox (some Termux setups) LeakSanitizer aborts with -# "Tracer caught signal 11"; add ASAN_OPTIONS=detect_leaks=0 there. +# AddressSanitizer + UBSan rebuild (clears objects so sanitizer flags apply). asan: $(MAKE) clean - $(MAKE) sashidown-lib - $(MAKE) CFLAGS="-std=c11 -Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" cst-agent check + $(MAKE) CFLAGS="-std=c11 -Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" \ + sashidown-lib cst-agent check demo: cst-agent ./cst-agent --demo-ctl --workspace . clean: - rm -f $(OBJ) cst-agent $(CST_CORE_LIB) $(TESTS) .gen/models_builtin.inc \ - .gen/web_assets.inc + rm -f $(OBJ) $(DEPFILES) cst-agent $(CST_CORE_LIB) $(TESTS) \ + .gen/models_builtin.inc .gen/web_assets.inc install: cst-agent install -d "$(DESTDIR)$(PREFIX)/bin" install -m 0755 cst-agent "$(DESTDIR)$(PREFIX)/bin/cst-agent" +# Optional systemd --user unit for the parliament seat service. +install-service: install + install -d "$(DESTDIR)$(HOME)/.config/systemd/user" + sed -e 's|@BIN@|$(PREFIX)/bin/cst-agent|g' \ + scripts/cst-agent-parliament.service.in \ + > "$(DESTDIR)$(HOME)/.config/systemd/user/cst-agent-parliament.service" + @echo "Installed user unit. Enable with:" + @echo " systemctl --user daemon-reload" + @echo " systemctl --user enable --now cst-agent-parliament.service" + @echo "(set CST_REALM_PASSWORD / CST_INSTANCE_ID in the drop-in or environment)" + uninstall: rm -f "$(DESTDIR)$(PREFIX)/bin/cst-agent" + rm -f "$(HOME)/.config/systemd/user/cst-agent-parliament.service" + +-include $(DEPFILES) diff --git a/README.md b/README.md index 7c42c85..8a27899 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ irm https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/install_window 也可以下载 Release 里的安装包 `cst-agent-VERSION-windows-x86_64-setup.exe`(按用户安装、自动加 PATH、 开始菜单有 WebUI 入口),或便携版 zip。详见 [docs/README-windows.md](docs/README-windows.md)。 -一键脚本可以重复运行:已有仓库会 `git pull --ff-only`,代码有更新时干净重编,然后重新安装; -同时装一个 `cst-agent-update` 命令(等同重跑脚本)。NixOS 上会自动进 `nix-shell` 编译。 +一键脚本可以重复运行:已有仓库会 `git pull --ff-only`,**增量编译**后重新安装(`CST_AGENT_CLEAN=1` 才强制 clean); +同时装 `cst-agent-update` 与 `cst-agent-service`(议会席位服务辅助)。NixOS 上会自动进 `nix-shell` 编译。 之后再启动会沿用上次的 provider、模型和推理强度(low / medium / high / max),不需要任何参数或环境变量。 界面里用 `/provider` 切换(没保存过 key 的会当场询问),`/model`、`/effort` 切换模型和推理强度,`/key` 更换 key。 @@ -90,14 +90,15 @@ scripts/run_matrix.sh -p "native-eos native-inc ctl ctl-heredoc xml" \ ```bash cd CST-agent -make # 依赖 pkg-config 找 libcurl / ncursesw(找不到时退回 ncurses) -make check # 单元测试 + 截断实验(含真实录制流) -make asan # AddressSanitizer + UBSan 构建并跑全部测试 +make -j$(nproc) # 增量编译;头文件依赖走 -MMD,测试复用 libcstcore.a +make -j$(nproc) check # 并行编测试二进制,再串行跑 +make asan # AddressSanitizer + UBSan 构建并跑全部测试 make CC=tcc AR=ar check # TinyCC:整套构建 + 测试十几秒(CI 的校验就是这条) make install PREFIX=$HOME/.local # 或 sudo make install(默认 /usr/local) +make install-service PREFIX=$HOME/.local # 可选:systemd --user 议会服务单元 ``` -NixOS:`nix-shell`(仓库自带 [`shell.nix`](shell.nix))后再 `make`。 +NixOS:`nix-shell --run 'make -j$(nproc) && make install PREFIX=$HOME/.local'`(CLI);Qt 桌面用 `nix-build` / `cst-agent-app`。 macOS:`brew install curl ncurses pkg-config` 后直接 `make`(Makefile 会找 Homebrew 的 keg-only 库)。 各平台细节与验收清单见 [docs/compat.md](docs/compat.md)。 diff --git a/docs/parliament.md b/docs/parliament.md index 245c610..bb062e4 100644 --- a/docs/parliament.md +++ b/docs/parliament.md @@ -35,6 +35,22 @@ cst-agent # TUI: /status 或 /realm cst-agent --service-stop ``` +### 常驻服务(推荐) + +登录后会 fork 后台席位进程;也可用 systemd --user 托管前台循环(崩溃自动拉起): + +```bash +# 安装脚本附带的辅助命令 +cst-agent-service install-unit +# 在 ~/.config/cst-agent/parliament.env 写入: +# CST_REALM_PASSWORD=… +# CST_INSTANCE_ID=my-laptop +systemctl --user enable --now cst-agent-parliament.service +cst-agent-service status +``` + +或手动前台:`CST_REALM_PASSWORD=… cst-agent --service-fg`。 + 密码 Realm 开启后,直接打开 agent 会要求登录(TTY 下可当场输入密码)。 ## 议员协作协议 diff --git a/include/cst_version.h b/include/cst_version.h index 398fe15..c389f18 100644 --- a/include/cst_version.h +++ b/include/cst_version.h @@ -3,7 +3,7 @@ #define CST_VERSION_MAJOR 0 #define CST_VERSION_MINOR 9 -#define CST_VERSION_PATCH 19 +#define CST_VERSION_PATCH 20 #define CST_VERSION_STR_(x) #x #define CST_VERSION_STR(x) CST_VERSION_STR_(x) diff --git a/scripts/bootstrap_linux.sh b/scripts/bootstrap_linux.sh index f2b5ad9..de72aee 100755 --- a/scripts/bootstrap_linux.sh +++ b/scripts/bootstrap_linux.sh @@ -84,15 +84,20 @@ main() { local jobs jobs="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)" - echo "==> build" - if ((changed)); then + echo "==> build (incremental, -j$jobs)" + # Prefer incremental rebuilds. Set CST_AGENT_CLEAN=1 to force a clean tree. + if [[ "${CST_AGENT_CLEAN:-0}" == "1" ]]; then run_make clean >/dev/null 2>&1 || true fi + # `changed` is only used so re-exec after pull still builds; keep for clarity. + : "${changed:=0}" if ! run_make -j"$jobs"; then deps_hint return 1 fi - run_make check >/dev/null 2>&1 || echo "warning: some tests failed (run 'make check')" + if [[ "${CST_AGENT_SKIP_CHECK:-0}" != "1" ]]; then + run_make -j"$jobs" check >/dev/null 2>&1 || echo "warning: some tests failed (run 'make check')" + fi echo "==> install to $prefix/bin" mkdir -p "$prefix/bin" @@ -103,6 +108,35 @@ export CST_AGENT_PREFIX="\${CST_AGENT_PREFIX:-$prefix}" exec bash "$dir/scripts/bootstrap_linux.sh" "$dir" "\$@" EOF chmod 0755 "$prefix/bin/cst-agent-update" + cat >"$prefix/bin/cst-agent-service" </dev/null 2>&1 && systemctl --user is-active cst-agent-parliament.service >/dev/null 2>&1; then + systemctl --user status cst-agent-parliament.service --no-pager + else + "\$BIN" --service-stop >/dev/null 2>&1 || true + echo "parliament service: not managed by systemd --user (use --login / --service)" + fi + ;; +install-unit) + make -C "$dir" install-service PREFIX="$prefix" + ;; +start) systemctl --user start cst-agent-parliament.service ;; +stop) systemctl --user stop cst-agent-parliament.service; "\$BIN" --service-stop || true ;; +restart) systemctl --user restart cst-agent-parliament.service ;; +*) + echo "usage: cst-agent-service {status|install-unit|start|stop|restart}" >&2 + exit 2 + ;; +esac +EOF + chmod 0755 "$prefix/bin/cst-agent-service" echo echo "OK: $("$prefix/bin/cst-agent" --version)" @@ -110,7 +144,8 @@ EOF *":$prefix/bin:"*) ;; *) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;; esac - echo "Run 'cst-agent' inside a project directory; upgrade later with 'cst-agent-update'." + echo "Run 'cst-agent' inside a project directory; upgrade with 'cst-agent-update'." + echo "Parliament seat service helper: 'cst-agent-service status|install-unit'." echo "The first start asks for the provider and its API key and saves them." } diff --git a/scripts/cst-agent-parliament.service.in b/scripts/cst-agent-parliament.service.in new file mode 100644 index 0000000..e4b86c2 --- /dev/null +++ b/scripts/cst-agent-parliament.service.in @@ -0,0 +1,21 @@ +[Unit] +Description=CST-Agent parliament seat service +Documentation=file://%h/CST-agent/docs/parliament.md +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +# Prefer --service-fg under systemd so the unit tracks the real process. +ExecStart=@BIN@ --service-fg --peer-port ${CST_PEER_PORT:-9100} +Restart=on-failure +RestartSec=3 +Environment=CST_INSTANCE_ID=%H +# Put the Realm password in a drop-in, e.g.: +# ~/.config/systemd/user/cst-agent-parliament.service.d/override.conf +# [Service] +# Environment=CST_REALM_PASSWORD=… +EnvironmentFile=-%h/.config/cst-agent/parliament.env + +[Install] +WantedBy=default.target diff --git a/src/discover.c b/src/discover.c index 0e38b17..172c5cc 100644 --- a/src/discover.c +++ b/src/discover.c @@ -139,6 +139,26 @@ int cst_discover_local_put(const char *realm_fp, const char *instance_id, arr = cJSON_CreateArray(); cJSON_AddItemToObject(root, "instances", arr); } + time_t now = time(NULL); + const cJSON *it; + cJSON_ArrayForEach(it, arr) { + const cJSON *iid = cJSON_GetObjectItemCaseSensitive(it, "iid"); + const cJSON *rid = cJSON_GetObjectItemCaseSensitive(it, "rid"); + const cJSON *port = cJSON_GetObjectItemCaseSensitive(it, "port"); + const cJSON *updated = cJSON_GetObjectItemCaseSensitive(it, "updated"); + if (!cJSON_IsString(iid) || strcmp(iid->valuestring, instance_id) != 0) + continue; + if (cJSON_IsString(rid) && !strcmp(rid->valuestring, realm_fp) && + cJSON_IsNumber(port) && port->valueint == peer_port && + cJSON_IsNumber(updated) && + now - (time_t)updated->valuedouble < 60) { + /* Same seat advertisement still fresh — skip disk rewrite. */ + cJSON_Delete(root); + pthread_mutex_unlock(&local_mu); + return 0; + } + break; + } local_remove_iid(arr, instance_id); cJSON *row = cJSON_CreateObject(); cJSON_AddStringToObject(row, "rid", realm_fp); @@ -146,7 +166,7 @@ int cst_discover_local_put(const char *realm_fp, const char *instance_id, cJSON_AddStringToObject(row, "name", name ? name : ""); cJSON_AddNumberToObject(row, "port", peer_port); cJSON_AddNumberToObject(row, "pid", (double)os_getpid()); - cJSON_AddNumberToObject(row, "updated", (double)time(NULL)); + cJSON_AddNumberToObject(row, "updated", (double)now); cJSON_AddItemToArray(arr, row); int rc = save_local(root); cJSON_Delete(root); @@ -283,12 +303,15 @@ static void *discover_main(void *arg) { cst_discover_t *d = arg; int ticks = 0; while (!d->stop) { - if ((ticks++ % 10) == 0) { + /* ~100ms recv timeout: beacon every ~5s, rewrite local registry ~60s. */ + if ((ticks % 50) == 0) { send_beacon(d); - (void)cst_discover_local_put(d->realm_fp, d->instance_id, d->name, - d->peer_port); + if ((ticks / 50) % 12 == 0) + (void)cst_discover_local_put(d->realm_fp, d->instance_id, d->name, + d->peer_port); ingest_local(d); } + ticks++; if (d->sock == DISC_BAD) { struct timespec ts = {0, 100 * 1000 * 1000}; nanosleep(&ts, NULL); diff --git a/src/extensions.c b/src/extensions.c index e8fbb1b..713232a 100644 --- a/src/extensions.c +++ b/src/extensions.c @@ -20,7 +20,12 @@ void cst_extensions_wait(FILE *log) { cst_mcp_counts(&ready, &total); if (total == 0) return; - cst_mcp_wait_ready(30000); + /* Default 8s (was 30s); override with CST_MCP_WAIT_MS. */ + const char *e = getenv("CST_MCP_WAIT_MS"); + int ms = e && atoi(e) >= 0 ? atoi(e) : 8000; + if (ms > 60000) + ms = 60000; + cst_mcp_wait_ready(ms); cst_mcp_info_t info[CST_MCP_MAX]; int n = cst_mcp_info(info, CST_MCP_MAX); for (int i = 0; log && i < n; i++) { diff --git a/src/main.c b/src/main.c index a748b3f..1fa6542 100644 --- a/src/main.c +++ b/src/main.c @@ -22,6 +22,21 @@ #include #endif +/* Defer libcurl init: --version / --login / parliament CLI need none of it. */ +static int g_curl_ready; +static void cst_curl_ensure(void) { + if (!g_curl_ready) { + curl_global_init(CURL_GLOBAL_DEFAULT); + g_curl_ready = 1; + } +} +static void cst_curl_cleanup(void) { + if (g_curl_ready) { + curl_global_cleanup(); + g_curl_ready = 0; + } +} + static void usage(const char *argv0) { fprintf(stderr, "Usage: %s [options] (works in the current directory)\n" @@ -311,7 +326,6 @@ int main(int argc, char **argv) { "1903 or later is required); non-ASCII text may break\n"); os_set_prog_path(argv[0]); os_temp_dir_export(); - curl_global_init(CURL_GLOBAL_DEFAULT); cst_agent_config_t cfg = {0}; char cwd[4096]; @@ -532,7 +546,7 @@ int main(int argc, char **argv) { case 'r': if (cst_protocol_parse(optarg, &cfg.protocol) != 0) { fprintf(stderr, "unknown protocol: %s\n", optarg); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } break; @@ -545,7 +559,7 @@ int main(int argc, char **argv) { if (cst_tool_profile_parse(optarg, &cfg.tool_profile) != 0) { fprintf(stderr, "unknown tool profile: %s (default | minimal)\n", optarg); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } break; @@ -580,14 +594,14 @@ int main(int argc, char **argv) { case 'A': if (cst_approval_parse(optarg, &approval) != 0) { fprintf(stderr, "bad --approval %s (ask | auto | yolo)\n", optarg); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } break; case 'C': if (parse_confirm(optarg, &cfg) != 0) { fprintf(stderr, "bad --confirm %s (shell,write,net | none)\n", optarg); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } break; @@ -595,7 +609,7 @@ int main(int argc, char **argv) { if (!cst_effort_valid(optarg)) { fprintf(stderr, "bad --effort %s (low | medium | high | max)\n", optarg); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } snprintf(cfg.effort, sizeof(cfg.effort), "%s", optarg); @@ -603,21 +617,22 @@ int main(int argc, char **argv) { case 'M': if (!cst_mode_valid(optarg)) { fprintf(stderr, "bad --mode %s (coding | general)\n", optarg); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } snprintf(cfg.mode, sizeof(cfg.mode), "%s", optarg); break; case 'h': usage(argv[0]); - curl_global_cleanup(); + cst_curl_cleanup(); return 0; case 0x100: { const char *only = optarg; if (!only && optind < argc && argv[optind][0] != '-') only = argv[optind++]; + cst_curl_ensure(); int rc = list_models(only); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } case 0x101: @@ -626,7 +641,7 @@ int main(int argc, char **argv) { case 0x102: if (parse_port(optarg, &web.port) != 0) { fprintf(stderr, "--web-port %s is not a port (0-65535)\n", optarg); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } web_mode = 1; @@ -652,30 +667,30 @@ int main(int argc, char **argv) { break; case 'V': printf("cst-agent %s\n", CST_VERSION); - curl_global_cleanup(); + cst_curl_cleanup(); return 0; default: usage(argv[0]); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } } if (do_migrate) { int rc = migrate_store(cfg.workspace_root); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } if (do_realm_new) { int rc = cst_parliament_cli_realm_new(); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } if (do_realm_pw_init) { const char *pw = getenv("CST_REALM_PASSWORD"); int rc = cst_parliament_cli_realm_password_init(pw); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } if (do_login) { @@ -687,17 +702,17 @@ int main(int argc, char **argv) { iid = iidbuf; } int rc = cst_parliament_cli_login(pw, iid, realm_label, discover_peer_port); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } if (do_new_project) { int rc = cst_parliament_cli_new_project(new_project_id); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } if (do_service_stop) { int rc = cst_parliament_cli_service_stop(); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } if (do_service || do_service_fg) { @@ -710,12 +725,12 @@ int main(int argc, char **argv) { } int rc = cst_parliament_cli_service(pw, iid, discover_peer_port, do_service_fg); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } if (do_realm_enroll) { int rc = cst_parliament_cli_realm_enroll(realm_enroll_id, realm_label); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } if (do_peer_listen) { @@ -723,7 +738,7 @@ int main(int argc, char **argv) { if (!iid) iid = getenv("CST_INSTANCE_ID"); int rc = cst_parliament_cli_peer_listen(peer_listen_port, iid); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } if (do_peer_dial) { @@ -743,13 +758,13 @@ int main(int argc, char **argv) { } } int rc = cst_parliament_cli_peer_dial(host, port, iid); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } if (do_discover) { const char *iid = parliament_instance ? parliament_instance : getenv("CST_INSTANCE_ID"); int rc = cst_parliament_cli_discover(iid, discover_peer_port, discover_wait_ms); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } @@ -778,7 +793,7 @@ int main(int argc, char **argv) { fprintf(stderr, "%s\n", err[0] ? err : "login required"); if (pw == pwbuf) memset(pwbuf, 0, sizeof(pwbuf)); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } free(hand); @@ -792,7 +807,7 @@ int main(int argc, char **argv) { fprintf(stderr, "password Realm active: run --login (or set " "CST_REALM_PASSWORD) before opening the agent\n"); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } } @@ -800,10 +815,13 @@ int main(int argc, char **argv) { if (do_list) { int rc = list_sessions(cfg.workspace_root); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } + /* Agent / TUI / WebUI / headless need libcurl; parliament CLI above does not. */ + cst_curl_ensure(); + /* Explicit option > environment > last used (settings.json) > preset. */ cst_settings_t saved; cst_settings_load(&saved); @@ -815,7 +833,7 @@ int main(int argc, char **argv) { prov = cst_provider_find(provider_name); if (!prov) { fprintf(stderr, "unknown provider: %s\n", provider_name); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } } @@ -894,7 +912,7 @@ int main(int argc, char **argv) { if (rc != 0 || cst_agent_store_path_for_id(cfg.workspace_root, id, cfg.session_path, sizeof(cfg.session_path)) != 0) { - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } cst_agent_store_set_current(cfg.workspace_root, id); @@ -906,7 +924,7 @@ int main(int argc, char **argv) { sizeof(id)) != 0) { fprintf(stderr, "failed to init .agent/sessions under %s\n", cfg.workspace_root); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } fprintf(stderr, "session %s\n", id); @@ -914,7 +932,7 @@ int main(int argc, char **argv) { if (do_demo) { int rc = demo_ctl_impl(cfg.workspace_root); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } @@ -924,7 +942,7 @@ int main(int argc, char **argv) { if (cst_creds_prompt_console(cfg.provider, cfg.api_key, sizeof(cfg.api_key)) != 0) { fprintf(stderr, "no API key entered\n"); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } char dir[1024] = "the config directory"; @@ -936,7 +954,7 @@ int main(int argc, char **argv) { dir); /* --ask-key alone: save and exit */ if (force_ask_key && !cfg.headless && !prompt) { - curl_global_cleanup(); + cst_curl_cleanup(); return 0; } } @@ -945,7 +963,7 @@ int main(int argc, char **argv) { "no API key for %s: start cst-agent once to enter it, or run " "cst-agent --ask-key --provider %s\n", cfg.provider, cfg.provider); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } /* Router presets have no fixed model: take the first one the API lists @@ -968,7 +986,7 @@ int main(int argc, char **argv) { } else { fprintf(stderr, "no model for %s: pass --model (%s)\n", cfg.provider, err[0] ? err : "the API listed none"); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } } @@ -989,7 +1007,7 @@ int main(int argc, char **argv) { cst_agent_t *agent = cst_agent_create(&cfg); if (!agent) { fprintf(stderr, "failed to create agent (session path?)\n"); - curl_global_cleanup(); + cst_curl_cleanup(); return 1; } @@ -1021,6 +1039,6 @@ int main(int argc, char **argv) { cst_agent_destroy(agent); cst_extensions_unload(); - curl_global_cleanup(); + cst_curl_cleanup(); return rc; } diff --git a/src/parliament_daemon.c b/src/parliament_daemon.c index 5eae88f..8350a47 100644 --- a/src/parliament_daemon.c +++ b/src/parliament_daemon.c @@ -272,33 +272,43 @@ int cst_parliament_daemon_run(const char *password, const char *instance_id, signal(SIGHUP, on_sig); #endif g_stop = 0; - int tick = 0; + int tick = 0, idle = 0; while (!g_stop) { cst_peer_session_t sess; char herr[128]; - if (cst_peer_accept((intptr_t)ls, &cert, sk, realm.pk, 500, &sess, herr, - sizeof(herr)) == 0) { + /* Accept wait backs off when idle so a quiet seat barely wakes. */ + int accept_ms = idle > 20 ? 2000 : idle > 5 ? 1000 : 400; + if (cst_peer_accept((intptr_t)ls, &cert, sk, realm.pk, accept_ms, &sess, + herr, sizeof(herr)) == 0) { sync_on_session(&sess); cst_peer_close(&sess); + idle = 0; + } else { + idle++; } int push = cst_parliament_sync_pending(); tick++; - if ((push || tick % 10 == 0) && disc) { + /* Dial peers on push, or periodically (slower while idle). */ + int period = idle > 20 ? 45 : idle > 5 ? 20 : 8; + if ((push || tick % period == 0) && disc) { cst_peer_info_t peers[16]; int n = cst_discover_peers(disc, peers, 16); - for (int i = 0; i < n; i++) { - if (peers[i].port <= 0) - continue; - if (cst_peer_dial(peers[i].host, peers[i].port, &cert, sk, realm.pk, - 3000, &sess, herr, sizeof(herr)) == 0) { - sync_on_session(&sess); - cst_peer_close(&sess); + if (n > 0 || push) { + for (int i = 0; i < n; i++) { + if (peers[i].port <= 0) + continue; + if (cst_peer_dial(peers[i].host, peers[i].port, &cert, sk, realm.pk, + 3000, &sess, herr, sizeof(herr)) == 0) { + sync_on_session(&sess); + cst_peer_close(&sess); + idle = 0; + } } } if (push) cst_parliament_sync_ack(); } - sleep_ms(200); + /* No extra sleep: accept_ms already paced the loop. */ } if (disc) cst_discover_stop(disc); diff --git a/src/settings.c b/src/settings.c index 5dc1074..0fdd808 100644 --- a/src/settings.c +++ b/src/settings.c @@ -7,6 +7,8 @@ #include #include #include +#include +#include static int settings_path(char *out, size_t n) { char dir[OS_PATH_MAX]; @@ -20,11 +22,32 @@ static void copy_str(char *dst, size_t n, const cJSON *v) { snprintf(dst, n, "%s", v->valuestring); } +static struct { + int valid; + time_t mtime; + off_t size; + cst_settings_t s; +} g_settings_cache; + +static void settings_cache_invalidate(void) { + g_settings_cache.valid = 0; + g_settings_cache.mtime = 0; + g_settings_cache.size = 0; +} + int cst_settings_load(cst_settings_t *s) { memset(s, 0, sizeof(*s)); char path[OS_PATH_MAX]; if (settings_path(path, sizeof(path)) != 0) return -1; + struct stat st; + int have_st = stat(path, &st) == 0; + if (have_st && g_settings_cache.valid && + g_settings_cache.mtime == st.st_mtime && + g_settings_cache.size == st.st_size) { + *s = g_settings_cache.s; + return 0; + } FILE *fp = fopen(path, "rb"); if (!fp) return -1; @@ -94,10 +117,19 @@ int cst_settings_load(cst_settings_t *s) { s->nparams++; } cJSON_Delete(root); + if (!have_st) + have_st = stat(path, &st) == 0; + if (have_st) { + g_settings_cache.s = *s; + g_settings_cache.mtime = st.st_mtime; + g_settings_cache.size = st.st_size; + g_settings_cache.valid = 1; + } return 0; } int cst_settings_save(const cst_settings_t *s) { + settings_cache_invalidate(); char dir[OS_PATH_MAX], path[OS_PATH_MAX], tmp[OS_PATH_MAX + 8]; if (os_config_dir(dir, sizeof(dir)) != 0 || os_mkdir_p(dir, 0700) != 0 || settings_path(path, sizeof(path)) != 0)