Files

308 lines
6.8 KiB
Markdown
Raw Permalink Normal View History

2026-06-22 11:33:18 +08:00
# 命令执行日志获取
> 实时捕获命令输出,写入日志文件
---
## 1. 基础日志捕获
```go
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. 实时逐行日志 + 写入文件
```go
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. 结构化日志
```go
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. 带时间戳的日志文件
```go
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. 滚动日志(限制大小)
```go
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. 超时 + 日志
```go
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 日志查看器
```go
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. 关键类型速查
```go
// 一次性获取全部输出
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()
```