Files
agentic-mobile-control/apps/device-host-agent/host_agent/cli.py
T
q792602257andClaude Opus 4.6 d00ada67a5 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>
2026-07-14 15:49:30 +08:00

84 lines
2.8 KiB
Python

from __future__ import annotations
import argparse
import getpass
import sys
from collections.abc import Sequence
from dataclasses import replace
from host_agent.app import create_application
from host_agent.config import load_host_agent_config
from host_agent.instance_lock import InstanceAlreadyRunningError
from host_agent.local_account import LocalAccountStore
class LocalAccountSetupError(RuntimeError):
"""Raised when local account bootstrap cannot proceed."""
def main(argv: Sequence[str] | None = None) -> None:
parser = argparse.ArgumentParser(description="Run the Device Host Agent")
subparsers = parser.add_subparsers(dest="command")
subparsers.add_parser("setup", help="Create the local operator account")
args = parser.parse_args(argv)
try:
if args.command == "setup":
_run_setup()
return
config = _resolve_config_with_local_account()
except LocalAccountSetupError as exc:
print(f"error: {exc}", file=sys.stderr)
raise SystemExit(1) from exc
try:
create_application(config=config).run()
except InstanceAlreadyRunningError as exc:
print(f"error: {exc}", file=sys.stderr)
raise SystemExit(1) from exc
def _run_setup() -> None:
config = load_host_agent_config()
store = LocalAccountStore(config.local_account_path)
if store.load() is not None:
confirm = (
input("A local account already exists. Overwrite it? [y/N] ")
.strip()
.lower()
)
if confirm != "y":
print("Setup cancelled; existing account left unchanged.")
return
account = _prompt_and_create(store)
print(f"Local account '{account.username}' created.")
def _resolve_config_with_local_account():
config = load_host_agent_config()
store = LocalAccountStore(config.local_account_path)
account = store.load()
if account is None:
if not sys.stdin.isatty():
raise LocalAccountSetupError(
"no local account configured; run `device-host-agent setup` "
"on an interactive terminal to create one"
)
account = _prompt_and_create(store)
if not config.display_name:
config = replace(config, display_name=account.username)
return config
def _prompt_and_create(store: LocalAccountStore):
username = input("Username: ").strip()
if not username:
raise LocalAccountSetupError("username must not be empty")
password = getpass.getpass("Password: ")
confirm = getpass.getpass("Confirm password: ")
if not password:
raise LocalAccountSetupError("password must not be empty")
if password != confirm:
raise LocalAccountSetupError("passwords do not match")
return store.create(username, password)