feat(host-agent): add single-instance lock to prevent duplicate-process dispatch races
Acquire an exclusive, non-blocking filelock on the identity state directory as the first action of create_application(), before resolve_host_identity() or any enrollment/heartbeat side effect. A second process against the same identity_path exits immediately with InstanceAlreadyRunningError naming the lock path; the lock releases automatically on any process exit (including SIGKILL) via OS-level advisory locking, and explicitly during run_async()'s shutdown finally block. filelock is promoted from transitive to direct dependency (version unchanged at 3.29.7). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import sys
|
||||
import pytest
|
||||
|
||||
from host_agent import cli
|
||||
from host_agent.instance_lock import InstanceAlreadyRunningError
|
||||
from host_agent.local_account import LocalAccountStore
|
||||
|
||||
|
||||
@@ -60,7 +61,9 @@ def test_existing_account_fast_path_skips_prompting(monkeypatch, tmp_path) -> No
|
||||
assert captured["config"].display_name == "operator"
|
||||
|
||||
|
||||
def test_interactive_first_run_prompts_and_creates_account(monkeypatch, tmp_path) -> None:
|
||||
def test_interactive_first_run_prompts_and_creates_account(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
_set_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
|
||||
inputs = iter(["operator"])
|
||||
@@ -120,3 +123,26 @@ def test_setup_subcommand_refuses_overwrite_without_confirmation(
|
||||
cli.main(["setup"])
|
||||
|
||||
assert store.load() == original
|
||||
|
||||
|
||||
def test_duplicate_instance_exits_with_clear_error(
|
||||
monkeypatch, tmp_path, capsys
|
||||
) -> None:
|
||||
_set_env(monkeypatch, tmp_path)
|
||||
LocalAccountStore(tmp_path / "account.json").create(
|
||||
"operator", "correct horse battery staple"
|
||||
)
|
||||
lock_path = tmp_path / "identity_dir" / "host_agent.lock"
|
||||
|
||||
def raising_create_application(*, config=None, **kwargs):
|
||||
raise InstanceAlreadyRunningError(lock_path)
|
||||
|
||||
monkeypatch.setattr(cli, "create_application", raising_create_application)
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
cli.main([])
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
err = capsys.readouterr().err
|
||||
assert "another Host Agent instance" in err
|
||||
assert str(lock_path) in err
|
||||
|
||||
Reference in New Issue
Block a user