diff --git a/cmd/helper/main.go b/cmd/helper/main.go index 5ad61d8..a1f1032 100644 --- a/cmd/helper/main.go +++ b/cmd/helper/main.go @@ -8,10 +8,20 @@ import ( ) func main() { - verbose := flag.Bool("v", false, "verbose mode") + verbose := flag.String("v", "", "verbose mode, optionally specify log file path") flag.Parse() - config.Global.Verbose = *verbose + if *verbose != "" { + config.Global.Verbose = true + // 如果不是 "true" 或 "1",则作为文件路径 + if *verbose != "true" && *verbose != "1" { + config.Global.LogFile = *verbose + if err := config.Global.InitLogFile(); err != nil { + panic(err) + } + defer config.Global.CloseLogFile() + } + } helper, err := initHelper() if err != nil { diff --git a/internal/system/executor.go b/internal/system/executor.go index 3bbd908..acd4539 100644 --- a/internal/system/executor.go +++ b/internal/system/executor.go @@ -19,8 +19,8 @@ type CommandResult struct { Stderr string } -// VerboseCallback verbose 模式回调 -type VerboseCallback func(cmd string) +// VerboseCallback verbose 模式回调 (cmd, stdout, stderr, err) +type VerboseCallback func(cmd, stdout, stderr string, err error) // Executor 命令执行器接口 type Executor interface { @@ -47,19 +47,40 @@ func (e *executor) SetVerboseCallback(cb VerboseCallback) { e.verboseCallback = cb } -func (e *executor) logVerbose(name string, args ...string) { - if config.Global.Verbose && e.verboseCallback != nil { - cmdStr := name - if len(args) > 0 { - cmdStr += " " + strings.Join(args, " ") +func (e *executor) logVerbose(name string, args []string, result *CommandResult, err error) { + cmdStr := name + if len(args) > 0 { + cmdStr += " " + strings.Join(args, " ") + } + + // 写入日志文件 + if config.Global.LogFile != "" { + config.Global.WriteLog("$ %s", cmdStr) + if result != nil { + if result.Stdout != "" { + config.Global.WriteLog("stdout: %s", strings.TrimSpace(result.Stdout)) + } + if result.Stderr != "" { + config.Global.WriteLog("stderr: %s", strings.TrimSpace(result.Stderr)) + } } - e.verboseCallback(cmdStr) + if err != nil { + config.Global.WriteLog("error: %v", err) + } + } + + // 回调 UI + if config.Global.Verbose && e.verboseCallback != nil { + stdout, stderr := "", "" + if result != nil { + stdout = result.Stdout + stderr = result.Stderr + } + e.verboseCallback(cmdStr, stdout, stderr, err) } } func (e *executor) Run(ctx context.Context, name string, args ...string) (*CommandResult, error) { - e.logVerbose(name, args...) - cmd := exec.CommandContext(ctx, name, args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout @@ -79,17 +100,15 @@ func (e *executor) Run(ctx context.Context, name string, args ...string) (*Comma // 包含 stderr 信息到错误中 stderrStr := strings.TrimSpace(stderr.String()) if stderrStr != "" { - return result, fmt.Errorf("%w: %s", err, stderrStr) + err = fmt.Errorf("%w: %s", err, stderrStr) } - return result, err } - return result, nil + e.logVerbose(name, args, result, err) + return result, err } func (e *executor) RunWithInput(ctx context.Context, input string, name string, args ...string) (*CommandResult, error) { - e.logVerbose(name, args...) - cmd := exec.CommandContext(ctx, name, args...) cmd.Stdin = bytes.NewBufferString(input) var stdout, stderr bytes.Buffer @@ -110,19 +129,31 @@ func (e *executor) RunWithInput(ctx context.Context, input string, name string, // 包含 stderr 信息到错误中 stderrStr := strings.TrimSpace(stderr.String()) if stderrStr != "" { - return result, fmt.Errorf("%w: %s", err, stderrStr) + err = fmt.Errorf("%w: %s", err, stderrStr) } - return result, err } - return result, nil + e.logVerbose(name, args, result, err) + return result, err } -func (e *executor) RunStream(ctx context.Context, stdout, stderr io.Writer, name string, args ...string) error { - e.logVerbose(name, args...) - +func (e *executor) RunStream(ctx context.Context, stdoutW, stderrW io.Writer, name string, args ...string) error { + var stdoutBuf, stderrBuf bytes.Buffer cmd := exec.CommandContext(ctx, name, args...) - cmd.Stdout = stdout - cmd.Stderr = stderr - return cmd.Run() + cmd.Stdout = io.MultiWriter(stdoutW, &stdoutBuf) + cmd.Stderr = io.MultiWriter(stderrW, &stderrBuf) + + err := cmd.Run() + result := &CommandResult{ + Stdout: stdoutBuf.String(), + Stderr: stderrBuf.String(), + } + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + result.ExitCode = exitErr.ExitCode() + } + } + e.logVerbose(name, args, result, err) + return err } diff --git a/internal/ui/app.go b/internal/ui/app.go index d78e57e..f18493f 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -417,9 +417,19 @@ func (a *App) startInstall() tea.Cmd { // 设置 verbose 回调 if config.Global.Verbose { - a.installer.SetVerboseCallback(func(cmd string) { + a.installer.SetVerboseCallback(func(cmd, stdout, stderr string, err error) { if a.program != nil { - a.program.Send(verboseMsg("$ " + cmd)) + msg := "$ " + cmd + if stdout != "" { + msg += "\n" + strings.TrimSpace(stdout) + } + if stderr != "" { + msg += "\n[stderr] " + strings.TrimSpace(stderr) + } + if err != nil { + msg += "\n[error] " + err.Error() + } + a.program.Send(verboseMsg(msg)) } }) } @@ -611,9 +621,19 @@ func (a *App) startUninstall() tea.Cmd { // 设置 verbose 回调 if config.Global.Verbose { - a.uninstaller.SetVerboseCallback(func(cmd string) { + a.uninstaller.SetVerboseCallback(func(cmd, stdout, stderr string, err error) { if a.program != nil { - a.program.Send(verboseMsg("$ " + cmd)) + msg := "$ " + cmd + if stdout != "" { + msg += "\n" + strings.TrimSpace(stdout) + } + if stderr != "" { + msg += "\n[stderr] " + strings.TrimSpace(stderr) + } + if err != nil { + msg += "\n[error] " + err.Error() + } + a.program.Send(verboseMsg(msg)) } }) } @@ -900,9 +920,19 @@ func (a *App) startMount() tea.Cmd { // 设置 verbose 回调 if config.Global.Verbose { - a.mounter.SetVerboseCallback(func(cmd string) { + a.mounter.SetVerboseCallback(func(cmd, stdout, stderr string, err error) { if a.program != nil { - a.program.Send(verboseMsg("$ " + cmd)) + msg := "$ " + cmd + if stdout != "" { + msg += "\n" + strings.TrimSpace(stdout) + } + if stderr != "" { + msg += "\n[stderr] " + strings.TrimSpace(stderr) + } + if err != nil { + msg += "\n[error] " + err.Error() + } + a.program.Send(verboseMsg(msg)) } }) } diff --git a/pkg/config/config.go b/pkg/config/config.go index 76293e5..e4601e7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,9 +1,51 @@ package config +import ( + "fmt" + "os" + "sync" + "time" +) + // Config 全局配置 type Config struct { - Verbose bool + Verbose bool + LogFile string + logWriter *os.File + logMu sync.Mutex } // Global 全局配置实例 var Global = &Config{} + +// InitLogFile 初始化日志文件 +func (c *Config) InitLogFile() error { + if c.LogFile == "" { + return nil + } + f, err := os.OpenFile(c.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return err + } + c.logWriter = f + return nil +} + +// CloseLogFile 关闭日志文件 +func (c *Config) CloseLogFile() { + if c.logWriter != nil { + c.logWriter.Close() + } +} + +// WriteLog 写日志 +func (c *Config) WriteLog(format string, args ...interface{}) { + if c.logWriter == nil { + return + } + c.logMu.Lock() + defer c.logMu.Unlock() + timestamp := time.Now().Format("2006-01-02 15:04:05") + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(c.logWriter, "[%s] %s\n", timestamp, msg) +}