Skip to content

Commit 84f72d0

Browse files
authored
fix: bring down daemon in more reliable way (#68)
1 parent 9000f1d commit 84f72d0

6 files changed

Lines changed: 93 additions & 17 deletions

File tree

src/cocoindex_code/cli.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -287,16 +287,29 @@ def daemon_restart() -> None:
287287
@daemon_app.command("stop")
288288
def daemon_stop() -> None:
289289
"""Stop the daemon."""
290-
from .client import DaemonClient
290+
from .client import stop_daemon
291+
from .daemon import daemon_pid_path
291292

292-
try:
293-
client = DaemonClient.connect()
294-
client.handshake()
295-
client.stop()
296-
client.close()
297-
_typer.echo("Daemon stopped.")
298-
except (ConnectionRefusedError, OSError):
293+
pid_path = daemon_pid_path()
294+
if not pid_path.exists():
299295
_typer.echo("Daemon is not running.")
296+
return
297+
298+
stop_daemon()
299+
300+
# Wait for process to exit
301+
import time
302+
303+
deadline = time.monotonic() + 5.0
304+
while time.monotonic() < deadline:
305+
if not pid_path.exists():
306+
break
307+
time.sleep(0.1)
308+
309+
if pid_path.exists():
310+
_typer.echo("Warning: daemon may not have stopped cleanly.", err=True)
311+
else:
312+
_typer.echo("Daemon stopped.")
300313

301314

302315
@app.command("run-daemon", hidden=True)

src/cocoindex_code/client.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,26 +162,43 @@ def _find_ccc_executable() -> str | None:
162162

163163

164164
def stop_daemon() -> None:
165-
"""Stop the daemon gracefully."""
165+
"""Stop the daemon gracefully.
166+
167+
Sends a StopRequest, waits for the process to exit, falls back to SIGTERM.
168+
"""
169+
# Step 1: try sending StopRequest
166170
try:
167171
client = DaemonClient.connect()
168172
client.handshake()
169173
client.stop()
170174
client.close()
171-
except (ConnectionRefusedError, OSError):
175+
except (ConnectionRefusedError, OSError, RuntimeError):
172176
pass
173177

174-
# If daemon doesn't respond, try SIGTERM via PID
178+
# Step 2: wait for process to exit (up to 5s)
175179
pid_path = daemon_pid_path()
180+
deadline = time.monotonic() + 5.0
181+
while time.monotonic() < deadline and pid_path.exists():
182+
time.sleep(0.1)
183+
184+
if not pid_path.exists():
185+
return # Clean exit
186+
187+
# Step 3: if still running, try SIGTERM
176188
if pid_path.exists():
177189
try:
178190
pid = int(pid_path.read_text().strip())
179-
if pid != os.getpid(): # Never kill ourselves (happens when daemon runs in a thread)
191+
if pid != os.getpid():
180192
os.kill(pid, signal.SIGTERM)
181193
except (ValueError, ProcessLookupError, PermissionError):
182194
pass
183195

184-
# Clean up stale files (named pipes on Windows clean up automatically)
196+
# Wait a bit more
197+
deadline = time.monotonic() + 2.0
198+
while time.monotonic() < deadline and pid_path.exists():
199+
time.sleep(0.1)
200+
201+
# Step 4: clean up stale files
185202
if sys.platform != "win32":
186203
sock = daemon_socket_path()
187204
try:

src/cocoindex_code/daemon.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,18 @@ async def handle_connection(
210210
loop = asyncio.get_event_loop()
211211
handshake_done = False
212212

213+
def _recv() -> bytes:
214+
"""Blocking recv that also checks for shutdown."""
215+
# Use poll with a timeout so we can check shutdown_event periodically
216+
while not shutdown_event.is_set():
217+
if conn.poll(0.5):
218+
return conn.recv_bytes()
219+
raise EOFError("shutdown")
220+
213221
try:
214222
while not shutdown_event.is_set():
215223
try:
216-
data: bytes = await loop.run_in_executor(None, conn.recv_bytes)
224+
data: bytes = await loop.run_in_executor(None, _recv)
217225
except (EOFError, OSError):
218226
break
219227

src/cocoindex_code/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def main() -> None:
188188
LanguageOverride,
189189
default_project_settings,
190190
default_user_settings,
191-
find_parent_with_marker,
191+
find_legacy_project_root,
192192
find_project_root,
193193
project_settings_path,
194194
save_project_settings,
@@ -216,8 +216,8 @@ def main() -> None:
216216
project_root = Path(env_root).resolve()
217217
else:
218218
# Use marker-based discovery
219-
marker_root = find_parent_with_marker(cwd)
220-
project_root = marker_root if marker_root is not None else cwd
219+
legacy_root = find_legacy_project_root(cwd)
220+
project_root = legacy_root if legacy_root is not None else cwd
221221

222222
# --- Auto-create project settings if needed ---
223223
proj_settings_file = project_settings_path(project_root)

src/cocoindex_code/settings.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,22 @@ def find_project_root(start: Path) -> Path | None:
151151
current = parent
152152

153153

154+
def find_legacy_project_root(start: Path) -> Path | None:
155+
"""Walk up from *start* looking for a ``.cocoindex_code/`` dir that contains ``cocoindex.db``.
156+
157+
Used by the backward-compat ``cocoindex-code`` entrypoint to re-anchor to a
158+
previously-indexed project tree. Returns the first matching directory, or ``None``.
159+
"""
160+
current = start.resolve()
161+
while True:
162+
if (current / _SETTINGS_DIR_NAME / "cocoindex.db").exists():
163+
return current
164+
parent = current.parent
165+
if parent == current:
166+
return None
167+
current = parent
168+
169+
154170
def find_parent_with_marker(start: Path) -> Path | None:
155171
"""Walk up from *start* looking for ``.cocoindex_code/`` or ``.git/``.
156172

tests/test_backward_compat.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
UserSettings,
1414
default_project_settings,
1515
default_user_settings,
16+
find_legacy_project_root,
1617
load_project_settings,
1718
load_user_settings,
1819
save_project_settings,
@@ -104,6 +105,27 @@ def test_legacy_extra_extensions_conversion(tmp_path: Path) -> None:
104105
assert "**/*.toml" in loaded.include_patterns
105106

106107

108+
def test_legacy_root_discovery_requires_cocoindex_db(tmp_path: Path) -> None:
109+
"""A .cocoindex_code dir without cocoindex.db should not be matched."""
110+
(tmp_path / ".cocoindex_code").mkdir()
111+
assert find_legacy_project_root(tmp_path) is None
112+
113+
114+
def test_legacy_root_discovery_with_cocoindex_db(tmp_path: Path) -> None:
115+
"""A .cocoindex_code dir with cocoindex.db should be matched, including from a subdirectory."""
116+
idx_dir = tmp_path / ".cocoindex_code"
117+
idx_dir.mkdir()
118+
(idx_dir / "cocoindex.db").touch()
119+
120+
# Exact directory
121+
assert find_legacy_project_root(tmp_path) == tmp_path
122+
123+
# From a subdirectory — should walk up and find the root
124+
sub = tmp_path / "src" / "pkg"
125+
sub.mkdir(parents=True)
126+
assert find_legacy_project_root(sub) == tmp_path
127+
128+
107129
def test_legacy_excluded_patterns_conversion(tmp_path: Path) -> None:
108130
"""COCOINDEX_CODE_EXCLUDED_PATTERNS should be appended to default exclude_patterns."""
109131

0 commit comments

Comments
 (0)