修复进度和潜在的锁问题
This commit is contained in:
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -14,10 +15,12 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 任务状态
|
// 任务状态
|
||||||
@@ -37,6 +40,7 @@ type ImageTask struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
OutputPath string `json:"-"` // 输出文件路径,不发送给客户端
|
OutputPath string `json:"-"` // 输出文件路径,不发送给客户端
|
||||||
|
lock sync.Mutex `json:"-"` // 镜像任务自己的锁
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载任务
|
// 下载任务
|
||||||
@@ -47,7 +51,19 @@ type DownloadTask struct {
|
|||||||
Status TaskStatus `json:"status"`
|
Status TaskStatus `json:"status"`
|
||||||
OutputFile string `json:"-"` // 最终输出文件
|
OutputFile string `json:"-"` // 最终输出文件
|
||||||
TempDir string `json:"-"` // 临时目录
|
TempDir string `json:"-"` // 临时目录
|
||||||
Lock sync.Mutex `json:"-"` // 锁,防止并发冲突
|
StatusLock sync.RWMutex `json:"-"` // 状态锁,使用读写锁提高并发性
|
||||||
|
ProgressLock sync.RWMutex `json:"-"` // 进度锁
|
||||||
|
ImageLock sync.RWMutex `json:"-"` // 镜像列表锁
|
||||||
|
updateChan chan *ProgressUpdate `json:"-"` // 进度更新通道
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进度更新消息
|
||||||
|
type ProgressUpdate struct {
|
||||||
|
TaskID string
|
||||||
|
ImageIndex int
|
||||||
|
Progress float64
|
||||||
|
Status string
|
||||||
|
Error string
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket客户端
|
// WebSocket客户端
|
||||||
@@ -170,7 +186,39 @@ func getTaskStatus(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, task)
|
// 创建任务状态副本以避免序列化过程中的锁
|
||||||
|
taskCopy := &DownloadTask{
|
||||||
|
ID: task.ID,
|
||||||
|
TotalProgress: 0,
|
||||||
|
Status: TaskStatus(""),
|
||||||
|
Images: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制状态信息
|
||||||
|
task.StatusLock.RLock()
|
||||||
|
taskCopy.Status = task.Status
|
||||||
|
task.StatusLock.RUnlock()
|
||||||
|
|
||||||
|
task.ProgressLock.RLock()
|
||||||
|
taskCopy.TotalProgress = task.TotalProgress
|
||||||
|
task.ProgressLock.RUnlock()
|
||||||
|
|
||||||
|
// 复制镜像信息
|
||||||
|
task.ImageLock.RLock()
|
||||||
|
taskCopy.Images = make([]*ImageTask, len(task.Images))
|
||||||
|
for i, img := range task.Images {
|
||||||
|
img.lock.Lock()
|
||||||
|
taskCopy.Images[i] = &ImageTask{
|
||||||
|
Image: img.Image,
|
||||||
|
Progress: img.Progress,
|
||||||
|
Status: img.Status,
|
||||||
|
Error: img.Error,
|
||||||
|
}
|
||||||
|
img.lock.Unlock()
|
||||||
|
}
|
||||||
|
task.ImageLock.RUnlock()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, taskCopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成随机任务ID
|
// 生成随机任务ID
|
||||||
@@ -180,6 +228,93 @@ func generateTaskID() string {
|
|||||||
return hex.EncodeToString(b)
|
return hex.EncodeToString(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化任务并启动进度处理器
|
||||||
|
func initTask(task *DownloadTask) {
|
||||||
|
// 创建进度更新通道
|
||||||
|
task.updateChan = make(chan *ProgressUpdate, 100)
|
||||||
|
|
||||||
|
// 启动进度处理goroutine
|
||||||
|
go func() {
|
||||||
|
for update := range task.updateChan {
|
||||||
|
if update == nil {
|
||||||
|
// 通道关闭信号
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取更新的镜像
|
||||||
|
task.ImageLock.RLock()
|
||||||
|
if update.ImageIndex < 0 || update.ImageIndex >= len(task.Images) {
|
||||||
|
task.ImageLock.RUnlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
imgTask := task.Images[update.ImageIndex]
|
||||||
|
task.ImageLock.RUnlock()
|
||||||
|
|
||||||
|
// 更新镜像进度和状态
|
||||||
|
imgTask.lock.Lock()
|
||||||
|
if update.Progress > 0 {
|
||||||
|
imgTask.Progress = update.Progress
|
||||||
|
}
|
||||||
|
if update.Status != "" {
|
||||||
|
imgTask.Status = update.Status
|
||||||
|
}
|
||||||
|
if update.Error != "" {
|
||||||
|
imgTask.Error = update.Error
|
||||||
|
}
|
||||||
|
imgTask.lock.Unlock()
|
||||||
|
|
||||||
|
// 更新总进度
|
||||||
|
updateTaskTotalProgress(task)
|
||||||
|
|
||||||
|
// 发送更新到客户端
|
||||||
|
sendTaskUpdate(task)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送进度更新
|
||||||
|
func sendProgressUpdate(task *DownloadTask, index int, progress float64, status string, errorMsg string) {
|
||||||
|
select {
|
||||||
|
case task.updateChan <- &ProgressUpdate{
|
||||||
|
TaskID: task.ID,
|
||||||
|
ImageIndex: index,
|
||||||
|
Progress: progress,
|
||||||
|
Status: status,
|
||||||
|
Error: errorMsg,
|
||||||
|
}:
|
||||||
|
// 成功发送
|
||||||
|
default:
|
||||||
|
// 通道已满,丢弃更新
|
||||||
|
fmt.Printf("Warning: Update channel for task %s is full\n", task.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新总进度 - 不需要外部锁
|
||||||
|
func updateTaskTotalProgress(task *DownloadTask) {
|
||||||
|
task.ProgressLock.Lock()
|
||||||
|
defer task.ProgressLock.Unlock()
|
||||||
|
|
||||||
|
totalProgress := 0.0
|
||||||
|
|
||||||
|
task.ImageLock.RLock()
|
||||||
|
imageCount := len(task.Images)
|
||||||
|
task.ImageLock.RUnlock()
|
||||||
|
|
||||||
|
if imageCount == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task.ImageLock.RLock()
|
||||||
|
for _, img := range task.Images {
|
||||||
|
img.lock.Lock()
|
||||||
|
totalProgress += img.Progress
|
||||||
|
img.lock.Unlock()
|
||||||
|
}
|
||||||
|
task.ImageLock.RUnlock()
|
||||||
|
|
||||||
|
task.TotalProgress = totalProgress / float64(imageCount)
|
||||||
|
}
|
||||||
|
|
||||||
// 处理下载请求
|
// 处理下载请求
|
||||||
func handleDownload(c *gin.Context) {
|
func handleDownload(c *gin.Context) {
|
||||||
type DownloadRequest struct {
|
type DownloadRequest struct {
|
||||||
@@ -221,6 +356,9 @@ func handleDownload(c *gin.Context) {
|
|||||||
TempDir: tempDir,
|
TempDir: tempDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化任务通道和处理器
|
||||||
|
initTask(task)
|
||||||
|
|
||||||
// 保存任务
|
// 保存任务
|
||||||
tasksLock.Lock()
|
tasksLock.Lock()
|
||||||
tasks[taskID] = task
|
tasks[taskID] = task
|
||||||
@@ -229,6 +367,8 @@ func handleDownload(c *gin.Context) {
|
|||||||
// 异步处理下载
|
// 异步处理下载
|
||||||
go func() {
|
go func() {
|
||||||
processDownloadTask(task, req.Platform)
|
processDownloadTask(task, req.Platform)
|
||||||
|
// 任务完成后关闭更新通道
|
||||||
|
close(task.updateChan)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -239,86 +379,146 @@ func handleDownload(c *gin.Context) {
|
|||||||
|
|
||||||
// 处理下载任务
|
// 处理下载任务
|
||||||
func processDownloadTask(task *DownloadTask, platform string) {
|
func processDownloadTask(task *DownloadTask, platform string) {
|
||||||
task.Lock.Lock()
|
// 设置任务状态为运行中
|
||||||
|
task.StatusLock.Lock()
|
||||||
task.Status = StatusRunning
|
task.Status = StatusRunning
|
||||||
task.Lock.Unlock()
|
task.StatusLock.Unlock()
|
||||||
|
|
||||||
// 通知客户端任务已开始
|
// 通知客户端任务已开始
|
||||||
sendTaskUpdate(task)
|
sendTaskUpdate(task)
|
||||||
|
|
||||||
// 使用WaitGroup等待所有镜像下载完成
|
// 创建错误组,用于管理所有下载goroutine
|
||||||
var wg sync.WaitGroup
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
wg.Add(len(task.Images))
|
defer cancel() // 确保资源被释放
|
||||||
|
|
||||||
// 使用并发下载镜像
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
for i, imgTask := range task.Images {
|
|
||||||
go func(idx int, imgTask *ImageTask) {
|
// 启动并发下载
|
||||||
defer wg.Done()
|
task.ImageLock.RLock()
|
||||||
downloadImage(task, idx, imgTask, platform)
|
imageCount := len(task.Images)
|
||||||
}(i, imgTask)
|
task.ImageLock.RUnlock()
|
||||||
|
|
||||||
|
// 创建工作池限制并发数
|
||||||
|
const maxConcurrent = 5
|
||||||
|
semaphore := make(chan struct{}, maxConcurrent)
|
||||||
|
|
||||||
|
// 添加下载任务
|
||||||
|
for i := 0; i < imageCount; i++ {
|
||||||
|
index := i // 捕获循环变量
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
// 获取信号量,限制并发
|
||||||
|
semaphore <- struct{}{}
|
||||||
|
defer func() { <-semaphore }()
|
||||||
|
|
||||||
|
task.ImageLock.RLock()
|
||||||
|
imgTask := task.Images[index]
|
||||||
|
task.ImageLock.RUnlock()
|
||||||
|
|
||||||
|
// 下载镜像
|
||||||
|
err := downloadImageWithContext(ctx, task, index, imgTask, platform)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("镜像 %s 下载失败: %v\n", imgTask.Image, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待所有下载完成
|
// 等待所有下载完成
|
||||||
wg.Wait()
|
err := g.Wait()
|
||||||
|
|
||||||
|
// 检查是否有错误发生
|
||||||
|
if err != nil {
|
||||||
|
task.StatusLock.Lock()
|
||||||
|
task.Status = StatusFailed
|
||||||
|
task.StatusLock.Unlock()
|
||||||
|
sendTaskUpdate(task)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 判断是单个tar还是需要打包
|
// 判断是单个tar还是需要打包
|
||||||
var finalFilePath string
|
var finalFilePath string
|
||||||
var err error
|
|
||||||
|
|
||||||
task.Lock.Lock()
|
task.StatusLock.Lock()
|
||||||
|
|
||||||
|
// 检查是否所有镜像都下载成功
|
||||||
allSuccess := true
|
allSuccess := true
|
||||||
|
task.ImageLock.RLock()
|
||||||
for _, img := range task.Images {
|
for _, img := range task.Images {
|
||||||
if img.Status == string(StatusFailed) {
|
img.lock.Lock()
|
||||||
|
if img.Status != string(StatusCompleted) {
|
||||||
allSuccess = false
|
allSuccess = false
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
img.lock.Unlock()
|
||||||
}
|
}
|
||||||
|
task.ImageLock.RUnlock()
|
||||||
|
|
||||||
if !allSuccess {
|
if !allSuccess {
|
||||||
task.Status = StatusFailed
|
task.Status = StatusFailed
|
||||||
task.Lock.Unlock()
|
task.StatusLock.Unlock()
|
||||||
sendTaskUpdate(task)
|
sendTaskUpdate(task)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果只有一个文件,直接使用它
|
// 如果只有一个文件,直接使用它
|
||||||
if len(task.Images) == 1 && task.Images[0].Status == string(StatusCompleted) {
|
task.ImageLock.RLock()
|
||||||
finalFilePath = task.Images[0].OutputPath
|
if imageCount == 1 {
|
||||||
// 重命名为更友好的名称
|
imgTask := task.Images[0]
|
||||||
imageName := strings.ReplaceAll(task.Images[0].Image, "/", "_")
|
imgTask.lock.Lock()
|
||||||
imageName = strings.ReplaceAll(imageName, ":", "_")
|
if imgTask.Status == string(StatusCompleted) {
|
||||||
newPath := filepath.Join(task.TempDir, imageName+".tar")
|
finalFilePath = imgTask.OutputPath
|
||||||
os.Rename(finalFilePath, newPath)
|
// 重命名为更友好的名称
|
||||||
finalFilePath = newPath
|
imageName := strings.ReplaceAll(imgTask.Image, "/", "_")
|
||||||
|
imageName = strings.ReplaceAll(imageName, ":", "_")
|
||||||
|
newPath := filepath.Join(task.TempDir, imageName+".tar")
|
||||||
|
os.Rename(finalFilePath, newPath)
|
||||||
|
finalFilePath = newPath
|
||||||
|
}
|
||||||
|
imgTask.lock.Unlock()
|
||||||
} else {
|
} else {
|
||||||
// 多个文件打包成zip
|
// 多个文件打包成zip
|
||||||
finalFilePath, err = createZipArchive(task)
|
task.ImageLock.RUnlock()
|
||||||
if err != nil {
|
var zipErr error
|
||||||
|
finalFilePath, zipErr = createZipArchive(task)
|
||||||
|
if zipErr != nil {
|
||||||
task.Status = StatusFailed
|
task.Status = StatusFailed
|
||||||
task.Lock.Unlock()
|
task.StatusLock.Unlock()
|
||||||
sendTaskUpdate(task)
|
sendTaskUpdate(task)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if imageCount == 1 {
|
||||||
|
task.ImageLock.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
task.OutputFile = finalFilePath
|
task.OutputFile = finalFilePath
|
||||||
task.Status = StatusCompleted
|
task.Status = StatusCompleted
|
||||||
|
|
||||||
|
// 设置总进度为100%
|
||||||
|
task.ProgressLock.Lock()
|
||||||
task.TotalProgress = 100
|
task.TotalProgress = 100
|
||||||
task.Lock.Unlock()
|
task.ProgressLock.Unlock()
|
||||||
|
|
||||||
|
task.StatusLock.Unlock()
|
||||||
|
|
||||||
// 发送最终状态更新
|
// 发送最终状态更新
|
||||||
sendTaskUpdate(task)
|
sendTaskUpdate(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载单个镜像
|
// 下载单个镜像(带上下文控制)
|
||||||
func downloadImage(task *DownloadTask, index int, imgTask *ImageTask, platform string) {
|
func downloadImageWithContext(ctx context.Context, task *DownloadTask, index int, imgTask *ImageTask, platform string) error {
|
||||||
imgTask.Status = string(StatusRunning)
|
// 更新状态为运行中
|
||||||
sendImageUpdate(task, index)
|
sendProgressUpdate(task, index, 0, string(StatusRunning), "")
|
||||||
|
|
||||||
// 创建输出文件名
|
// 创建输出文件名
|
||||||
outputFileName := fmt.Sprintf("image_%d.tar", index)
|
outputFileName := fmt.Sprintf("image_%d.tar", index)
|
||||||
outputPath := filepath.Join(task.TempDir, outputFileName)
|
outputPath := filepath.Join(task.TempDir, outputFileName)
|
||||||
|
|
||||||
|
imgTask.lock.Lock()
|
||||||
imgTask.OutputPath = outputPath
|
imgTask.OutputPath = outputPath
|
||||||
|
imgTask.lock.Unlock()
|
||||||
|
|
||||||
// 创建skopeo命令
|
// 创建skopeo命令
|
||||||
platformArg := ""
|
platformArg := ""
|
||||||
@@ -345,158 +545,180 @@ func downloadImage(task *DownloadTask, index int, imgTask *ImageTask, platform s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建命令
|
// 构建命令
|
||||||
cmd := fmt.Sprintf("skopeo copy %s docker://%s docker-archive:%s",
|
cmdStr := fmt.Sprintf("skopeo copy %s docker://%s docker-archive:%s",
|
||||||
platformArg, imgTask.Image, outputPath)
|
platformArg, imgTask.Image, outputPath)
|
||||||
|
|
||||||
fmt.Printf("执行命令: %s\n", cmd)
|
fmt.Printf("执行命令: %s\n", cmdStr)
|
||||||
|
|
||||||
// 执行命令
|
// 创建可取消的命令
|
||||||
command := exec.Command("sh", "-c", cmd)
|
cmd := exec.CommandContext(ctx, "sh", "-c", cmdStr)
|
||||||
|
|
||||||
// 获取命令输出
|
// 获取命令输出
|
||||||
stderr, err := command.StderrPipe()
|
stderr, err := cmd.StderrPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
imgTask.Status = string(StatusFailed)
|
errMsg := fmt.Sprintf("无法创建输出管道: %v", err)
|
||||||
imgTask.Error = fmt.Sprintf("无法创建输出管道: %v", err)
|
sendProgressUpdate(task, index, 0, string(StatusFailed), errMsg)
|
||||||
sendImageUpdate(task, index)
|
return fmt.Errorf(errMsg)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, err := command.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
imgTask.Status = string(StatusFailed)
|
errMsg := fmt.Sprintf("无法创建标准输出管道: %v", err)
|
||||||
imgTask.Error = fmt.Sprintf("无法创建标准输出管道: %v", err)
|
sendProgressUpdate(task, index, 0, string(StatusFailed), errMsg)
|
||||||
sendImageUpdate(task, index)
|
return fmt.Errorf(errMsg)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := command.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
imgTask.Status = string(StatusFailed)
|
errMsg := fmt.Sprintf("启动命令失败: %v", err)
|
||||||
imgTask.Error = fmt.Sprintf("启动命令失败: %v", err)
|
sendProgressUpdate(task, index, 0, string(StatusFailed), errMsg)
|
||||||
sendImageUpdate(task, index)
|
return fmt.Errorf(errMsg)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟逐步进度增加,确保用户体验更好
|
// 使用进度通道传递进度信息
|
||||||
|
progressChan := make(chan float64, 10)
|
||||||
|
outputChan := make(chan string, 20)
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
// 初始进度
|
||||||
|
sendProgressUpdate(task, index, 5, "", "")
|
||||||
|
|
||||||
|
// 进度聚合器
|
||||||
go func() {
|
go func() {
|
||||||
// 每500ms检查一次进度,如果进度没有变化,则稍微增加一点
|
// 镜像获取阶段的进度标记
|
||||||
ticker := time.NewTicker(500 * time.Millisecond)
|
downloadStages := map[string]float64{
|
||||||
|
"Getting image source signatures": 10,
|
||||||
|
"Copying blob": 30,
|
||||||
|
"Copying config": 70,
|
||||||
|
"Writing manifest": 90,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进度增长的定时器
|
||||||
|
ticker := time.NewTicker(100 * time.Millisecond)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
lastProgress := 0.0
|
lastProgress := 5.0
|
||||||
stagnantCount := 0
|
stagnantTime := 0
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// 上下文取消
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-done:
|
||||||
|
// 命令完成
|
||||||
|
return
|
||||||
|
|
||||||
|
case output := <-outputChan:
|
||||||
|
// 解析输出更新进度
|
||||||
|
for marker, progress := range downloadStages {
|
||||||
|
if strings.Contains(output, marker) && progress > lastProgress {
|
||||||
|
lastProgress = progress
|
||||||
|
sendProgressUpdate(task, index, progress, "", "")
|
||||||
|
stagnantTime = 0
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析百分比
|
||||||
|
if strings.Contains(output, "%") {
|
||||||
|
parts := strings.Split(output, "%")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
numStr := strings.TrimSpace(parts[0])
|
||||||
|
fields := strings.Fields(numStr)
|
||||||
|
if len(fields) > 0 {
|
||||||
|
lastField := fields[len(fields)-1]
|
||||||
|
parsedProgress := 0.0
|
||||||
|
_, err := fmt.Sscanf(lastField, "%f", &parsedProgress)
|
||||||
|
if err == nil && parsedProgress > 0 && parsedProgress <= 100 {
|
||||||
|
// 根据当前阶段调整进度值
|
||||||
|
var adjustedProgress float64
|
||||||
|
if lastProgress < 30 {
|
||||||
|
// Copying blob阶段,进度在10-30%之间
|
||||||
|
adjustedProgress = 10 + (parsedProgress / 100) * 20
|
||||||
|
} else if lastProgress < 70 {
|
||||||
|
// Copying config阶段,进度在30-70%之间
|
||||||
|
adjustedProgress = 30 + (parsedProgress / 100) * 40
|
||||||
|
} else if lastProgress < 90 {
|
||||||
|
// Writing manifest阶段,进度在70-90%之间
|
||||||
|
adjustedProgress = 70 + (parsedProgress / 100) * 20
|
||||||
|
}
|
||||||
|
|
||||||
|
if adjustedProgress > lastProgress {
|
||||||
|
lastProgress = adjustedProgress
|
||||||
|
sendProgressUpdate(task, index, adjustedProgress, "", "")
|
||||||
|
stagnantTime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// 检查命令是否还在运行
|
// 如果进度长时间无变化,缓慢增加
|
||||||
if command.ProcessState != nil && command.ProcessState.Exited() {
|
stagnantTime += 100 // 100ms
|
||||||
return
|
if stagnantTime >= 10000 && lastProgress < 95 { // 10秒无变化
|
||||||
}
|
// 每10秒增加5%进度,确保不超过95%
|
||||||
|
newProgress := lastProgress + 5
|
||||||
// 如果进度停滞,小幅增加进度,提高用户体验
|
if newProgress > 95 {
|
||||||
task.Lock.Lock()
|
newProgress = 95
|
||||||
currentProgress := imgTask.Progress
|
|
||||||
if currentProgress == lastProgress {
|
|
||||||
stagnantCount++
|
|
||||||
if stagnantCount > 5 && currentProgress < 90 { // 连续5次无变化且未接近完成
|
|
||||||
// 缓慢增加进度,但不超过95%
|
|
||||||
newProgress := currentProgress + 0.5
|
|
||||||
if newProgress > 95 {
|
|
||||||
newProgress = 95
|
|
||||||
}
|
|
||||||
imgTask.Progress = newProgress
|
|
||||||
updateTaskProgress(task)
|
|
||||||
sendImageUpdate(task, index)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stagnantCount = 0
|
|
||||||
lastProgress = currentProgress
|
|
||||||
}
|
|
||||||
task.Lock.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 读取stderr以获取进度信息
|
|
||||||
go func() {
|
|
||||||
scanner := bufio.NewScanner(stderr)
|
|
||||||
for scanner.Scan() {
|
|
||||||
output := scanner.Text()
|
|
||||||
fmt.Printf("镜像 %s 进度输出: %s\n", imgTask.Image, output)
|
|
||||||
|
|
||||||
// 解析进度信息
|
|
||||||
if strings.Contains(output, "%") {
|
|
||||||
parts := strings.Split(output, "%")
|
|
||||||
if len(parts) > 0 {
|
|
||||||
numStr := strings.TrimSpace(parts[0])
|
|
||||||
numStr = strings.TrimLeft(numStr, "Copying blob ")
|
|
||||||
numStr = strings.TrimLeft(numStr, "Copying config ")
|
|
||||||
numStr = strings.TrimRight(numStr, " / ")
|
|
||||||
numStr = strings.TrimSpace(numStr)
|
|
||||||
|
|
||||||
// 尝试提取最后一个数字作为进度
|
|
||||||
fields := strings.Fields(numStr)
|
|
||||||
if len(fields) > 0 {
|
|
||||||
lastField := fields[len(fields)-1]
|
|
||||||
progress := 0.0
|
|
||||||
fmt.Sscanf(lastField, "%f", &progress)
|
|
||||||
if progress > 0 && progress <= 100 {
|
|
||||||
task.Lock.Lock()
|
|
||||||
imgTask.Progress = progress
|
|
||||||
task.Lock.Unlock()
|
|
||||||
updateTaskProgress(task)
|
|
||||||
sendImageUpdate(task, index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
lastProgress = newProgress
|
||||||
|
sendProgressUpdate(task, index, newProgress, "", "")
|
||||||
|
stagnantTime = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// 读取stdout
|
// 读取标准输出
|
||||||
go func() {
|
go func() {
|
||||||
scanner := bufio.NewScanner(stdout)
|
scanner := bufio.NewScanner(stdout)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
output := scanner.Text()
|
output := scanner.Text()
|
||||||
fmt.Printf("镜像 %s 标准输出: %s\n", imgTask.Image, output)
|
fmt.Printf("镜像 %s 标准输出: %s\n", imgTask.Image, output)
|
||||||
|
select {
|
||||||
|
case outputChan <- output:
|
||||||
|
default:
|
||||||
|
// 通道已满,丢弃
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := command.Wait(); err != nil {
|
// 读取错误输出
|
||||||
imgTask.Status = string(StatusFailed)
|
go func() {
|
||||||
imgTask.Error = fmt.Sprintf("命令执行失败: %v", err)
|
scanner := bufio.NewScanner(stderr)
|
||||||
sendImageUpdate(task, index)
|
for scanner.Scan() {
|
||||||
return
|
output := scanner.Text()
|
||||||
|
fmt.Printf("镜像 %s 错误输出: %s\n", imgTask.Image, output)
|
||||||
|
select {
|
||||||
|
case outputChan <- output:
|
||||||
|
default:
|
||||||
|
// 通道已满,丢弃
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 等待命令完成
|
||||||
|
cmdErr := cmd.Wait()
|
||||||
|
close(done) // 通知进度聚合器退出
|
||||||
|
|
||||||
|
if cmdErr != nil {
|
||||||
|
errMsg := fmt.Sprintf("命令执行失败: %v", cmdErr)
|
||||||
|
sendProgressUpdate(task, index, 0, string(StatusFailed), errMsg)
|
||||||
|
return fmt.Errorf(errMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查文件是否成功创建
|
// 检查文件是否成功创建
|
||||||
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
|
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
|
||||||
imgTask.Status = string(StatusFailed)
|
errMsg := "文件未成功创建"
|
||||||
imgTask.Error = "文件未成功创建"
|
sendProgressUpdate(task, index, 0, string(StatusFailed), errMsg)
|
||||||
sendImageUpdate(task, index)
|
return fmt.Errorf(errMsg)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新状态为已完成
|
// 更新状态为已完成
|
||||||
task.Lock.Lock()
|
sendProgressUpdate(task, index, 100, string(StatusCompleted), "")
|
||||||
imgTask.Status = string(StatusCompleted)
|
return nil
|
||||||
imgTask.Progress = 100
|
|
||||||
task.Lock.Unlock()
|
|
||||||
updateTaskProgress(task)
|
|
||||||
sendImageUpdate(task, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新任务总进度
|
|
||||||
func updateTaskProgress(task *DownloadTask) {
|
|
||||||
task.Lock.Lock()
|
|
||||||
defer task.Lock.Unlock()
|
|
||||||
|
|
||||||
totalProgress := 0.0
|
|
||||||
for _, img := range task.Images {
|
|
||||||
totalProgress += img.Progress
|
|
||||||
}
|
|
||||||
task.TotalProgress = totalProgress / float64(len(task.Images))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建ZIP归档
|
// 创建ZIP归档
|
||||||
@@ -504,39 +726,50 @@ func createZipArchive(task *DownloadTask) (string, error) {
|
|||||||
zipFilePath := filepath.Join(task.TempDir, "images.zip")
|
zipFilePath := filepath.Join(task.TempDir, "images.zip")
|
||||||
zipFile, err := os.Create(zipFilePath)
|
zipFile, err := os.Create(zipFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", fmt.Errorf("创建ZIP文件失败: %w", err)
|
||||||
}
|
}
|
||||||
defer zipFile.Close()
|
defer zipFile.Close()
|
||||||
|
|
||||||
zipWriter := zip.NewWriter(zipFile)
|
zipWriter := zip.NewWriter(zipFile)
|
||||||
defer zipWriter.Close()
|
defer zipWriter.Close()
|
||||||
|
|
||||||
for _, img := range task.Images {
|
task.ImageLock.RLock()
|
||||||
if img.Status != string(StatusCompleted) || img.OutputPath == "" {
|
images := make([]*ImageTask, len(task.Images))
|
||||||
|
copy(images, task.Images) // 创建副本避免长时间持有锁
|
||||||
|
task.ImageLock.RUnlock()
|
||||||
|
|
||||||
|
for _, img := range images {
|
||||||
|
img.lock.Lock()
|
||||||
|
status := img.Status
|
||||||
|
outputPath := img.OutputPath
|
||||||
|
image := img.Image
|
||||||
|
img.lock.Unlock()
|
||||||
|
|
||||||
|
if status != string(StatusCompleted) || outputPath == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建ZIP条目
|
// 创建ZIP条目
|
||||||
imgFile, err := os.Open(img.OutputPath)
|
imgFile, err := os.Open(outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", fmt.Errorf("无法打开镜像文件 %s: %w", outputPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用镜像名作为文件名
|
// 使用镜像名作为文件名
|
||||||
imageName := strings.ReplaceAll(img.Image, "/", "_")
|
imageName := strings.ReplaceAll(image, "/", "_")
|
||||||
imageName = strings.ReplaceAll(imageName, ":", "_")
|
imageName = strings.ReplaceAll(imageName, ":", "_")
|
||||||
fileName := imageName + ".tar"
|
fileName := imageName + ".tar"
|
||||||
|
|
||||||
fileInfo, err := imgFile.Stat()
|
fileInfo, err := imgFile.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
imgFile.Close()
|
imgFile.Close()
|
||||||
return "", err
|
return "", fmt.Errorf("无法获取文件信息: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
header, err := zip.FileInfoHeader(fileInfo)
|
header, err := zip.FileInfoHeader(fileInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
imgFile.Close()
|
imgFile.Close()
|
||||||
return "", err
|
return "", fmt.Errorf("创建ZIP头信息失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
header.Name = fileName
|
header.Name = fileName
|
||||||
@@ -545,13 +778,13 @@ func createZipArchive(task *DownloadTask) (string, error) {
|
|||||||
writer, err := zipWriter.CreateHeader(header)
|
writer, err := zipWriter.CreateHeader(header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
imgFile.Close()
|
imgFile.Close()
|
||||||
return "", err
|
return "", fmt.Errorf("添加文件到ZIP失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.Copy(writer, imgFile)
|
_, err = io.Copy(writer, imgFile)
|
||||||
imgFile.Close()
|
imgFile.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", fmt.Errorf("写入ZIP文件失败: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,7 +793,40 @@ func createZipArchive(task *DownloadTask) (string, error) {
|
|||||||
|
|
||||||
// 发送任务更新到WebSocket
|
// 发送任务更新到WebSocket
|
||||||
func sendTaskUpdate(task *DownloadTask) {
|
func sendTaskUpdate(task *DownloadTask) {
|
||||||
taskJSON, err := json.Marshal(task)
|
// 复制任务状态避免序列化时锁定
|
||||||
|
taskCopy := &DownloadTask{
|
||||||
|
ID: task.ID,
|
||||||
|
TotalProgress: 0,
|
||||||
|
Status: TaskStatus(""),
|
||||||
|
Images: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制状态信息
|
||||||
|
task.StatusLock.RLock()
|
||||||
|
taskCopy.Status = task.Status
|
||||||
|
task.StatusLock.RUnlock()
|
||||||
|
|
||||||
|
task.ProgressLock.RLock()
|
||||||
|
taskCopy.TotalProgress = task.TotalProgress
|
||||||
|
task.ProgressLock.RUnlock()
|
||||||
|
|
||||||
|
// 复制镜像信息
|
||||||
|
task.ImageLock.RLock()
|
||||||
|
taskCopy.Images = make([]*ImageTask, len(task.Images))
|
||||||
|
for i, img := range task.Images {
|
||||||
|
img.lock.Lock()
|
||||||
|
taskCopy.Images[i] = &ImageTask{
|
||||||
|
Image: img.Image,
|
||||||
|
Progress: img.Progress,
|
||||||
|
Status: img.Status,
|
||||||
|
Error: img.Error,
|
||||||
|
}
|
||||||
|
img.lock.Unlock()
|
||||||
|
}
|
||||||
|
task.ImageLock.RUnlock()
|
||||||
|
|
||||||
|
// 序列化并发送
|
||||||
|
taskJSON, err := json.Marshal(taskCopy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("序列化任务失败: %v\n", err)
|
fmt.Printf("序列化任务失败: %v\n", err)
|
||||||
return
|
return
|
||||||
@@ -573,13 +839,14 @@ func sendTaskUpdate(task *DownloadTask) {
|
|||||||
if exists {
|
if exists {
|
||||||
select {
|
select {
|
||||||
case client.Send <- taskJSON:
|
case client.Send <- taskJSON:
|
||||||
|
// 成功发送
|
||||||
default:
|
default:
|
||||||
// 通道已满或关闭,忽略
|
// 通道已满或关闭,忽略
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送单个镜像更新
|
// 发送单个镜像更新 - 保持兼容性
|
||||||
func sendImageUpdate(task *DownloadTask, imageIndex int) {
|
func sendImageUpdate(task *DownloadTask, imageIndex int) {
|
||||||
sendTaskUpdate(task)
|
sendTaskUpdate(task)
|
||||||
}
|
}
|
||||||
@@ -657,8 +924,8 @@ func cleanupTempFiles() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果文件或目录超过24小时未修改,则删除
|
// 如果文件或目录超过2小时未修改,则删除
|
||||||
if time.Since(info.ModTime()) > 24*time.Hour {
|
if time.Since(info.ModTime()) > 2*time.Hour {
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
os.RemoveAll(path)
|
os.RemoveAll(path)
|
||||||
return filepath.SkipDir
|
return filepath.SkipDir
|
||||||
|
|||||||
Reference in New Issue
Block a user