308 lines
6.8 KiB
Markdown
308 lines
6.8 KiB
Markdown
# 命令执行日志获取
|
||
|
||
> 实时捕获命令输出,写入日志文件
|
||
|
||
---
|
||
|
||
## 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()
|
||
```
|