55 "encoding/json"
66 "errors"
77 "fmt"
8+ "io"
89
910 "github.com/YuLaiZ/token-usage/internal/config"
1011 "github.com/YuLaiZ/token-usage/internal/control"
@@ -17,7 +18,7 @@ import (
1718// 2. 等待父进程退出(释放对旧 .exe 的句柄);
1819// 3. 在 control lock 内:确认 daemon 未在停止后意外运行 → 备份旧 target →
1920// MoveFileEx(stage→target) → 校验新 hash → 按 wasRunning 重启 daemon;
20- // 4. 把执行结果写入 result 文件,供父进程下一次 update --check 展示 ;
21+ // 4. 把执行结果写入 result 文件,供下次完整 update(Apply)在来源校验通过后消费 ;
2122// 5. 任一步失败:从 backup 回滚 target、必要时重启旧 daemon、写失败 result。
2223//
2324// 所有外部依赖(等待父进程、文件移动、结果写入、control lock)经字段注入,
@@ -38,15 +39,18 @@ type helperRunner struct {
3839 resultWriter ResultWriter // 写 result 文件
3940 controlMgr ControlManager // control lock(daemon 检查 / 重启)
4041 configLoader control.ConfigLoader // 锁内加载有效配置
42+ helperLog * stepLogger // [helper] 步骤日志(从 logWriter 构造,nil=静默)
4143}
4244
43- // NewHelperRunner 构造后台 helper 编排器。所有依赖必须非空(否则 Run 返回装配错误)。
45+ // NewHelperRunner 构造后台 helper 编排器。前五个依赖必须非空(否则返回装配错误);
46+ // logWriter 可为 nil(静默),生产由 CLI 注入 os.Stderr(父进程 spawn 时重定向到日志文件)。
4447func NewHelperRunner (
4548 parentWaiter ParentWaiter ,
4649 fileMover FileMover ,
4750 resultWriter ResultWriter ,
4851 controlMgr ControlManager ,
4952 configLoader control.ConfigLoader ,
53+ logWriter io.Writer ,
5054) (* helperRunner , error ) {
5155 if parentWaiter == nil || fileMover == nil || resultWriter == nil || controlMgr == nil || configLoader == nil {
5256 return nil , errors .New ("helperRunner 所有依赖不能为空" )
@@ -57,6 +61,7 @@ func NewHelperRunner(
5761 resultWriter : resultWriter ,
5862 controlMgr : controlMgr ,
5963 configLoader : configLoader ,
64+ helperLog : newStepLogger (logWriter , "helper" , nil ),
6065 }, nil
6166}
6267
@@ -77,12 +82,14 @@ func (r *helperRunner) Run(ctx context.Context, selfExe, planPath string) error
7782// execute 在计划校验通过后执行:等待父进程 → 加载配置 → control lock 内替换。
7883func (r * helperRunner ) execute (ctx context.Context , validated validatedHelperPlan ) error {
7984 plan , paths := validated .Plan , validated .Paths
85+ r .helperLog .step ("started (nonce=%s)" , plan .Nonce )
8086
8187 // 2. 等待父进程退出(据 plan 中的显式身份,杜绝 PID 复用 TOCTOU)。
8288 if err := r .parentWaiter .WaitParentExit (ctx , plan .Parent ); err != nil {
8389 r .fail (paths .Result , fmt .Errorf ("等待父进程失败: %w" , err ), "" )
8490 return err
8591 }
92+ r .helperLog .step ("parent exited" )
8693
8794 // 3. 加载有效配置(control lock 内复用)。
8895 cfg , err := r .configLoader ()
@@ -96,9 +103,20 @@ func (r *helperRunner) execute(ctx context.Context, validated validatedHelperPla
96103 }
97104
98105 // 4. control lock 内完成 daemon 检查 + 替换 + 重启。
99- return r .controlMgr .WithLock (ctx , func (sess ControlSession ) error {
106+ // WithLock 在锁获取失败(超时/context 取消)时不执行回调——此时 executeUnderLock 不会被
107+ // 调用,fail/succeed 也不会写 result。用 lockEntered 跟踪回调是否已进入:未进入时补写
108+ // 失败 result,使下次 update 能消费该失败(父命令已报告 Deferred,用户以为升级在进行中);
109+ // 已进入时 executeUnderLock 内部已通过 fail/succeed 写了 result,不覆盖。
110+ lockEntered := false
111+ lockErr := r .controlMgr .WithLock (ctx , func (sess ControlSession ) error {
112+ lockEntered = true
100113 return r .executeUnderLock (ctx , sess , cfg , plan , paths )
101114 })
115+ if lockErr != nil && ! lockEntered {
116+ r .fail (paths .Result , fmt .Errorf ("获取 control lock 失败: %w" , lockErr ), "" )
117+ return lockErr
118+ }
119+ return lockErr
102120}
103121
104122// executeUnderLock 在 control lock 持有期内完成替换与 daemon 切换。
@@ -122,12 +140,14 @@ func (r *helperRunner) executeUnderLock(ctx context.Context, sess ControlSession
122140 r .fail (paths .Result , errors .New ("daemon 在停止后意外运行,放弃替换" ), "" )
123141 return errors .New ("daemon 运行中,放弃替换" )
124142 }
143+ r .helperLog .step ("daemon wasRunning=%v" , plan .WasRunning )
125144
126145 // b. 备份旧 target → backup,校验旧 hash。
127146 if err := backupForHelper (paths .Target , paths .Backup , plan .OldSHA256 ); err != nil {
128147 r .fail (paths .Result , fmt .Errorf ("备份旧 target 失败: %w" , err ), "" )
129148 return fmt .Errorf ("备份旧 target 失败: %w" , err )
130149 }
150+ r .helperLog .step ("backup OK" )
131151
132152 // c. MoveFileEx(stage → target)。
133153 if err := r .fileMover .MoveReplace (paths .Stage , paths .Target ); err != nil {
@@ -136,13 +156,15 @@ func (r *helperRunner) executeUnderLock(ctx context.Context, sess ControlSession
136156 r .fail (paths .Result , fmt .Errorf ("MoveFileEx 替换失败: %w" , err ), errToString (rbErr ))
137157 return fmt .Errorf ("MoveFileEx 替换失败(已回滚): %w" , errors .Join (err , rbErr ))
138158 }
159+ r .helperLog .step ("MoveFileEx OK" )
139160
140161 // 校验新 target hash(防御移动过程中损坏)。
141162 if err := verifyFileHash (paths .Target , plan .NewSHA256 ); err != nil {
142163 rbErr := rollbackForHelper (paths .Target , paths .Backup , plan .OldSHA256 )
143164 r .fail (paths .Result , fmt .Errorf ("替换后新 target 校验失败: %w" , err ), errToString (rbErr ))
144165 return fmt .Errorf ("替换后新 target 校验失败(已回滚): %w" , errors .Join (err , rbErr ))
145166 }
167+ r .helperLog .step ("hash verified" )
146168
147169 // d. wasRunning → 启动新 daemon。
148170 if plan .WasRunning {
@@ -158,6 +180,7 @@ func (r *helperRunner) executeUnderLock(ctx context.Context, sess ControlSession
158180 fmt .Sprintf ("rollback=%v restart=%v" , rbErr , restartErr ))
159181 return fmt .Errorf ("启动新 daemon 失败(已回滚重启): %w" , errors .Join (serr , rbErr , restartErr ))
160182 }
183+ r .helperLog .step ("daemon restarted" )
161184 }
162185
163186 // e. 成功。
@@ -190,24 +213,35 @@ func rollbackForHelper(target, backup, expectedOldHash string) error {
190213 return verifyFileHash (target , expectedOldHash )
191214}
192215
193- // succeed 写成功 result。
216+ // succeed 写成功 result,并据写入结果记终态日志(result=ok 或 result write failed) 。
194217func (r * helperRunner ) succeed (resultPath string ) {
195- r .writeResult (resultPath , helperResult {Success : true })
218+ if err := r .writeResult (resultPath , helperResult {Success : true }); err != nil {
219+ r .helperLog .step ("result write failed: %v" , err )
220+ } else {
221+ r .helperLog .step ("result=ok" )
222+ }
196223}
197224
198- // fail 写失败 result(error + 可选 rollback 信息)。
225+ // fail 写失败 result(error + 可选 rollback 信息),并据写入结果记终态日志。
226+ // 注意:result=ok 表示 result 文件写入成功(终态日志三态:ok/failure recorded/write failed),
227+ // fail 路径用 result=failure recorded 区分于 succeed 路径的 result=ok。
199228func (r * helperRunner ) fail (resultPath string , cause error , rollback string ) {
200- r .writeResult (resultPath , helperResult {Success : false , Error : cause .Error (), Rollback : rollback })
229+ if err := r .writeResult (resultPath , helperResult {Success : false , Error : cause .Error (), Rollback : rollback }); err != nil {
230+ r .helperLog .step ("result write failed: %v" , err )
231+ } else {
232+ r .helperLog .step ("result=failure recorded" )
233+ }
201234}
202235
203- // writeResult 把 result JSON 序列化后写入 resultPath(0600 权限)。
204- func (r * helperRunner ) writeResult (resultPath string , res helperResult ) {
236+ // writeResult 把 result JSON 序列化后写入 resultPath(0600 权限),返回写入错误。
237+ // 序列化失败或写入失败返回 error;调用方据此记终态日志。result 写入失败不改变已完成
238+ // 替换的主成功结果(仅记日志),下次 consume 读不到则留待人工。
239+ func (r * helperRunner ) writeResult (resultPath string , res helperResult ) error {
205240 data , err := marshalHelperResult (res )
206241 if err != nil {
207- // 序列化失败属于编程错误;result 写不了,只能放弃(不影响主流程错误)。
208- return
242+ return fmt .Errorf ("序列化 result 失败: %w" , err )
209243 }
210- _ = r .resultWriter .WriteResult (resultPath , data , 0o600 )
244+ return r .resultWriter .WriteResult (resultPath , data , 0o600 )
211245}
212246
213247// marshalHelperResult 序列化 helperResult 为 JSON。
0 commit comments