diff --git a/hermes_cli/main.py b/hermes_cli/main.py index f7e73c41a5..e3419c7297 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -3269,8 +3269,8 @@ def cmd_update(args): from gateway.status import get_running_pid, remove_pid_file from hermes_cli.gateway import ( get_service_name, get_launchd_plist_path, is_macos, is_linux, - refresh_launchd_plist_if_needed, - _ensure_user_systemd_env, get_systemd_linger_status, + launchd_restart, _ensure_user_systemd_env, + get_systemd_linger_status, ) import signal as _signal @@ -3374,26 +3374,16 @@ def cmd_update(args): print(" System services may require root. Try:") print(f" sudo systemctl restart {_gw_service_name}") elif has_launchd_service: - # Refresh the plist first (picks up --replace and other - # changes from the update we just pulled). - refresh_launchd_plist_if_needed() - # Explicit stop+start — don't rely on KeepAlive respawn - # after a manual SIGTERM, which would race with the - # PID file cleanup. + # Use the shared launchd restart helper so we wait for the + # old gateway process to fully exit before starting the new + # one. This avoids stop/start races during self-update. print("→ Restarting gateway service...") - _launchd_label = get_launchd_label() - stop = subprocess.run( - ["launchctl", "stop", _launchd_label], - capture_output=True, text=True, timeout=10, - ) - start = subprocess.run( - ["launchctl", "start", _launchd_label], - capture_output=True, text=True, timeout=10, - ) - if start.returncode == 0: + try: + launchd_restart() print("✓ Gateway restarted via launchd.") - else: - print(f"⚠ Gateway restart failed: {start.stderr.strip()}") + except subprocess.CalledProcessError as e: + stderr = (getattr(e, "stderr", "") or "").strip() + print(f"⚠ Gateway restart failed: {stderr}") print(" Try manually: hermes gateway restart") elif existing_pid: try: diff --git a/tests/hermes_cli/test_update_gateway_restart.py b/tests/hermes_cli/test_update_gateway_restart.py index 1d6b064af6..9697dc7cb8 100644 --- a/tests/hermes_cli/test_update_gateway_restart.py +++ b/tests/hermes_cli/test_update_gateway_restart.py @@ -307,21 +307,14 @@ class TestCmdUpdateLaunchdRestart: # Mock get_running_pid to return a PID with patch("gateway.status.get_running_pid", return_value=12345), \ - patch("gateway.status.remove_pid_file"): + patch("gateway.status.remove_pid_file"), \ + patch.object(gateway_cli, "launchd_restart") as mock_launchd_restart: cmd_update(mock_args) captured = capsys.readouterr().out assert "Gateway restarted via launchd" in captured assert "Restart it with: hermes gateway run" not in captured - # Verify launchctl stop + start were called (not manual SIGTERM) - launchctl_calls = [ - c for c in mock_run.call_args_list - if len(c.args[0]) > 0 and c.args[0][0] == "launchctl" - ] - stop_calls = [c for c in launchctl_calls if "stop" in c.args[0]] - start_calls = [c for c in launchctl_calls if "start" in c.args[0]] - assert len(stop_calls) >= 1 - assert len(start_calls) >= 1 + mock_launchd_restart.assert_called_once_with() @patch("shutil.which", return_value=None) @patch("subprocess.run")