mirror of
https://github.com/cexll/myclaude.git
synced 2026-03-27 20:00:41 +08:00
Merge branch 'master' into rc/5.2
解决合并冲突: - .github/workflows/release.yml: 统一使用codeagent-wrapper目录和输出名称 - codeagent-wrapper/main_test.go: 合并HEAD的新测试(Claude/Gemini事件解析)和master的测试改进 - 接受master的新文件: .gitignore, process_check_* 到codeagent-wrapper目录 - 确认删除: codex-wrapper/main.go (已重命名) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,12 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -25,6 +28,7 @@ type Logger struct {
|
||||
closeOnce sync.Once
|
||||
workerWG sync.WaitGroup
|
||||
pendingWG sync.WaitGroup
|
||||
flushMu sync.Mutex
|
||||
}
|
||||
|
||||
type logEntry struct {
|
||||
@@ -32,6 +36,25 @@ type logEntry struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
// CleanupStats captures the outcome of a cleanupOldLogs run.
|
||||
type CleanupStats struct {
|
||||
Scanned int
|
||||
Deleted int
|
||||
Kept int
|
||||
Errors int
|
||||
DeletedFiles []string
|
||||
KeptFiles []string
|
||||
}
|
||||
|
||||
var (
|
||||
processRunningCheck = isProcessRunning
|
||||
processStartTimeFn = getProcessStartTime
|
||||
removeLogFileFn = os.Remove
|
||||
globLogFiles = filepath.Glob
|
||||
fileStatFn = os.Lstat // Use Lstat to detect symlinks
|
||||
evalSymlinksFn = filepath.EvalSymlinks
|
||||
)
|
||||
|
||||
// NewLogger creates the async logger and starts the worker goroutine.
|
||||
// The log file is created under os.TempDir() using the required naming scheme.
|
||||
func NewLogger() (*Logger, error) {
|
||||
@@ -154,6 +177,9 @@ func (l *Logger) Flush() {
|
||||
return
|
||||
}
|
||||
|
||||
l.flushMu.Lock()
|
||||
defer l.flushMu.Unlock()
|
||||
|
||||
// Wait for pending entries with timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
@@ -199,7 +225,9 @@ func (l *Logger) log(level, msg string) {
|
||||
}
|
||||
|
||||
entry := logEntry{level: level, msg: msg}
|
||||
l.flushMu.Lock()
|
||||
l.pendingWG.Add(1)
|
||||
l.flushMu.Unlock()
|
||||
|
||||
select {
|
||||
case l.ch <- entry:
|
||||
@@ -241,3 +269,187 @@ func (l *Logger) run() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupOldLogs scans os.TempDir() for codex-wrapper-*.log files and removes those
|
||||
// whose owning process is no longer running (i.e., orphaned logs).
|
||||
// It includes safety checks for:
|
||||
// - PID reuse: Compares file modification time with process start time
|
||||
// - Symlink attacks: Ensures files are within TempDir and not symlinks
|
||||
func cleanupOldLogs() (CleanupStats, error) {
|
||||
var stats CleanupStats
|
||||
tempDir := os.TempDir()
|
||||
pattern := filepath.Join(tempDir, "codex-wrapper-*.log")
|
||||
|
||||
matches, err := globLogFiles(pattern)
|
||||
if err != nil {
|
||||
logWarn(fmt.Sprintf("cleanupOldLogs: failed to list logs: %v", err))
|
||||
return stats, fmt.Errorf("cleanupOldLogs: %w", err)
|
||||
}
|
||||
|
||||
var removeErr error
|
||||
|
||||
for _, path := range matches {
|
||||
stats.Scanned++
|
||||
filename := filepath.Base(path)
|
||||
|
||||
// Security check: Verify file is not a symlink and is within tempDir
|
||||
if shouldSkipFile, reason := isUnsafeFile(path, tempDir); shouldSkipFile {
|
||||
stats.Kept++
|
||||
stats.KeptFiles = append(stats.KeptFiles, filename)
|
||||
if reason != "" {
|
||||
logWarn(fmt.Sprintf("cleanupOldLogs: skipping %s: %s", filename, reason))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
pid, ok := parsePIDFromLog(path)
|
||||
if !ok {
|
||||
stats.Kept++
|
||||
stats.KeptFiles = append(stats.KeptFiles, filename)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if process is running
|
||||
if !processRunningCheck(pid) {
|
||||
// Process not running, safe to delete
|
||||
if err := removeLogFileFn(path); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// File already deleted by another process, don't count as success
|
||||
stats.Kept++
|
||||
stats.KeptFiles = append(stats.KeptFiles, filename+" (already deleted)")
|
||||
continue
|
||||
}
|
||||
stats.Errors++
|
||||
logWarn(fmt.Sprintf("cleanupOldLogs: failed to remove %s: %v", filename, err))
|
||||
removeErr = errors.Join(removeErr, fmt.Errorf("failed to remove %s: %w", filename, err))
|
||||
continue
|
||||
}
|
||||
stats.Deleted++
|
||||
stats.DeletedFiles = append(stats.DeletedFiles, filename)
|
||||
continue
|
||||
}
|
||||
|
||||
// Process is running, check for PID reuse
|
||||
if isPIDReused(path, pid) {
|
||||
// PID was reused, the log file is orphaned
|
||||
if err := removeLogFileFn(path); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
stats.Kept++
|
||||
stats.KeptFiles = append(stats.KeptFiles, filename+" (already deleted)")
|
||||
continue
|
||||
}
|
||||
stats.Errors++
|
||||
logWarn(fmt.Sprintf("cleanupOldLogs: failed to remove %s (PID reused): %v", filename, err))
|
||||
removeErr = errors.Join(removeErr, fmt.Errorf("failed to remove %s: %w", filename, err))
|
||||
continue
|
||||
}
|
||||
stats.Deleted++
|
||||
stats.DeletedFiles = append(stats.DeletedFiles, filename)
|
||||
continue
|
||||
}
|
||||
|
||||
// Process is running and owns this log file
|
||||
stats.Kept++
|
||||
stats.KeptFiles = append(stats.KeptFiles, filename)
|
||||
}
|
||||
|
||||
if removeErr != nil {
|
||||
return stats, fmt.Errorf("cleanupOldLogs: %w", removeErr)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// isUnsafeFile checks if a file is unsafe to delete (symlink or outside tempDir).
|
||||
// Returns (true, reason) if the file should be skipped.
|
||||
func isUnsafeFile(path string, tempDir string) (bool, string) {
|
||||
// Check if file is a symlink
|
||||
info, err := fileStatFn(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return true, "" // File disappeared, skip silently
|
||||
}
|
||||
return true, fmt.Sprintf("stat failed: %v", err)
|
||||
}
|
||||
|
||||
// Check if it's a symlink
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return true, "refusing to delete symlink"
|
||||
}
|
||||
|
||||
// Resolve any path traversal and verify it's within tempDir
|
||||
resolvedPath, err := evalSymlinksFn(path)
|
||||
if err != nil {
|
||||
return true, fmt.Sprintf("path resolution failed: %v", err)
|
||||
}
|
||||
|
||||
// Get absolute path of tempDir
|
||||
absTempDir, err := filepath.Abs(tempDir)
|
||||
if err != nil {
|
||||
return true, fmt.Sprintf("tempDir resolution failed: %v", err)
|
||||
}
|
||||
|
||||
// Ensure resolved path is within tempDir
|
||||
relPath, err := filepath.Rel(absTempDir, resolvedPath)
|
||||
if err != nil || strings.HasPrefix(relPath, "..") {
|
||||
return true, "file is outside tempDir"
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// isPIDReused checks if a PID has been reused by comparing file modification time
|
||||
// with process start time. Returns true if the log file was created by a different
|
||||
// process that previously had the same PID.
|
||||
func isPIDReused(logPath string, pid int) bool {
|
||||
// Get file modification time (when log was last written)
|
||||
info, err := fileStatFn(logPath)
|
||||
if err != nil {
|
||||
// If we can't stat the file, be conservative and keep it
|
||||
return false
|
||||
}
|
||||
fileModTime := info.ModTime()
|
||||
|
||||
// Get process start time
|
||||
procStartTime := processStartTimeFn(pid)
|
||||
if procStartTime.IsZero() {
|
||||
// Can't determine process start time
|
||||
// Check if file is very old (>7 days), likely from a dead process
|
||||
if time.Since(fileModTime) > 7*24*time.Hour {
|
||||
return true // File is old enough to be from a different process
|
||||
}
|
||||
return false // Be conservative for recent files
|
||||
}
|
||||
|
||||
// If the log file was modified before the process started, PID was reused
|
||||
// Add a small buffer (1 second) to account for clock skew and file system timing
|
||||
return fileModTime.Add(1 * time.Second).Before(procStartTime)
|
||||
}
|
||||
|
||||
func parsePIDFromLog(path string) (int, bool) {
|
||||
name := filepath.Base(path)
|
||||
if !strings.HasPrefix(name, "codex-wrapper-") || !strings.HasSuffix(name, ".log") {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
core := strings.TrimSuffix(strings.TrimPrefix(name, "codex-wrapper-"), ".log")
|
||||
if core == "" {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
pidPart := core
|
||||
if idx := strings.IndexRune(core, '-'); idx != -1 {
|
||||
pidPart = core[:idx]
|
||||
}
|
||||
|
||||
if pidPart == "" {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
pid, err := strconv.Atoi(pidPart)
|
||||
if err != nil || pid <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return pid, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user