Metadata-Version: 2.5
Name: usb-device-farm
Version: 0.5.0
Summary: 集群 USB 设备集中管理与租借平台（复用现有 USB/IP 工具链的编排层）
Author: h.t.x
License-Expression: MIT
Keywords: device-sharing,lease,orchestration,usb,usbip
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Hardware
Classifier: Topic :: System :: Networking
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# usb-device-farm — 集群 USB 设备集中管理与租借平台

> 编排层实现（v0.5.0 / M1 + M2 + M2.5 + M2.6）。**不重写 USB/IP 协议栈与内核驱动**（ADR-01），
> 只复用 Linux `usbip` / Windows `usbipd-win` + `usbip-win2` 工具链，自建账号、目录、租约、抢占、审计、路径决策与中继鉴权。
> PyPI 包名 `usb-device-farm`（导入名仍为 `usbipd`；命令入口 `usb-device-farm`，避免与 usbipd-win 的 `usbipd.exe` 冲突）。

## 三点结论

1. **零第三方依赖**：Python 3.9+ 标准库（`sqlite3` / `http.server` / `asyncio` / `urllib`）。项目用 [uv](https://docs.astral.sh/uv/) 管理：`uv run` 自动创建虚拟环境并执行；无 uv 的机器仍可 `python -m usbipd` 直接跑，无需 pip install。
2. **默认 `dry-run`**：整条链路（查询→租用→attach→抢占→收回→到期断链）都能在无真机环境跑通并验收，但不会改系统防火墙、不会复位真实 USB 设备；`--live` 才真正调用现成工具。
3. **强制断开是真断数据面**：撤准入白名单 + relay 主动关闭长连接 + detach + 设备复位 + 审计，且抢占在单事务内原子转交，不留可被第三人抢走的空窗（E-16）；断链校验失败设备置 `fault`（E-18）。

## 环境准备（uv）

```bash
# 需 uv ≥ 0.4（项目已含 .python-version=3.12 与 uv.lock）
uv sync            # 创建 .venv 并以可编辑方式安装本项目（零第三方依赖）
uv run usb-device-farm --help

# 发布后可从 PyPI 安装：uv tool install usb-device-farm 或 pip install usb-device-farm
```

| 命令 | 作用 |
| --- | --- |
| `uv sync` | 按 `uv.lock` 同步虚拟环境与本项目 |
| `uv run python -m usbipd ...` | 在 `.venv` 中运行（下文示例均可加 `uv run` 前缀） |
| `uv run usb-device-farm ...` | 同上，等价的安装后命令入口（名称避开 usbipd-win 的 `usbipd`） |
| `uv run python -m unittest discover -s tests -q` | 运行 37 项单测 |
| `uv lock --upgrade` | 升级锁文件（当前无第三方依赖，仅锁定本项目与 Python 版本） |
| `uv python install 3.12` | 无系统 Python 时由 uv 托管解释器 |
| `uv build` / `uv publish` | 构建并发布 sdist+wheel 到 PyPI |

## 架构

```mermaid
flowchart LR
    C[client CLI<br/>query / map / unmap] -- 控制面 HTTPS/JSON --> H[hub<br/>目录·账号·优先级·租约·审计]
    A[node-agent<br/>采集·心跳·策略] -- 控制面 --> H
    H -- connect_plan --> C
    H -- 下发命令/复位 --> A
    C -. 直连 direct: USB/IP 3240 .-> N[宿主 usbipd / usbipd-win]
    C -. 不可达 relay: USB/IP 3240 .-> R[relay<br/>v1.1.1 协议感知]
    R -. 长连接转发 .-> N
    H -. 会话注册/主动断链 .-> R
```

`hub` 与 `relay` 目前同进程共享会话注册表（`RelayRegistry`）；独立 relay 节点池是 Q-15 的后续工作。

## 快速开始（三终端，dry-run 演示）

以下命令均在 `uv sync` 之后执行（无 uv 时去掉 `uv run` 前缀即可）：

```bash
# 1) 中心服务：首次启动自动生成管理员并打印一次性口令
#    加 --web 才启用浏览器控制台（默认关闭，纯 API）
USBIPD_BOOTSTRAP_PASSWORD=admin123 USBIPD_AGENT_TOKEN=secret \
  uv run python -m usbipd hub --host 0.0.0.0 --port 8420 --relay-host <hub对外IP> --web

# 2) 宿主代理（无真机时 --mock 上报 2 台模拟设备）
USBIPD_AGENT_TOKEN=secret uv run python -m usbipd agent \
  --hub http://<hub>:8420 --node-id node-1 --host <宿主IP> --mock --interval 5
#   跨网段不可直连时加 --unreachable，hub 会自动对该节点走 relay

# 3) 使用者电脑
uv run python -m usbipd client --hub http://<hub>:8420 --user admin --password admin123 \
  admin user-add alice pass123 --priority 10
uv run python -m usbipd client --hub http://<hub>:8420 --user alice --password pass123 query
uv run python -m usbipd client --hub http://<hub>:8420 --user alice --password pass123 map <device_id>
uv run python -m usbipd client --hub http://<hub>:8420 --user alice --password pass123 status
uv run python -m usbipd client --hub http://<hub>:8420 --user alice --password pass123 unmap
```

`map` 默认只打印将要执行的 `usbip attach -r <host> -b <busid>`；加 `--live` 才真正执行，且直连失败会自动请求 hub 切换到 relay（Q-13 自动回退）。

## Web 控制台（可选，`--web` 启用）

默认 **不启用**，`hub` 只提供 JSON API。加 `--web`（或 `USBIPD_WEB=1`）后访问 `http://<hub>:<port>/`：

| 角色 | 能做什么 |
| --- | --- |
| 用户 | 设备目录（按状态/关键词筛选）、租用与抢占、查看 connect_plan 与 attach 命令、我的租约（激活/续租/断开，剩余时间倒计时） |
| 管理员 | 上述全部 + 新建/停用账号、改优先级、节点列表、停/开共享、复位 fault、强制收回（原因写审计）、审计日志 |

单文件 `usbipd/hub/static/index.html`，原生 JS、无 CDN、无构建步骤；token 存 localStorage，页面每 5s 刷新，登出即清除。未启用时 `GET /` 与 `/ui` 返回 `404 WEB_UI_DISABLED` 并提示加 `--web`。

```bash
python -m usbipd hub --port 8420 --web      # 浏览器打开 http://127.0.0.1:8420/
```

## 命令速查

| 组件 | 命令 | 说明 |
| --- | --- | --- |
| hub | `python -m usbipd hub --help` | 中心服务；`--web` 开控制台、`--no-relay` / `--force-relay` / `--live` |
| agent | `python -m usbipd agent --help` | `--os auto/linux/windows`、`--mock`、`--once`、`--unreachable` |
| client | `query` / `map` / `unmap` / `renew` / `activate` / `switch-relay` / `status` / `login` | 三动作 + 续租/回退 |
| client | `admin users|user-add|user-disable|user-set|device-set|force-release|reset-fault|audit|nodes|sessions` | 管理员账号与设备管理 |
| 自检 | `uv run python -m usbipd selftest` | 端到端 11 项冒烟，无需真机 |
| 单测 | `uv run python -m unittest discover -s tests -v` | 37 项，含真实 TCP 中继越权、非默认端口一致性、Windows 未知设备类禁抢、Web UI 启用/禁用 |

全局参数 `--hub` / `--user` / `--password` / `--token` / `--live` / `--json` 放在**动作之前**；也可用环境变量 `USBIPD_HUB` / `USBIPD_USER` / `USBIPD_PASSWORD`。`--json` 输出纯 JSON 供脚本消费（提示信息走 stderr）。

## 主要配置（环境变量，CLI 显式参数优先）

| 变量 | 默认值 | 用途 |
| --- | --- | --- |
| `USBIPD_SECRET` / `USBIPD_SECRET_FILE` | 首次自动生成并落盘 | HMAC 令牌签名密钥，不硬编码 |
| `USBIPD_DB` | `usbipd.db` | SQLite 单文件（WAL） |
| `USBIPD_AGENT_TOKEN` | 空（不校验） | node-agent 注册令牌，**生产必填** |
| `USBIPD_BOOTSTRAP_PASSWORD` | 随机生成并打印一次 | 首次启动管理员口令 |
| `USBIPD_RELAY_HOST` / `USBIPD_RELAY_PORT` / `USBIPD_RELAY_BIND` | `127.0.0.1` / `3240` / `127.0.0.1` | relay 客户端连接地址、监听端口与绑定地址 |
| `USBIPD_DRY_RUN` | `1` | `0` 时 agent 才真正执行防火墙与复位命令 |
| `USBIPD_LEASE_TTL` / `USBIPD_LEASE_MAX_TTL` / `USBIPD_LEASE_ABSOLUTE_MAX` | 3600 / 28800 / 86400 秒 | 默认租期、单次上限、累计绝对上限 |
| `USBIPD_FORCE_RELAY` / `USBIPD_ALLOW_SAME_PRIORITY_PREEMPT` / `USBIPD_STORAGE_PREEMPTIBLE` | `0` | Q-13 / Q-18 / Q-21 策略开关 |
| `USBIPD_HEARTBEAT_TIMEOUT` | 90 秒 | 节点掉线收敛 |
| `USBIPD_WEB` | `0` | `1` 启用 Web 控制台（等价 `hub --web`） |

## 实现范围与规格映射

| 状态 | 规格项 |
| --- | --- |
| 🔵 已实现 | S-01～S-12（注册/采集/刷新/查询/账号/设备/租约/直连准入/客户端三动作/自愈骨架/审计）、S-13 Web 控制台（可选 `--web`）、S-15～S-18（路径决策、中继转发、协议感知校验、虚拟 busid）、S-20～S-25（三动作、到期强断、优先级、抢占、管理员收回） |
| 🟡 简化实现 | S-11 自愈（客户端需手工重连；无自动重连守护进程）、S-19 中继高可用（单 relay）、S-14 预约排队（未做） |
| ⛔ 未做 | 重写 USB/IP 协议栈（N-07）、计费/审批（N-02）、2×2 平台矩阵实机验证（M0） |

验收映射：`A-07/A-18/A-19/A-20/A-21/A-22` 由 `tests/test_lease.py` + `selftest.py` 覆盖；`A-15`（DEVLIST 过滤、越权 IMPORT、主动断链）由 `tests/test_relay_integration.py` 用真实 TCP + 假宿主验证；`A-23`（2×2 平台矩阵）需 M0 实机验证，代码侧已具备 Linux/Windows 双采集与双策略实现。

## 生产部署前置（务必阅读）

1. **直连准入必须预置默认 DROP**（Q-03 方案 A），否则白名单形同虚设：
   - Linux：`iptables -A INPUT -p tcp --dport 3240 -j DROP`
   - Windows：`netsh advfirewall firewall add rule name=usbipd-default-block dir=in action=block protocol=TCP localport=3240`
   agent 只增删 ACCEPT 规则；relay 路径下宿主只放行 relay 主机 IP。
2. **relay 端口与客户端端口必须一致**：默认 TCP 3240。Linux `usbip` 与 Windows `usbip-win2` 的 `usbip.exe` 都支持全局 `--tcp-port` / `-t` 指定端口（ArchWiki《USB/IP》、usbip-win2 CLI 文档；检索 2026-11-05），所以 `USBIPD_RELAY_PORT` 改成非 3240 时，客户端 `map` 会自动在 `usbip attach` 前追加 `--tcp-port <port>`。之所以仍选**单中继端口 + 全局 `virtual_busid` + 源 IP 路由**（而非每租约一个端口），是为了少开防火墙端口、兼容不带该选项的旧工具链、简化会话管理；`connect_plan.attach_port` 已保留，未来可切动态端口。跨网段时把 `--relay-host` 设为 hub 对外 IP、`--relay-bind 0.0.0.0`；Windows 上 3240 若被保留，先查 `netsh interface ipv4 show excludedportrange protocol=tcp`。宿主导出服务端口若改成非 3240，用 `agent --usbip-port <port>` 注册，`connect_plan` 与白名单增删会同步该端口。
3. **必须实现 USB/IP v1.1.1 报文**（F-08）：Linux 内核宿主与本项目 relay 均满足；否则 Windows 客户端连不上。Windows 客户端优先用 `usbip-win2 ≥ v0.9.7.3` 微软签名驱动（ADR-06），测试签名仅作降级且**装后不得关闭 testsigning**（F-09）。
4. **安全**：设置 `USBIPD_AGENT_TOKEN`、`USBIPD_SECRET`，控制面前面挂 HTTPS 反向代理（当前为 HTTP + Bearer 令牌），审计表只追加不提供删除接口；`.usbipd_client.json` / `.usbipd_sessions.json` 含令牌，勿提交版本库。
5. **强拆数据风险（Q-21）**：存储/音视频类设备默认禁止抢占；强制断开前是否已优雅 detach 取决于客户端实现，U 盘等正在读写时仍可能损坏数据。

## 目录结构

```
usbipd/
  common/    配置、错误码、PBKDF2/HMAC 鉴权、JSON HTTP 客户端
  core/      模型、SQLite 事务、租约状态机、数据面执行器
  hub/       REST 路由、后台到期扫描、服务启动、CLI
  hub/static/index.html  Web 控制台（可选 --web，单文件原生 JS）
  agent/     Linux sysfs / Windows usbipd list 采集、防火墙与复位策略、心跳
  client/    三动作 CLI、usbip attach/detach 封装
  relay/     v1.1.1 报文解析、会话注册表、asyncio 长连接中继
  selftest.py
tests/      37 项 unittest（含真实 TCP 中继集成测试、Web UI 启用/禁用）
pyproject.toml / uv.lock / .python-version   uv 项目管理（Python 3.12）
目标与规格书.md
```

## 已知限制

- hub 为单实例 SQLite，未做多副本/高可用；relay 重启会断开全部活跃 attach（F-07，规格已标注）。
- 数据面连接与租约状态的强一致依赖 node-agent 在线回报；`dry-run` 下「断链校验」只验证会话已关闭，真机需配合定期对账把残留升级为 `fault`。
- Windows 采集依赖 `usbipd list` 文本输出，只能拿到 busid／VID:PID／STATE；设备类、序列号、速度、`driver_bound` 无法准确判定（如实标 `unknown`，Linux 侧由 sysfs 判定）。因此 Windows 节点新上报的设备**默认禁止抢占**，管理员确认设备类型后可用 `PATCH /devices/{id} {"preemptible": true}` 显式放开；心跳对账不会覆盖该设置。
- 非 3240 宿主端口需用 `agent --usbip-port PORT` 注册，`connect_plan` 与防火墙准入/撤销命令会同步使用该端口。
- Web 控制台默认关闭；启用后与 JSON API 共用同一端口与 Bearer 令牌（token 存 localStorage），控制面仍是 HTTP，生产环境必须在前面挂 HTTPS 反向代理，并只对受控网络开放 `--web`。
- 尚未做 M0 实机 2×2 矩阵验证，Windows 客户端驱动与设备类兼容性需实测（F-04/F-08）；本仓所有自动化验证均在 `dry-run` 下完成，**从未发生过真实 `usbip attach`**。
