Tests / Test passed: 759
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>
31 lines
3.8 KiB
Markdown
31 lines
3.8 KiB
Markdown
## 1. Dependency
|
|
|
|
- [x] 1.1 Add `filelock` to `apps/device-host-agent/pyproject.toml` `dependencies` (already resolved transitively in `uv.lock`; this only adds a direct edge).
|
|
- [x] 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
|
|
|
|
- [x] 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.
|
|
- [x] 2.2 Expose an explicit `release()` (or context-manager `__exit__`) that unlocks and closes the underlying handle, per design.md Decision 5.
|
|
- [x] 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
|
|
|
|
- [x] 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`.
|
|
- [x] 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.
|
|
- [x] 3.3 Release the lock in `run_async()`'s existing shutdown `finally` block (`app.py:88-111`), alongside the existing heartbeat/console/supervisor teardown.
|
|
- [x] 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.
|
|
- [x] 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.
|
|
- [x] 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
|
|
|
|
- [x] 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
|
|
|
|
- [x] 5.1 Run `uv run --all-packages pytest -m "not integration"` and confirm no regressions.
|
|
- [x] 5.2 `ruff check` and `ruff format --check` on all changed/new files.
|
|
- [x] 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`.)*
|