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>
3.8 KiB
3.8 KiB
1. Dependency
- 1.1 Add
filelocktoapps/device-host-agent/pyproject.tomldependencies(already resolved transitively inuv.lock; this only adds a direct edge). - 1.2 Run
uv lock(workspace-wide) and confirm the resolvedfilelockversion/hash is unchanged from what's already pinned transitively.
2. Instance lock module
- 2.1 Create
apps/device-host-agent/host_agent/instance_lock.pywrappingfilelock.FileLock: a function/class that takes the state directory (derived fromidentity_path.parent), acquireshost_agent.lockinside it withtimeout=0, and raises a dedicatedInstanceAlreadyRunningError(naming the lock file path) onfilelock.Timeoutinstead 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 raisesInstanceAlreadyRunningErrorwhen 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, beforeresolve_host_identity(), usingstartup_config.identity_path.parent. - 3.2 Add an
instance_lockfield toHostAgentApplication(app.py:35-42) so the held lock stays alive for the object's full lifetime rather than being released whencreate_application()returns. - 3.3 Release the lock in
run_async()'s existing shutdownfinallyblock (app.py:88-111), alongside the existing heartbeat/console/supervisor teardown. - 3.4 In
cli.py:main(), catchInstanceAlreadyRunningErroraround thecreate_application(...).run()call, print a clear duplicate-instance error to stderr (naming the lock path), and exit non-zero — matching the existingLocalAccountSetupErrorhandling pattern already in that function. - 3.5 Unit tests in
test_app.py: a secondcreate_application()call against the sameidentity_path(while the firstHostAgentApplication's lock is still held) raisesInstanceAlreadyRunningErrorbefore any enrollment call occurs (assert the enrollment client'senroll_host/enroll_deviceare never invoked for the second call); twocreate_application()calls against differentidentity_pathvalues both succeed and can coexist. - 3.6 Unit test confirming the lock is released after
run_async()completes its shutdown sequence, allowing an immediately-followingcreate_application()call against the sameidentity_pathto succeed (simulating a clean restart).
4. Documentation
- 4.1 Add a short note to
docs/MACOS_IPHONE_SETUP.mddescribing 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 checkandruff format --checkon all changed/new files. - 5.3
openspec validate host-agent-single-instance-lock --strictand 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, andtest_independent_paths_never_contendintest_app.py/test_instance_lock.py.)