Files
q792602257andClaude Opus 4.6 82567fd248
Tests / Test passed: 759
chore(openspec): add host-agent-single-instance-lock change artifacts
Proposal, design, spec, and tasks for the per-installation exclusive
instance lock. 15/16 tasks complete; only manual real-environment
verification (5.4) remains, with semantics covered by unit tests in
test_app.py and test_instance_lock.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 15:49:45 +08:00

3.8 KiB

1. Dependency

  • 1.1 Add filelock to apps/device-host-agent/pyproject.toml dependencies (already resolved transitively in uv.lock; this only adds a direct edge).
  • 1.2 Run uv lock (workspace-wide) and confirm the resolved filelock version/hash is unchanged from what's already pinned transitively.

2. Instance lock module

  • 2.1 Create apps/device-host-agent/host_agent/instance_lock.py wrapping filelock.FileLock: a function/class that takes the state directory (derived from identity_path.parent), acquires host_agent.lock inside it with timeout=0, and raises a dedicated InstanceAlreadyRunningError (naming the lock file path) on filelock.Timeout instead of leaking the library's own exception type.
  • 2.2 Expose an explicit release() (or context-manager __exit__) that unlocks and closes the underlying handle, per design.md Decision 5.
  • 2.3 Unit tests for instance_lock.py: acquire succeeds when free; acquire raises InstanceAlreadyRunningError when already held (same process, second handle against the same path); release-then-reacquire succeeds; two different paths never contend.

3. Wiring into application startup/shutdown

  • 3.1 In create_application() (apps/device-host-agent/host_agent/app.py:145), acquire the instance lock as the first statement, before resolve_host_identity(), using startup_config.identity_path.parent.
  • 3.2 Add an instance_lock field to HostAgentApplication (app.py:35-42) so the held lock stays alive for the object's full lifetime rather than being released when create_application() returns.
  • 3.3 Release the lock in run_async()'s existing shutdown finally block (app.py:88-111), alongside the existing heartbeat/console/supervisor teardown.
  • 3.4 In cli.py:main(), catch InstanceAlreadyRunningError around the create_application(...).run() call, print a clear duplicate-instance error to stderr (naming the lock path), and exit non-zero — matching the existing LocalAccountSetupError handling pattern already in that function.
  • 3.5 Unit tests in test_app.py: a second create_application() call against the same identity_path (while the first HostAgentApplication's lock is still held) raises InstanceAlreadyRunningError before any enrollment call occurs (assert the enrollment client's enroll_host/enroll_device are never invoked for the second call); two create_application() calls against different identity_path values both succeed and can coexist.
  • 3.6 Unit test confirming the lock is released after run_async() completes its shutdown sequence, allowing an immediately-following create_application() call against the same identity_path to succeed (simulating a clean restart).

4. Documentation

  • 4.1 Add a short note to docs/MACOS_IPHONE_SETUP.md describing the new duplicate-instance error (what it means, how to resolve it — find and stop the other process) near the existing Host Agent startup instructions.

5. Validation

  • 5.1 Run uv run --all-packages pytest -m "not integration" and confirm no regressions.
  • 5.2 ruff check and ruff format --check on all changed/new files.
  • 5.3 openspec validate host-agent-single-instance-lock --strict and fix any reported issues.
  • 5.4 Manual verification: start one Host Agent instance, attempt to start a second against the same identity, confirm the second exits immediately with the duplicate-instance error and the first is unaffected; stop the first, confirm a subsequent start succeeds. (Pending: requires a real Host Agent deployment environment — covered indirectly by test_second_create_application_against_held_lock_raises_before_enrollment, test_lock_released_after_run_async_allows_restart, and test_independent_paths_never_contend in test_app.py/test_instance_lock.py.)