Repository navigation
fix: 修复 uv 成功退出后仍被报错的问题 (T5.18) #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # T5.18 实施计划 | ||
|
|
||
| 1. 在 `internal/uv/runner_isolation_windows_test.go` 固定短进程已退出的原生 API 顺序,并从实际 `UVRunner.Run` 的 Job 边界复现错误。固定测试名为 `TestRunner_ExitedProcessCannotJoinJob`、`TestRunner_AssignsBeforeExecuting`。基线执行 `go test ./internal/uv -run 'TestRunner_(ExitedProcessCannotJoinJob|AssignsBeforeExecuting)$' -v -count=1`,前者通过,后者因子进程已退出、隔离失败而失败。 | ||
| 2. 在 `internal/uv/runner_windows.go` 挂起创建,`internal/uv/runner.go` 分配后恢复,`internal/process/managed_windows.go` 复用现有恢复线程实现;保留非 Windows 编译边界。补恢复前取消验证,复用现有执行器检查。 | ||
| 3. 运行 gofmt、vet、build、完整测试、protocol 100 轮和可用时的 race,复核真实 uv 探针。按实际结果更新设计;提交时显式暂存本任务源码、测试和文档,不改其他在途任务。代码与进度分别提交。提交后独立复核,不合入或推送其他分支。 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # T5.18 短命 uv 进程纳管 | ||
|
|
||
| ## 问题与证据 | ||
|
|
||
| `UVRunner.Run` 原先先执行 `command.Start()`,再创建 Job 并按 PID 分配。Windows 原生检查固定子进程成功退出、父进程尚未 `Wait` 的顺序后,`OpenProcess` 成功、`WaitForSingleObject` 返回 `WAIT_OBJECT_0`、退出码为 0,而 `AssignProcessToJobObject` 返回 `ERROR_ACCESS_DENIED (5)`。在实际 `UVRunner.Run` 的 Job 边界延后 Assign,同样把成功的版本探针报告为 `uv.verify` 隔离失败。 | ||
|
|
||
| 这确认了短进程退出早于 Assign 的竞态。用户原始日志只有 `process_isolation_failed`,没有失败 API 或 Windows 错误号,不能据此断言用户现场必为本竞态。v0.1.24、v0.1.28、v0.1.29 的执行顺序相同,本问题并非 v0.1.29 新引入。 | ||
|
|
||
| ## 方案与边界 | ||
|
|
||
| 一次性 uv 同长驻进程一样以 `CREATE_SUSPENDED | CREATE_NO_WINDOW` 创建。Job 分配成功后才恢复初始线程;恢复逻辑复用 `process` 现有线程查找、恢复和句柄关闭实现。`UVRunner` 继续拥有 `exec.Cmd`、原始双流管道、输出上限、逐行回调、Wait 和取消,不改为 `ManagedProcess`,避免改变一次性命令的输出及退出码语义。 | ||
|
|
||
| Job 创建、分配、线程恢复任一步失败均结束并等待本次进程,保留真实错误,不恢复未纳管进程。取消在恢复前再次检查,取消不允许子进程开始业务工作。恢复阶段的本地诊断沿用已有隔离诊断,操作名为 `resume_process`;不新增协议字段,不改变错误码四元组,不影响宿主控制台或网络策略。 | ||
|
|
||
| ## 验收 | ||
|
|
||
| 实际 Windows API 检查保留已退出进程分配失败的证据;实际 `UVRunner` 检查在 Assign 前进程尚未执行、恢复后双流输出完整且退出码正确。复用已有非零退出、回调失败、输出上限、超时、取消和进程树测试,新增恢复前取消检查。检查使用进程信号和有界等待,不以 sleep 安排顺序。 | ||
|
|
||
| ## 本地验收,2026-11-12 | ||
|
|
||
| 环境为 Windows amd64、Go 1.26.11。检查源码与 `v0.1.24`、`v0.1.28`、`v0.1.29`,历史执行器和 Job 文件在这些 tag 间无差异。以下命令均在独立 worktree 执行,原始开机/休眠现场未复测。 | ||
|
|
||
| - `go test ./internal/uv -run 'TestRunner_(ExitedProcessCannotJoinJob|AssignsBeforeExecuting)$' -v -count=1`:原始执行器通过 Go overlay 复跑退出 1,日志给出原生 API 错误 5 和成功退出码 0;修改后退出 0,Assign 前为 `WAIT_TIMEOUT` / `STILL_ACTIVE`,随后双流输出完整、退出码 0。 | ||
| - `go test ./internal/uv -run 'TestRunner_(ExitedProcessCannotJoinJob|AssignsBeforeExecuting|CancelBeforeResumeDoesNotExecute)$' -v -count=100`:退出 0。`go test ./internal/uv -run '^TestRunner_CancelReapsDescendants$' -v -count=20`:退出 0,取消后通过原生进程句柄确认后代退出。 | ||
| - 作者隔离探针用同一真实 `UVRunner.Run` 执行固定 uv 0.12.3 和本机 uv 0.13.7 的 `--version`。在 Assign 边界等待快进程退出时,原始执行器均退出 1、报告隔离失败;修改后均有完整真实版本输出、退出码 0。探针和原始源码 overlay 留在本地证据目录,没有加入默认测试或修改安装版。 | ||
| - `go test ./internal/uv -count=1` 退出 0;执行器、原有 Job、M5 事务定向检查退出 0,覆盖非零退出、无窗口、参数、双流、输出边界、回调失败、取消和进程树回收。定向检查明确排除了下面已经在基线复现的 breakaway 夹具。 | ||
| - `gofmt -l .` 无输出;`go vet ./...`、`go build -buildvcs=false ./...`、`git diff --check` 均退出 0;`go test ./internal/protocol -count=100` 退出 0。 | ||
| - 完整 `go test ./... -count=1` 退出 1:`lock` 的 SUBST W: 未就绪、`process` 的 breakaway 孙进程不属于任何 Job 断言失败,均在未修改源码的定向检查中复现。另有 `cli` 的 TempDir 清理偶发目录非空,原始执行器定向十轮和修改后定向均通过,未归因于本改动。其余包通过,不能表述为完整门禁通过。 | ||
| - `go test -race ./... -count=1` 退出 2,提示需要 cgo。本机 `CGO_ENABLED=0` 且 PATH 无 GCC,race 未执行,仍需在具备工具链的 Windows 环境验证。 | ||
|
|
||
| 本次只复用既有结构化错误、本地诊断和 Sentry 结果归属;`resume_process` 为现有 operation 字段的稳定值,不新增采集字段。主仓恢复修复可以独立交付,不依赖本 Runtime 的未发布制品。 | ||
|
|
||
| 实际证据文件包括 `isolation-baseline-final.log`、`relevant-tests-final.log`、`isolation-repeat.log`、`descendant-cancel.log`、`uv0123-baseline.log`、`uv0123-fixed.log`、`full-tests.log`、`breakaway-baseline.log`、`subst-unmodified.log`、`race.log`。用户现场仍需使用发布后的 Runtime 收集具体失败 API 和 Windows 错误号,才能判断是否同一原因。 | ||
|
|
||
| 复审发现恢复前取消和恢复失败的新增分支丢弃了清理错误,现用既有错误链保留 Kill、Wait、Job.Close 和原始原因。`TestRunner_CancelBeforeResumePreservesCleanupError` 在修正前失败、修正后通过;真实 Job 纳管后取消,并在实际关闭后注入清理错误,确认取消与清理原因均可追溯。修正后的完整 uv 包、vet 和 build 再次通过。 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -294,6 +294,29 @@ func (r *UVRunner) Run( | |
| return UVResult{}, failure | ||
| } | ||
| } | ||
| // 挂起创建阻止短命探针先于 Assign 退出,也阻止未纳管的后代逃出 Job。 | ||
| // 取消先于恢复检查,避免退出期间再开始 uv 的业务工作。 | ||
| if err := ctx.Err(); err != nil { | ||
| killErr := command.Process.Kill() | ||
| waitErr := command.Wait() | ||
| var closeErr error | ||
| if job != nil { | ||
| closeErr = job.Close() | ||
| } | ||
| return UVResult{}, runnerCancellationError(options.Stage, map[string]any{}, errors.Join(err, killErr, waitErr, closeErr)) | ||
| } | ||
| if err := process.ResumeSuspended(command.Process); err != nil { | ||
| killErr := command.Process.Kill() | ||
| waitErr := command.Wait() | ||
| var closeErr error | ||
| if job != nil { | ||
| closeErr = job.Close() | ||
| } | ||
| failure := newError(protocol.CodeUVExecFailed, options.Stage, "uv 进程隔离失败", | ||
| map[string]any{"failureKind": "process_isolation_failed"}, errors.Join(err, killErr, waitErr, closeErr)) | ||
| failure.diagnostics = isolationDiagnostics(UVResult{ExitCode: exitCode(waitErr), Duration: clock().Sub(started)}, resolved, "resume_process", err) | ||
|
Comment on lines
+308
to
+317
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: A cancellation occurring during Suggested FixIn the Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
| return UVResult{}, failure | ||
| } | ||
| jobStop := make(chan struct{}) | ||
| jobDone := make(chan struct{}) | ||
| if job != nil { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Windows, if cancellation occurs after the preceding
ctx.Err()check but before or duringResumeSuspended, theexec.CommandContextwatcher can kill the suspended child before its initial thread is found or resumed. This branch then reportsUV_EXEC_FAILED:command.Wait()returns an*exec.ExitErrorfor the killed child, so the joined errors need not contain the context cancellation. Callers such asenvironment checkpropagate that error toclassifyFailure, which consequently reports an isolation failure with exit code 50 instead ofOPERATION_CANCELLEDwith exit code 130. Recheckctx.Err()after cleanup and userunnerCancellationError, preserving the cleanup errors, when cancellation won this race; the existing cancellation test only covers cancellation before the preceding check.Useful? React with 👍 / 👎.