Files
MailUI4Agents/deploy/check-file-modes.sh
JianFeeeee 590a72a715 修复: job 集合从 glob 改成**清单**(+ 清单外即报,与 SUITE 同形状)+ RESULT 行带集合指纹;baseline 播报区分"底本过期"与"变异残留";权限政策补上**目录可进入性**(原来只管文件)
pi 2026-09-18 报的三件,逐条实测后处置。

## 一、job 集合是 glob ⇒ "权威"是**树的函数**(同一段代码两个数)

pi 独立复算对上了(48/48/0、A=36/B=41 与我一致),但发现 48 与 52 **都不是错的** ——
它们读的是**不同的集合**,而**没有任何东西说明读的是哪个**:

| 快照 | job 文件 | 输出 |
|---|---|---|
| 提交态 | 9 个(全跟踪) | `mutants=48 …` |
| 工作树 | 11 个(2 个未跟踪) | `mutants=52 …` |

真因:`glob(JOBS_DIR + '/jobs*.json')` ⇒ **未跟踪的 job 文件静默进入统计**。
这与"写了判据忘了接线"同族,区别是 `run-all.mjs` 有**自检 2** 挡着(清单外的 `*.test.mjs` 直接红),
这边没有对等物。**本仓用"清单 + 清单外即红"解决过同一个问题两次,这是第三次。**

**处置(pi 建议的前者)**:新增 `jobs.manifest.json` 显式列 job 集合,`summary.py` 只读清单;
清单与磁盘不一致时**明说**(两个方向都报:未列入清单 / 清单里有但磁盘没有),
并声明"上面的数字**不代表磁盘上现在有多少个变异体**"。
**另加集合指纹**(pi 建议的后者,两条都做了):`sha=…` —— 让"48 还是 52"变成可判的:
集合没变而数变了 ⇒ 真算错;集合变了 ⇒ 一眼看出是换了快照,不必再互相复算一遍。

**变异验证**(两个方向都试):
```
加一个未列入清单的 job 文件 ⇒ 报「未列入清单:jobs-UNLISTED-probe.json」
                            且 mutants=**48**(未被静默计入)✓
清单里加个不存在的文件      ⇒ 报「清单里有、磁盘上没有:jobs-GHOST.json」✓
```
(`_` 开头的条目是说明,读时滤掉 —— 否则会被当成文件名,`ghosts` 假报一堆。)

## 二、`baseline=4/7✗**有文件没还原**` 是**假警报指向最危险的结论**

实测三个不匹配的文件 **全部与 HEAD 逐字节相同**(`git diff --quiet HEAD` 为空):
`AdminUsersPage.ets`/`SettingsPage.ets` 是提交 `6861934`(09-17 21:15)改的、
`ApiClient.ets` 是 `9c6e9c6` 改的 ⇒ **底本过期,不是变异残留**。

★ 而原来一律打"✗**有文件没还原**" —— 那会让人去翻变异,而真因只是底本没跟上提交。
**两种成因在 `sha256sum -c` 眼里一模一样**,所以播报必须分开,并各给判别方法:

```
baseline=4/7⚠**底本过期**(3 个文件与 HEAD 逐字节相同 ⇒ 是正常提交改过、不是变异残留)
```

**底本已重算**,并按要求在文件里记一行"为什么"(含"重算前必须先证是提交态"这个前提)。
**重算后仍在校验**(变异验证):给 `AppearanceApi.ets` 追加一行 ⇒ 立刻 `FAILED`;还原 ⇒ `7/7 OK`。

## 三、`summary.py` 对非 root 是坏的:`jobs/` 缺 `x` 位

```
drw-r--r-- client/electron/test/mutants/jobs      ← 缺 x
$ runuser -u nobody -- python3 …/summary.py
PermissionError: [Errno 13] Permission denied: '…/jobs/jobs-all.json'
```
⇒ 那份"口径的唯一权威"**只有 root 跑得起来**。已 `chmod 755`,非 root 复跑输出一致。

★ pi 的深层判断成立且我核实了:**这条落在任何判据的射程之外** ——
`deploy/check-file-modes.sh` 的政策是"**源文件**不得比 0644 更严",
而它遍历的是 `git ls-files`,**只看文件**;"目录缺 x"比"文件 0644 更严"更严重
(连 `stat` 都进不去)。
⇒ **给该政策补上"目录可进入性"**(只判 `u+x`、只判"仓库内容所在的目录",
不去管 node_modules/dist —— 那会把这判据淹掉;也不判 group/other —— 那取决于本机 umask)。

**★ 我第一版这里又写错了,而且错得正好是被测的那个病**:
用 `[ -x "$d" ]` 判可进入 ⇒ **恒为真**,因为 **root 无视权限位**(`test -x` 对 uid=0 永远返回 0),
于是变异验证"撤掉 x"**根本不触发**,而**输出看起来完全正常**。
⇒ 改成**直接读权限位**(与同文件里文件那段的位运算同一做法)。
改后变异验证:撤 x ⇒ `[FAIL] 目录不可进入:…(drw-r--r--,缺属主 x 位)`、退出码 1;还原 ⇒ 0。

## 四、顺带修好三处既有权限违规(`check-file-modes.sh` 原来一直红着)

```
.githooks/pre-push                            711 ⇒ 755(保留执行位,**不是**去掉)
test/harmony-arkts.test.mjs                   600 ⇒ 644
model/DeviceProbe.ts                          600 ⇒ 644
```
三者都是**提交态**就这样(不是并发会话弄的)。按脚本自己的"药方按类型分岔"修的。
现在该政策 **退出码 0**。★ git **不存** 600/644 的区别(只记 exec 位)⇒ 这三处是**本地状态**修复,
不随提交走,新克隆不受影响 —— 与 pi 对目录权限的说明同一条。

## 五、pi 报的另两件:**在 HEAD 上已经不存在了**(是过时读数)

- **ArkTS 两处阻断**:`MainPage.ets` 的 import 现在在 71-74 行、最后一个 `const` 在 88 行
  ⇒ **位置正确**;`AdminUsersPage.ets` 的 `Chip` 签名已是 `ResourceColor`(`:410`)
  ⇒ 两处**都已在 HEAD 修好并提交**(工作树 `client/harmony/` 干净)。`harmony-arkts` 判据 3/3 绿。
- **套件现状**:现在 **29** 个判据文件(不是 23),`unreported=0 broken=0`。

## 六、测量

| 相位 | RESULT |
|---|---|
| build | `files=29 ran=29 checks=456 pass=453 fail=3 red=9 broken=0 unreported=0` |
| install | `files=29 ran=27 checks=444 pass=442 fail=2 red=8 broken=0 unreported=0` |

三种 cwd 仍逐字节一致(上一轮的修复保持)。
2026-09-18 04:07:26 +08:00

139 lines
8.2 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# 源文件的权限位**政策**:不许比 0644 更严。
#
# # 为什么要这条(一个实测出来的实例)
#
# 这台机器上写文件的工具**不理会 umask**(umask 是 022,但它建的仍是 0600)。
# 后果不是"本地难看",而是它会**进生产**:`deploy/redeploy-plugin.sh` 用
# `cp -a "$SRC/." "$STAGING/"` 打快照,`-a` 保留权限位 ——
# 于是快照里 `src/paths.mjs`/`src/turn-cwd.mjs` 是 **0600**,而仓库是 0644。
#
# 两头都不报警:
# · `cmp` 只比**内容** ⇒ 报"逐字节相同"(实测就是这样);
# · `check-deploy-drift.mjs` 只比**内容**、且只在脚本上判**可执行位**(`mode & 0o111`)。
# ⇒ "线上与仓库不一致"里,**只有权限位这一类**完全没有判据看着。
#
# # 与漂移侧那条的分工(pi 评审 2026-09-15 §三 明确要求分开)
#
# · **本脚本 = 政策**:源文件不得比 0644 更严。它**不**看快照,
# 所以"两边都是 0600"这种情况它能抓到,而漂移侧永远抓不到(两边一致 ⇒ 恒绿)。
# · **漂移侧 = 一致性**:部署副本的权限位 = 仓库那一份。
# 它抓不到"两边都错"。
# 两条别合成一条:合成的结果是"看起来覆盖了、其实只覆盖一半"。
#
# # 为什么"更严"也算错
#
# 这些文件是**源码**,要被构建/测试/部署以不同身份读(`install.sh` 会切身份做可写性判定,
# 判据里还有以 nobody 跑的分支)。0600 在"跑的人恰好是属主"时不炸,一旦换身份就是
# EACCES —— 而那串报错看起来是**代码问题**。这条链一路上在消的就是这一类。
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO"
# 允许的最严权限:0644。比它更严(组/其他读位缺失)即判红。
# 例外:**可执行**脚本的 0755 不是"更严",它多了执行位、读位齐全,属正常。
# 允许的最严权限:0644(非可执行)/ 0755(可执行)。
#
# ★ 可执行与否按 **git 记录的那个位**判(`git ls-files -s` 的 100755),不按当前文件系统权限
# —— 否则"执行位被谁弄丢了"会被这条判据**追认**成正常。
#
# ★ 这条我第一版写错了,代价是**真的弄坏了一个脚本**:`client/electron/scripts/release-linux.sh`
# 原本是 **711**(group/other 读位缺失 ⇒ 本判据判红,判得对),但我给的药方是无脑
# `chmod 644` —— 那是**去掉执行位**,等于把"读位缺失"换成"不能执行",比原来更坏,
# 而判据随后报"通过"。⇒ 一条判据说"这个文件被改错了"时,**药方必须按文件类型分岔**;
# 只有一个药方就会把另一类文件改坏,且同样显示"已修好"。
fail=0
worst=""
count=0
while IFS= read -r line; do
gmode="${line%% *}"
f="${line##* }"
[ -n "$f" ] || continue
[ -f "$f" ] || continue
mode="$(stat -c '%a' "$f" 2>/dev/null)" || continue
m="${mode: -3}"
o="${m: -1}"; g="${m:1:1}"; u="${m:0:1}"
bad=0
# group 与 other 必须有读位
[ "$(( o & 4 ))" -eq 0 ] && bad=1
[ "$(( g & 4 ))" -eq 0 ] && bad=1
# git 记为可执行的,三处执行位都要在
if [ "$gmode" = "100755" ]; then
[ "$(( u & 1 ))" -eq 0 ] && bad=1
[ "$(( g & 1 ))" -eq 0 ] && bad=1
[ "$(( o & 1 ))" -eq 0 ] && bad=1
fi
[ "$bad" -eq 0 ] && continue
count=$((count + 1))
[ -z "$worst" ] && worst="$f"
if [ "$gmode" = "100755" ]; then
printf ' [FAIL] 权限不对:%s 是 %s(git 记为可执行 ⇒ 应为 0755,注意别去掉执行位)\n' "$f" "$mode" >&2
else
printf ' [FAIL] 权限过严:%s 是 %s(政策要求不严于 0644)\n' "$f" "$mode" >&2
fi
done < <(git ls-files -s | awk '{print $1" "$4}')
# ══════════════════════════════════════════════════════════════════════════════
# ★★ 目录的**可访问性**(2026-09-18,pi 报的洞)
#
# 上面那段只遍历 `git ls-files` ⇒ **只看文件**。而"目录缺 `x`"比"文件 0644 更严"**更严重**:
# 目录没有 `x` 就**不能访问其中任何文件**(连 stat 都不行),而 root 绕过这一层,
# 所以维护者**永远不会在自己身上看到症状**。实测(pi 报、我复现):
#
# $ stat -c '%A %n' client/electron/test/mutants/jobs
# drw-r--r-- client/electron/test/mutants/jobs ← 缺 x
# $ runuser -u nobody -- python3 …/summary.py
# PermissionError: [Errno 13] Permission denied: '…/jobs/jobs-all.json'
#
# ⇒ 那份"口径的唯一权威"在这份工作树里**只有 root 跑得起来**,而它落在**任何判据的射程之外**
# (本政策只看文件;权限位判据当初就是为"该不该算漂移"立的,覆盖的形状是"文件")。
#
# ⚠️ 范围与取向(**保守,只判"不可访问"**):
# · git **不存目录权限** ⇒ 这是**本地状态**,不跟着提交走、新克隆不受影响。
# 所以这里判的是"**这份工作树此刻能不能被别人用**",不是"仓库内容对不对"。
# · 只判**有 `git ls-files` 条目的目录**(即"仓库内容所在的目录"),
# 不去管 node_modules / dist 这类产物的目录 —— 那会把这条判据淹掉。
# · 只判 `u+x`(**属主**可进入):这是"别把自己锁在外面"的最低要求,
# 不去管 group/other —— 那取决于本机 umask 与身份,判严了会到处误红。
# ══════════════════════════════════════════════════════════════════════════════
dir_bad=0
dir_worst=""
while IFS= read -r d; do
[ -n "$d" ] && [ -d "$d" ] || continue
# ⚠️⚠️ **不许用 `[ -x "$d" ]`** —— 这是我第一版写的,而它**恒为真**(我实测抓到的):
# root **无视权限位**(这正是本政策存在的理由之一),所以 `test -x` 对 uid=0
# **永远返回 0**,哪怕目录是 `drw-r--r--`。⇒ 变异验证时"撤掉 x"根本不触发,
# 而**输出看起来完全正常**(这正是被测的那个病本身长在判据上)。
# ⇒ 必须**直接读权限位**(与上面文件那段的 `o/g/u` 位运算同一个做法)。
dperm="$(stat -c '%a' "$d" 2>/dev/null)" || continue
dm="${dperm: -3}"
[ "$(( ${dm:0:1} & 1 ))" -eq 1 ] && continue # 属主有 x ⇒ 可进入,放行
dir_bad=$((dir_bad + 1))
[ -z "$dir_worst" ] && dir_worst="$d"
printf ' [FAIL] 目录不可进入:%s(%s,缺属主 x 位)\n' "$d" "$(stat -c '%A' "$d" 2>/dev/null)" >&2
done < <(git ls-files | awk -F/ 'NF>1 { for (i=1;i<NF;i++) { p=""; for (j=1;j<=i;j++) p = (p=="" ? $j : p"/"$j); print p } }' | sort -u)
if [ "$dir_bad" -gt 0 ]; then
printf '\n 共 %d 个目录不可进入(首个:%s)。\n' "$dir_bad" "$dir_worst" >&2
printf ' 药方:chmod +x <目录>(**只加执行位,别动读位**)—— 目录的 `x` 是"能进入/能 stat 里面"。\n' >&2
printf ' 为什么单列一条:目录缺 `x` 时,**里面每个文件都读不到**,而 root 绕过它 ⇒\n' >&2
printf ' 维护者在自己身上永远看不到症状,只有换个身份(或别人来跑)才炸。\n' >&2
printf ' git 不存目录权限 ⇒ 这是**本地状态**,不跟提交走(新克隆不受影响)。\n' >&2
printf ' 这是**政策问题**(工作树应当能被其它身份使用),不是代码问题。\n' >&2
exit 1
fi
if [ "$count" -gt 0 ]; then
printf '\n 共 %d 个源文件权限不对(首个:%s)。\n' "$count" "$worst" >&2
printf ' 药方(**按类型分岔**):非可执行文件补齐到 644;git 记为 100755 的补齐到 755。\n' >&2
printf ' 原因:写文件的工具不理会 umask,而 `cp -a` 会把这个权限位带进生产快照,\n' >&2
printf ' 且 `cmp` / ① 内容判据都看不见它。\n' >&2
printf ' 这是**政策问题**(源文件应当可被构建/测试/部署以不同身份读),不是代码问题。\n' >&2
exit 1
fi
printf ' 源文件权限政策:通过(已跟踪文件都够读;git 记为可执行的都保留了执行位)\n'
exit 0