Skip to content

Commit 21f858e

Browse files
georgeh0claude
andauthored
refactor: extract daemon path helpers to avoid CLI importing cocoindex (#130)
* refactor: extract daemon path helpers to avoid CLI importing cocoindex Move daemon_dir, daemon_socket_path, daemon_pid_path, daemon_log_path, and connection_family from daemon.py into a new lightweight _daemon_paths.py module. This prevents the CLI client from transitively importing cocoindex and its heavy dependencies (numpy, torch, etc.) when it only needs path utilities. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: mark free-threaded Python CI jobs as continue-on-error tokenizers lacks pre-built wheels for cp314t (free-threaded ABI), so uv falls back to compiling from Rust source. This source build is fragile across macOS runner image updates and broke after the 20260406 image bump. Mark 3.14t jobs as continue-on-error since free-threaded wheel coverage across the ecosystem is still limited. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 34565b4 commit 21f858e

6 files changed

Lines changed: 72 additions & 52 deletions

File tree

.github/workflows/pre-commit.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ on:
99
jobs:
1010
pre-commit:
1111
runs-on: ${{ matrix.os }}
12+
# Free-threaded Python (3.14t) lacks pre-built wheels for tokenizers;
13+
# the fallback source build is fragile across macOS toolchain updates.
14+
continue-on-error: ${{ matrix.python-version == '3.14t' }}
1215
strategy:
1316
fail-fast: false
1417
matrix:
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Daemon filesystem paths and connection helpers.
2+
3+
Lightweight module with no cocoindex dependency so that the CLI client
4+
can import these without pulling in the full daemon stack.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import sys
10+
from pathlib import Path
11+
12+
from .settings import user_settings_dir
13+
14+
15+
def daemon_dir() -> Path:
16+
"""Return the daemon directory (``~/.cocoindex_code/``)."""
17+
return user_settings_dir()
18+
19+
20+
def connection_family() -> str:
21+
"""Return the multiprocessing connection family for this platform."""
22+
return "AF_PIPE" if sys.platform == "win32" else "AF_UNIX"
23+
24+
25+
def daemon_socket_path() -> str:
26+
"""Return the daemon socket/pipe address."""
27+
if sys.platform == "win32":
28+
import hashlib
29+
30+
# Hash the daemon dir so COCOINDEX_CODE_DIR overrides create unique pipe names,
31+
# preventing conflicts between different daemon instances (tests, users, etc.)
32+
dir_hash = hashlib.md5(str(daemon_dir()).encode()).hexdigest()[:12]
33+
return rf"\\.\pipe\cocoindex_code_{dir_hash}"
34+
return str(daemon_dir() / "daemon.sock")
35+
36+
37+
def daemon_pid_path() -> Path:
38+
"""Return the path for the daemon's PID file."""
39+
return daemon_dir() / "daemon.pid"
40+
41+
42+
def daemon_log_path() -> Path:
43+
"""Return the path for the daemon's log file."""
44+
return daemon_dir() / "daemon.log"

src/cocoindex_code/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ def doctor() -> None:
599599

600600
# --- 8. Log files ---
601601
_print_section("Log Files")
602-
from .daemon import daemon_log_path as _daemon_log_path
602+
from ._daemon_paths import daemon_log_path as _daemon_log_path
603603

604604
_typer.echo(f" Daemon logs: {_daemon_log_path()}")
605605
_typer.echo(" Check logs above for further troubleshooting.")
@@ -675,8 +675,8 @@ def daemon_restart() -> None:
675675
@daemon_app.command("stop")
676676
def daemon_stop() -> None:
677677
"""Stop the daemon."""
678+
from ._daemon_paths import daemon_pid_path
678679
from .client import is_daemon_running, stop_daemon
679-
from .daemon import daemon_pid_path
680680

681681
pid_path = daemon_pid_path()
682682
if not pid_path.exists() and not is_daemon_running():

src/cocoindex_code/client.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@
1717
from multiprocessing.connection import Client, Connection
1818
from pathlib import Path
1919

20+
from ._daemon_paths import (
21+
connection_family,
22+
daemon_dir,
23+
daemon_log_path,
24+
daemon_pid_path,
25+
daemon_socket_path,
26+
)
2027
from ._version import __version__
21-
from .daemon import _connection_family, daemon_log_path, daemon_pid_path, daemon_socket_path
2228
from .protocol import (
2329
DaemonEnvRequest,
2430
DaemonEnvResponse,
@@ -105,7 +111,7 @@ def _raw_connect_and_handshake() -> Connection:
105111
if sys.platform != "win32" and not os.path.exists(sock):
106112
raise ConnectionRefusedError(f"Daemon socket not found: {sock}")
107113
try:
108-
conn = Client(sock, family=_connection_family())
114+
conn = Client(sock, family=connection_family())
109115
except (ConnectionRefusedError, FileNotFoundError, OSError) as e:
110116
raise ConnectionRefusedError(f"Cannot connect to daemon: {e}") from e
111117

@@ -329,7 +335,7 @@ def is_daemon_running() -> bool:
329335
"""Check if the daemon is running."""
330336
if sys.platform == "win32":
331337
try:
332-
conn = Client(daemon_socket_path(), family=_connection_family())
338+
conn = Client(daemon_socket_path(), family=connection_family())
333339
conn.close()
334340
return True
335341
except (ConnectionRefusedError, OSError):
@@ -343,8 +349,6 @@ def start_daemon() -> subprocess.Popen[bytes]:
343349
Returns the ``Popen`` object so callers can detect early process death
344350
(via ``proc.poll()``) instead of waiting for a full timeout.
345351
"""
346-
from .daemon import daemon_dir, daemon_log_path
347-
348352
daemon_dir().mkdir(parents=True, exist_ok=True)
349353
log_path = daemon_log_path()
350354

@@ -518,7 +522,7 @@ def _wait_for_daemon(
518522

519523
if sys.platform == "win32":
520524
try:
521-
conn = Client(sock_path, family=_connection_family())
525+
conn = Client(sock_path, family=connection_family())
522526
conn.close()
523527
return
524528
except (ConnectionRefusedError, OSError):

src/cocoindex_code/daemon.py

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
from pathlib import Path
1616
from typing import Any
1717

18+
from ._daemon_paths import (
19+
connection_family,
20+
daemon_dir,
21+
daemon_log_path,
22+
daemon_pid_path,
23+
daemon_socket_path,
24+
)
1825
from ._version import __version__
1926
from .chunking import ChunkerFn as _ChunkerFn
2027
from .project import Project
@@ -53,7 +60,6 @@
5360
load_project_settings,
5461
load_user_settings,
5562
target_sqlite_db_path,
56-
user_settings_dir,
5763
)
5864
from .shared import Embedder, create_embedder
5965

@@ -79,43 +85,6 @@ def _resolve_chunker_registry(mappings: list[ChunkerMapping]) -> dict[str, _Chun
7985
return registry
8086

8187

82-
# ---------------------------------------------------------------------------
83-
# Daemon paths
84-
# ---------------------------------------------------------------------------
85-
86-
87-
def daemon_dir() -> Path:
88-
"""Return the daemon directory (``~/.cocoindex_code/``)."""
89-
return user_settings_dir()
90-
91-
92-
def _connection_family() -> str:
93-
"""Return the multiprocessing connection family for this platform."""
94-
return "AF_PIPE" if sys.platform == "win32" else "AF_UNIX"
95-
96-
97-
def daemon_socket_path() -> str:
98-
"""Return the daemon socket/pipe address."""
99-
if sys.platform == "win32":
100-
import hashlib
101-
102-
# Hash the daemon dir so COCOINDEX_CODE_DIR overrides create unique pipe names,
103-
# preventing conflicts between different daemon instances (tests, users, etc.)
104-
dir_hash = hashlib.md5(str(daemon_dir()).encode()).hexdigest()[:12]
105-
return rf"\\.\pipe\cocoindex_code_{dir_hash}"
106-
return str(daemon_dir() / "daemon.sock")
107-
108-
109-
def daemon_pid_path() -> Path:
110-
"""Return the path for the daemon's PID file."""
111-
return daemon_dir() / "daemon.pid"
112-
113-
114-
def daemon_log_path() -> Path:
115-
"""Return the path for the daemon's log file."""
116-
return daemon_dir() / "daemon.log"
117-
118-
11988
# ---------------------------------------------------------------------------
12089
# Project Registry
12190
# ---------------------------------------------------------------------------
@@ -540,7 +509,7 @@ def run_daemon() -> None:
540509
except Exception:
541510
pass
542511

543-
listener = Listener(sock_path, family=_connection_family())
512+
listener = Listener(sock_path, family=connection_family())
544513
logger.info("Listening on %s", sock_path)
545514

546515
loop = asyncio.new_event_loop()

tests/test_daemon.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
import pytest
1818

19+
from cocoindex_code._daemon_paths import connection_family
1920
from cocoindex_code._version import __version__
20-
from cocoindex_code.daemon import _connection_family
2121
from cocoindex_code.protocol import (
2222
DaemonStatusRequest,
2323
HandshakeRequest,
@@ -96,7 +96,7 @@ def daemon_sock() -> Iterator[str]:
9696

9797
# Gracefully shut down the daemon thread so named pipes are released on Windows
9898
try:
99-
conn = Client(sock_path, family=_connection_family())
99+
conn = Client(sock_path, family=connection_family())
100100
conn.send_bytes(encode_request(HandshakeRequest(version=__version__)))
101101
conn.recv_bytes()
102102
conn.send_bytes(encode_request(StopRequest()))
@@ -136,7 +136,7 @@ def daemon_project(daemon_sock: str) -> str:
136136
save_project_settings(project, default_project_settings())
137137
(project / "main.py").write_text(SAMPLE_MAIN_PY)
138138

139-
conn = Client(daemon_sock, family=_connection_family())
139+
conn = Client(daemon_sock, family=connection_family())
140140
conn.send_bytes(encode_request(HandshakeRequest(version=__version__)))
141141
decode_response(conn.recv_bytes())
142142
conn.send_bytes(encode_request(IndexRequest(project_root=str(project))))
@@ -148,7 +148,7 @@ def daemon_project(daemon_sock: str) -> str:
148148

149149

150150
def _connect_and_handshake(sock_path: str) -> tuple[Connection, Response]:
151-
conn = Client(sock_path, family=_connection_family())
151+
conn = Client(sock_path, family=connection_family())
152152
conn.send_bytes(encode_request(HandshakeRequest(version=__version__)))
153153
resp = decode_response(conn.recv_bytes())
154154
return conn, resp
@@ -162,7 +162,7 @@ def test_daemon_starts_and_accepts_handshake(daemon_sock: str) -> None:
162162

163163

164164
def test_daemon_rejects_version_mismatch(daemon_sock: str) -> None:
165-
conn = Client(daemon_sock, family=_connection_family())
165+
conn = Client(daemon_sock, family=connection_family())
166166
conn.send_bytes(encode_request(HandshakeRequest(version="0.0.0-fake")))
167167
resp = decode_response(conn.recv_bytes())
168168
assert resp.ok is False

0 commit comments

Comments
 (0)