mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-15 03:32:43 +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"`
|
Message string `json:"message"`
|
||||||
SessionID string `json:"session_id"`
|
SessionID string `json:"session_id"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
|
LogPath string `json:"log_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var backendRegistry = map[string]Backend{
|
var backendRegistry = map[string]Backend{
|
||||||
|
|||||||
@@ -11,10 +11,105 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"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 {
|
type parseResult struct {
|
||||||
message string
|
message string
|
||||||
threadID string
|
threadID string
|
||||||
@@ -196,6 +291,9 @@ func generateFinalOutput(results []TaskResult) string {
|
|||||||
if res.SessionID != "" {
|
if res.SessionID != "" {
|
||||||
sb.WriteString(fmt.Sprintf("Session: %s\n", 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 != "" {
|
if res.Message != "" {
|
||||||
sb.WriteString(fmt.Sprintf("\n%s\n", 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())
|
return fmt.Sprintf("%s; stderr: %s", msg, stderrBuf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := commandContext(ctx, codexCommand, codexArgs...)
|
cmd := newCommandRunner(ctx, codexCommand, codexArgs...)
|
||||||
|
|
||||||
stderrWriters := []io.Writer{stderrBuf}
|
stderrWriters := []io.Writer{stderrBuf}
|
||||||
if stderrLogger != nil {
|
if stderrLogger != nil {
|
||||||
@@ -345,9 +443,9 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, custo
|
|||||||
stderrWriters = append([]io.Writer{os.Stderr}, stderrWriters...)
|
stderrWriters = append([]io.Writer{os.Stderr}, stderrWriters...)
|
||||||
}
|
}
|
||||||
if len(stderrWriters) == 1 {
|
if len(stderrWriters) == 1 {
|
||||||
cmd.Stderr = stderrWriters[0]
|
cmd.SetStderr(stderrWriters[0])
|
||||||
} else {
|
} else {
|
||||||
cmd.Stderr = io.MultiWriter(stderrWriters...)
|
cmd.SetStderr(io.MultiWriter(stderrWriters...))
|
||||||
}
|
}
|
||||||
|
|
||||||
var stdinPipe io.WriteCloser
|
var stdinPipe io.WriteCloser
|
||||||
@@ -391,7 +489,7 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, custo
|
|||||||
return result
|
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 {
|
if logger := activeLogger(); logger != nil {
|
||||||
logInfoFn(fmt.Sprintf("Log capturing to: %s", logger.Path()))
|
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.ExitCode = 0
|
||||||
result.Message = message
|
result.Message = message
|
||||||
result.SessionID = threadID
|
result.SessionID = threadID
|
||||||
|
if logger := activeLogger(); logger != nil {
|
||||||
|
result.LogPath = logger.Path()
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
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)
|
sigCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
@@ -488,11 +589,11 @@ func forwardSignals(ctx context.Context, cmd *exec.Cmd, logErrorFn func(string))
|
|||||||
select {
|
select {
|
||||||
case sig := <-sigCh:
|
case sig := <-sigCh:
|
||||||
logErrorFn(fmt.Sprintf("Received signal: %v", sig))
|
logErrorFn(fmt.Sprintf("Received signal: %v", sig))
|
||||||
if cmd.Process != nil {
|
if proc := cmd.Process(); proc != nil {
|
||||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
_ = proc.Signal(syscall.SIGTERM)
|
||||||
time.AfterFunc(time.Duration(forceKillDelay)*time.Second, func() {
|
time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||||
if cmd.Process != nil {
|
if p := cmd.Process(); p != nil {
|
||||||
_ = cmd.Process.Kill()
|
_ = p.Kill()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -513,16 +614,60 @@ func cancelReason(ctx context.Context) string {
|
|||||||
return "Execution cancelled, terminating codex process"
|
return "Execution cancelled, terminating codex process"
|
||||||
}
|
}
|
||||||
|
|
||||||
func terminateProcess(cmd *exec.Cmd) *time.Timer {
|
type forceKillTimer struct {
|
||||||
if cmd == nil || cmd.Process == nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
_ = proc.Signal(syscall.SIGTERM)
|
||||||
|
|
||||||
return time.AfterFunc(time.Duration(forceKillDelay)*time.Second, func() {
|
done := make(chan struct{}, 1)
|
||||||
if cmd.Process != nil {
|
timer := time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||||
_ = cmd.Process.Kill()
|
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)
|
t.Skipf("cannot start sleep command: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
timer := terminateProcess(cmd)
|
timer := terminateProcess(&realCmd{cmd: cmd})
|
||||||
if timer == nil {
|
if timer == nil {
|
||||||
t.Fatalf("terminateProcess returned nil timer for active process")
|
t.Fatalf("terminateProcess returned nil timer for active process")
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ func TestRunTerminateProcessNil(t *testing.T) {
|
|||||||
if timer := terminateProcess(nil); timer != nil {
|
if timer := terminateProcess(nil); timer != nil {
|
||||||
t.Fatalf("terminateProcess(nil) should return nil timer")
|
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")
|
t.Fatalf("terminateProcess with nil process should return nil timer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -19,6 +21,12 @@ const (
|
|||||||
stderrCaptureLimit = 4 * 1024
|
stderrCaptureLimit = 4 * 1024
|
||||||
defaultBackendName = "codex"
|
defaultBackendName = "codex"
|
||||||
wrapperName = "codeagent-wrapper"
|
wrapperName = "codeagent-wrapper"
|
||||||
|
|
||||||
|
// stdout close reasons
|
||||||
|
stdoutCloseReasonWait = "wait-done"
|
||||||
|
stdoutCloseReasonDrain = "drain-timeout"
|
||||||
|
stdoutCloseReasonCtx = "context-cancel"
|
||||||
|
stdoutDrainTimeout = 100 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
// Test hooks for dependency injection
|
// Test hooks for dependency injection
|
||||||
@@ -33,9 +41,64 @@ var (
|
|||||||
selectBackendFn = selectBackend
|
selectBackendFn = selectBackend
|
||||||
commandContext = exec.CommandContext
|
commandContext = exec.CommandContext
|
||||||
jsonMarshal = json.Marshal
|
jsonMarshal = json.Marshal
|
||||||
forceKillDelay = 5 // seconds - made variable for testability
|
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() {
|
func main() {
|
||||||
exitCode := run()
|
exitCode := run()
|
||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
@@ -52,6 +115,8 @@ func run() (exitCode int) {
|
|||||||
case "--help", "-h":
|
case "--help", "-h":
|
||||||
printHelp()
|
printHelp()
|
||||||
return 0
|
return 0
|
||||||
|
case "--cleanup":
|
||||||
|
return runCleanupMode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2002,7 +2002,7 @@ func TestRunCodexTask_SignalHandling(t *testing.T) {
|
|||||||
func TestForwardSignals_ContextCancel(t *testing.T) {
|
func TestForwardSignals_ContextCancel(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
forwardSignals(ctx, &exec.Cmd{}, func(string) {})
|
forwardSignals(ctx, &realCmd{cmd: &exec.Cmd{}}, func(string) {})
|
||||||
cancel()
|
cancel()
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
}
|
}
|
||||||
@@ -3070,13 +3070,13 @@ func TestRunForwardSignals(t *testing.T) {
|
|||||||
t.Skip("sleep command not available on Windows")
|
t.Skip("sleep command not available on Windows")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("sleep", "5")
|
execCmd := exec.Command("sleep", "5")
|
||||||
if err := cmd.Start(); err != nil {
|
if err := execCmd.Start(); err != nil {
|
||||||
t.Skipf("unable to start sleep command: %v", err)
|
t.Skipf("unable to start sleep command: %v", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = cmd.Process.Kill()
|
_ = execCmd.Process.Kill()
|
||||||
cmd.Wait()
|
execCmd.Wait()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -3099,6 +3099,7 @@ func TestRunForwardSignals(t *testing.T) {
|
|||||||
|
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
var logs []string
|
var logs []string
|
||||||
|
cmd := &realCmd{cmd: execCmd}
|
||||||
forwardSignals(ctx, cmd, func(msg string) {
|
forwardSignals(ctx, cmd, func(msg string) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|||||||
Reference in New Issue
Block a user