Skip to content

Commit b8ed86f

Browse files
authored
feat: automatically restart daemon on global settings change (#88)
1 parent 59e8ffe commit b8ed86f

4 files changed

Lines changed: 50 additions & 11 deletions

File tree

src/cocoindex_code/client.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -302,20 +302,37 @@ def _wait_for_daemon(timeout: float = 30.0) -> None:
302302
raise TimeoutError("Daemon did not start in time")
303303

304304

305+
def _needs_restart(resp: HandshakeResponse) -> bool:
306+
"""Check if the daemon needs to be restarted.
307+
308+
Returns True if the version mismatches or if global_settings.yml has been
309+
modified since the daemon loaded it.
310+
"""
311+
if not resp.ok:
312+
return True
313+
from .settings import global_settings_mtime_us
314+
315+
current_mtime = global_settings_mtime_us()
316+
if current_mtime != resp.global_settings_mtime_us:
317+
return True
318+
return False
319+
320+
305321
def ensure_daemon() -> DaemonClient:
306322
"""Connect to daemon, starting or restarting as needed.
307323
308324
1. Try to connect to existing daemon.
309325
2. If connection refused: start daemon, retry connect with backoff.
310-
3. If connected but version mismatch: stop old daemon, start new one.
326+
3. If connected but version mismatch or global settings changed:
327+
stop old daemon, start new one.
311328
"""
312329
# Try connecting to existing daemon
313330
try:
314331
client = DaemonClient.connect()
315332
resp = client.handshake()
316-
if resp.ok:
333+
if not _needs_restart(resp):
317334
return client
318-
# Version mismatch — restart
335+
# Version or settings mismatch — restart
319336
client.close()
320337
stop_daemon()
321338
except (ConnectionRefusedError, OSError):
@@ -326,14 +343,15 @@ def ensure_daemon() -> DaemonClient:
326343
_wait_for_daemon()
327344

328345
# Connect with retries
329-
for attempt in range(10):
346+
for _attempt in range(10):
330347
try:
331348
client = DaemonClient.connect()
332349
resp = client.handshake()
333-
if resp.ok:
350+
if not _needs_restart(resp):
334351
return client
335352
raise RuntimeError(
336-
f"Daemon version mismatch: expected {__version__}, got {resp.daemon_version}"
353+
f"Daemon mismatch after fresh start: version={resp.daemon_version}, "
354+
f"settings_mtime={resp.global_settings_mtime_us}"
337355
)
338356
except (ConnectionRefusedError, OSError):
339357
time.sleep(0.5)

src/cocoindex_code/daemon.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
)
4646
from .query import query_codebase
4747
from .settings import (
48+
global_settings_mtime_us,
4849
load_project_settings,
4950
load_user_settings,
5051
user_settings_dir,
@@ -288,6 +289,7 @@ async def handle_connection(
288289
registry: ProjectRegistry,
289290
start_time: float,
290291
shutdown_event: asyncio.Event,
292+
settings_mtime_us: int | None,
291293
) -> None:
292294
"""Handle a single client connection."""
293295
loop = asyncio.get_event_loop()
@@ -322,7 +324,11 @@ def _recv() -> bytes:
322324
break
323325

324326
ok = req.version == __version__
325-
resp = HandshakeResponse(ok=ok, daemon_version=__version__)
327+
resp = HandshakeResponse(
328+
ok=ok,
329+
daemon_version=__version__,
330+
global_settings_mtime_us=settings_mtime_us,
331+
)
326332
conn.send_bytes(encode_response(resp))
327333
if not ok:
328334
break
@@ -419,8 +425,9 @@ def run_daemon() -> None:
419425
"""Main entry point for the daemon process (blocking)."""
420426
daemon_dir().mkdir(parents=True, exist_ok=True)
421427

422-
# Load user settings
428+
# Load user settings and record mtime for staleness detection
423429
user_settings = load_user_settings()
430+
settings_mtime_us = global_settings_mtime_us()
424431

425432
# Set environment variables from settings
426433
for key, value in user_settings.envs.items():
@@ -445,7 +452,7 @@ def run_daemon() -> None:
445452
logger.info("Daemon starting (PID %d, version %s)", os.getpid(), __version__)
446453

447454
try:
448-
asyncio.run(_async_daemon_main(embedder))
455+
asyncio.run(_async_daemon_main(embedder, settings_mtime_us))
449456
finally:
450457
# Clean up PID file and socket (named pipes on Windows clean up automatically)
451458
try:
@@ -461,7 +468,7 @@ def run_daemon() -> None:
461468
logger.info("Daemon stopped")
462469

463470

464-
async def _async_daemon_main(embedder: Embedder) -> None:
471+
async def _async_daemon_main(embedder: Embedder, settings_mtime_us: int | None) -> None:
465472
"""Async main loop for the daemon."""
466473
start_time = time.monotonic()
467474
registry = ProjectRegistry(embedder)
@@ -496,7 +503,7 @@ async def _spawn_handler(
496503
evt: asyncio.Event,
497504
task_set: set[asyncio.Task[Any]],
498505
) -> None:
499-
task = asyncio.create_task(handle_connection(conn, reg, st, evt))
506+
task = asyncio.create_task(handle_connection(conn, reg, st, evt, settings_mtime_us))
500507
task_set.add(task)
501508
task.add_done_callback(task_set.discard)
502509

src/cocoindex_code/protocol.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class StopRequest(_msgspec.Struct, tag="stop"):
6161
class HandshakeResponse(_msgspec.Struct, tag="handshake"):
6262
ok: bool
6363
daemon_version: str
64+
global_settings_mtime_us: int | None = None
6465

6566

6667
class IndexResponse(_msgspec.Struct, tag="index"):

src/cocoindex_code/settings.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,19 @@ def find_parent_with_marker(start: Path) -> Path | None:
195195
current = parent
196196

197197

198+
def global_settings_mtime_us() -> int | None:
199+
"""Return the mtime of ``global_settings.yml`` as integer microseconds.
200+
201+
Returns ``None`` if the file does not exist. Used by the daemon to record
202+
the mtime at startup and by the client to detect staleness.
203+
"""
204+
path = user_settings_path()
205+
try:
206+
return int(path.stat().st_mtime * 1_000_000)
207+
except FileNotFoundError:
208+
return None
209+
210+
198211
def load_gitignore_spec(project_root: Path) -> GitIgnoreSpec | None:
199212
"""Load a GitIgnoreSpec for the project's ``.gitignore`` if present."""
200213
gitignore = project_root / ".gitignore"

0 commit comments

Comments
 (0)