fix: loading print panic

This commit is contained in:
loveuer 2024-12-26 22:55:13 -08:00
parent 8235631d4f
commit c108679fc9
7 changed files with 286 additions and 253 deletions

123
nft/loading/loading.go Normal file
View File

@ -0,0 +1,123 @@
package loading
import (
"context"
"fmt"
"time"
)
type Type int
const (
TypeProcessing Type = iota
TypeInfo
TypeSuccess
TypeWarning
TypeError
)
func (t Type) Symbol() string {
switch t {
case TypeSuccess:
return "✔️ "
case TypeWarning:
return "❗ "
case TypeError:
return "❌ "
case TypeInfo:
return "❕ "
default:
return ""
}
}
type _msg struct {
msg string
t Type
}
var frames = []string{"|", "/", "-", "\\"}
func Do(ctx context.Context, fn func(ctx context.Context, print func(msg string, types ...Type)) error) (err error) {
start := time.Now()
ch := make(chan *_msg)
go func() {
var (
m *_msg
ok bool
processing string
)
defer func() {
fmt.Printf("\r\033[K")
}()
for {
for _, frame := range frames {
select {
case <-ctx.Done():
return
case m, ok = <-ch:
if !ok || m == nil {
return
}
switch m.t {
case TypeProcessing:
if m.msg != "" {
processing = m.msg
}
case TypeInfo,
TypeSuccess,
TypeWarning,
TypeError:
// Clear the loading animation
fmt.Printf("\r\033[K")
fmt.Printf("%s%s\n", m.t.Symbol(), m.msg)
}
default:
elapsed := time.Since(start).Seconds()
if processing != "" {
fmt.Printf("\r\033[K%s %s (%.2fs)", frame, processing, elapsed)
}
time.Sleep(100 * time.Millisecond)
}
}
}
}()
printFn := func(msg string, types ...Type) {
if msg == "" {
return
}
m := &_msg{
msg: msg,
t: TypeProcessing,
}
if len(types) > 0 {
m.t = types[0]
}
ch <- m
}
done := make(chan struct{})
go func() {
if err = fn(ctx, printFn); err != nil {
ch <- &_msg{msg: err.Error(), t: TypeError}
}
close(ch)
done <- struct{}{}
}()
select {
case <-ctx.Done():
case <-done:
}
return err
}

View File

@ -0,0 +1,25 @@
package loading
import (
"context"
"os/signal"
"syscall"
"testing"
"time"
)
func TestLoadingPrint(t *testing.T) {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
defer cancel()
Do(ctx, func(ctx context.Context, print func(msg string, types ...Type)) error {
print("start task 1...")
time.Sleep(3 * time.Second)
print("warning...1", TypeWarning)
time.Sleep(2 * time.Second)
return nil
})
}

View File

@ -3,6 +3,7 @@ package cmd
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -11,9 +12,9 @@ import (
"strings" "strings"
"text/template" "text/template"
"github.com/loveuer/nf/nft/loading"
"github.com/loveuer/nf/nft/log" "github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/nfctl/internal/opt" "github.com/loveuer/nf/nft/nfctl/internal/opt"
"github.com/loveuer/nf/nft/nfctl/pkg/loading"
"github.com/loveuer/nf/nft/tool" "github.com/loveuer/nf/nft/tool"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -32,7 +33,7 @@ func initNew() *cobra.Command {
return newCmd return newCmd
} }
func doNew(cmd *cobra.Command, args []string) error { func doNew(cmd *cobra.Command, args []string) (err error) {
if len(args) == 0 { if len(args) == 0 {
return errors.New("必须提供 project 名称") return errors.New("必须提供 project 名称")
} }
@ -46,15 +47,11 @@ func doNew(cmd *cobra.Command, args []string) error {
return errors.New("project 名称不能以 . 开头") return errors.New("project 名称不能以 . 开头")
} }
ch := make(chan *loading.Loading) return loading.Do(cmd.Context(), func(ctx context.Context, print func(msg string, types ...loading.Type)) error {
defer close(ch) print("开始新建项目: "+args[0], loading.TypeInfo)
go loading.Print(cmd.Context(), ch)
ch <- &loading.Loading{Content: "开始新建项目: " + args[0], Type: loading.TypeInfo}
pwd, err := os.Getwd() pwd, err := os.Getwd()
if err != nil { if err != nil {
ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError}
return err return err
} }
@ -63,7 +60,7 @@ func doNew(cmd *cobra.Command, args []string) error {
log.Debug("cmd.new: new project, pwd = %s, name = %s, template = %s", pwd, moduleName, opt.Cfg.New.Template) log.Debug("cmd.new: new project, pwd = %s, name = %s, template = %s", pwd, moduleName, opt.Cfg.New.Template)
ch <- &loading.Loading{Content: "开始下载模板: " + opt.Cfg.New.Template, Type: loading.TypeProcessing} print("开始下载模板: "+opt.Cfg.New.Template, loading.TypeProcessing)
repo := opt.Cfg.New.Template repo := opt.Cfg.New.Template
if v, ok := opt.TemplateMap[repo]; ok { if v, ok := opt.TemplateMap[repo]; ok {
@ -71,17 +68,16 @@ func doNew(cmd *cobra.Command, args []string) error {
} }
if err = tool.Clone(pwd, repo); err != nil { if err = tool.Clone(pwd, repo); err != nil {
ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError}
return err return err
} }
ch <- &loading.Loading{Content: "下载模板完成: " + opt.Cfg.New.Template, Type: loading.TypeSuccess} print("下载模板完成: "+opt.Cfg.New.Template, loading.TypeSuccess)
if err = os.RemoveAll(path.Join(pwd, ".git")); err != nil { if err = os.RemoveAll(path.Join(pwd, ".git")); err != nil {
ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeWarning} print(err.Error(), loading.TypeWarning)
} }
ch <- &loading.Loading{Content: "开始初始化项目: " + args[0], Type: loading.TypeProcessing} print("开始初始化项目: "+args[0], loading.TypeProcessing)
if err = filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error { if err = filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
@ -95,8 +91,8 @@ func doNew(cmd *cobra.Command, args []string) error {
if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "go.mod") { if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "go.mod") {
var content []byte var content []byte
if content, err = os.ReadFile(path); err != nil { if content, err = os.ReadFile(path); err != nil {
ch <- &loading.Loading{Content: "初始化文件失败: " + err.Error(), Type: loading.TypeWarning} print("初始化文件失败: "+err.Error(), loading.TypeWarning)
ch <- &loading.Loading{Content: "开始初始化项目: " + args[0], Type: loading.TypeProcessing} print("开始初始化项目: "+args[0], loading.TypeProcessing)
return nil return nil
} }
@ -117,7 +113,6 @@ func doNew(cmd *cobra.Command, args []string) error {
return nil return nil
}); err != nil { }); err != nil {
ch <- &loading.Loading{Content: "初始化文件失败: " + err.Error(), Type: loading.TypeWarning}
return err return err
} }
@ -128,13 +123,13 @@ func doNew(cmd *cobra.Command, args []string) error {
if render, err = template.New(base).Parse(opt.README); err != nil { if render, err = template.New(base).Parse(opt.README); err != nil {
log.Debug("cmd.new: new text template err, err = %s", err.Error()) log.Debug("cmd.new: new text template err, err = %s", err.Error())
ch <- &loading.Loading{Content: "生成 readme 失败", Type: loading.TypeWarning} print("生成 readme 失败", loading.TypeWarning)
goto END goto END
} }
if rf, err = os.OpenFile(path.Join(pwd, "readme.md"), os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o644); err != nil { if rf, err = os.OpenFile(path.Join(pwd, "readme.md"), os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o644); err != nil {
log.Debug("cmd.new: new readme file err, err = %s", err.Error()) log.Debug("cmd.new: new readme file err, err = %s", err.Error())
ch <- &loading.Loading{Content: "生成 readme 失败", Type: loading.TypeWarning} print("生成 readme 失败", loading.TypeWarning)
goto END goto END
} }
defer rf.Close() defer rf.Close()
@ -143,11 +138,12 @@ func doNew(cmd *cobra.Command, args []string) error {
"project_name": base, "project_name": base,
}); err != nil { }); err != nil {
log.Debug("cmd.new: template execute err, err = %s", err.Error()) log.Debug("cmd.new: template execute err, err = %s", err.Error())
ch <- &loading.Loading{Content: "生成 readme 失败", Type: loading.TypeWarning} print("生成 readme 失败", loading.TypeWarning)
} }
END: END:
ch <- &loading.Loading{Content: fmt.Sprintf("项目: %s 初始化成功", args[0]), Type: loading.TypeSuccess} print(fmt.Sprintf("项目: %s 初始化成功", args[0]), loading.TypeSuccess)
return nil return nil
})
} }

View File

@ -9,16 +9,19 @@ import (
"time" "time"
resty "github.com/go-resty/resty/v2" resty "github.com/go-resty/resty/v2"
"github.com/loveuer/nf/nft/loading"
"github.com/loveuer/nf/nft/log" "github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/nfctl/internal/opt" "github.com/loveuer/nf/nft/nfctl/internal/opt"
"github.com/loveuer/nf/nft/nfctl/pkg/loading" "github.com/loveuer/nf/nft/tool"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var updateCmd = &cobra.Command{ var updateCmd = &cobra.Command{
Use: "update", Use: "update",
Short: "update nfctl self", Short: "update nfctl self",
RunE: func(cmd *cobra.Command, args []string) error { return nil }, RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
} }
func initUpdate() *cobra.Command { func initUpdate() *cobra.Command {
@ -26,15 +29,9 @@ func initUpdate() *cobra.Command {
} }
func doUpdate(ctx context.Context) (err error) { func doUpdate(ctx context.Context) (err error) {
ch := make(chan *loading.Loading) return loading.Do(tool.TimeoutCtx(ctx, 30), func(ctx context.Context, print func(msg string, types ...loading.Type)) error {
defer close(ch) print("正在检查更新...")
tip := "❗ 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@master"
go func() {
loading.Print(ctx, ch)
}()
ch <- &loading.Loading{Content: "正在检查更新...", Type: loading.TypeProcessing}
tip := "❗ 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@latest"
version := "" version := ""
var rr *resty.Response var rr *resty.Response
@ -42,7 +39,6 @@ func doUpdate(ctx context.Context) (err error) {
SetContext(ctx). SetContext(ctx).
Get(opt.VersionURL); err != nil { Get(opt.VersionURL); err != nil {
err = fmt.Errorf("检查更新失败: %s\n%s", err.Error(), tip) err = fmt.Errorf("检查更新失败: %s\n%s", err.Error(), tip)
ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError}
return err return err
} }
@ -50,8 +46,7 @@ func doUpdate(ctx context.Context) (err error) {
if rr.StatusCode() != 200 { if rr.StatusCode() != 200 {
err = fmt.Errorf("检查更新失败: %s\n%s", rr.Status(), tip) err = fmt.Errorf("检查更新失败: %s\n%s", rr.Status(), tip)
ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError} return err
return
} }
reg := regexp.MustCompile(`const Version = "v\d{2}\.\d{2}\.\d{2}-r\d{1,2}"`) reg := regexp.MustCompile(`const Version = "v\d{2}\.\d{2}\.\d{2}-r\d{1,2}"`)
@ -65,22 +60,24 @@ func doUpdate(ctx context.Context) (err error) {
if version == "" { if version == "" {
err = fmt.Errorf("检查更新失败: 未找到版本信息\n%s", tip) err = fmt.Errorf("检查更新失败: 未找到版本信息\n%s", tip)
ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError}
return err return err
} }
log.Debug("cmd.update: find version = %s, now_version = %s", version, opt.Version) log.Debug("cmd.update: find version = %s, now_version = %s", version, opt.Version)
if version <= opt.Version { if version <= opt.Version {
ch <- &loading.Loading{Content: fmt.Sprintf("已是最新版本: %s", opt.Version), Type: loading.TypeSuccess} print(fmt.Sprintf("已是最新版本: %s", opt.Version), loading.TypeSuccess)
return nil return nil
} }
ch <- &loading.Loading{Content: fmt.Sprintf("发现新版本: %s", version), Type: loading.TypeInfo} print(fmt.Sprintf("发现新版本: %s", version), loading.TypeInfo)
ch <- &loading.Loading{Content: fmt.Sprintf("正在更新到 %s ...", version)} print(fmt.Sprintf("正在更新到 %s ...", version))
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
ch <- &loading.Loading{Content: "暂时无法自动更新, 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@latest", Type: loading.TypeWarning}
print("暂时无法自动更新, 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@master", loading.TypeWarning)
return nil return nil
})
} }

View File

@ -1,6 +1,6 @@
package opt package opt
const Version = "v24.12.27-r02" const Version = "v24.12.27-r03"
// const VersionURL = "https://github.com/loveuer/nf/nft/nfctl/internal/opt/version.go" // const VersionURL = "https://github.com/loveuer/nf/nft/nfctl/internal/opt/version.go"

View File

@ -1,81 +0,0 @@
package loading
import (
"context"
"fmt"
"time"
)
type Type int
const (
TypeProcessing Type = iota
TypeInfo
TypeSuccess
TypeWarning
TypeError
)
func (t Type) Symbol() string {
switch t {
case TypeSuccess:
return "✔️ "
case TypeWarning:
return "❗ "
case TypeError:
return "❌ "
case TypeInfo:
return "❕ "
default:
return ""
}
}
type Loading struct {
Content string
Type Type
}
func Print(ctx context.Context, ch <-chan *Loading) {
var (
ok bool
frames = []string{"|", "/", "-", "\\"}
start = time.Now()
loading = &Loading{}
)
for {
for _, frame := range frames {
select {
case <-ctx.Done():
return
case loading, ok = <-ch:
if !ok || loading == nil {
return
}
if loading.Content == "" {
time.Sleep(100 * time.Millisecond)
continue
}
switch loading.Type {
case TypeInfo,
TypeSuccess,
TypeWarning,
TypeError:
// Clear the loading animation
fmt.Printf("\r\033[K")
fmt.Printf("%s%s\n", loading.Type.Symbol(), loading.Content)
loading.Content = ""
}
default:
elapsed := time.Since(start).Seconds()
if loading.Content != "" {
fmt.Printf("\r\033[K%s %s (%.2fs)", frame, loading.Content, elapsed)
}
time.Sleep(100 * time.Millisecond)
}
}
}
}

View File

@ -1,27 +0,0 @@
package loading
import (
"context"
"testing"
"time"
)
func TestLoadingPrint(t *testing.T) {
ch := make(chan *Loading)
Print(context.TODO(), ch)
ch <- &Loading{Content: "处理中(1)..."}
time.Sleep(3 * time.Second)
ch <- &Loading{Content: "处理完成(1)", Type: TypeSuccess}
ch <- &Loading{Content: "处理中(2)..."}
time.Sleep(4 * time.Second)
ch <- &Loading{Content: "处理失败(2)", Type: TypeError}
time.Sleep(2 * time.Second)
close(ch)
}