5151# ---------------------------------------------------------------------------
5252
5353
54+ _daemon_ensured = False
55+
56+
5457def _connect_and_handshake () -> Connection :
5558 """Connect to the daemon and perform the version handshake.
5659
5760 Returns the open connection for the caller to send exactly one request.
58- Raises ``ConnectionRefusedError`` if the daemon is not running, or
59- ``RuntimeError`` on protocol/version errors.
61+
62+ On the first call, automatically starts or
63+ restarts the daemon if needed. Subsequent calls fail fast with
64+ ``DaemonVersionError`` on mismatch (indicating the daemon was replaced
65+ mid-session, e.g. after a tool upgrade).
6066 """
67+ global _daemon_ensured # noqa: PLW0603
68+
69+ if _daemon_ensured :
70+ return _raw_connect_and_handshake ()
71+
72+ # First connection — auto-start/restart as needed.
73+ try :
74+ conn = _raw_connect_and_handshake ()
75+ _daemon_ensured = True
76+ return conn
77+ except DaemonVersionError :
78+ stop_daemon ()
79+ except (ConnectionRefusedError , OSError ):
80+ pass
81+
82+ start_daemon ()
83+ _wait_for_daemon ()
84+
85+ # Verify the fresh daemon is reachable
86+ for _attempt in range (10 ):
87+ try :
88+ conn = _raw_connect_and_handshake ()
89+ _daemon_ensured = True
90+ return conn
91+ except (ConnectionRefusedError , OSError ):
92+ time .sleep (0.5 )
93+
94+ raise RuntimeError ("Failed to connect to daemon after starting it" )
95+
96+
97+ def _raw_connect_and_handshake () -> Connection :
98+ """Low-level connect + handshake without auto-start logic."""
6199 sock = daemon_socket_path ()
62100 if sys .platform != "win32" and not os .path .exists (sock ):
63101 raise ConnectionRefusedError (f"Daemon socket not found: { sock } " )
@@ -80,20 +118,25 @@ def _connect_and_handshake() -> Connection:
80118 if not isinstance (resp , HandshakeResponse ):
81119 conn .close ()
82120 raise RuntimeError (f"Unexpected handshake response: { type (resp ).__name__ } " )
83- if not resp .ok :
121+ if not resp .ok or _needs_restart ( resp ) :
84122 conn .close ()
85- raise _VersionMismatchError (resp )
123+ raise DaemonVersionError (resp )
86124 return conn
87125
88126
89- class _VersionMismatchError (Exception ):
90- """Raised when the daemon version or settings are stale."""
127+ class DaemonVersionError (RuntimeError ):
128+ """Raised when the daemon has a version or settings mismatch.
129+
130+ The first ``_connect_and_handshake()`` call handles this by restarting
131+ the daemon. If a mismatch occurs on a subsequent call, it means the
132+ daemon was replaced mid-session (e.g. after a tool upgrade).
133+ """
91134
92135 def __init__ (self , resp : HandshakeResponse ) -> None :
93136 self .resp = resp
94137 super ().__init__ (
95- f"Daemon version mismatch: { resp .daemon_version } "
96- f"(settings_mtime= { resp . global_settings_mtime_us } ) "
138+ f"Daemon version mismatch (daemon= { resp .daemon_version } , "
139+ f"client= { __version__ } ). Please retry — the daemon may need a restart. "
97140 )
98141
99142
@@ -319,6 +362,8 @@ def stop_daemon() -> None:
319362
320363 Escalation: StopRequest → SIGTERM → SIGKILL.
321364 """
365+ global _daemon_ensured # noqa: PLW0603
366+ _daemon_ensured = False
322367 pid_path = daemon_pid_path ()
323368
324369 pid : int | None = None
@@ -329,10 +374,15 @@ def stop_daemon() -> None:
329374 except (FileNotFoundError , ValueError ):
330375 pass
331376
332- # 1) Graceful StopRequest via socket
377+ # 1) Graceful StopRequest via socket (bypass auto-start)
333378 try :
334- stop ()
335- except (ConnectionRefusedError , OSError , RuntimeError ):
379+ conn = _raw_connect_and_handshake ()
380+ try :
381+ conn .send_bytes (encode_request (StopRequest ()))
382+ conn .recv_bytes ()
383+ finally :
384+ conn .close ()
385+ except (ConnectionRefusedError , OSError , RuntimeError , DaemonVersionError ):
336386 pass
337387
338388 if _wait_for_daemon_exit (timeout = 3.0 ):
@@ -408,37 +458,3 @@ def _needs_restart(resp: HandshakeResponse) -> bool:
408458 if current_mtime != resp .global_settings_mtime_us :
409459 return True
410460 return False
411-
412-
413- def ensure_daemon () -> None :
414- """Ensure daemon is running with correct version. Starts or restarts as needed.
415-
416- After this returns, per-request functions (``index``, ``search``, etc.)
417- can be called directly — each opens its own connection.
418- """
419- # Try connecting to existing daemon
420- try :
421- conn = _connect_and_handshake ()
422- conn .close ()
423- return # daemon is up and version matches
424- except _VersionMismatchError :
425- stop_daemon ()
426- except (ConnectionRefusedError , OSError ):
427- pass
428-
429- # Start daemon
430- start_daemon ()
431- _wait_for_daemon ()
432-
433- # Verify with retries
434- for _attempt in range (10 ):
435- try :
436- conn = _connect_and_handshake ()
437- conn .close ()
438- return
439- except _VersionMismatchError as e :
440- raise RuntimeError (f"Daemon mismatch after fresh start: { e } " ) from e
441- except (ConnectionRefusedError , OSError ):
442- time .sleep (0.5 )
443-
444- raise RuntimeError ("Failed to connect to daemon after starting it" )
0 commit comments