mirror of
https://github.com/cexll/myclaude.git
synced 2025-12-24 13:47:58 +08:00
fix(merge): 修复master合并后的编译和测试问题
在重构代码后合并master分支时需要的适配:
1. **接口定义恢复** (executor.go)
- 添加 commandRunner 和 processHandle 接口
- 实现 realCmd 和 realProcess 适配器
- 添加 newCommandRunner 测试钩子
2. **TaskResult扩展** (config.go)
- 添加 LogPath 字段支持日志路径跟踪
- 在 generateFinalOutput 中输出 LogPath
3. **原子变量适配** (main.go, executor.go)
- forceKillDelay 从int改为 atomic.Int32
- 添加测试钩子: cleanupLogsFn, signalNotifyFn, signalStopFn
- 添加 stdout 关闭原因常量
4. **功能函数添加**
- runStartupCleanup: 启动时清理旧日志
- runCleanupMode: --cleanup 模式处理
- forceKillTimer 类型和 terminateCommand 函数
- terminateProcess nil 安全检查
5. **测试适配** (logger_test.go, main_test.go)
- 将 *exec.Cmd 包装为 &realCmd{cmd}
- 修复 forwardSignals 等函数调用
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ type TaskResult struct {
|
||||
Message string `json:"message"`
|
||||
SessionID string `json:"session_id"`
|
||||
Error string `json:"error"`
|
||||
LogPath string `json:"log_path"`
|
||||
}
|
||||
|
||||
var backendRegistry = map[string]Backend{
|
||||
|
||||
@@ -11,10 +11,105 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// commandRunner abstracts exec.Cmd for testability
|
||||
type commandRunner interface {
|
||||
Start() error
|
||||
Wait() error
|
||||
StdoutPipe() (io.ReadCloser, error)
|
||||
StdinPipe() (io.WriteCloser, error)
|
||||
SetStderr(io.Writer)
|
||||
Process() processHandle
|
||||
}
|
||||
|
||||
// processHandle abstracts os.Process for testability
|
||||
type processHandle interface {
|
||||
Pid() int
|
||||
Kill() error
|
||||
Signal(os.Signal) error
|
||||
}
|
||||
|
||||
// realCmd implements commandRunner using exec.Cmd
|
||||
type realCmd struct {
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
func (r *realCmd) Start() error {
|
||||
if r.cmd == nil {
|
||||
return errors.New("command is nil")
|
||||
}
|
||||
return r.cmd.Start()
|
||||
}
|
||||
|
||||
func (r *realCmd) Wait() error {
|
||||
if r.cmd == nil {
|
||||
return errors.New("command is nil")
|
||||
}
|
||||
return r.cmd.Wait()
|
||||
}
|
||||
|
||||
func (r *realCmd) StdoutPipe() (io.ReadCloser, error) {
|
||||
if r.cmd == nil {
|
||||
return nil, errors.New("command is nil")
|
||||
}
|
||||
return r.cmd.StdoutPipe()
|
||||
}
|
||||
|
||||
func (r *realCmd) StdinPipe() (io.WriteCloser, error) {
|
||||
if r.cmd == nil {
|
||||
return nil, errors.New("command is nil")
|
||||
}
|
||||
return r.cmd.StdinPipe()
|
||||
}
|
||||
|
||||
func (r *realCmd) SetStderr(w io.Writer) {
|
||||
if r.cmd != nil {
|
||||
r.cmd.Stderr = w
|
||||
}
|
||||
}
|
||||
|
||||
func (r *realCmd) Process() processHandle {
|
||||
if r == nil || r.cmd == nil || r.cmd.Process == nil {
|
||||
return nil
|
||||
}
|
||||
return &realProcess{proc: r.cmd.Process}
|
||||
}
|
||||
|
||||
// realProcess implements processHandle using os.Process
|
||||
type realProcess struct {
|
||||
proc *os.Process
|
||||
}
|
||||
|
||||
func (p *realProcess) Pid() int {
|
||||
if p == nil || p.proc == nil {
|
||||
return 0
|
||||
}
|
||||
return p.proc.Pid
|
||||
}
|
||||
|
||||
func (p *realProcess) Kill() error {
|
||||
if p == nil || p.proc == nil {
|
||||
return nil
|
||||
}
|
||||
return p.proc.Kill()
|
||||
}
|
||||
|
||||
func (p *realProcess) Signal(sig os.Signal) error {
|
||||
if p == nil || p.proc == nil {
|
||||
return nil
|
||||
}
|
||||
return p.proc.Signal(sig)
|
||||
}
|
||||
|
||||
// newCommandRunner creates a new commandRunner (test hook injection point)
|
||||
var newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return &realCmd{cmd: commandContext(ctx, name, args...)}
|
||||
}
|
||||
|
||||
type parseResult struct {
|
||||
message string
|
||||
threadID string
|
||||
@@ -196,6 +291,9 @@ func generateFinalOutput(results []TaskResult) string {
|
||||
if res.SessionID != "" {
|
||||
sb.WriteString(fmt.Sprintf("Session: %s\n", res.SessionID))
|
||||
}
|
||||
if res.LogPath != "" {
|
||||
sb.WriteString(fmt.Sprintf("Log: %s\n", res.LogPath))
|
||||
}
|
||||
if res.Message != "" {
|
||||
sb.WriteString(fmt.Sprintf("\n%s\n", res.Message))
|
||||
}
|
||||
@@ -335,7 +433,7 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, custo
|
||||
return fmt.Sprintf("%s; stderr: %s", msg, stderrBuf.String())
|
||||
}
|
||||
|
||||
cmd := commandContext(ctx, codexCommand, codexArgs...)
|
||||
cmd := newCommandRunner(ctx, codexCommand, codexArgs...)
|
||||
|
||||
stderrWriters := []io.Writer{stderrBuf}
|
||||
if stderrLogger != nil {
|
||||
@@ -345,9 +443,9 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, custo
|
||||
stderrWriters = append([]io.Writer{os.Stderr}, stderrWriters...)
|
||||
}
|
||||
if len(stderrWriters) == 1 {
|
||||
cmd.Stderr = stderrWriters[0]
|
||||
cmd.SetStderr(stderrWriters[0])
|
||||
} else {
|
||||
cmd.Stderr = io.MultiWriter(stderrWriters...)
|
||||
cmd.SetStderr(io.MultiWriter(stderrWriters...))
|
||||
}
|
||||
|
||||
var stdinPipe io.WriteCloser
|
||||
@@ -391,7 +489,7 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, custo
|
||||
return result
|
||||
}
|
||||
|
||||
logInfoFn(fmt.Sprintf("Starting %s with PID: %d", codexCommand, cmd.Process.Pid))
|
||||
logInfoFn(fmt.Sprintf("Starting %s with PID: %d", codexCommand, cmd.Process().Pid()))
|
||||
if logger := activeLogger(); logger != nil {
|
||||
logInfoFn(fmt.Sprintf("Log capturing to: %s", logger.Path()))
|
||||
}
|
||||
@@ -475,11 +573,14 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, custo
|
||||
result.ExitCode = 0
|
||||
result.Message = message
|
||||
result.SessionID = threadID
|
||||
if logger := activeLogger(); logger != nil {
|
||||
result.LogPath = logger.Path()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func forwardSignals(ctx context.Context, cmd *exec.Cmd, logErrorFn func(string)) {
|
||||
func forwardSignals(ctx context.Context, cmd commandRunner, logErrorFn func(string)) {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
@@ -488,11 +589,11 @@ func forwardSignals(ctx context.Context, cmd *exec.Cmd, logErrorFn func(string))
|
||||
select {
|
||||
case sig := <-sigCh:
|
||||
logErrorFn(fmt.Sprintf("Received signal: %v", sig))
|
||||
if cmd.Process != nil {
|
||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
||||
time.AfterFunc(time.Duration(forceKillDelay)*time.Second, func() {
|
||||
if cmd.Process != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
if proc := cmd.Process(); proc != nil {
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||
if p := cmd.Process(); p != nil {
|
||||
_ = p.Kill()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -513,16 +614,60 @@ func cancelReason(ctx context.Context) string {
|
||||
return "Execution cancelled, terminating codex process"
|
||||
}
|
||||
|
||||
func terminateProcess(cmd *exec.Cmd) *time.Timer {
|
||||
if cmd == nil || cmd.Process == nil {
|
||||
type forceKillTimer struct {
|
||||
timer *time.Timer
|
||||
done chan struct{}
|
||||
stopped atomic.Bool
|
||||
drained atomic.Bool
|
||||
}
|
||||
|
||||
func (t *forceKillTimer) Stop() {
|
||||
if t == nil || t.timer == nil {
|
||||
return
|
||||
}
|
||||
if !t.timer.Stop() {
|
||||
<-t.done
|
||||
t.drained.Store(true)
|
||||
}
|
||||
t.stopped.Store(true)
|
||||
}
|
||||
|
||||
func terminateCommand(cmd commandRunner) *forceKillTimer {
|
||||
if cmd == nil {
|
||||
return nil
|
||||
}
|
||||
proc := cmd.Process()
|
||||
if proc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
|
||||
return time.AfterFunc(time.Duration(forceKillDelay)*time.Second, func() {
|
||||
if cmd.Process != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
done := make(chan struct{}, 1)
|
||||
timer := time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||
if p := cmd.Process(); p != nil {
|
||||
_ = p.Kill()
|
||||
}
|
||||
close(done)
|
||||
})
|
||||
|
||||
return &forceKillTimer{timer: timer, done: done}
|
||||
}
|
||||
|
||||
func terminateProcess(cmd commandRunner) *time.Timer {
|
||||
if cmd == nil {
|
||||
return nil
|
||||
}
|
||||
proc := cmd.Process()
|
||||
if proc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
|
||||
return time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||
if p := cmd.Process(); p != nil {
|
||||
_ = p.Kill()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ func TestRunLoggerTerminateProcessActive(t *testing.T) {
|
||||
t.Skipf("cannot start sleep command: %v", err)
|
||||
}
|
||||
|
||||
timer := terminateProcess(cmd)
|
||||
timer := terminateProcess(&realCmd{cmd: cmd})
|
||||
if timer == nil {
|
||||
t.Fatalf("terminateProcess returned nil timer for active process")
|
||||
}
|
||||
@@ -197,7 +197,7 @@ func TestRunTerminateProcessNil(t *testing.T) {
|
||||
if timer := terminateProcess(nil); timer != nil {
|
||||
t.Fatalf("terminateProcess(nil) should return nil timer")
|
||||
}
|
||||
if timer := terminateProcess(&exec.Cmd{}); timer != nil {
|
||||
if timer := terminateProcess(&realCmd{cmd: &exec.Cmd{}}); timer != nil {
|
||||
t.Fatalf("terminateProcess with nil process should return nil timer")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -19,6 +21,12 @@ const (
|
||||
stderrCaptureLimit = 4 * 1024
|
||||
defaultBackendName = "codex"
|
||||
wrapperName = "codeagent-wrapper"
|
||||
|
||||
// stdout close reasons
|
||||
stdoutCloseReasonWait = "wait-done"
|
||||
stdoutCloseReasonDrain = "drain-timeout"
|
||||
stdoutCloseReasonCtx = "context-cancel"
|
||||
stdoutDrainTimeout = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// Test hooks for dependency injection
|
||||
@@ -29,13 +37,68 @@ var (
|
||||
cleanupHook func()
|
||||
loggerPtr atomic.Pointer[Logger]
|
||||
|
||||
buildCodexArgsFn = buildCodexArgs
|
||||
selectBackendFn = selectBackend
|
||||
commandContext = exec.CommandContext
|
||||
jsonMarshal = json.Marshal
|
||||
forceKillDelay = 5 // seconds - made variable for testability
|
||||
buildCodexArgsFn = buildCodexArgs
|
||||
selectBackendFn = selectBackend
|
||||
commandContext = exec.CommandContext
|
||||
jsonMarshal = json.Marshal
|
||||
cleanupLogsFn = cleanupOldLogs
|
||||
signalNotifyFn = signal.Notify
|
||||
signalStopFn = signal.Stop
|
||||
terminateCommandFn = terminateCommand
|
||||
)
|
||||
|
||||
var forceKillDelay atomic.Int32
|
||||
|
||||
func init() {
|
||||
forceKillDelay.Store(5) // seconds - default value
|
||||
}
|
||||
|
||||
func runStartupCleanup() {
|
||||
if cleanupLogsFn == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logWarn(fmt.Sprintf("cleanupOldLogs panic: %v", r))
|
||||
}
|
||||
}()
|
||||
if _, err := cleanupLogsFn(); err != nil {
|
||||
logWarn(fmt.Sprintf("cleanupOldLogs error: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
func runCleanupMode() int {
|
||||
if cleanupLogsFn == nil {
|
||||
fmt.Fprintln(os.Stderr, "Cleanup failed: log cleanup function not configured")
|
||||
return 1
|
||||
}
|
||||
|
||||
stats, err := cleanupLogsFn()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cleanup failed: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
fmt.Println("Cleanup completed")
|
||||
fmt.Printf("Files scanned: %d\n", stats.Scanned)
|
||||
fmt.Printf("Files deleted: %d\n", stats.Deleted)
|
||||
if len(stats.DeletedFiles) > 0 {
|
||||
for _, f := range stats.DeletedFiles {
|
||||
fmt.Printf(" - %s\n", f)
|
||||
}
|
||||
}
|
||||
fmt.Printf("Files kept: %d\n", stats.Kept)
|
||||
if len(stats.KeptFiles) > 0 {
|
||||
for _, f := range stats.KeptFiles {
|
||||
fmt.Printf(" - %s\n", f)
|
||||
}
|
||||
}
|
||||
if stats.Errors > 0 {
|
||||
fmt.Printf("Deletion errors: %d\n", stats.Errors)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func main() {
|
||||
exitCode := run()
|
||||
os.Exit(exitCode)
|
||||
@@ -52,6 +115,8 @@ func run() (exitCode int) {
|
||||
case "--help", "-h":
|
||||
printHelp()
|
||||
return 0
|
||||
case "--cleanup":
|
||||
return runCleanupMode()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2002,7 +2002,7 @@ func TestRunCodexTask_SignalHandling(t *testing.T) {
|
||||
func TestForwardSignals_ContextCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
forwardSignals(ctx, &exec.Cmd{}, func(string) {})
|
||||
forwardSignals(ctx, &realCmd{cmd: &exec.Cmd{}}, func(string) {})
|
||||
cancel()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
@@ -3070,13 +3070,13 @@ func TestRunForwardSignals(t *testing.T) {
|
||||
t.Skip("sleep command not available on Windows")
|
||||
}
|
||||
|
||||
cmd := exec.Command("sleep", "5")
|
||||
if err := cmd.Start(); err != nil {
|
||||
execCmd := exec.Command("sleep", "5")
|
||||
if err := execCmd.Start(); err != nil {
|
||||
t.Skipf("unable to start sleep command: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
_ = execCmd.Process.Kill()
|
||||
execCmd.Wait()
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -3099,6 +3099,7 @@ func TestRunForwardSignals(t *testing.T) {
|
||||
|
||||
var mu sync.Mutex
|
||||
var logs []string
|
||||
cmd := &realCmd{cmd: execCmd}
|
||||
forwardSignals(ctx, cmd, func(msg string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user