mirror of
https://github.com/acepanel/helper.git
synced 2026-02-04 04:07:16 +08:00
feat: 添加verbose模式
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/acepanel/helper/pkg/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
verbose := flag.Bool("v", false, "verbose mode")
|
||||
flag.Parse()
|
||||
|
||||
config.Global.Verbose = *verbose
|
||||
|
||||
helper, err := initHelper()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
// Installer 安装器接口
|
||||
type Installer interface {
|
||||
Install(ctx context.Context, cfg *types.InstallConfig, progress chan<- types.Progress) error
|
||||
SetVerboseCallback(cb system.VerboseCallback)
|
||||
}
|
||||
|
||||
type installer struct {
|
||||
@@ -48,6 +49,10 @@ func NewInstaller(
|
||||
}
|
||||
}
|
||||
|
||||
func (i *installer) SetVerboseCallback(cb system.VerboseCallback) {
|
||||
i.executor.SetVerboseCallback(cb)
|
||||
}
|
||||
|
||||
func (i *installer) Install(ctx context.Context, cfg *types.InstallConfig, progress chan<- types.Progress) error {
|
||||
steps := []struct {
|
||||
name string
|
||||
|
||||
@@ -17,6 +17,7 @@ type Mounter interface {
|
||||
ListDisks(ctx context.Context) ([]types.DiskInfo, error)
|
||||
IsPartitioned(disk string) bool
|
||||
Mount(ctx context.Context, cfg *types.MountConfig, progress ProgressCallback) error
|
||||
SetVerboseCallback(cb system.VerboseCallback)
|
||||
}
|
||||
|
||||
type mounter struct {
|
||||
@@ -32,6 +33,10 @@ func NewMounter(detector system.Detector, executor system.Executor) Mounter {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mounter) SetVerboseCallback(cb system.VerboseCallback) {
|
||||
m.executor.SetVerboseCallback(cb)
|
||||
}
|
||||
|
||||
func (m *mounter) ListDisks(ctx context.Context) ([]types.DiskInfo, error) {
|
||||
return m.detector.ListDisks(ctx)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type ProgressCallback func(step, message string)
|
||||
// Uninstaller 卸载器接口
|
||||
type Uninstaller interface {
|
||||
Uninstall(ctx context.Context, setupPath string, progress ProgressCallback) error
|
||||
SetVerboseCallback(cb system.VerboseCallback)
|
||||
}
|
||||
|
||||
type uninstaller struct {
|
||||
@@ -36,6 +37,10 @@ func NewUninstaller(
|
||||
}
|
||||
}
|
||||
|
||||
func (u *uninstaller) SetVerboseCallback(cb system.VerboseCallback) {
|
||||
u.executor.SetVerboseCallback(cb)
|
||||
}
|
||||
|
||||
func (u *uninstaller) Uninstall(ctx context.Context, setupPath string, progress ProgressCallback) error {
|
||||
// 检查root权限
|
||||
if err := u.detector.CheckRoot(); err != nil {
|
||||
|
||||
@@ -91,7 +91,7 @@ func (d *detector) detectOS() types.OSType {
|
||||
return types.OSDebian
|
||||
case "ubuntu":
|
||||
return types.OSUbuntu
|
||||
case "rhel", "centos", "rocky", "almalinux", "fedora":
|
||||
case "rhel", "centos", "rocky", "almalinux", "fedora", "tencentos":
|
||||
return types.OSRHEL
|
||||
default:
|
||||
// 检查ID_LIKE
|
||||
|
||||
@@ -4,8 +4,12 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/acepanel/helper/pkg/config"
|
||||
)
|
||||
|
||||
// CommandResult 命令执行结果
|
||||
@@ -15,6 +19,9 @@ type CommandResult struct {
|
||||
Stderr string
|
||||
}
|
||||
|
||||
// VerboseCallback verbose 模式回调
|
||||
type VerboseCallback func(cmd string)
|
||||
|
||||
// Executor 命令执行器接口
|
||||
type Executor interface {
|
||||
// Run 执行命令并等待完成
|
||||
@@ -23,16 +30,36 @@ type Executor interface {
|
||||
RunWithInput(ctx context.Context, input string, name string, args ...string) (*CommandResult, error)
|
||||
// RunStream 执行命令并流式输出
|
||||
RunStream(ctx context.Context, stdout, stderr io.Writer, name string, args ...string) error
|
||||
// SetVerboseCallback 设置 verbose 回调
|
||||
SetVerboseCallback(cb VerboseCallback)
|
||||
}
|
||||
|
||||
type executor struct{}
|
||||
type executor struct {
|
||||
verboseCallback VerboseCallback
|
||||
}
|
||||
|
||||
// NewExecutor 创建执行器
|
||||
func NewExecutor() Executor {
|
||||
return &executor{}
|
||||
}
|
||||
|
||||
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, " ")
|
||||
}
|
||||
e.verboseCallback(cmdStr)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -49,6 +76,11 @@ func (e *executor) Run(ctx context.Context, name string, args ...string) (*Comma
|
||||
if errors.As(err, &exitErr) {
|
||||
result.ExitCode = exitErr.ExitCode()
|
||||
}
|
||||
// 包含 stderr 信息到错误中
|
||||
stderrStr := strings.TrimSpace(stderr.String())
|
||||
if stderrStr != "" {
|
||||
return result, fmt.Errorf("%w: %s", err, stderrStr)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -56,6 +88,8 @@ func (e *executor) Run(ctx context.Context, name string, args ...string) (*Comma
|
||||
}
|
||||
|
||||
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
|
||||
@@ -73,6 +107,11 @@ func (e *executor) RunWithInput(ctx context.Context, input string, name string,
|
||||
if errors.As(err, &exitErr) {
|
||||
result.ExitCode = exitErr.ExitCode()
|
||||
}
|
||||
// 包含 stderr 信息到错误中
|
||||
stderrStr := strings.TrimSpace(stderr.String())
|
||||
if stderrStr != "" {
|
||||
return result, fmt.Errorf("%w: %s", err, stderrStr)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -80,6 +119,8 @@ func (e *executor) RunWithInput(ctx context.Context, input string, name string,
|
||||
}
|
||||
|
||||
func (e *executor) RunStream(ctx context.Context, stdout, stderr io.Writer, name string, args ...string) error {
|
||||
e.logVerbose(name, args...)
|
||||
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = stderr
|
||||
|
||||
@@ -8,9 +8,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/helper/pkg/config"
|
||||
"github.com/acepanel/helper/pkg/embed"
|
||||
"github.com/charmbracelet/bubbles/progress"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
@@ -45,6 +47,7 @@ const (
|
||||
// 消息类型
|
||||
type (
|
||||
progressMsg types.Progress
|
||||
verboseMsg string // verbose 模式命令输出
|
||||
installCompleteMsg struct {
|
||||
err error
|
||||
info string
|
||||
@@ -90,6 +93,10 @@ type App struct {
|
||||
installDone bool
|
||||
installInfo string // 安装完成后的面板信息
|
||||
|
||||
// verbose 模式
|
||||
verboseLogs []string
|
||||
verboseViewport viewport.Model
|
||||
|
||||
// 卸载
|
||||
uninstallCountdown int
|
||||
uninstallSkipCount int // 连按enter跳过倒计时的计数
|
||||
@@ -146,6 +153,13 @@ func NewApp(installer service.Installer, uninstaller service.Uninstaller, mounte
|
||||
// 初始化进度条
|
||||
app.installProgress = progress.New(progress.WithDefaultGradient())
|
||||
|
||||
// 初始化 verbose viewport
|
||||
app.verboseViewport = viewport.New(80, 8)
|
||||
app.verboseViewport.Style = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(ColorMuted).
|
||||
Padding(0, 1)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -342,6 +356,16 @@ func (a *App) updateInstall(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return a, nil
|
||||
|
||||
case verboseMsg:
|
||||
a.verboseLogs = append(a.verboseLogs, string(msg))
|
||||
// 保留最近 100 条
|
||||
if len(a.verboseLogs) > 100 {
|
||||
a.verboseLogs = a.verboseLogs[len(a.verboseLogs)-100:]
|
||||
}
|
||||
a.verboseViewport.SetContent(strings.Join(a.verboseLogs, "\n"))
|
||||
a.verboseViewport.GotoBottom()
|
||||
return a, nil
|
||||
|
||||
case installCompleteMsg:
|
||||
a.installRunning = false
|
||||
a.installDone = true
|
||||
@@ -366,6 +390,7 @@ func (a *App) updateInstall(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if a.installForm.State == huh.StateCompleted {
|
||||
if a.installConfirmed {
|
||||
a.installRunning = true
|
||||
a.verboseLogs = nil // 清空 verbose 日志
|
||||
return a, tea.Batch(a.installSpinner.Tick, a.startInstall())
|
||||
}
|
||||
a.state = ViewMainMenu
|
||||
@@ -390,6 +415,15 @@ func (a *App) startInstall() tea.Cmd {
|
||||
}
|
||||
}()
|
||||
|
||||
// 设置 verbose 回调
|
||||
if config.Global.Verbose {
|
||||
a.installer.SetVerboseCallback(func(cmd string) {
|
||||
if a.program != nil {
|
||||
a.program.Send(verboseMsg("$ " + cmd))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
cfg := &types.InstallConfig{
|
||||
SetupPath: a.setupPath,
|
||||
AutoSwap: true,
|
||||
@@ -440,6 +474,13 @@ func (a *App) viewInstall() string {
|
||||
sb.WriteString(LogStyle.Render(log))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
// verbose 模式显示命令日志
|
||||
if config.Global.Verbose && len(a.verboseLogs) > 0 {
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(MutedStyle.Render("Commands:"))
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(a.verboseViewport.View())
|
||||
}
|
||||
} else {
|
||||
sb.WriteString(a.installForm.View())
|
||||
}
|
||||
@@ -498,6 +539,15 @@ func (a *App) updateUninstall(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return a, nil
|
||||
|
||||
case verboseMsg:
|
||||
a.verboseLogs = append(a.verboseLogs, string(msg))
|
||||
if len(a.verboseLogs) > 100 {
|
||||
a.verboseLogs = a.verboseLogs[len(a.verboseLogs)-100:]
|
||||
}
|
||||
a.verboseViewport.SetContent(strings.Join(a.verboseLogs, "\n"))
|
||||
a.verboseViewport.GotoBottom()
|
||||
return a, nil
|
||||
|
||||
case uninstallCompleteMsg:
|
||||
a.uninstallRunning = false
|
||||
a.uninstallDone = true
|
||||
@@ -523,6 +573,7 @@ func (a *App) updateUninstall(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if a.uninstallConfirm {
|
||||
a.uninstallState = 3
|
||||
a.uninstallRunning = true
|
||||
a.verboseLogs = nil // 清空 verbose 日志
|
||||
return a, tea.Batch(a.uninstallSpinner.Tick, a.startUninstall())
|
||||
}
|
||||
a.state = ViewMainMenu
|
||||
@@ -557,6 +608,16 @@ func (a *App) tickCountdown() tea.Cmd {
|
||||
func (a *App) startUninstall() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
|
||||
// 设置 verbose 回调
|
||||
if config.Global.Verbose {
|
||||
a.uninstaller.SetVerboseCallback(func(cmd string) {
|
||||
if a.program != nil {
|
||||
a.program.Send(verboseMsg("$ " + cmd))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
err := a.uninstaller.Uninstall(ctx, a.setupPath, func(step, message string) {
|
||||
if a.program != nil {
|
||||
a.program.Send(progressMsg{Step: step, Message: message})
|
||||
@@ -608,6 +669,13 @@ func (a *App) viewUninstall() string {
|
||||
sb.WriteString(LogStyle.Render(log))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
// verbose 模式显示命令日志
|
||||
if config.Global.Verbose && len(a.verboseLogs) > 0 {
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(MutedStyle.Render("Commands:"))
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(a.verboseViewport.View())
|
||||
}
|
||||
|
||||
case 4: // done
|
||||
if a.uninstallErr != nil {
|
||||
@@ -678,6 +746,15 @@ func (a *App) updateMount(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return a, nil
|
||||
|
||||
case verboseMsg:
|
||||
a.verboseLogs = append(a.verboseLogs, string(msg))
|
||||
if len(a.verboseLogs) > 100 {
|
||||
a.verboseLogs = a.verboseLogs[len(a.verboseLogs)-100:]
|
||||
}
|
||||
a.verboseViewport.SetContent(strings.Join(a.verboseLogs, "\n"))
|
||||
a.verboseViewport.GotoBottom()
|
||||
return a, nil
|
||||
|
||||
case mountCompleteMsg:
|
||||
a.mountRunning = false
|
||||
a.mountDone = true
|
||||
@@ -738,6 +815,7 @@ func (a *App) updateMount(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if a.mountConfirmed {
|
||||
a.mountState = 5
|
||||
a.mountRunning = true
|
||||
a.verboseLogs = nil // 清空 verbose 日志
|
||||
return a, tea.Batch(a.mountSpinner.Tick, a.startMount())
|
||||
}
|
||||
a.state = ViewMainMenu
|
||||
@@ -819,6 +897,16 @@ func (a *App) initMountConfirmForm() {
|
||||
func (a *App) startMount() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
|
||||
// 设置 verbose 回调
|
||||
if config.Global.Verbose {
|
||||
a.mounter.SetVerboseCallback(func(cmd string) {
|
||||
if a.program != nil {
|
||||
a.program.Send(verboseMsg("$ " + cmd))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
err := a.mounter.Mount(ctx, &a.mountConfig, func(step, message string) {
|
||||
if a.program != nil {
|
||||
a.program.Send(progressMsg{Step: step, Message: message})
|
||||
@@ -885,6 +973,13 @@ func (a *App) viewMount() string {
|
||||
sb.WriteString(LogStyle.Render(log))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
// verbose 模式显示命令日志
|
||||
if config.Global.Verbose && len(a.verboseLogs) > 0 {
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(MutedStyle.Render("Commands:"))
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(a.verboseViewport.View())
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
|
||||
9
pkg/config/config.go
Normal file
9
pkg/config/config.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Config 全局配置
|
||||
type Config struct {
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
// Global 全局配置实例
|
||||
var Global = &Config{}
|
||||
Reference in New Issue
Block a user