Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions stackvox/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,9 +389,18 @@ def _cmd_stop(_: argparse.Namespace) -> int:


def _cmd_status(_: argparse.Namespace) -> int:
installed = updates._current_version()
if daemon.is_running():
print(f"running (pid {daemon.PID_PATH.read_text().strip()}) on {daemon.SOCKET_PATH}")
rc = 0
# The running daemon can lag the installed package if it wasn't restarted
# after an upgrade — surface that skew instead of trusting it blindly.
got, running_version = daemon.version()
if got and running_version != installed:
print(
f"daemon running {running_version}, but {installed} is installed "
f"— restart the daemon (stackvox stop; stackvox serve) to pick it up"
)
else:
print("stopped")
rc = 1
Expand Down
15 changes: 15 additions & 0 deletions stackvox/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ def handle(self) -> None:
self.wfile.write(b"ok\n")
return

if command == "version":
self.wfile.write(f"{updates._current_version()}\n".encode())
return

if command == "stop":
self.wfile.write(b"ok\n")
threading.Thread(target=self.server.shutdown, daemon=True).start()
Expand Down Expand Up @@ -343,3 +347,14 @@ def stop() -> tuple[bool, str]:

def ping() -> tuple[bool, str]:
return send({"command": "ping"}, timeout=PING_TIMEOUT_SECONDS)


def version(timeout: float = PING_TIMEOUT_SECONDS) -> tuple[bool, str]:
"""(got_version, version_or_error) reported by the running daemon.

The running daemon can differ from the installed package if it wasn't
restarted after an upgrade, so this reports what is actually serving — not
what's on disk. A version reply starts with a digit; error strings don't.
"""
_, resp = send({"command": "version"}, timeout=timeout)
return resp[:1].isdigit(), resp
20 changes: 20 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ def test_running_prints_pid_and_returns_zero(self, mocker, capsys):
pid_path.read_text.return_value = "12345\n"
mocker.patch.object(cli.daemon, "PID_PATH", pid_path)
mocker.patch.object(cli.daemon, "SOCKET_PATH", "/tmp/x.sock")
# status now queries the running daemon's version; stub it so the test
# doesn't reach a real socket.
mocker.patch.object(cli.daemon, "version", return_value=(False, "n/a"))

rc = cli._cmd_status(_ns())

Expand All @@ -269,6 +272,23 @@ def test_running_prints_pid_and_returns_zero(self, mocker, capsys):
assert "running" in out
assert "12345" in out

def test_running_flags_daemon_version_skew(self, mocker, capsys):
mocker.patch.object(cli.daemon, "is_running", return_value=True)
pid_path = mocker.MagicMock()
pid_path.read_text.return_value = "1\n"
mocker.patch.object(cli.daemon, "PID_PATH", pid_path)
mocker.patch.object(cli.daemon, "SOCKET_PATH", "/tmp/x.sock")
mocker.patch.object(cli.daemon, "version", return_value=(True, "0.5.0"))
mocker.patch.object(cli.updates, "_current_version", return_value="0.8.0")
mocker.patch.object(cli.updates, "check_for_update", return_value=None)

rc = cli._cmd_status(_ns())

assert rc == 0
out = capsys.readouterr().out
assert "0.5.0" in out and "0.8.0" in out # the skew is surfaced
assert "restart" in out.lower()

def test_stopped_returns_one(self, mocker, capsys):
mocker.patch.object(cli.daemon, "is_running", return_value=False)
rc = cli._cmd_status(_ns())
Expand Down
16 changes: 16 additions & 0 deletions tests/test_daemon_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ def test_ping_returns_ok(server: ServerHarness):
assert _roundtrip(server.sock, json.dumps({"command": "ping"}) + "\n") == "ok"


def test_version_returns_installed_version(server: ServerHarness):
from stackvox import updates

reply = _roundtrip(server.sock, json.dumps({"command": "version"}) + "\n")
assert reply == updates._current_version()


def test_version_client_helper_reports_running_version(server: ServerHarness, monkeypatch):
from stackvox import daemon, updates

monkeypatch.setattr(daemon, "SOCKET_PATH", server.sock)
got, resp = daemon.version()
assert got is True
assert resp == updates._current_version()


def test_plain_text_is_treated_as_text_field(server: ServerHarness):
"""Non-JSON payloads are accepted and wrapped as `{"text": line}`."""
import time
Expand Down
Loading