fix: loading print panic
This commit is contained in:
		
							
								
								
									
										123
									
								
								nft/loading/loading.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								nft/loading/loading.go
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								nft/loading/loading_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								nft/loading/loading_test.go
									
									
									
									
									
										Normal 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
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -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,108 +47,103 @@ func doNew(cmd *cobra.Command, args []string) error {
 | 
			
		||||
		return errors.New("project 名称不能以 . 开头")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ch := make(chan *loading.Loading)
 | 
			
		||||
	defer close(ch)
 | 
			
		||||
	return loading.Do(cmd.Context(), func(ctx context.Context, print func(msg string, types ...loading.Type)) error {
 | 
			
		||||
		print("开始新建项目: "+args[0], loading.TypeInfo)
 | 
			
		||||
 | 
			
		||||
	go loading.Print(cmd.Context(), ch)
 | 
			
		||||
	ch <- &loading.Loading{Content: "开始新建项目: " + args[0], Type: loading.TypeInfo}
 | 
			
		||||
 | 
			
		||||
	pwd, err := os.Getwd()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	moduleName := args[0]
 | 
			
		||||
	pwd = path.Join(filepath.ToSlash(pwd), base)
 | 
			
		||||
 | 
			
		||||
	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}
 | 
			
		||||
 | 
			
		||||
	repo := opt.Cfg.New.Template
 | 
			
		||||
	if v, ok := opt.TemplateMap[repo]; ok {
 | 
			
		||||
		repo = v
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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}
 | 
			
		||||
 | 
			
		||||
	if err = os.RemoveAll(path.Join(pwd, ".git")); err != nil {
 | 
			
		||||
		ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeWarning}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ch <- &loading.Loading{Content: "开始初始化项目: " + args[0], Type: loading.TypeProcessing}
 | 
			
		||||
 | 
			
		||||
	if err = filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
 | 
			
		||||
		pwd, err := os.Getwd()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if info.IsDir() {
 | 
			
		||||
			return nil
 | 
			
		||||
		moduleName := args[0]
 | 
			
		||||
		pwd = path.Join(filepath.ToSlash(pwd), base)
 | 
			
		||||
 | 
			
		||||
		log.Debug("cmd.new: new project, pwd = %s, name = %s, template = %s", pwd, moduleName, opt.Cfg.New.Template)
 | 
			
		||||
 | 
			
		||||
		print("开始下载模板: "+opt.Cfg.New.Template, loading.TypeProcessing)
 | 
			
		||||
 | 
			
		||||
		repo := opt.Cfg.New.Template
 | 
			
		||||
		if v, ok := opt.TemplateMap[repo]; ok {
 | 
			
		||||
			repo = v
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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}
 | 
			
		||||
		if err = tool.Clone(pwd, repo); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		print("下载模板完成: "+opt.Cfg.New.Template, loading.TypeSuccess)
 | 
			
		||||
 | 
			
		||||
		if err = os.RemoveAll(path.Join(pwd, ".git")); err != nil {
 | 
			
		||||
			print(err.Error(), loading.TypeWarning)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		print("开始初始化项目: "+args[0], loading.TypeProcessing)
 | 
			
		||||
 | 
			
		||||
		if err = filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if info.IsDir() {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			scanner := bufio.NewScanner(bytes.NewReader(content))
 | 
			
		||||
			replaced := make([]string, 0, 16)
 | 
			
		||||
			for scanner.Scan() {
 | 
			
		||||
				line := scanner.Text()
 | 
			
		||||
				// 操作 go.mod 文件时, 忽略 toolchain 行, 以更好的兼容 go1.20
 | 
			
		||||
				if strings.HasSuffix(path, "go.mod") && strings.HasPrefix(line, "toolchain") {
 | 
			
		||||
					continue
 | 
			
		||||
			if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "go.mod") {
 | 
			
		||||
				var content []byte
 | 
			
		||||
				if content, err = os.ReadFile(path); err != nil {
 | 
			
		||||
					print("初始化文件失败: "+err.Error(), loading.TypeWarning)
 | 
			
		||||
					print("开始初始化项目: "+args[0], loading.TypeProcessing)
 | 
			
		||||
					return nil
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				scanner := bufio.NewScanner(bytes.NewReader(content))
 | 
			
		||||
				replaced := make([]string, 0, 16)
 | 
			
		||||
				for scanner.Scan() {
 | 
			
		||||
					line := scanner.Text()
 | 
			
		||||
					// 操作 go.mod 文件时, 忽略 toolchain 行, 以更好的兼容 go1.20
 | 
			
		||||
					if strings.HasSuffix(path, "go.mod") && strings.HasPrefix(line, "toolchain") {
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
					replaced = append(replaced, strings.ReplaceAll(line, opt.Cfg.New.Template, moduleName))
 | 
			
		||||
				}
 | 
			
		||||
				if err = os.WriteFile(path, []byte(strings.Join(replaced, "\n")), 0o644); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				replaced = append(replaced, strings.ReplaceAll(line, opt.Cfg.New.Template, moduleName))
 | 
			
		||||
			}
 | 
			
		||||
			if err = os.WriteFile(path, []byte(strings.Join(replaced, "\n")), 0o644); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return nil
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var (
 | 
			
		||||
			render *template.Template
 | 
			
		||||
			rf     *os.File
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		if render, err = template.New(base).Parse(opt.README); err != nil {
 | 
			
		||||
			log.Debug("cmd.new: new text template err, err = %s", err.Error())
 | 
			
		||||
			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())
 | 
			
		||||
			print("生成 readme 失败", loading.TypeWarning)
 | 
			
		||||
			goto END
 | 
			
		||||
		}
 | 
			
		||||
		defer rf.Close()
 | 
			
		||||
 | 
			
		||||
		if err = render.Execute(rf, map[string]any{
 | 
			
		||||
			"project_name": base,
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			log.Debug("cmd.new: template execute err, err = %s", err.Error())
 | 
			
		||||
			print("生成 readme 失败", loading.TypeWarning)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	END:
 | 
			
		||||
		print(fmt.Sprintf("项目: %s 初始化成功", args[0]), loading.TypeSuccess)
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		ch <- &loading.Loading{Content: "初始化文件失败: " + err.Error(), Type: loading.TypeWarning}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		render *template.Template
 | 
			
		||||
		rf     *os.File
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	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}
 | 
			
		||||
		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}
 | 
			
		||||
		goto END
 | 
			
		||||
	}
 | 
			
		||||
	defer rf.Close()
 | 
			
		||||
 | 
			
		||||
	if err = render.Execute(rf, map[string]any{
 | 
			
		||||
		"project_name": base,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		log.Debug("cmd.new: template execute err, err = %s", err.Error())
 | 
			
		||||
		ch <- &loading.Loading{Content: "生成 readme 失败", Type: loading.TypeWarning}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
END:
 | 
			
		||||
	ch <- &loading.Loading{Content: fmt.Sprintf("项目: %s 初始化成功", args[0]), Type: loading.TypeSuccess}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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,61 +29,55 @@ func initUpdate() *cobra.Command {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func doUpdate(ctx context.Context) (err error) {
 | 
			
		||||
	ch := make(chan *loading.Loading)
 | 
			
		||||
	defer close(ch)
 | 
			
		||||
	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 := ""
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		loading.Print(ctx, ch)
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	ch <- &loading.Loading{Content: "正在检查更新...", Type: loading.TypeProcessing}
 | 
			
		||||
	tip := "❗ 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@latest"
 | 
			
		||||
	version := ""
 | 
			
		||||
 | 
			
		||||
	var rr *resty.Response
 | 
			
		||||
	if rr, err = resty.New().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).R().
 | 
			
		||||
		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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug("cmd.update: url = %s, raw_response = %s", opt.VersionURL, rr.String())
 | 
			
		||||
 | 
			
		||||
	if rr.StatusCode() != 200 {
 | 
			
		||||
		err = fmt.Errorf("检查更新失败: %s\n%s", rr.Status(), tip)
 | 
			
		||||
		ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	reg := regexp.MustCompile(`const Version = "v\d{2}\.\d{2}\.\d{2}-r\d{1,2}"`)
 | 
			
		||||
	for _, line := range strings.Split(rr.String(), "\n") {
 | 
			
		||||
		if reg.MatchString(line) {
 | 
			
		||||
			version = strings.TrimSpace(strings.TrimPrefix(line, "const Version = "))
 | 
			
		||||
			version = version[1 : len(version)-1]
 | 
			
		||||
			break
 | 
			
		||||
		var rr *resty.Response
 | 
			
		||||
		if rr, err = resty.New().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).R().
 | 
			
		||||
			SetContext(ctx).
 | 
			
		||||
			Get(opt.VersionURL); err != nil {
 | 
			
		||||
			err = fmt.Errorf("检查更新失败: %s\n%s", err.Error(), tip)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if version == "" {
 | 
			
		||||
		err = fmt.Errorf("检查更新失败: 未找到版本信息\n%s", tip)
 | 
			
		||||
		ch <- &loading.Loading{Content: err.Error(), Type: loading.TypeError}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
		log.Debug("cmd.update: url = %s, raw_response = %s", opt.VersionURL, rr.String())
 | 
			
		||||
 | 
			
		||||
	log.Debug("cmd.update: find version = %s, now_version = %s", version, opt.Version)
 | 
			
		||||
		if rr.StatusCode() != 200 {
 | 
			
		||||
			err = fmt.Errorf("检查更新失败: %s\n%s", rr.Status(), tip)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		reg := regexp.MustCompile(`const Version = "v\d{2}\.\d{2}\.\d{2}-r\d{1,2}"`)
 | 
			
		||||
		for _, line := range strings.Split(rr.String(), "\n") {
 | 
			
		||||
			if reg.MatchString(line) {
 | 
			
		||||
				version = strings.TrimSpace(strings.TrimPrefix(line, "const Version = "))
 | 
			
		||||
				version = version[1 : len(version)-1]
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if version == "" {
 | 
			
		||||
			err = fmt.Errorf("检查更新失败: 未找到版本信息\n%s", tip)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Debug("cmd.update: find version = %s, now_version = %s", version, opt.Version)
 | 
			
		||||
 | 
			
		||||
		if version <= opt.Version {
 | 
			
		||||
			print(fmt.Sprintf("已是最新版本: %s", opt.Version), loading.TypeSuccess)
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		print(fmt.Sprintf("发现新版本: %s", version), loading.TypeInfo)
 | 
			
		||||
 | 
			
		||||
		print(fmt.Sprintf("正在更新到 %s ...", version))
 | 
			
		||||
 | 
			
		||||
		time.Sleep(2 * time.Second)
 | 
			
		||||
 | 
			
		||||
		print("暂时无法自动更新, 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@master", loading.TypeWarning)
 | 
			
		||||
 | 
			
		||||
	if version <= opt.Version {
 | 
			
		||||
		ch <- &loading.Loading{Content: fmt.Sprintf("已是最新版本: %s", opt.Version), Type: loading.TypeSuccess}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ch <- &loading.Loading{Content: fmt.Sprintf("发现新版本: %s", version), Type: loading.TypeInfo}
 | 
			
		||||
 | 
			
		||||
	ch <- &loading.Loading{Content: 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}
 | 
			
		||||
	return nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = `        ___    __  __
 | 
			
		||||
  ___  / _/___/ /_/ /
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user