fix: loading print panic

This commit is contained in:
loveuer 2024-12-26 22:55:13 -08:00
parent 8235631d4f
commit 3c1dd29d5f
7 changed files with 287 additions and 254 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)
defer func() {
fmt.Printf("\r\033[K")
}()
go func() {
var (
m *_msg
ok bool
processing string
)
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 (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"os"
@ -11,9 +12,9 @@ import (
"strings"
"text/template"
"github.com/loveuer/nf/nft/loading"
"github.com/loveuer/nf/nft/log"
"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"
)
@ -32,7 +33,7 @@ func initNew() *cobra.Command {
return newCmd
}
func doNew(cmd *cobra.Command, args []string) error {
func doNew(cmd *cobra.Command, args []string) (err error) {
if len(args) == 0 {
return errors.New("必须提供 project 名称")
}
@ -46,15 +47,11 @@ func doNew(cmd *cobra.Command, args []string) error {
return errors.New("project 名称不能以 . 开头")
}
ch := make(chan *loading.Loading)
defer close(ch)
go loading.Print(cmd.Context(), ch)
ch <- &loading.Loading{Content: "开始新建项目: " + args[0], Type: loading.TypeInfo}
return loading.Do(cmd.Context(), func(ctx context.Context, print func(msg string, types ...loading.Type)) error {
print("开始新建项目: "+args[0], loading.TypeInfo)
pwd, err := os.Getwd()
if err != nil {
ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError}
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)
ch <- &loading.Loading{Content: "开始下载模板: " + opt.Cfg.New.Template, Type: loading.TypeProcessing}
print("开始下载模板: "+opt.Cfg.New.Template, loading.TypeProcessing)
repo := opt.Cfg.New.Template
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 {
ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError}
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 {
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 != nil {
@ -95,8 +91,8 @@ func doNew(cmd *cobra.Command, args []string) error {
if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "go.mod") {
var content []byte
if content, err = os.ReadFile(path); err != nil {
ch <- &loading.Loading{Content: "初始化文件失败: " + err.Error(), Type: loading.TypeWarning}
ch <- &loading.Loading{Content: "开始初始化项目: " + args[0], Type: loading.TypeProcessing}
print("初始化文件失败: "+err.Error(), loading.TypeWarning)
print("开始初始化项目: "+args[0], loading.TypeProcessing)
return nil
}
@ -117,7 +113,6 @@ func doNew(cmd *cobra.Command, args []string) error {
return nil
}); err != nil {
ch <- &loading.Loading{Content: "初始化文件失败: " + err.Error(), Type: loading.TypeWarning}
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 {
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
}
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())
ch <- &loading.Loading{Content: "生成 readme 失败", Type: loading.TypeWarning}
print("生成 readme 失败", loading.TypeWarning)
goto END
}
defer rf.Close()
@ -143,11 +138,12 @@ func doNew(cmd *cobra.Command, args []string) error {
"project_name": base,
}); err != nil {
log.Debug("cmd.new: template execute err, err = %s", err.Error())
ch <- &loading.Loading{Content: "生成 readme 失败", Type: loading.TypeWarning}
print("生成 readme 失败", loading.TypeWarning)
}
END:
ch <- &loading.Loading{Content: fmt.Sprintf("项目: %s 初始化成功", args[0]), Type: loading.TypeSuccess}
print(fmt.Sprintf("项目: %s 初始化成功", args[0]), loading.TypeSuccess)
return nil
})
}

View File

@ -9,16 +9,19 @@ import (
"time"
resty "github.com/go-resty/resty/v2"
"github.com/loveuer/nf/nft/loading"
"github.com/loveuer/nf/nft/log"
"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"
)
var updateCmd = &cobra.Command{
Use: "update",
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 {
@ -26,15 +29,9 @@ func initUpdate() *cobra.Command {
}
func doUpdate(ctx context.Context) (err error) {
ch := make(chan *loading.Loading)
defer close(ch)
go func() {
loading.Print(ctx, ch)
}()
ch <- &loading.Loading{Content: "正在检查更新...", Type: loading.TypeProcessing}
tip := "❗ 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@latest"
return loading.Do(tool.TimeoutCtx(ctx, 30), func(ctx context.Context, print func(msg string, types ...loading.Type)) error {
print("正在检查更新...")
tip := "❗ 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@master"
version := ""
var rr *resty.Response
@ -42,7 +39,6 @@ func doUpdate(ctx context.Context) (err error) {
SetContext(ctx).
Get(opt.VersionURL); err != nil {
err = fmt.Errorf("检查更新失败: %s\n%s", err.Error(), tip)
ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError}
return err
}
@ -50,8 +46,7 @@ func doUpdate(ctx context.Context) (err error) {
if rr.StatusCode() != 200 {
err = fmt.Errorf("检查更新失败: %s\n%s", rr.Status(), tip)
ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError}
return
return err
}
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 == "" {
err = fmt.Errorf("检查更新失败: 未找到版本信息\n%s", tip)
ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError}
return err
}
log.Debug("cmd.update: find version = %s, now_version = %s", 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
}
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)
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
})
}

View File

@ -1,10 +1,10 @@
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://raw.githubusercontent.com/loveuer/nf/refs/heads/master/nft/nfctl/internal/opt/version.go"
const VersionURL = "https://gitea.loveuer.com/loveuer/nf/raw/branch/master/nft/nfctl/internal/opt/version.go"
const Banner = ` ___ __ __
___ / _/___/ /_/ /

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)
}