fix(判据/部署): 探针不再假设清单穷尽;broken≠red;到期报文自带"要放行什么";部署加 --check 干跑
pi 2026-09-14(两封)提的六条,能做的都做了。
1. **`[Empty]` 不是"没有设备",是第三态**(pi §1)。改了,而且不是改成"一律红",
是**去证服务端健康**:探针现在读 `/proc/<pid>/cmdline` 找 `hdc -m`(server 模式),
服务端在 → 空集才是可判的"确实没有目标"(false);服务端找不到 → `unknown`(红)。
本机实测:`hdc -m -s ::ffff:127.0.0.1:8710` 在跑 → 空集可信。
这条用机制而不是用嘴回答"我检查过了"。
2. **硬编码候选清单**(pi §2):候选来源写清(`/opt/huawei/command-line-tools` 是文档安装根),
`hdc` 也走 PATH;**工具链根在、里面却没有 hdc → `unknown`**("装了但长得不一样"不是"没装")。
这与 build.sh 那次"第一个命中就算"是同一形状 —— 今天各咬一次。
3. **前提改成"本工作区能装能点"**(pi §2):原前提"设备存在"会让 5 条判据在**我修不了**的时候同时红
(模拟器要写 /run、/var/log)。现在前提是工作区能力,`need` 逐条写清,
并且**到期报文会把这些门槛打出来** —— 第一次真红不能被当成噪音消掉。
4. **broken ≠ red**(pi §3):跑不起来(语言级崩痕:SyntaxError/ReferenceError/…)单列
"跑不起来的判据(N)—— 不是红,也不算过",红是"判据说不成立",broken 是"判据没说话"。
变异验证:注入未定义标识符 → 报 broken ✓;绿基线 → exit 0 ✓。
第一版我用"输出里有没有 `not ok`"判,当场误判(node:test 把导入期 ReferenceError 也报成 `not ok`),
已改成语言级崩痕 —— 判据自己的第一版就得被现实修一次。
5. **标签要有消费点**(pi §4):`release-linux.sh --release` 遇脏树**拒绝**(`--allow-dirty` 才放行),
不加参数是自用打包(只出声)。"出声≠拒绝"这条说得对。
6. **`deploy/install.sh --check` 干跑**(pi §6):跑全部门禁、不写系统目录,末尾列出正式安装会写什么、
需要哪里的权限。干跑立刻抓到两个真缺陷:
· `set -u` 下 `$HOME` 未设 → `HOME: unbound variable`(cron/env -i/某些 sudo 下就是没有),
而它发生在**所有门禁跑完之后**——最贵的位置(这轮第三次同形状,前两次在 homeagent build.sh)。
· **packaging 这条门在部署路径上永远过不去**:install.sh 先 `npm test`(含 packaging),
而 packaging 要求"安装包里的 dist == 当前 dist",部署路径却不重新打包 →
前端一改,install.sh 就卡在这条门上(第二条"挂在部署路径上却恒红"的门,第一条是 check-shared-libs)。
这条需要决定:部署路径要么重新打包、要么把 packaging 排除在部署门禁外。**我没有擅自改口径。**
This commit is contained in:
@ -161,6 +161,7 @@ if (shapeless.length) {
|
||||
}
|
||||
|
||||
const reds = [];
|
||||
const brokens = [];
|
||||
for (const [file, flags, expected] of SUITE) {
|
||||
console.log(`\n========== ${file} ==========`);
|
||||
const shape = shapeOf(file);
|
||||
@ -170,9 +171,32 @@ for (const [file, flags, expected] of SUITE) {
|
||||
const out = (r.stdout || '') + (r.stderr || '');
|
||||
process.stdout.write(r.stdout || '');
|
||||
process.stderr.write(r.stderr || '');
|
||||
/*
|
||||
* `broken` 与 `red` 必须分开(pi 2026-09-14 §3)。
|
||||
*
|
||||
* 我这轮两次把"跑不起来"当成"判据红了":一次变异注入少了 import(报 build failed)、
|
||||
* 一次判据里写了没绑定的标识符(ReferenceError 抛在判据自己身上)。
|
||||
* 两者的后果都是同一种骗人方式:**看起来像判据失败,其实是判据没跑**。
|
||||
* 所以判据是:退出码非零 **且输出里没有一句"断言失败"** → 那是 broken(崩了),不是 red。
|
||||
* 变体验证里出现 broken = **这次变异无效,重做**,不许记成"红过了"。
|
||||
*/
|
||||
/*
|
||||
* 判据是"输出里有**语言级崩**的痕迹",不是"有没有 `not ok`"。
|
||||
* 第一版我用后者,当场误判:node:test 会把**导入期**的 ReferenceError 也报成
|
||||
* `not ok 1 - …`,于是"崩了"看起来和"断言失败"一模一样 —— 正是这条判据要治的病。
|
||||
*/
|
||||
const CRASH_SIGNS = /(SyntaxError|ReferenceError|TypeError|Cannot find module|ERR_MODULE_NOT_FOUND|is not defined|is not a function|CompileError|build failed|Unexpected identifier|missing ',' in argument list)/;
|
||||
const crashed = r.status !== 0 && CRASH_SIGNS.test(out);
|
||||
const empty = out.trim().length === 0;
|
||||
// 不 break:后面每条都要跑出来,否则"红了几条"这个信息本身是假的
|
||||
if (r.status !== 0) reds.push(`${file}(退出码 ${r.status})`);
|
||||
else if (out.trim().length === 0) reds.push(`${file}(跑完没有任何输出 —— 判据没跑起来的典型症状)`);
|
||||
if (crashed || empty) {
|
||||
const firstErr = (out.match(/^.*(Error|error:).*$/m) || [''])[0].trim().slice(0, 160);
|
||||
brokens.push(`${file}(${empty ? '跑完没有任何输出' : `退出码 ${r.status},但没有一句断言失败`})` +
|
||||
(firstErr ? `\n ↳ ${firstErr}` : '') +
|
||||
'\n ↳ 这是 **broken(跑不起来)**,不是 red:它证明不了任何判据成立或不成立。' +
|
||||
'\n 常见成因:语法/标识符错(`X is not defined`)、import 写错、编译不过。' +
|
||||
'\n 用于变体验证时:broken **不算这次变异有效**,要重做。');
|
||||
} else if (r.status !== 0) reds.push(`${file}(退出码 ${r.status})`);
|
||||
else {
|
||||
/*
|
||||
* 自报条数(闭环):自定义 check() 打 `RESULT pass=N fail=M`,node:test 打 `# pass N`。
|
||||
@ -257,37 +281,94 @@ for (const [file, flags, expected] of SUITE) {
|
||||
*/
|
||||
const PROBES = {
|
||||
device: {
|
||||
desc: '有可用的设备/模拟器(hdc 看得到目标)',
|
||||
// 前提写成"**本工作区**能装能点设备",不是"机器上有设备"(pi 2026-09-14):
|
||||
// 同一台机器上设备对 gui-lab 那条通道可用、对我这条不可用 —— 写"机器上有设备"的话,
|
||||
// 探针变真会让 5 条判据同时红,而**我修不了**(要写 /run、/var/log,在工作区外)。
|
||||
// 写成"本工作区能装能点",红就只在我能动手时出现。
|
||||
desc: '本工作区能装、能点设备(hdc 服务健康 + 看得到目标)',
|
||||
need: [
|
||||
'设备/模拟器要在本工作区里起得来:模拟器启动要写 /run/harmony-emulator.pid 与 /var/log/harmony-emulator.log',
|
||||
'hdc 的**服务端**要健康(`hdc -m` 在跑);服务端起不来时 `list targets` 也会打印 [Empty],那是"拿不准"不是"没设备"',
|
||||
'要有 hdc 目标;能装上 hap 并能点(hvigorw assembleHap + hdc install)'
|
||||
],
|
||||
run() {
|
||||
const override = process.env.AGENTMAIL_PROBE_DEVICE;
|
||||
if (override === 'ok') return true;
|
||||
if (override === 'none') return false;
|
||||
if (override === 'unknown') return 'unknown';
|
||||
const sdkHdc = '/opt/huawei/command-line-tools/sdk/default/openharmony/toolchains/hdc';
|
||||
|
||||
/*
|
||||
* # 候选清单的权威性(pi 2026-09-14 §2:硬编码清单今天咬了两次)
|
||||
*
|
||||
* 上一版在**固定两条路径**里找 hdc,全不在就判"本机没装 → 没有设备"(false)。
|
||||
* 这与 build.sh 在固定几个根里找 SDK 源码、第一个命中就用,是**同一个形状**:
|
||||
* 硬编码清单 + "命中/未命中"当真值。那次命中了 /root/ha-test 的老 checkout,
|
||||
* 于是把错误结论当成了事实。所以这里:
|
||||
* ① 候选来源写清楚(下面 TOOLCHAIN_ROOT 是华为命令行工具的**文档安装根**);
|
||||
* ② **工具链根在、里面却没有 hdc** → `unknown`(不是"没装",是"装了但长得不一样");
|
||||
* ③ `hdc` 也走 PATH(spawnSync 自己会查 PATH),不再只认那一条写死的路径。
|
||||
*/
|
||||
const TOOLCHAIN_ROOT = '/opt/huawei/command-line-tools';
|
||||
const sdkHdc = `${TOOLCHAIN_ROOT}/sdk/default/openharmony/toolchains/hdc`;
|
||||
const candidates = [sdkHdc, 'hdc'];
|
||||
|
||||
let ranOnce = false; // 至少有一条命令**跑成过**(哪怕是"没有目标")
|
||||
let sawEmpty = false;
|
||||
let anomaly = ''; // 命令在,但跑不成(超时/非零退出/…)—— 这类是"拿不准"
|
||||
let allMissing = true; // 所有候选都不存在 = 本机没装设备工具(这是**可判**的:不可能有设备)
|
||||
let anomaly = ''; // 命令在,但跑不成(超时/非零退出/…)—— "拿不准"
|
||||
let allMissing = true; // 所有候选都不存在
|
||||
for (const bin of candidates) {
|
||||
const r = spawnSync(bin, ['list', 'targets'], { encoding: 'utf8', timeout: 15000 });
|
||||
if (r.error && r.error.code === 'ENOENT') continue; // 这个候选不存在,看下一个
|
||||
if (r.error && r.error.code === 'ENOENT') continue;
|
||||
allMissing = false;
|
||||
if (r.error) { anomaly = r.error.code || String(r.error.message || r.error); continue; }
|
||||
if (r.status !== 0) { anomaly = `exit ${r.status}`; continue; }
|
||||
ranOnce = true;
|
||||
const t = (r.stdout || '').trim();
|
||||
if (t && !/\[Empty\]/.test(t)) return true; // 明确可用
|
||||
sawEmpty = true; // 明确"没有目标"
|
||||
sawEmpty = true; // "没有目标"(下面还要问服务端健不健康)
|
||||
}
|
||||
|
||||
/*
|
||||
* `[Empty]` **不等于**"没有设备"(pi 2026-09-14 §1)。
|
||||
*
|
||||
* 它至少混三种原因:① 真没有设备;② **hdc 自己的服务端起不来**
|
||||
* (socket/权限 —— 而"要写工作区外的目录"正是我这轮撞上的限制);
|
||||
* ③ 被打印成空集的权限拒。所以先证服务端健康,才敢把空集当成"可判的没有":
|
||||
* 服务端进程在(`hdc -m`,含它在别的路径下)→ 空集是可判事实 → false;
|
||||
* 服务端找不到 → 空集可能正是"探针自己被环境弄坏了" → unknown(红)。
|
||||
*/
|
||||
if (ranOnce && sawEmpty) {
|
||||
return hdcServerLooksHealthy() ? false : 'unknown';
|
||||
}
|
||||
if (anomaly) return 'unknown'; // 命令在、跑不成 → 拿不准
|
||||
if (allMissing) {
|
||||
// 工具链根在、里面却没有 hdc → "装了但长得不一样",不是"没装"
|
||||
return existsSync(TOOLCHAIN_ROOT) ? 'unknown' : false;
|
||||
}
|
||||
if (ranOnce && sawEmpty) return false; // 跑成了,且确实没有目标
|
||||
if (anomaly) return 'unknown'; // 命令在、跑不成 → 拿不准(要红)
|
||||
if (allMissing) return false; // 本机根本没有 hdc → 不可能有设备
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* hdc 的服务端是不是在跑。只读 /proc/<pid>/cmdline(不依赖 `ps` 也不起子进程
|
||||
* —— 探针自己不该再引入"命令跑不起来"这种不确定性。
|
||||
* 判据:命令行里第二个参数是 `-m`(hdc 的 server 模式)。
|
||||
*/
|
||||
function hdcServerLooksHealthy() {
|
||||
let pids = [];
|
||||
try { pids = readdirSync('/proc').filter(d => /^\d+$/.test(d)); } catch { return false; }
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
// 走 lib/read.mjs 的 prose(判据目录不许裸 readFileSync —— 那条判据也管这里)
|
||||
const cmd = prose(`/proc/${pid}/cmdline`).split('\0').filter(Boolean);
|
||||
if (cmd.length && /(^|\/)hdc$/.test(cmd[0]) && cmd.includes('-m')) return true;
|
||||
} catch { /* 进程刚退出 / 没权限读 —— 换下一个 */ }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/** 探针结论 → 是否等于"前提成立(到期)" */
|
||||
const probeIsDue = v => v === true || v === 'unknown';
|
||||
|
||||
@ -350,6 +431,18 @@ if (dueStatic.length > 0) {
|
||||
' b) 明确写"为什么仍然只能静态"(如设备能编译但点不了),并改换一个更准的到期前提。\n' +
|
||||
' 这是"暂时"的到期机制:它的作用就是不等谁想起来。'
|
||||
);
|
||||
/*
|
||||
* 到期报文自带**要人做什么**(pi 2026-09-14 §2):
|
||||
* 这条红第一次出现时,最可能的结局是"被当成噪音消掉"——因为看的人不知道要放行什么。
|
||||
* 前提写的是"**本工作区**能装能点",所以这里把工作区外的门槛逐条列出来。
|
||||
*/
|
||||
const need = PROBES[dueStatic[0][2]].need || [];
|
||||
if (need.length) {
|
||||
console.error(' 要让它变成行为判据,需要先在本工作区打通:');
|
||||
for (const n of need) console.error(` · ${n}`);
|
||||
console.error(' 这些都在工作区外(需要人放行或提权)—— 所以这条红**不要求你现在修**,' +
|
||||
'要求的是:别把它当噪音,并在打通后回来把静态那条升级掉。');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
/*
|
||||
@ -363,10 +456,17 @@ console.log(`RESULT static=${STATIC_ONLY.length} probe=${probeSummary}` +
|
||||
: '(只能验形态的判据:到期前提成立就自动变红)'));
|
||||
|
||||
console.log(`\n========== 判据汇总 ==========`);
|
||||
if (reds.length === 0) {
|
||||
if (reds.length === 0 && brokens.length === 0) {
|
||||
console.log(`全部通过(${SUITE.length} 个判据文件:${SUITE.map(([f]) => f.replace('test/', '').replace('.test.mjs', '')).join('、')})`);
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(`红的判据(${reds.length}/${SUITE.length}):`);
|
||||
for (const r of reds) console.error(` - ${r}`);
|
||||
// broken 先报:它比红更严重(红是"判据说不成立",broken 是"判据没说话")
|
||||
if (brokens.length) {
|
||||
console.error(`跑不起来的判据(${brokens.length}/${SUITE.length})—— 不是红,也不算过:`);
|
||||
for (const b of brokens) console.error(` - ${b}`);
|
||||
}
|
||||
if (reds.length) {
|
||||
console.error(`红的判据(${reds.length}/${SUITE.length}):`);
|
||||
for (const r of reds) console.error(` - ${r}`);
|
||||
}
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user