背景與挑戰
許多開發工具需要一個長時間執行的本地進程——LSP 伺服器、檔案監控程式、索引服務。「不可見」守護程序(daemon)的真正難題不在於構建它,而在於讓它自動啟動、升級時自動重啟、設定更改時自動重新載入、正常關閉——整個過程對使用者透明。CocoIndex 在建立 cocoindex-code(一個基於 AST 的語意程式搜尋工具,用於加速和改進程式編寫 Agent 的品質)的過程中學到了這一點。該工具最初是一個單體進程,每個 session 都要載入 ML 模型、索引程式庫並提供搜尋服務。團隊將重型任務遷移到持久的守護程序,同時提供 CLI(ccc)和輕量級 MCP 客戶端作為前端。
使用者旅程設計
- ccc init:純本地操作,建立設定檔和 .gitignore 項目,無需啟動守護程序
- ccc index:建置搜尋索引,守護程序在此自動啟動
- ccc search "auth logic":語意搜尋,守護程序處理請求
- ccc search --refresh "auth":重新索引然後搜尋(兩次守護程序請求)
使用者永遠不需手動啟動、停止或重啟守護程序——它只是運作。
自動啟動與版本握手
首次執行 ccc index 或 ccc search 時,客戶端嘗試連接到守護程序的 Unix socket。若失敗,則啟動守護程序作為獨立背景進程並等待 socket 出現。這個邏輯內建在最低層的連接函數中,而非獨立的「確保守護程序」步驟。每次連接都從版本握手開始——客戶端發送其版本,守護程序回應其版本。版本不匹配時會觸發重啟(首次呼叫)或報錯(後續呼叫),確保使用者升級套件後無需手動重啟。
雙層設定與狀態管理策略
- 全域設定(~/.cocoindex_code/global_settings.yml):嵌入式模型、API 金鑰、環境變數。這些影響守護程序範圍的狀態,守護程序必須重啟。客戶端比較檔案的 mtime(以微秒整數記錄),偵測變化。
- 專案設定($PROJECT/.cocoindex_code/settings.yml):包含/排除模式、語言覆蓋。這些僅影響個別操作行為。每次索引都從磁碟讀取新的設定檔,無快取失效或重啟。
核心設計簡化:per-request 連接
初期設計採用持久連接——客戶端連接一次,守護程序透過循環讀取多個請求。但這導致三個棧疊的 bug:
- 連接洩漏:CLI 命令未總是關閉連接,導致處理器任務永遠等待
- 守護程序訊號無效:asyncio.Event 在執行器執行緒中調用(非執行緒安全),shutdown 信號永遠被看不見
- 殭屍進程檢測失敗:os.kill(pid, 0) 對殭屍進程返回 True,客戶端持續等待已退出的進程
團隊捨棄持久連接,改採 per-request 模式:每個請求開啟新連接 → 握手 → 單個請求 → 回應 → 關閉。處理器變成直線程式:讀取兩則訊息、回應、關閉,就這樣。複合操作(如搜尋並刷新)只需開啟多個連接——沒有共享狀態。Unix socket 設定開銷約 0.1ms,握手是微小的 msgspec 承載,CLI 命令都是人工啟動的,開銷可忽略。
關閉:圍繞資源關閉設計,不是執行緒信號
Per-request 連接解決了最難的關閉問題(無需信號空閒任務)。接受循環在背景執行緒執行,阻塞在 listener.accept()。停止它的方法不是設定旗標,而是關閉 listener——accept() 拋出 OSError,執行緒立即退出。整個關閉序列優先順序是:停止接受 → 取消處理器 → 釋放資源 → 移除 socket 和 PID 檔案 → os._exit(0)(跳過緩慢的 Python 清理)。PID 檔案作為退出訊號,客戶端輪詢其缺失來確認守護程序完成清理。
效果對比
| 面向 | 之前 | 之後 |
|---|---|---|
| 連接處理器 | ~70 行,recv 循環、輪詢、shutdown 事件 | ~40 行,直線讀-回應-關閉 |
| 客戶端 | DaemonClient 類、enter/exit、.close() | 模組層級函數,無狀態 |
| 關閉概念 | threading.Event、執行器輪詢、5 步升級 | listener.close()、loop.stop()、os._exit(0) |
| 守護程序停止時間 | ~15 秒(最後落回 SIGKILL) | <1 秒 |
建議模式
讓守護程序不可見:自動首次啟動、版本不匹配時自動重啟、可能時自動重新載入設定。偏好無狀態 IPC:per-request 連接防止洩漏、握手捕捉過時狀態更快。區分狀態類別:什麼必須在守護程序中(已載入 ML 模型、開啟的資料庫)vs 什麼可以重新讀取(設定檔、檔案清單)。只為前者重啟。圍繞資源關閉設計關閉,不是執行緒信號。當偵錯變複雜時,質疑設計——團隊用精心的執行緒、事件類型、進程活躍檢查修復了三個棧疊 bug,後來移除持久連接,三個 bug 全數消失。
Building an Invisible Daemon🔥🔥 - Everyone is building CLI for developer tools these days. We just published how we built invisible deamon with cocoindex-code. The challenge isn't just building the daemon. It's making it invisible: something that starts when needed, upgrades… pic.twitter.com/0Y9OBcWif1
— Linghua Jin 🥥 🌴 (@LinghuaJ) March 24, 2026
We learned this building cocoindex-code, an embedded AST-based semantic code search tool that speeds up and improve the quality for coding agents. It started as a monolithic process that loaded an ML model, indexed a codebase, and served searches — all per session.
— Linghua Jin 🥥 🌴 (@LinghuaJ) March 24, 2026
We moved the…
Checkout https://t.co/c3bTuSHmV6
— Linghua Jin 🥥 🌴 (@LinghuaJ) March 24, 2026
