Skip to content

修复 Windows 下 resident 进程继承管道句柄导致的挂起(#207)#210

Closed
hqy2435662352 wants to merge 1 commit into
iOfficeAI:mainfrom
hqy2435662352:fix-windows-resident-hang
Closed

修复 Windows 下 resident 进程继承管道句柄导致的挂起(#207)#210
hqy2435662352 wants to merge 1 commit into
iOfficeAI:mainfrom
hqy2435662352:fix-windows-resident-hang

Conversation

@hqy2435662352

@hqy2435662352 hqy2435662352 commented Jul 14, 2026

Copy link
Copy Markdown

摘要

修复 #207

在 Windows 上,当使用 UseShellExecute=false 启动自动 resident 子进程时,子进程会继承调用方的 stdout/stderr 管道句柄。当通过 PowerShell(或任何将 stdout 重定向为管道的 shell,例如 CI agent / SDK 包装器)调用 officecli 时,管道会一直保持打开状态,直到 resident 的空闲超时到期(约 60 秒),因此每条命令看起来都会卡住一分钟,即使实际工作早已完成。

根因

.NET 的 ProcessStartInfoUseShellExecute=false 时会调用 Win32 CreateProcess 并传入 bInheritHandles=TRUE,导致 resident 子进程继承父进程的控制台/管道句柄。之前尝试的 SetHandleInformation 方案只能标记父进程自身句柄表的继承标志,但运行时仍会为子进程创建可继承的句柄副本,因此调用方的 stdout 管道仍然保持打开。

修复方案

  • Windows:使用 UseShellExecute=true 启动 resident,通过 shell 启动不会继承句柄。由于 UseShellExecute=true 时不能使用 ProcessStartInfo.Environment,空闲超时改为通过命令行参数 --idle-seconds 传递。
  • macOS/Linux:保持现有行为(UseShellExecute=false + RedirectStandardOutput/Error=true),该方案已经能阻止调用方管道被继承。
  • 新增 EscapeWindowsArgument 辅助函数,确保带空格、引号或尾部反斜杠的文件路径能被 CommandLineToArgvW 正确解析。
  • ResidentServer 构造函数新增可选的 initialIdleTimeout 参数,隐藏命令 __resident-serve__ 新增 --idle-seconds 选项。环境变量 OFFICECLI_RESIDENT_IDLE_SECONDS 仍作为测试/CI 的后备方案被保留。
  • Windows 启动失败时的错误提示增加可操作的建议,引导用户使用 OFFICECLI_NO_AUTO_RESIDENT=1 回退或手动启动 resident;同时将完整异常写入可选的 CLI 日志(officecli config log true)以便运维诊断。

改动文件

  • src/officecli/CommandBuilder.cs
  • src/officecli/ResidentServer.cs

审计结果

在仓库中搜索了 ProcessStartInfo.EnvironmentRedirectStandardOutput/RedirectStandardErrorProcess.StandardError/OutputOFFICECLI_RESIDENT_IDLE_SECONDSnew ResidentServer 等关键字,确认主要影响范围如下:

  • ProcessStartInfo.Environment 的写入点仅在 CommandBuilder.TryStartResidentProcess 中;PR 已将该处对 OFFICECLI_RESIDENT_IDLE_SECONDS 的注入改为命令行参数传递。
  • Process.StandardError.ReadToEnd() 的读取已在 PR 中改为仅在 startInfo.RedirectStandardError 为 true 时才执行,避免 UseShellExecute=true 下抛异常。未发现仓库中其它依赖子进程 stdout/stderr 的代码路径。
  • ResidentServer 仍从 OFFICECLI_RESIDENT_IDLE_SECONDS 环境变量读取作为后备;其它日志/刷新相关变量未受此次更改破坏。

空闲超时优先级

ResidentServer.ResolveIdleTimeout 的解析顺序已在代码注释中明确:

  1. __resident-serve__ --idle-seconds 命令行参数(Windows 生产路径)
  2. OFFICECLI_RESIDENT_IDLE_SECONDS 环境变量(测试/CI 后备)
  3. 默认值 12 分钟

验证

所有测试均在 Windows 11 上针对从源码构建的 officecli.exe 执行。

复现

修复前,以下每条命令都会因为 PowerShell 等待被继承的管道关闭而耗时约 62 秒:

officecli view "minimal.pptx" outline
officecli get "minimal.pptx" "/slide[1]"
officecli view "minimal.xlsx" outline
officecli get "minimal.xlsx" "/Sheet1/A1"
officecli query "minimal.xlsx" cell

修复后,每条自动 resident 命令均在约 1 秒内完成:

测试 模式 结果 耗时
PPTX view auto-resident OK 1.1 s
PPTX view no resident OK 0.5 s
XLSX view auto-resident OK 1.0 s
XLSX view no resident OK 0.5 s
PPTX get /slide[1] auto-resident OK 1.2 s
XLSX get /Sheet1/A1 auto-resident OK 1.0 s
XLSX query cells auto-resident OK 1.1 s

边界测试

针对 EscapeWindowsArgument 和 Windows 启动路径进行了额外验证:

  • 相对路径 PPTX view(auto-resident):约 1.5 s 完成
  • 带空格路径 PPTX view(auto-resident):约 1.5 s 完成
  • 带单引号路径 PPTX view(auto-resident):约 1.6 s 完成
  • 直接启动 __resident-serve__ 并设置 OFFICECLI_RESIDENT_IDLE_SECONDS=3:命令正常,resident 3.0 s 后退出
  • 直接启动 __resident-serve__ 并传入 --idle-seconds 3:命令正常,resident 3.0 s 后退出

这些测试覆盖了 EscapeWindowsArgument 对空格、单引号等常见特殊字符的处理;对于尾部反斜杠、内嵌双引号、制表符、极长路径、特殊 Unicode 等更复杂的 Windows 路径场景,建议后续在 CI 中补充 round-trip 测试(构造命令行 -> CommandLineToArgvW 解析 -> 验证与原 args 一致)。

启动失败回退提示

Windows 启动失败时的错误信息现在包含:

Auto-start via shell failed — you can disable auto-resident with OFFICECLI_NO_AUTO_RESIDENT=1
or start the resident manually: officecli __resident-serve__ <file> --idle-seconds N.

完整异常会同时写入 CLI 日志(启用后),便于在受限/沙箱环境中定位问题。

构建

dotnet build src\officecli\officecli.csproj -c Release

构建成功,0 错误(1 个与本次改动无关的预先存在的空引用警告)。

已知限制与后续建议(非阻塞)

  • 受限/沙箱环境:当前方案在 UseShellExecute=true 被阻止时会给用户明确回退提示并记录日志,但不会自动回退到旧方案。若后续收到相关反馈,可考虑增加更智能的回退策略。
  • Windows 子进程 stderr:Windows 分支不重定向 child stderr,父进程无法直接读取。启动阶段的完整异常已通过 CliLogger.LogError 写入可选日志文件作为替代诊断通道。
  • 自动化 round-trip 测试:由于本仓库目前没有测试项目(tests/ 目录不存在),本次 PR 未包含自动化测试。强烈建议维护者在建立测试基础设施后,尽快为 EscapeWindowsArgument 添加 CommandLineToArgvW round-trip 测试,覆盖空串、空格、制表符、内嵌引号、尾部多重反斜杠、极长路径、非 BMP Unicode 等边界。

@hqy2435662352
hqy2435662352 force-pushed the fix-windows-resident-hang branch 2 times, most recently from c402903 to 9e6c534 Compare July 14, 2026 08:30
On Windows, spawn the resident with UseShellExecute=true so it does not inherit the caller's stdout/stderr pipe handles. This eliminates the ~60s (or ~12min) hang seen when officecli is invoked through PowerShell or any shell whose stdout is a pipe.

The idle timeout is now passed via --idle-seconds because ProcessStartInfo.Environment cannot be used with UseShellExecute=true. Unix keeps the existing UseShellExecute=false + redirected streams behavior.
@goworm

goworm commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@hqy2435662352 感谢反馈和这么完整的复现脚本!

我们定位下来:这个卡顿很可能是 Windows 上某些第三方软件(例如安全/杀毒类) 在进程启动时对管道句柄留了额外引用,导致后台常驻进程被动持有了你命令行的 stdout 管道,一直到它空闲退出(约 60s)才释放——所以你会看到命令输出早就回来了、进程却迟迟不退。

我们没有继续用之前的环境变量绕过,而是换了一种方式从根上处理:让后台进程启动时只继承它自己需要的句柄,这样任何第三方软件留下的多余句柄都不会再泄漏进去。改动已合入 main,会在下个版本发布。

麻烦你更新到带此修复的版本后再测一下是否恢复正常;如果还卡,顺便告诉我们你装的是哪款安全软件,方便进一步确认。再次感谢!

@hqy2435662352

hqy2435662352 commented Jul 18, 2026

Copy link
Copy Markdown
Author

@goworm 感谢处理,经测试 v1.0.137(commit 991bea6)中通过 PROC_THREAD_ATTRIBUTE_HANDLE_LIST 句柄白名单的方式已经修复了该PR指向的问题:resident 子进程只继承自身需要的 std 句柄,不再继承调用方多余的管道句柄,从而避免了 60 秒挂起。

我已从最新 main 分支构建 v1.0.137 并在 Windows 11 上验证:

  • PPTX/XLSX 的 view/get/query 在 auto-resident 模式下均在 2 秒左右完成
  • OFFICECLI_NO_AUTO_RESIDENT=1 模式仍正常工作
  • 之前的 60 秒挂起现象已消失

因此本 PR 不再需要合并,关闭。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants