From c2796eaa52d29a210cb32294629040aae4a5e01e Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 28 Sep 2026 08:49:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E9=83=A8=E7=BD=B2=E5=88=A4=E6=8D=AE):=20?= =?UTF-8?q?=E6=82=AC=E7=A9=BA=E8=BD=AF=E9=93=BE=E4=B8=8D=E5=86=8D=E8=AE=A9?= =?UTF-8?q?=E7=AC=AC=20=E2=91=A0=20=E6=9D=A1=E5=8F=98=E6=88=90=E3=80=8C?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E8=BE=BE=E6=A3=80=E6=9F=A5=E3=80=8D=EF=BC=8B?= =?UTF-8?q?=20=E8=A1=A5=E4=B8=8A=E5=AE=83=E6=8C=87=E5=90=91=E5=93=AA?= =?UTF-8?q?=E9=87=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 起因 第 ① 条("没有任何 unit 引用源码目录")在本机**长期红**,note 写的是 「读不到(这几条没被检查):…/sysinit.target.wants/mdadm-shutdown.service(ENOENT)」。 查下去:那是**系统自带的悬空软链**(mdadm 未装 ⇒ 目标不存在), 不是本仓的部署事故。但判据仍然红,而它红得**没道理可讲**。 ## ★ 真缺陷:那个软链分支是**不可达代码** /etc/systemd/system/sysinit.target.wants/mdadm-shutdown.service → /lib/systemd/system/mdadm-shutdown.service (不存在) `readFile` 对悬空软链抛 ENOENT ⇒ 走 `catch { …; continue; }` ⇒ **下面那段专门判软链的代码永远不会执行** —— 而那恰恰是对软链该做的检查(看**目标位置**)。 ⇒ 这不是"误报",是**一个该跑的检查没跑**。判据没回答"这个软链指向仓库吗", 只回答了"我读不到它"。 ## 修法 `readlink` 只读**链接自身**的内容(那个路径字符串),**不要求目标存在** —— 这正是悬空软链唯一还能回答的问题。 现在:既把目标查出来并判(指向仓库 ⇒ 进 `repoLinks` 报红), 又**仍然记进 `unreadable` 继续判红** —— 「没能检查它的内容」≠「它没问题」,这条不能因为解释清楚了就不算。 ## note 里单独一句「悬空软链(**非本仓违规**)」 它与「真有 unit 读不到」要处理的人不同: 前者是装软件留下的系统状态,后者可能是部署事故。 混在一堆里 ⇒ 读的人不知道该不该管。 ## 残余(有意保留) 第 ① 条**仍然红**,但现在红得**有理由且已说明**。 要让它转绿只能二选一:装 mdadm、或删掉那个悬空软链 —— **都不是本仓该做的事**,所以不为了转绿而放宽判据。 自检 48 格全过(本次未新增样本;这一格的真机样本就摆在那里, 且 `--self-check` 的假 fs 走不到 `sysinit.target.wants` 这条路径)。 --- deploy/check-deploy-drift.mjs | 47 +++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/deploy/check-deploy-drift.mjs b/deploy/check-deploy-drift.mjs index a393671..b453f56 100644 --- a/deploy/check-deploy-drift.mjs +++ b/deploy/check-deploy-drift.mjs @@ -45,7 +45,7 @@ */ import { createHash } from 'node:crypto'; -import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs'; +import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs'; import { join, relative, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { tmpdir } from 'node:os'; @@ -984,6 +984,13 @@ export function checkLayout(inject = {}) { const repoLinks = []; const lstatForLinks = inject.lstat ?? lstatSync; const realpathForLinks = inject.realpath ?? realpathSync; + // 悬空软链(目标不存在):它**不是**本仓的违规,但“读不到”这件事必须单独说, + // 否则会与“真有 unit 读不到”混成一堆(两者都进 unreadable ⇒ 都判红), + // 而它们要处理的人不同:前者是系统装软件留下的,后者可能是部署事故。 + const dangling = []; + // readlink 只读**链接自身**的内容(那个路径字符串),**不要求目标存在** —— + // 这正是悬空软链唯一还能回答的问题(“它指向哪里”)。 + const readlinkForLinks = inject.readlink ?? readlinkSync; let scanned = 0; let scannedLinks = 0; // 存在、但**读不到内容**的条目:它们既不能算"比过",也不能当成"没问题"。 @@ -1035,6 +1042,36 @@ export function checkLayout(inject = {}) { try { text = String(readFile(full, 'utf8')); } catch (err) { + // ★★ 软链要在这里**换一条路**(2026-09-28 修):悬空软链上 `readFile` 抛 + // ENOENT,直接 `continue` 就把下面那个软链分支**变成了不可达代码** —— + // 而那分支恰恰是对软链该做的检查(看**目标位置**)。 + // + // 实测:本机 `/etc/systemd/system/sysinit.target.wants/mdadm-shutdown.service` + // → `/lib/systemd/system/mdadm-shutdown.service`(mdadm 未装 ⇒ 目标不存在)。 + // 后果是判据第 1 条报「读不到(这几条没被检查)」而**长期红**, + // 而它本该告诉我们的恰恰是「这个软链指向哪里」。 + // + // 为什么 `readlink` 能回答而 `readFile` 不能:readlink 只读**链接自身** + // 的内容(那个路径字符串),不要求目标存在 —— + // **这正是软链能被判定的那条信息**。 + // + // ⚠️ 与「读不到就当没事」相反:判据仍**不知道目标 unit 的内容**, + // 所以下面照样把目标记进 unreadable(红),但**额外**回答了 + // 「这个软链指向仓库吗」——那才是第 1 条真正要问的问题。 + let isLink = false; + try { isLink = lstatForLinks(full).isSymbolicLink(); } catch { /* 不是软链 */ } + if (isLink) { + scannedLinks++; + let target = ''; + try { target = String(realpathForLinks(full)); } + catch { + // realpath 对悬空软链也失败 ⇒ 退回读链接字面量(不跟随) + try { target = readlinkForLinks(full); } catch { target = ''; } + } + const resolved = target.startsWith('/') ? target : `${dir}/${target}`; + if (resolved.startsWith(`${REPO}/`)) repoLinks.push(`${full} → ${resolved}`); + else dangling.push(`${full} → ${target || '(读不出目标)'}`); + } unreadable.push(`${full}(${err?.code ?? err?.message ?? '读取失败'})`); continue; } @@ -1061,13 +1098,19 @@ export function checkLayout(inject = {}) { const refBad = [ ...offenders, ...repoLinks, - ...(unreadable.length ? [`读不到(**这几条没被检查**,不是"它们没问题"):${unreadable.join(';')}`] : []) + ...(unreadable.length ? [`读不到(**这几条没被检查**,不是"它们没问题"):${unreadable.join(';')}`] : []), + // 悬空软链**单独一句**:它不是本仓的违规(是系统装软件留下的), + // 但“目标不存在”这件事必须说出来 —— 不说的话,那条红会长期没人知道该不该管。 + // 已知实例:`sysinit.target.wants/mdadm-shutdown.service → /lib/systemd/system/mdadm-shutdown.service` + // (mdadm 未装 ⇒ 目标不存在)。这与真有 unit 读不到要处理的人不同。 + ...(dangling.length ? [`悬空软链(目标不存在,**非本仓违规**,但它指向哪里已查清):${dangling.join(';')}`] : []) ].join(' '); push( '没有任何 unit/drop-in/.bak 引用源码目录(含软链指向仓库)', refOk, refOk ? `比了 ${scanned} 个文件(**全部**,不筛后缀)、命中 0;其中软链 ${scannedLinks} 个另按 realpath 判目标` + + (dangling.length ? `(另有 ${dangling.length} 个悬空软链:${dangling.join(';')})` : '') : refBad );