Files
SRCC/docs/command-log-capture.md
2026-06-22 11:33:18 +08:00

6.8 KiB
Raw Permalink Blame History

命令执行日志获取

实时捕获命令输出,写入日志文件


1. 基础日志捕获

package main

import (
    "bytes"
    "fmt"
    "os/exec"
)

func runAndLog(command string) string {
    var stdout, stderr bytes.Buffer

    cmd := exec.Command("cmd", "/c", command) // Windows
    // cmd := exec.Command("sh", "-c", command) // Linux
    cmd.Stdout = &stdout
    cmd.Stderr = &stderr

    err := cmd.Run()

    result := fmt.Sprintf("[命令] %s\n", command)
    result += fmt.Sprintf("[stdout]\n%s\n", stdout.String())
    if stderr.Len() > 0 {
        result += fmt.Sprintf("[stderr]\n%s\n", stderr.String())
    }
    if err != nil {
        result += fmt.Sprintf("[错误] %v\n", err)
    }

    return result
}

2. 实时逐行日志 + 写入文件

func runWithLiveLog(command string, logPath string) error {
    cmd := exec.Command("cmd", "/c", command)

    stdoutPipe, _ := cmd.StdoutPipe()
    stderrPipe, _ := cmd.StderrPipe()

    logFile, err := os.Create(logPath)
    if err != nil {
        return err
    }
    defer logFile.Close()

    writer := io.MultiWriter(os.Stdout, logFile) // 同时输出到终端和文件

    cmd.Start()

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        scanner := bufio.NewScanner(stdoutPipe)
        for scanner.Scan() {
            writer.Write([]byte("[OUT] " + scanner.Text() + "\n"))
        }
    }()

    go func() {
        defer wg.Done()
        scanner := bufio.NewScanner(stderrPipe)
        for scanner.Scan() {
            writer.Write([]byte("[ERR] " + scanner.Text() + "\n"))
        }
    }()

    wg.Wait()
    return cmd.Wait()
}

3. 结构化日志

type CommandLog struct {
    Timestamp string `json:"timestamp"`
    Command   string `json:"command"`
    Stdout    string `json:"stdout"`
    Stderr    string `json:"stderr"`
    ExitCode  int    `json:"exit_code"`
    Duration  string `json:"duration"`
    Success   bool   `json:"success"`
}

func runStructuredLog(command string) *CommandLog {
    start := time.Now()

    var stdout, stderr bytes.Buffer
    cmd := exec.Command("cmd", "/c", command)
    cmd.Stdout = &stdout
    cmd.Stderr = &stderr

    err := cmd.Run()
    elapsed := time.Since(start)

    exitCode := 0
    if err != nil {
        if exitErr, ok := err.(*exec.ExitError); ok {
            exitCode = exitErr.ExitCode()
        } else {
            exitCode = -1
        }
    }

    return &CommandLog{
        Timestamp: start.Format(time.RFC3339),
        Command:   command,
        Stdout:    stdout.String(),
        Stderr:    stderr.String(),
        ExitCode:  exitCode,
        Duration:  elapsed.String(),
        Success:   err == nil,
    }
}

// 追加写入 JSON 日志文件
func appendLogJSON(logPath string, entry *CommandLog) error {
    data, _ := json.Marshal(entry)
    data = append(data, '\n')

    f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return err
    }
    defer f.Close()
    _, err = f.Write(data)
    return err
}

输出示例 (commands.log)

{"timestamp":"2026-06-22T05:30:00+08:00","command":"ipconfig","stdout":"...","stderr":"","exit_code":0,"duration":"1.2s","success":true}
{"timestamp":"2026-06-22T05:31:15+08:00","command":"dir X:\\","stdout":"","stderr":"系统找不到指定的路径。\n","exit_code":1,"duration":"0.3s","success":false}

4. 带时间戳的日志文件

func createLogFile(dir string) (*os.File, error) {
    os.MkdirAll(dir, 0755)
    name := fmt.Sprintf("cmd_%s.log", time.Now().Format("20060102_150405"))
    return os.Create(filepath.Join(dir, name))
}

5. 滚动日志(限制大小)

const maxLogSize = 1 * 1024 * 1024 // 1MB

func appendRollingLog(logPath string, line string) error {
    info, err := os.Stat(logPath)
    if err == nil && info.Size() > maxLogSize {
        // 超过 1MB 自动归档
        backup := logPath + "." + time.Now().Format("20060102_150405")
        os.Rename(logPath, backup)
    }

    f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return err
    }
    defer f.Close()
    _, err = f.WriteString(line + "\n")
    return err
}

6. 超时 + 日志

func runWithTimeoutLog(command string, timeout time.Duration, logPath string) error {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    cmd := exec.CommandContext(ctx, "cmd", "/c", command)

    var stdout, stderr bytes.Buffer
    cmd.Stdout = &stdout
    cmd.Stderr = &stderr

    err := cmd.Run()

    entry := fmt.Sprintf("[%s] 命令: %s | 超时设置: %v | 成功: %v | stdout: %s | stderr: %s",
        time.Now().Format(time.RFC3339),
        command, timeout, err == nil,
        stdout.String(), stderr.String(),
    )

    if ctx.Err() == context.DeadlineExceeded {
        entry += " | 结果: 超时"
    }

    appendRollingLog(logPath, entry)
    return err
}

7. Fyne GUI 日志查看器

func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("命令日志查看器")
    myWindow.Resize(fyne.NewSize(700, 500))

    logPath := filepath.Join(os.TempDir(), "cmd_history.log")
    var mu sync.Mutex

    cmdEntry := widget.NewEntry()
    cmdEntry.SetPlaceHolder("输入命令...")

    logView := widget.NewMultiLineEntry()
    logView.Disable()

    // 清除按钮
    widget.NewButton("清除日志", func() {
        logView.SetText("")
        os.Remove(logPath)
    })

    // 执行按钮
    widget.NewButton("执行并记录日志", func() {
        command := cmdEntry.Text
        if command == "" {
            return
        }

        go func() {
            mu.Lock()
            defer mu.Unlock()

            entry := runAndLog(command)
            appendRollingLog(logPath, entry)

            current := logView.Text
            logView.SetText(current + entry + "\n---\n")
        }()
    })

    // 加载历史日志
    widget.NewButton("加载历史日志", func() {
        data, err := os.ReadFile(logPath)
        if err == nil {
            logView.SetText(string(data))
        }
    })

    myWindow.SetContent(container.NewBorder(
        container.NewVBox(cmdEntry,
            container.NewHBox(/* 按钮们 */),
        ),
        nil, nil, nil, logView,
    ))
    myWindow.ShowAndRun()
}

8. 关键类型速查

// 一次性获取全部输出
cmd.Output()          // stdout
cmd.CombinedOutput()  // stdout + stderr

// 手动分离获取
cmd.Stdout = &stdout  // stdout 写入 buffer
cmd.Stderr = &stderr  // stderr 写入 buffer

// 实时流式获取
cmd.StdoutPipe()      // 返回 io.ReadCloser,需在 cmd.Start() 前调用
cmd.StderrPipe()      // 同上

// 带超时
exec.CommandContext(ctx, name, args...)

// 获取退出码
exitErr, ok := err.(*exec.ExitError)
code := exitErr.ExitCode()