fix(部署判据): 悬空软链不再让第 ① 条变成「不可达检查」+ 补上它指向哪里
## 起因
第 ① 条("没有任何 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` 这条路径)。
This commit is contained in:
@ -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
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user