茶器艺科智造HarmonyOS应用实战-18-watch漏掉libraryhar和libraryhsp,默认部署又遍历所有设备:给调试脚本加变更域与目标门禁
茶器艺科智造HarmonyOS应用实战-18-watch漏掉libraryhar和libraryhsp,默认部署又遍历所有设备:给调试脚本加变更域与目标门禁
先做三个由当前脚本分支推导出的独立风险演练。开发者修改 libraryhar/src/main/ets/model/CupLatheSilhouette.ets,watch 终端不会因这次保存启动;修改 entry 后,watcher 才会调用 Shell chaqi-auto-debug.sh,未设置 HDC_TARGET 时由 hdc 自己选择默认目标,安装使用 install -r,不会执行卸载。另一次独立操作中,开发者若在 Windows 执行 debug.ps1 run 且没有设置 HDC_TARGET,脚本才会遍历全部连接并逐台卸载、安装、启动。Pad 包装脚本则有第三种风险:它从 loopback 连接里取第一条,再把该值传给 Shell 自动调试。这三条调用链没有互相调用,本文把它们并列审查,而不伪造成同一次事故。
我核对的源码修订为 f671fcd8de277973bd7518a3c462f68384575f1e。当前 chaqi-watch-debug.sh 只监听 entry 的 ets/resources、AppScope 和部分根/entry 配置,没有 libraryhsp、libraryhar;Windows debug.ps1 在没有 HDC_TARGET 时读取全部 hdc list targets 并逐台 uninstall/install/start;Pad 脚本从所有 127.0.0.1:端口 中取第一条。本文只给出脚本设计与安全验证方案,没有修改脚本,没有启动 watcher、构建包、连接设备或执行卸载安装。

本文会解决:
- 用项目依赖图确定 watcher 应覆盖哪些源码和配置。
- 把文件变化映射为明确构建域,而不是任何变化都盲目 run。
- 让安装、卸载、启动必须绑定一个可回读目标。
- 把多设备部署从默认行为改成显式批次。
- 避免 Pad 脚本把第一个 loopback 连接误认成目标实例。
- 用 dry-run 和失败注入证明门禁有效。
一、先把三个入口的调用链分开
依据当前脚本分支,真实调用关系应画成三类入口、四个执行分支:
A. 修改 libraryhar
→ chaqi-watch-debug.sh 范围未命中
→ 本次保存不触发构建或部署
B. 修改 entry
→ chaqi-watch-debug.sh 命中
→ chaqi-auto-debug.sh
→ chaqi-build.sh → chaqi-install-hap.sh → chaqi-launch-app.sh
→ 未设置 HDC_TARGET 时交给 hdc 默认目标
→ install -r,不执行 uninstall
C. 开发者另行执行 Windows debug.ps1 run
→ 未设置 HDC_TARGET
→ Resolve-HdcTargets 返回全部连接
→ foreach 逐台 uninstall/install/start
D. 开发者另行执行 chaqi-auto-debug-pad.sh
→ 从 loopback 候选取第一条
→ 导出 HDC_TARGET
→ 调用 chaqi-auto-debug.sh
这里有三个可以分别复现的问题:watcher 的变更域不完整;Windows run 把空目标解释为全部连接;Pad 包装脚本把列表顺序当成设备身份。它们共同说明调试入口缺少统一的变更域与目标契约,但 watcher 不会调用 Windows debug.ps1,Pad 包装脚本也不是 Windows 多设备遍历的前置步骤。
静态源码能证明这些分支存在,却不能证明某次真实运行已经删除了数据或选错设备。只有对应入口的命令日志、设备列表与目标回读才能确认具体影响,因此本文把它们作为应当分别防住的风险,而非已经发生的事故记录。
二、现有 watcher 只看到产品入口的一部分
当前 chaqi-watch-debug.sh 的核心列表是:
fswatch -1 -r -l "$LAT" --exclude '/build/' --exclude '/\.hvigor/' --exclude '/oh_modules/' --exclude '/node_modules/' "$ROOT/entry/src/main/ets" "$ROOT/entry/src/main/resources" "$ROOT/AppScope" "$ROOT/build-profile.json5" "$ROOT/entry/build-profile.json5" "$ROOT/oh-package.json5" "$ROOT/entry/oh-package.json5"
它遗漏了至少这些直接影响运行结果的路径:
libraryhsp/src/main/ets:主页面和组件。libraryhsp/src/main/resources:启动叠层、rawfile Three.js、字体。libraryhsp/build-profile.json5与oh-package.json5。libraryhar/src/main/ets:路由、Preferences、杯体轮廓、切片估算。libraryhar/build-profile.json5与oh-package.json5。- 三个模块的 hvigorfile.ts,以及根 hvigorfile.ts。
libraryhar 当前没有 resources 目录,不能因此永远排除;watcher 可以只加入存在路径,未来新增资源时自动覆盖。

三、监听清单应从模块图生成
根 build-profile.json5 已声明 entry、libraryhsp、libraryhar,模块 oh-package 又给出依赖关系。建议 watcher 启动时生成路径清单,而不是长期手写一部分。
提议的 Bash 函数如下,尚未落地:
build_watch_paths() {
WATCH_PATHS=(
"$ROOT/AppScope"
"$ROOT/build-profile.json5"
"$ROOT/hvigorfile.ts"
)
for module in entry libraryhsp libraryhar; do
for path in "$ROOT/$module/src/main" "$ROOT/$module/build-profile.json5" "$ROOT/$module/oh-package.json5" "$ROOT/$module/hvigorfile.ts"
do
test -e "$path" && WATCH_PATHS+=("$path")
done
done
}
这不是动态解析 JSON5 的最终实现,但已经把三个已登记模块纳入同一规则。进一步可以从根配置读取 modules,避免新增模块后再次漏改脚本;解析 JSON5 需要使用仓库已有工具或受控解析器,不能用脆弱正则假装完整语法。
排除项仍应保留 build、.hvigor、oh_modules、node_modules,避免构建输出反过来触发无限循环。日志、截图和生成文档目录也应根据仓库实际位置排除。
四、文件变化要先归类,再选择构建计划
监听到变化不意味着立即部署。建议建立变更域:
| 变更域 | 典型路径 | 最小构建意图 | 是否默认部署 |
|---|---|---|---|
| entry-code | entry/src/main/ets | entry 与依赖一致的调试包 | 可 |
| entry-resource | entry/src/main/resources | 重建 entry | 可 |
| hsp-code | libraryhsp/src/main/ets | 重建 HSP/应用集合 | 可 |
| hsp-resource | libraryhsp/src/main/resources | 重建 HSP/应用集合 | 可 |
| har-code | libraryhar/src/main/ets | 重建所有消费 HAR 的产物 | 可 |
| app-config | AppScope、根 build-profile | full-app | 先 dry-run |
| module-config | 任一模块配置 | full-app | 先 dry-run |
| unknown | 其他 | 不自动部署 | 只报告 |
HAR 同时被 entry 和 HSP 依赖,变更后不能只更新其中一侧。文章 17 已讨论 full-app/module-set 产物语义;这里的 watcher 只负责选计划,不重新定义安装集合。
可以给事件批次一个结构:
interface ChangeBatch {
id: string;
paths: string[];
domains: string[];
firstEventAtMs: number;
lastEventAtMs: number;
buildMode: 'full-app' | 'module-set' | 'none';
requiresDryRun: boolean;
}
防抖应聚合同一保存动作的多条事件,随后根据所有 domain 取最宽构建范围。未知路径不能默认升级成“全设备 run”,应停止并告诉开发者为何未部署。
五、当前 Windows 默认把所有连接都当目标
Resolve-HdcTargets 在 HDC_TARGET 为空时调用 Get-HdcTargetsList,再原样返回全部目标。安装和启动阶段分别 foreach:
$targets = @(Resolve-HdcTargets $HdcExe)
foreach ($t in $targets) {
& $HdcExe -t $t uninstall $BundleName
& $HdcExe -t $t install @packagesToInstall
}
foreach ($t in $targets) {
& $HdcExe -t $t shell aa force-stop $BundleName
& $HdcExe -t $t shell aa start -a $AbilityName -b $BundleName
}
脚本注释把它描述为便利功能,但 uninstall 是会改变设备数据的动作,不能以“没设置变量”作为多设备授权。更稳的默认规则是:
- targets/list 可以无目标运行。
- build 不需要设备。
- log/start/install/run 必须解析为恰好一个目标。
- 多设备操作必须显式使用
--all或--targets a,b,并回显完整计划。 - clean/uninstall 再加一层明确模式,不能混在普通 run。
六、用 exactly-one 门禁替代隐式全选
PowerShell 提议实现:
function Resolve-ExactlyOneTarget([string]$Hdc, [string]$Requested) {
if ([string]::IsNullOrWhiteSpace($Requested)) {
$connected = @(Get-HdcTargetsList $Hdc)
if ($connected.Count -eq 1) {
return $connected[0]
}
throw "Set HDC_TARGET; connected count=$($connected.Count)"
}
$parts = @($Requested.Split(',') |
ForEach-Object { $_.Trim() } |
Where-Object { $_ -ne '' })
if ($parts.Count -ne 1) {
throw "Exactly one HDC_TARGET is required"
}
return $parts[0]
}
还要确认请求目标确实出现在当前连接列表中,避免拼错后把 hdc 默认行为带回来。所有有副作用的命令都必须带 -t $target,并在执行前打印 bundle、artifact hash、target、installPolicy。
显式批量模式则使用另一函数和另一命令名,不复用 exactly-one 的返回类型。类型层面分开能减少“数组意外进入单目标函数”。
七、Pad 脚本不能取第一个 127 目标
当前选择函数是:
pick_emulator_hdc_target() {
hdc_list_raw |
grep -E '127\.0\.0\.1:[0-9]+' |
head -1 |
awk '{ print $1 }' |
tr -d '[:space:]'
}
如果手机模拟器和 Pad 模拟器都在本地端口连接,第一条只反映 hdc 输出顺序,不反映 CHAQI_PAD_EMULATOR_NAME。更安全的策略按优先级处理:
- 用户显式提供
CHAQI_PAD_HDC_TARGET时,核对该目标在线后使用。 - 脚本启动新 Pad 前记录已有 loopback 集合;启动后取集合差集。
- 差集中恰好一条才自动选择。
- 若实例早已运行或候选多于一条,停止并打印候选,要求明确目标。
- 选择后回读设备类型/窗口能力,确认符合 Pad 测试意图。
差集算法草案:
before="$(list_loopback_targets)"
start_named_emulator "$NAME"
wait_for_loopback_change
after="$(list_loopback_targets)"
candidate="$(set_difference "$after" "$before")"
test "$(line_count "$candidate")" -eq 1 ||
fail_with_candidates "$after"
export HDC_TARGET="$candidate"
这些辅助函数当前不存在。若无法可靠把 emulator name 映射到 hdc target,宁可停止,也不要用 head -1 猜。

八、watch 循环还需要并发与失败边界
fswatch -1 每轮只等一批事件,随后同步执行 auto-debug,因此构建期间发生的后续修改可能没有被当前监听进程记录。建议明确状态机:
IDLE
└─文件事件→ DEBOUNCING
DEBOUNCING
└─归类完成→ PLANNED
PLANNED
├─门禁失败→ BLOCKED → IDLE
└─门禁通过→ BUILDING
BUILDING
├─构建失败→ FAILED → IDLE
└─构建成功→ READY_TO_INSTALL
READY_TO_INSTALL
├─目标失联→ BLOCKED → IDLE
└─安装启动→ VERIFYING → IDLE
实现可以在构建期间另开轻量事件记录器,把新变化标为 dirty;当前轮结束后若 dirty,再生成新批次。不要并发启动两个 hvigor 和两个安装进程,也不要丢掉构建期间保存的文件。
失败后应回到监听,但保留 batchId、失败阶段和变更路径。当前脚本用 “本轮失败(见上)” 可以继续循环,不过缺少可机器读取的失败回执。
九、安全验证先证明“不会做错设备”
下表是方案实现后的待执行矩阵,不代表已有运行结果:
| 场景 | 期望变更域 | 目标门禁 | 期望副作用 |
|---|---|---|---|
| 改 libraryhar 模型 | har-code | 单目标 | 触发覆盖所有消费者的构建 |
| 改 libraryhsp 页面 | hsp-code | 单目标 | 重建应用集合 |
| 改 rawfile Three.js | hsp-resource | 单目标 | 触发,不再漏掉 |
| build 输出变化 | 无 | 不进入 | 不递归触发 |
| 未知文档变化 | unknown | 不解析目标 | 只报告 |
| 零设备 | 任意可部署域 | 阻断 | 不安装、不启动 |
| 一台设备未设变量 | 任意可部署域 | 可选择唯一在线目标或要求显式值 | 规则需固定 |
| 两台设备未设变量 | 任意 | 阻断 | 不卸载任何设备 |
| 显式单 target | 任意 | 通过 | 所有 hdc 都带 -t |
| 显式 --all | 任意 | 二次计划确认 | 逐台回执 |
| 两个 loopback Pad 候选 | 任意 | 阻断 | 不用第一条猜 |
| 构建期间再保存 | 新 batch | 当前轮不并发 | 完成后再跑一轮 |
可先做命令桩测试,把 hdc 替换为只记录参数的 fake:
fake hdc list targets → [phone-1, pad-2]
run without HDC_TARGET
assert exit != 0
assert uninstall call count == 0
assert install call count == 0
assert start call count == 0
随后再用 --dry-run 对真实设备列表检查计划。只有无副作用用例全部通过,才进入单台测试设备;任何真实卸载都应由显式 clean 模式触发。
十、排错表与结论边界
| 现象 | 首查位置 | 根因候选 | 修复方向 |
|---|---|---|---|
| 改 HAR 无反应 | WATCH_PATHS | 没监听 libraryhar | 从模块图生成路径 |
| 改 HSP rawfile 无反应 | hsp resources | 只监听 entry | 加入整个 src/main |
| 保存一次跑多轮 | 防抖与输出排除 | 构建产物反触发 | 排除输出并聚合事件 |
| 构建时保存被漏 | watcher 状态 | 同步构建期间无人记录 | dirty 标记后补一轮 |
| 两台设备都被卸载 | Resolve-HdcTargets | 空变量等同全选 | exactly-one 门禁 |
| 指定 target 仍跑错 | 命令参数 | 某条 hdc 漏 -t | 统一命令包装器 |
| Pad 打到手机模拟器 | head -1 | loopback 顺序不代表实例 | 差集或显式目标 |
| unknown 文件触发全量 run | 域映射默认分支 | 过度兜底 | unknown 只报告 |
| watch 失败后无上下文 | 日志结构 | 只有“见上” | batch 回执记录阶段 |
| dry-run 与真实执行不一致 | 两套计划代码 | 执行阶段重新解析 | 同一不可变 Plan 驱动 |
当前源码能确认 watcher 漏掉 libraryhar/libraryhsp,Windows 空 HDC_TARGET 会遍历全部连接,Pad 脚本取第一条 loopback。本文提出的模块图清单、变更域、exactly-one、显式批量和 Pad 候选差集均未写入仓库,也没有执行 watcher、构建或设备操作。只有脚本桩用例证明门禁在多设备时零副作用,再由单目标真实回执验证,才能说自动调试既覆盖了正确变化,也只影响了正确设备。
更多推荐

所有评论(0)