fix(m196): r1 复审 P1(非 Windows dead_code)+ P2 四条

P1(阻断):console.rs 的 `INVALID_HANDLE_VALUE` 只被 `#[cfg(target_os = "windows")]`
的 usable() 用到,而这个模块在 Linux/macOS 上也编译(platform/mod.rs 的 pub mod win32;
没有 cfg)⇒ ubuntu 的 `clippy -- -D warnings` 会把它升级成 dead_code error。
按「精确 cfg」修(不用 dwm.rs 那种文件级 allow(dead_code):那会永久掩掉本文件在非
Windows 上的所有死项)。自证方式是真实文件翻转 cfg 的 rustc probe:修前 exit 1
(`constant INVALID_HANDLE_VALUE is never used`)、修后 exit 0 零告警;同一条命令写进
了注释,下一个人可以照着跑。

P2-1:自测条数按实现改成 8 条(红 6 / 绿 2),三处(脚本 docstring、release-build.yml
注释、README §12)一致。
P2-2:文件头那组句柄读数换成 release 产物的实测值(out 0x0→0x1F0 / err 0x0→0x1F4),
并说明数字随实例变化、单测里的 0xE4/0xE8 是示例。
P2-3:README §15 补这段边角语义——但**按实测改写了复审的结论**:两种「既无控制台也不
重定向」的宿主形态(父有句柄不继承 / 父子都无句柄)× 两条命令,child_exit_code 全是 0,
不是 EXIT_INTERNAL;原因是 Rust std 在 Windows 上对取不到的句柄直接返回 Ok(独立探针
实测 SetStdHandle(out)=0x0 / 0x1234 / (HANDLE)-1 三种取值 write/flush 都是 Ok),
所以 runner.rs 那条「写失败 ⇒ EXIT_INTERNAL」的分支不会被触发。
P2-4:NoParentConsole → NoConsole(这个名字同时覆盖「attach 成功但句柄仍不可用」),
诊断文案与注释同步。
This commit is contained in:
tower-worker committed 2026-11-08 20:12:36 +08:00
1 parent 14f9318e09
commit 3fd33a672f
4 files changed
+52 -18

No files matched your search

+1 -1
View File
@@ -265,7 +265,7 @@ jobs:
# 就是这么活到 2026-11-08 的。本步只认那 4 个字节:GUI 产物必须是 WINDOWS_GUI(2),非 2 即红。
# 反方向一并守:CLI 产物 `ull.exe` 必须仍是 CONSOLE(3)——它靠真控制台做 stdout/stderr,
# 谁把属性抄到它头上,`ull --version | …` 会静默丢输出。守门本体在 packaging/check_pe_subsystem.py
# (零第三方依赖,本地同一份代码可跑;`--self-test` 注入 5 条回归证明它会红)。
# (零第三方依赖,本地同一份代码可跑;`--self-test` 注入 8 条回归(红 6 / 绿 2)证明它会红)。
- name: PE 子系统门禁(GUI 产物必须 WINDOWS_GUI/2,CLI 产物必须 CONSOLE/3)
if: runner.os == 'Windows'
shell: bash
+32 -11
View File
@@ -18,10 +18,11 @@
//! | ④ 双击 | 0 / 0 | 丢了(本来也没人看) | 要(但会失败,见下) |
//!
//! ③ 的补救实测:`AttachConsole(ATTACH_PARENT_PROCESS)` 返回 1 之后 `GetStdHandle` 立刻给出
//! 控制台句柄(实测 `0x0` → `0xE4` / `0xE8`),随后的 `WriteFile` 与 Rust 的 `eprintln!`
//! 都真的落进了屏幕缓冲区(同一个控制台模型宿主读回的文本见 M196 报告)。所以**不需要**再自己
//! `CreateFileW("CONOUT$")` + `SetStdHandle`:那一层是多余的,而且它会覆盖用户自己重定向的
//! 句柄——①②两种情形会被打坏。
//! 控制台句柄(release 产物在 cmd 模型下实测 `out 0x0→0x1F0` / `err 0x0→0x1F4`),随后
//! `WriteFile` 与 Rust 的 `eprintln!` 都真的落进了屏幕缓冲区(同一模型宿主读回的文本见 M196
//! 报告)。具体数字随进程与控制台实例变化(调试构建里是 `0x208`/`0x20C`),**能跨实例复现的
//! 是「从 0 变成非 0」这件事本身**。所以**不需要**再自己 `CreateFileW("CONOUT$")` +
//! `SetStdHandle`:那一层是多余的,而且它会覆盖用户自己重定向的句柄——①②两种情形会被打坏。
//!
//! ④ 的边界:双击时祖先进程(explorer)没有控制台,`AttachConsole` 返回 0
//! (`GetLastError` = 6 `ERROR_INVALID_HANDLE`),`GetConsoleWindow` 仍是 0、`CONOUT$` 也打不开。
@@ -59,6 +60,22 @@ const STD_ERROR_HANDLE: u32 = 0xFFFF_FFF4; // -12
const ATTACH_PARENT_PROCESS: u32 = 0xFFFF_FFFF;
/// `FALSE`/`TRUE` 之外唯一需要认的无效句柄:`(HANDLE)-1`。
///
/// **必须带下面那行 `cfg`**:唯一使用点 [`usable`] 只活在 Windows 分支里,而本模块在
/// Linux/macOS 上也编译(`platform/mod.rs` 的 `pub mod win32;` 没有 cfg)。漏了它,
/// `ci.yml` 的 ubuntu clippy(`-- -D warnings`)会把它升级成 `dead_code` error——而本机只有
/// windows-gnu 工具链,非 Windows 的 cfg 一次都编不到,本地抓不住(M196 r1 复审 P1)。
/// 同目录 `transparency/dwm.rs` 用的是文件级 `#![cfg_attr(not(windows), allow(dead_code))]`;
/// 这里刻意**不用**那种写法:本文件的 Windows-only 面很小,每一项各自带 cfg 才能让编译器
/// 继续在非 Windows 上替我们盯着「又漏了一个」。
///
/// 自证办法(本机只有 Windows triple、交叉编 iced 会卡在 x11/fontconfig 时):把本文件的
/// `target_os = "windows"` 全换成不存在的目标名,再
/// `rustc --edition 2021 --crate-type lib -D warnings --crate-name console_probe <那份拷贝>`
/// ——漏了上面那行 cfg 时它是 exit 1 + `constant INVALID_HANDLE_VALUE is never used`,
/// 补齐后 exit 0 且零告警(**其他**任何非 Windows 死项也会一起报出来,所以这一个 probe 就是
/// 「本文件在 Linux 上能不能过 -D warnings」的整份答案)。
#[cfg(target_os = "windows")]
const INVALID_HANDLE_VALUE: isize = -1;
#[cfg(target_os = "windows")]
@@ -87,8 +104,11 @@ pub enum ConsoleOutcome {
/// 附着后 `(stdout, stderr)`。
after: (isize, isize),
},
/// 祖先进程没有控制台(④,或者服务/计划任务):附着失败,**也没有新建任何窗口**。
NoParentConsole {
/// 没有可写的控制台(④ / 服务 / 计划任务):附着失败**或**附着成功但句柄仍不可用。
///
/// 两种情况在调用点上是同一件事(都没有输出通道),所以合并成一个结论;`last_error` 与
/// 两对句柄一起进诊断文本,读的人能自己分清是哪一种。**没有新建任何窗口**。
NoConsole {
/// 附着前 `(stdout, stderr)`。
before: (isize, isize),
/// 附着后 `(stdout, stderr)`。
@@ -115,12 +135,12 @@ impl ConsoleOutcome {
"已附着父进程控制台:out 0x{:X}→0x{:X},err 0x{:X}→0x{:X}",
before.0, after.0, before.1, after.1
),
Self::NoParentConsole {
Self::NoConsole {
before,
after,
last_error,
} => format!(
"父进程没有控制台(attach 失败,GetLastError={}):out 0x{:X}→0x{:X},err 0x{:X}→0x{:X};未新建窗口",
"没有可用的控制台(attach 失败或句柄仍不可用,GetLastError={}):out 0x{:X}→0x{:X},err 0x{:X}→0x{:X};未新建窗口",
last_error, before.0, after.0, before.1, after.1
),
}
@@ -130,7 +150,7 @@ impl ConsoleOutcome {
#[must_use]
pub fn output_visible(self) -> bool {
match self {
Self::NotWindows | Self::NoParentConsole { .. } => false,
Self::NotWindows | Self::NoConsole { .. } => false,
Self::AlreadyUsable { .. } | Self::Attached { .. } => true,
}
}
@@ -192,7 +212,7 @@ pub fn attach_parent_console() -> ConsoleOutcome {
} else {
// 附着成功但句柄仍不可用,与「父进程根本没有控制台」在调用点上是同一件事:
// 都没有可写的输出通道,所以合并成一个结论(`describe` 里照样带着读数)。
ConsoleOutcome::NoParentConsole {
ConsoleOutcome::NoConsole {
before,
after,
last_error,
@@ -218,6 +238,7 @@ mod tests {
#[test]
fn describe_carries_the_runtime_readings() {
// 报告里贴的是这些行:句柄值必须真的出现在文本里,不然「已附着」就只是一句话。
// 下面的 0xE4/0xE8 是**示例值**(真实读数随进程与控制台实例变化,见模块文档 ③)。
let attached = ConsoleOutcome::Attached {
before: (0, 0),
after: (0xE4, 0xE8),
@@ -227,7 +248,7 @@ mod tests {
assert!(text.contains("0xE8"), "{text}");
assert!(attached.output_visible());
let missing = ConsoleOutcome::NoParentConsole {
let missing = ConsoleOutcome::NoConsole {
before: (0, 0),
after: (0, 0),
last_error: 6,
+16 -4
View File
@@ -65,7 +65,7 @@ Git Bash 下会静默落进 Docker VM(脚本内部用 `cygpath -m` 归一)
| `packaging/release-notes.md` | GitHub Release 说明模板(`{{VERSION}}` / `{{SUMS}}` 占位) | ≤400 |
| `packaging/check_screenshot.py` | 截图非空白判定(CI 门禁 + 本地手动跑,零第三方依赖) | ≤400 |
| `packaging/check_publish_slots.py` | 发布名单守卫(M164):名单 ⇄ 产物两个方向;`--self-test` 注入 8 条回归 | ≤400 |
| `packaging/check_pe_subsystem.py` | PE 子系统守卫(M196):**读产物 PE 头的 `Subsystem`**(GUI 必须 2 / CLI 必须 3),非 2 即红;`--self-test` 注入 5 条回归(见 §15) | ≤400 |
| `packaging/check_pe_subsystem.py` | PE 子系统守卫(M196):**读产物 PE 头的 `Subsystem`**(GUI 必须 2 / CLI 必须 3),非 2 即红;`--self-test` 注入 8 条回归(红 6 / 绿 2)(见 §15) | ≤400 |
| `packaging/check_test_source_off.py` | 测试通道守门(M169):workflow / 打包脚本 / `Cargo.toml default` 都不许打开 `update-test-source`;归属按 `jobs.*.steps[]` 列表项判、默认拒绝;`--self-test` 按 `tests/guard_vectors.json` 注入 | ≤400 |
| `packaging/tests/guard_vectors.json` | 守门自测向量(基线 + 32 条:26 红 / 6 绿),数据与代码分开 | ≤400 |
| `packaging/ci-runtime-smoke.md` | §11 的细节:断言口径/阈值依据/失效模式/复跑 | ≤400 |
@@ -158,10 +158,22 @@ CUI(3),`grep -rn windows_subsystem crates/` 当时零命中。同一个修复
四种启动情形的实测表在该文件顶部):重定向/管道两个句柄本来就可用(一个字节都不动)、cmd 里直接跑
附着后输出落进控制台、双击时附着失败且**绝不新建窗口**。
- 守门:`python3 packaging/check_pe_subsystem.py --gui <exe> --cli <exe>`,读 `MZ`→`e_lfanew`(`0x3C`)→
`PE`→COFF 头→可选头 `+0x44` 的 2 个字节,打印**文件偏移**(例:`Subsystem=2 (WINDOWS_GUI) @
文件偏移 0x1E6`)。两向断言:GUI 产物必须 2,CLI 产物 `ull.exe` 必须仍是 3(CONSOLE)——反方向
不守时,把属性贴错 crate 会被漏掉,而 `ull --version | …` 会静默丢输出。
`PE\0\0`→COFF 头→可选头 `+0x44` 的 2 个字节,打印**文件偏移**(release 产物实测:`Subsystem=2
(WINDOWS_GUI) @ 文件偏移 0xDC`,`e_lfanew=0x80`、PE32+)。两向断言:GUI 产物必须 2,CLI 产物
`ull.exe` 必须仍是 3(CONSOLE)——反方向不守时,把属性贴错 crate 会被漏掉,而 `ull --version | …`
会静默丢输出。
- 接在哪:`.github/workflows/release-build.yml`(发布侧,两个 Windows 槽位各跑一次)与
`.github/workflows/ci.yml` 的 `build` job(PR 阶段就红,不必等到 tag)。
- 抓不住(如实记):只看那 4 个字节,不看 GUI 双击的实际观感——「不再多一个黑框」在本仓库的验证环境
(RDP / 无干净无控制台桌面)只能由 PE 头 + 进程/窗口枚举间接证明,人眼验收归真机。
- **既无控制台也不重定向的宿主(快捷方式 / 服务 / 计划任务):输出照旧无处可去,退出码不变**
(M196 r2 实测,覆盖 r1 复审 P2-3 提出的「退出码会变成 `EXIT_INTERNAL`(2)」——那条实测不成立)。
两种宿主形态各测两条命令,`child_exit_code` 全是 **0**:①父进程有 std 句柄但子进程不继承;
②父子都没有 std 句柄(服务形状,子进程实测 `stdin/stdout/stderr = 0x0`、`AttachConsole` 返回 0 /
`GetLastError=6`)。原因在 Rust std 的 Windows 实现:句柄取不到时 `Write`/`flush` 直接返回 `Ok`
(独立探针实测 `SetStdHandle(out)` = `0x0` / `0x1234` / `(HANDLE)-1` 三种取值,`write=Ok(())`、
`flush=Ok(())` 全成立),所以 `crates/cli/src/runner.rs` 那条「写失败 ⇒ `EXIT_INTERNAL`」的分支不会
被触发。结论:本 diff **没有**给 CLI 退出码引入可观察差异;真正的差异只有「没控制台就没人看得见输出」
这件事,而 CUI 时代在服务里同样是隐形的。想让这种情形有专属退出码/文案,得在 CLI 侧显式判一次能力
(`platform::win32::console` 的 `output_visible()` 就是现成的判据),那在 `crates/cli/**`、不在 M196 的
scope 内——这里只如实记录形态,不改结构。
+3 -2
View File
@@ -23,10 +23,11 @@ COFF 头 20 字节 → 可选头起点 → `Subsystem` 在可选头 `+0x44`(
python3 packaging/check_pe_subsystem.py --gui target/release/unlucky-launcher-iced.exe \\
--cli target/release/ull.exe
python3 packaging/check_pe_subsystem.py --self-test # 合成 PE 桩,断言该红的红
python3 packaging/check_pe_subsystem.py --self-test # 合成 PE 桩,断言该红的红(8 条:红 6 / 绿 2)
`--self-test` 是这条守门的自测证据:它造合成 PE 头(GUI / CUI / 非 MZ / 截断 / 缺文件),
断言「GUI 读成 3 ⇒ 红」「CLI 读成 2 ⇒ 红」「两边都对 ⇒ 绿」。守门抓不住回归时它自己先红。
断言「GUI 读成 3 ⇒ 红」「CLI 读成 2 ⇒ 红」「两边都对 ⇒ 绿」——共 **8 条用例(红 6 / 绿 2)**,
另加一条「Subsystem 真落在报出来的那个偏移上」的偏移自证。守门抓不住回归时它自己先红。
"""
from __future__ import annotations