优化状态显示
This commit is contained in:
Binary file not shown.
@@ -17,6 +17,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -342,6 +343,15 @@ func getNpmPrefix() (string, error) {
|
|||||||
return cachedNpmPrefix, nil
|
return cachedNpmPrefix, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ResetPathCache() {
|
||||||
|
cachedNpmPrefix = ""
|
||||||
|
cachedNodePath = ""
|
||||||
|
cachedGitPath = ""
|
||||||
|
prefixOnce = sync.Once{}
|
||||||
|
nodePathOnce = sync.Once{}
|
||||||
|
gitPathOnce = sync.Once{}
|
||||||
|
}
|
||||||
|
|
||||||
// ConfigureNpmMirror 配置镜像
|
// ConfigureNpmMirror 配置镜像
|
||||||
func ConfigureNpmMirror() error {
|
func ConfigureNpmMirror() error {
|
||||||
npmPath, err := getNpmPath()
|
npmPath, err := getNpmPath()
|
||||||
@@ -374,7 +384,6 @@ func downloadFile(url, dest, expectedSHA256 string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("正在下载: %s\n", url)
|
|
||||||
if err := downloadWithResume(url, partPath, size, acceptRanges); err != nil {
|
if err := downloadWithResume(url, partPath, size, acceptRanges); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -394,16 +403,16 @@ func downloadFile(url, dest, expectedSHA256 string) error {
|
|||||||
func downloadWithResume(url, dest string, size int64, acceptRanges bool) error {
|
func downloadWithResume(url, dest string, size int64, acceptRanges bool) error {
|
||||||
if size > 0 && acceptRanges {
|
if size > 0 && acceptRanges {
|
||||||
if info, err := os.Stat(dest); err == nil && info.Size() > 0 && info.Size() < size {
|
if info, err := os.Stat(dest); err == nil && info.Size() > 0 && info.Size() < size {
|
||||||
return downloadRange(url, dest, info.Size(), size-1)
|
return downloadRange(url, dest, info.Size(), size-1, size)
|
||||||
}
|
}
|
||||||
if size >= downloadConcurrentThreshold {
|
if size >= downloadConcurrentThreshold {
|
||||||
return downloadConcurrent(url, dest, size, downloadConcurrentParts)
|
return downloadConcurrent(url, dest, size, downloadConcurrentParts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return downloadRange(url, dest, 0, -1)
|
return downloadRange(url, dest, 0, -1, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadRange(url, dest string, start, end int64) error {
|
func downloadRange(url, dest string, start, end, total int64) error {
|
||||||
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY, 0644)
|
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("创建文件失败: %v", err)
|
return fmt.Errorf("创建文件失败: %v", err)
|
||||||
@@ -442,15 +451,23 @@ func downloadRange(url, dest string, start, end int64) error {
|
|||||||
return fmt.Errorf("下载失败,状态码: %d", resp.StatusCode)
|
return fmt.Errorf("下载失败,状态码: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = io.Copy(out, resp.Body); err != nil {
|
if total <= 0 && resp.ContentLength > 0 {
|
||||||
|
total = start + resp.ContentLength
|
||||||
|
}
|
||||||
|
progress := newProgressReporter(total, start)
|
||||||
|
progress.Start()
|
||||||
|
reader := &countingReader{r: resp.Body, written: progress.written}
|
||||||
|
if _, err = io.Copy(out, reader); err != nil {
|
||||||
|
progress.Stop()
|
||||||
return fmt.Errorf("写入文件失败: %v", err)
|
return fmt.Errorf("写入文件失败: %v", err)
|
||||||
}
|
}
|
||||||
|
progress.Stop()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadConcurrent(url, dest string, size int64, parts int) error {
|
func downloadConcurrent(url, dest string, size int64, parts int) error {
|
||||||
if parts < 2 {
|
if parts < 2 {
|
||||||
return downloadRange(url, dest, 0, -1)
|
return downloadRange(url, dest, 0, -1, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
@@ -464,6 +481,8 @@ func downloadConcurrent(url, dest string, size int64, parts int) error {
|
|||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
errCh := make(chan error, parts)
|
errCh := make(chan error, parts)
|
||||||
|
progress := newProgressReporter(size, 0)
|
||||||
|
progress.Start()
|
||||||
|
|
||||||
partSize := size / int64(parts)
|
partSize := size / int64(parts)
|
||||||
for i := 0; i < parts; i++ {
|
for i := 0; i < parts; i++ {
|
||||||
@@ -493,7 +512,7 @@ func downloadConcurrent(url, dest string, size int64, parts int) error {
|
|||||||
errCh <- fmt.Errorf("分段下载失败,状态码: %d", resp.StatusCode)
|
errCh <- fmt.Errorf("分段下载失败,状态码: %d", resp.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writer := &writeAtWriter{file: out, offset: s}
|
writer := &writeAtWriter{file: out, offset: s, written: progress.written}
|
||||||
if _, err := io.Copy(writer, resp.Body); err != nil {
|
if _, err := io.Copy(writer, resp.Body); err != nil {
|
||||||
errCh <- fmt.Errorf("写入文件失败: %v", err)
|
errCh <- fmt.Errorf("写入文件失败: %v", err)
|
||||||
return
|
return
|
||||||
@@ -504,6 +523,7 @@ func downloadConcurrent(url, dest string, size int64, parts int) error {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
close(errCh)
|
close(errCh)
|
||||||
out.Close()
|
out.Close()
|
||||||
|
progress.Stop()
|
||||||
|
|
||||||
for err := range errCh {
|
for err := range errCh {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -516,14 +536,89 @@ func downloadConcurrent(url, dest string, size int64, parts int) error {
|
|||||||
type writeAtWriter struct {
|
type writeAtWriter struct {
|
||||||
file *os.File
|
file *os.File
|
||||||
offset int64
|
offset int64
|
||||||
|
written *int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *writeAtWriter) Write(p []byte) (int, error) {
|
func (w *writeAtWriter) Write(p []byte) (int, error) {
|
||||||
n, err := w.file.WriteAt(p, w.offset)
|
n, err := w.file.WriteAt(p, w.offset)
|
||||||
w.offset += int64(n)
|
w.offset += int64(n)
|
||||||
|
if w.written != nil && n > 0 {
|
||||||
|
atomic.AddInt64(w.written, int64(n))
|
||||||
|
}
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type countingReader struct {
|
||||||
|
r io.Reader
|
||||||
|
written *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *countingReader) Read(p []byte) (int, error) {
|
||||||
|
n, err := c.r.Read(p)
|
||||||
|
if n > 0 && c.written != nil {
|
||||||
|
atomic.AddInt64(c.written, int64(n))
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type progressReporter struct {
|
||||||
|
total int64
|
||||||
|
written *int64
|
||||||
|
done chan struct{}
|
||||||
|
once sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProgressReporter(total, initial int64) *progressReporter {
|
||||||
|
current := initial
|
||||||
|
return &progressReporter{
|
||||||
|
total: total,
|
||||||
|
written: ¤t,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *progressReporter) Start() {
|
||||||
|
if p == nil || p.total <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.print()
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(200 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
p.print()
|
||||||
|
case <-p.done:
|
||||||
|
p.print()
|
||||||
|
fmt.Print("\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *progressReporter) Stop() {
|
||||||
|
if p == nil || p.total <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.once.Do(func() {
|
||||||
|
close(p.done)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *progressReporter) print() {
|
||||||
|
current := atomic.LoadInt64(p.written)
|
||||||
|
if current < 0 {
|
||||||
|
current = 0
|
||||||
|
}
|
||||||
|
if current > p.total {
|
||||||
|
current = p.total
|
||||||
|
}
|
||||||
|
percent := float64(current) * 100 / float64(p.total)
|
||||||
|
fmt.Printf("\r下载进度: %.2f%%", percent)
|
||||||
|
}
|
||||||
|
|
||||||
func probeRemoteFile(url string) (int64, bool, error) {
|
func probeRemoteFile(url string) (int64, bool, error) {
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
req, err := http.NewRequest("HEAD", url, nil)
|
req, err := http.NewRequest("HEAD", url, nil)
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ type Model struct {
|
|||||||
gitOk bool
|
gitOk bool
|
||||||
gatewayOk bool
|
gatewayOk bool
|
||||||
checkDone bool
|
checkDone bool
|
||||||
|
|
||||||
|
envRefreshActive bool
|
||||||
|
envRefreshAttempt int
|
||||||
|
envRefreshMax int
|
||||||
|
envRefreshExpectInstalled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息定义
|
// 消息定义
|
||||||
@@ -99,6 +104,7 @@ type progressMsg string
|
|||||||
type tickMsg time.Time
|
type tickMsg time.Time
|
||||||
|
|
||||||
type gatewayStatusMsg bool
|
type gatewayStatusMsg bool
|
||||||
|
type envRefreshMsg int
|
||||||
|
|
||||||
func InitialModel() Model {
|
func InitialModel() Model {
|
||||||
s := spinner.New()
|
s := spinner.New()
|
||||||
@@ -162,6 +168,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.gitOk = msg.gitOk
|
m.gitOk = msg.gitOk
|
||||||
m.gatewayOk = msg.gatewayRunning
|
m.gatewayOk = msg.gatewayRunning
|
||||||
m.checkDone = true
|
m.checkDone = true
|
||||||
|
if m.envRefreshActive {
|
||||||
|
expect := m.envRefreshExpectInstalled
|
||||||
|
if m.nodeOk == expect && m.gitOk == expect && m.moltbotOk == expect {
|
||||||
|
m.envRefreshActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果在动作模式下检查环境,可能需要切回主菜单
|
// 如果在动作模式下检查环境,可能需要切回主菜单
|
||||||
if m.state == StateAction && m.actionType == ActionCheckEnv {
|
if m.state == StateAction && m.actionType == ActionCheckEnv {
|
||||||
@@ -188,10 +200,29 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if m.actionType == ActionStartGateway {
|
if m.actionType == ActionStartGateway {
|
||||||
m.DidStartGateway = true
|
m.DidStartGateway = true
|
||||||
}
|
}
|
||||||
|
if m.actionType == ActionInstall || m.actionType == ActionUninstall {
|
||||||
|
m.envRefreshActive = true
|
||||||
|
m.envRefreshAttempt = 0
|
||||||
|
m.envRefreshMax = 5
|
||||||
|
m.envRefreshExpectInstalled = m.actionType == ActionInstall
|
||||||
|
return m, envRefreshCmd(0)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
m.progressMsg = fmt.Sprintf("操作失败: %v", msg.err)
|
m.progressMsg = fmt.Sprintf("操作失败: %v", msg.err)
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
case envRefreshMsg:
|
||||||
|
if !m.envRefreshActive {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
attempt := int(msg)
|
||||||
|
m.envRefreshAttempt = attempt
|
||||||
|
cmds := []tea.Cmd{checkEnvCmd}
|
||||||
|
if attempt+1 < m.envRefreshMax {
|
||||||
|
cmds = append(cmds, envRefreshCmd(attempt+1))
|
||||||
|
}
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态更新
|
// 状态更新
|
||||||
@@ -602,6 +633,7 @@ func (m Model) renderAction() string {
|
|||||||
// 指令处理
|
// 指令处理
|
||||||
|
|
||||||
func checkEnvCmd() tea.Msg {
|
func checkEnvCmd() tea.Msg {
|
||||||
|
sys.ResetPathCache()
|
||||||
nodeVer, nodeOk := sys.CheckNode()
|
nodeVer, nodeOk := sys.CheckNode()
|
||||||
moltVer, moltOk := sys.CheckMoltbot()
|
moltVer, moltOk := sys.CheckMoltbot()
|
||||||
gitVer, gitOk := sys.CheckGit()
|
gitVer, gitOk := sys.CheckGit()
|
||||||
@@ -617,6 +649,23 @@ func checkEnvCmd() tea.Msg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func envRefreshDelay(attempt int) time.Duration {
|
||||||
|
delay := 300 * time.Millisecond
|
||||||
|
for i := 0; i < attempt; i++ {
|
||||||
|
delay *= 2
|
||||||
|
if delay > 3*time.Second {
|
||||||
|
return 3 * time.Second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return delay
|
||||||
|
}
|
||||||
|
|
||||||
|
func envRefreshCmd(attempt int) tea.Cmd {
|
||||||
|
return tea.Tick(envRefreshDelay(attempt), func(t time.Time) tea.Msg {
|
||||||
|
return envRefreshMsg(attempt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func runStartGatewayCmd() tea.Msg {
|
func runStartGatewayCmd() tea.Msg {
|
||||||
sys.StartGateway()
|
sys.StartGateway()
|
||||||
time.Sleep(1 * time.Second) // 等待启动
|
time.Sleep(1 * time.Second) // 等待启动
|
||||||
|
|||||||
Reference in New Issue
Block a user