diff --git a/debian/rules b/debian/rules index 893c6e1f6..d66a71a90 100755 --- a/debian/rules +++ b/debian/rules @@ -18,7 +18,7 @@ ifneq ($(DEB_BUILD_ARCH), mips64el) endif %: - dh $@ + dh $@ --buildsystem=makefile override_dh_install: dh_install --sourcedir=debian/tmp diff --git a/src/internal/system/apt/apt.go b/src/internal/system/apt/apt.go index 83ab7da8c..6ff91f2da 100644 --- a/src/internal/system/apt/apt.go +++ b/src/internal/system/apt/apt.go @@ -8,7 +8,6 @@ import ( "bytes" "os" "os/exec" - "strconv" "strings" "syscall" @@ -288,30 +287,3 @@ func DownloadPackages(packages []string, environ map[string]string, options map[ } return tmpPath, nil } - -// In incremental update mode, returns true if all packages are cached, otherwise returns false. -func IsIncrementalUpdateCached(sourceArgs string) bool { - cmd := exec.Command("/usr/sbin/deepin-immutable-ctl", "upgrade", "check") - if sourceArgs != "" { - cmd.Env = append(os.Environ(), "DEEPIN_IMMUTABLE_UPGRADE_APT_OPTION="+sourceArgs) - } - // Need download count: xxx - output, err := cmd.Output() - if err == nil { - matchFlag := "Need download count: " - lines := strings.Split(string(output), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - index := strings.Index(line, matchFlag) - if index >= 0 { - count, err := strconv.Atoi(strings.TrimSpace(line[index+len(matchFlag):])) - if err == nil { - if count == 0 { - return true - } - } - } - } - } - return false -} diff --git a/src/internal/system/system.go b/src/internal/system/system.go index 11af920c6..e8ba780a9 100644 --- a/src/internal/system/system.go +++ b/src/internal/system/system.go @@ -13,6 +13,7 @@ import ( const VarLibDir = "/var/lib/lastore" var IntranetUpdate bool +var IncrementalUpdate bool type Status string diff --git a/src/internal/system/system_apt.go b/src/internal/system/system_apt.go index f1d4861fd..e3ce57ebc 100644 --- a/src/internal/system/system_apt.go +++ b/src/internal/system/system_apt.go @@ -10,6 +10,7 @@ package system import ( "bytes" + "encoding/json" "errors" "fmt" "os" @@ -200,8 +201,107 @@ func QueryPackageDownloadSize(updateType UpdateType, packages ...string) (float6 return *downloadSize, *allPackageSize, nil } -// QuerySourceDownloadSize 根据更新类型(仓库),获取需要的下载量,return arg0:需要下载的量;arg1:所有包的大小;arg2:error +// queryDownloadSizeViaApt queries the download size information by running apt-get dist-upgrade +// with the given source path. It parses the command output to extract package size data. +// Returns: bytes needed to download, total size of all packages, error. +func queryDownloadSizeViaApt(path string, updateType UpdateType, pkgList []string) (float64, float64, error) { + var cmd *exec.Cmd + if utils2.IsDir(path) { + // #nosec G204 + cmd = exec.Command("/usr/bin/apt-get", + append([]string{"dist-upgrade", "-d", "-o", "Debug::NoLocking=1", "-c", LastoreAptV2CommonConfPath, "--assume-no", + "-o", fmt.Sprintf("%v=%v", "Dir::Etc::sourcelist", "/dev/null"), + "-o", fmt.Sprintf("%v=%v", "Dir::Etc::SourceParts", path)}, pkgList...)...) + } else { + // #nosec G204 + cmd = exec.Command("/usr/bin/apt-get", + append([]string{"dist-upgrade", "-d", "-o", "Debug::NoLocking=1", "-c", LastoreAptV2CommonConfPath, "--assume-no", + "-o", fmt.Sprintf("%v=%v", "Dir::Etc::sourcelist", path), + "-o", fmt.Sprintf("%v=%v", "Dir::Etc::SourceParts", "/dev/null")}, pkgList...)...) + } + cmd.Env = append(os.Environ(), "LC_ALL=C") + logger.Infof("%v download size cmd: %v", updateType.JobType(), cmd.String()) + lines, err := utils.FilterExecOutput(cmd, time.Second*120, func(line string) bool { + _, _, _err := parsePackageSize(line) + return _err == nil + }) + if err != nil && len(lines) == 0 { + return 0, 0, fmt.Errorf("run:%v failed-->%v", cmd.Args, err) + } + if len(lines) != 0 { + needDownloadSize, allSize, err := parsePackageSize(lines[0]) + if err != nil { + logger.Warning(err) + return 0, 0, err + } + return needDownloadSize, allSize, nil + } + return 0, 0, nil +} + +// immutableUpgradeCheckOutput represents the JSON output of deepin-immutable-ctl upgrade check -j. +type immutableUpgradeCheckOutput struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + TotalSize uint64 `json:"totalSize"` + CachedOstreePkgCount int `json:"cachedOstreePkgCount"` + NeedDownload struct { + Size uint64 `json:"size"` + } `json:"needDownload"` + } `json:"data"` +} + +// queryDownloadSizeForIncrementalUpdate queries the download size information for incremental updates +// by invoking deepin-immutable-ctl upgrade check. The source path is passed via the +// DEEPIN_IMMUTABLE_UPGRADE_APT_OPTION environment variable. +// Returns: bytes needed to download, total size of all packages, error. +func queryDownloadSizeForIncrementalUpdate(path string, updateType UpdateType, pkgList []string) (float64, float64, error) { + var sourceArgs string + modeStr := updateType.JobType() + if modeStr == "" { + modeStr = fmt.Sprintf("mode-%d", updateType) + } + cmd := exec.Command(DeepinImmutableCtlPath, "upgrade", "check", "-j", "-m", modeStr) + if path != "" { + if utils2.IsDir(path) { + sourceArgs = "-o Dir::Etc::sourcelist=/dev/null -o Dir::Etc::SourceParts=" + path + } else { + sourceArgs = "-o Dir::Etc::sourcelist=" + path + " -o Dir::Etc::SourceParts=/dev/null" + } + } + cmd.Env = append(os.Environ(), "LC_ALL=C", + "DEEPIN_IMMUTABLE_UPGRADE_APT_OPTION="+sourceArgs) + logger.Infof("%v download size cmd: %v, sourceArgs: %v", updateType.JobType(), cmd.String(), sourceArgs) + out, err := cmd.Output() + logger.Debugf("immutable upgrade check output: %s", out) + if err != nil { + return 0, 0, fmt.Errorf("run %v failed: %v", cmd.Args, err) + } + var result immutableUpgradeCheckOutput + if err := json.Unmarshal(out, &result); err != nil { + return 0, 0, fmt.Errorf("parse immutable upgrade check output failed: %v", err) + } + logger.Debugf("immutable upgrade check result: %+v", result) + if result.Code != 0 { + return 0, 0, fmt.Errorf("immutable upgrade check returned code %d: %s", result.Code, result.Message) + } + needDownload := result.Data.NeedDownload.Size + totalSize := result.Data.TotalSize + // If both needDownload and totalSize are 0 but cached ostree packages exist, the ostree + // transfer stats have likely been discarded. Return a non-zero totalSize (1) as a + // sentinel to prevent the control center upgrade UI from stalling on a zero-size result. + if needDownload == 0 && totalSize == 0 && result.Data.CachedOstreePkgCount > 0 { + return 0, 1, nil + } + return float64(needDownload), float64(totalSize), nil +} + +// QuerySourceDownloadSize returns the download size information for the given update type. +// It selects the appropriate backend (apt or immutable-ctl) based on the IncrementalUpdate flag. +// Returns: bytes needed to download, total size of all packages, error. func QuerySourceDownloadSize(updateType UpdateType, pkgList []string) (float64, float64, error) { + logger.Debugf("QuerySourceDownloadSize updateType: %v, pkgList: %v", updateType, pkgList) startTime := time.Now() downloadSize := new(float64) allPackageSize := new(float64) @@ -211,39 +311,16 @@ func QuerySourceDownloadSize(updateType UpdateType, pkgList []string) (float64, unref() } }() - var cmd *exec.Cmd - if utils2.IsDir(path) { - // #nosec G204 - cmd = exec.Command("/usr/bin/apt-get", - append([]string{"dist-upgrade", "-d", "-o", "Debug::NoLocking=1", "-c", LastoreAptV2CommonConfPath, "--assume-no", - "-o", fmt.Sprintf("%v=%v", "Dir::Etc::sourcelist", "/dev/null"), - "-o", fmt.Sprintf("%v=%v", "Dir::Etc::SourceParts", path)}, pkgList...)...) - } else { - // #nosec G204 - cmd = exec.Command("/usr/bin/apt-get", - append([]string{"dist-upgrade", "-d", "-o", "Debug::NoLocking=1", "-c", LastoreAptV2CommonConfPath, "--assume-no", - "-o", fmt.Sprintf("%v=%v", "Dir::Etc::sourcelist", path), - "-o", fmt.Sprintf("%v=%v", "Dir::Etc::SourceParts", "/dev/null")}, pkgList...)...) - } - cmd.Env = append(os.Environ(), "LC_ALL=C") - logger.Infof("%v download size cmd: %v", updateType.JobType(), cmd.String()) - lines, err := utils.FilterExecOutput(cmd, time.Second*120, func(line string) bool { - _, _, _err := parsePackageSize(line) - return _err == nil - }) - if err != nil && len(lines) == 0 { - return fmt.Errorf("run:%v failed-->%v", cmd.Args, err) + queryFn := queryDownloadSizeViaApt + if IncrementalUpdate { + queryFn = queryDownloadSizeForIncrementalUpdate } - - if len(lines) != 0 { - needDownloadSize, allSize, err := parsePackageSize(lines[0]) - if err != nil { - logger.Warning(err) - return err - } - *downloadSize = needDownloadSize - *allPackageSize = allSize + needDownloadSize, allSize, err := queryFn(path, updateType, pkgList) + if err != nil { + return err } + *downloadSize = needDownloadSize + *allPackageSize = allSize return nil }) if err != nil { @@ -251,6 +328,8 @@ func QuerySourceDownloadSize(updateType UpdateType, pkgList []string) (float64, return SizeDownloaded, SizeDownloaded, err } logger.Debug("end QuerySourceDownloadSize duration:", time.Now().Sub(startTime)) + logger.Debugf("QuerySourceDownloadSize result, download size: %s, all package size: %s", + utils.FormatSize(*downloadSize), utils.FormatSize(*allPackageSize)) return *downloadSize, *allPackageSize, nil } @@ -329,7 +408,7 @@ func QuerySourceAddSize(updateType UpdateType) (float64, error) { logger.Warning(err) return SizeUnknown, err } - logger.Debug("end QuerySourceDownloadSize duration:", time.Now().Sub(startTime)) + logger.Debug("end QuerySourceAddSize duration:", time.Now().Sub(startTime)) return *addSize, nil } diff --git a/src/internal/utils/utils.go b/src/internal/utils/utils.go index 2ba334760..bb9000eab 100644 --- a/src/internal/utils/utils.go +++ b/src/internal/utils/utils.go @@ -179,3 +179,25 @@ func doUnsetEnvC(envName string) { defer C.free(unsafe.Pointer(cname)) C.unsetenv(cname) } + +// FormatSize formats bytes to human readable format (e.g., 1.5 MiB, 2.3 GiB) +// Uses binary units (1 KiB = 1024 B) +func FormatSize(bytes float64) string { + if bytes < 0 { + return "unknown" + } + + units := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} + unitIndex := 0 + size := bytes + + for size >= 1024 && unitIndex < len(units)-1 { + size /= 1024 + unitIndex++ + } + + if unitIndex == 0 { + return fmt.Sprintf("%.0f %s", size, units[unitIndex]) + } + return fmt.Sprintf("%.2f %s", size, units[unitIndex]) +} diff --git a/src/internal/utils/utils_test.go b/src/internal/utils/utils_test.go index d0139648b..4f68ea508 100644 --- a/src/internal/utils/utils_test.go +++ b/src/internal/utils/utils_test.go @@ -3,3 +3,65 @@ // SPDX-License-Identifier: GPL-3.0-or-later package utils + +import ( + "testing" +) + +func TestFormatSize(t *testing.T) { + tests := []struct { + name string + bytes float64 + expected string + }{ + // 字节 (B) + {"zero bytes", 0, "0 B"}, + {"1 byte", 1, "1 B"}, + {"500 bytes", 500, "500 B"}, + {"999 bytes", 999, "999 B"}, + + // KiB (1024 bytes) + {"1 KiB", 1024, "1.00 KiB"}, + {"1.5 KiB", 1536, "1.50 KiB"}, + {"10 KiB", 10240, "10.00 KiB"}, + {"100 KiB", 102400, "100.00 KiB"}, + {"1023.99 KiB", 1048575, "1024.00 KiB"}, + + // MiB (1024 KiB = 1,048,576 bytes) + {"1 MiB", 1048576, "1.00 MiB"}, + {"1.5 MiB", 1572864, "1.50 MiB"}, + {"15.03 MiB", 15754620, "15.02 MiB"}, + {"100 MiB", 104857600, "100.00 MiB"}, + {"999.99 MiB", 1048575000, "1000.00 MiB"}, + + // GiB (1024 MiB = 1,073,741,824 bytes) + {"1 GiB", 1073741824, "1.00 GiB"}, + {"1.5 GiB", 1610612736, "1.50 GiB"}, + {"2.3 GiB", 2469606195, "2.30 GiB"}, + {"10 GiB", 10737418240, "10.00 GiB"}, + {"100 GiB", 107374182400, "100.00 GiB"}, + + // TiB (1024 GiB) + {"1 TiB", 1099511627776, "1.00 TiB"}, + {"2 TiB", 2199023255552, "2.00 TiB"}, + + // 边界值测试 + {"boundary 1023 bytes", 1023, "1023 B"}, + {"boundary 1024 bytes", 1024, "1.00 KiB"}, + {"boundary 1048575 bytes", 1048575, "1024.00 KiB"}, + {"boundary 1048576 bytes", 1048576, "1.00 MiB"}, + + // 负数处理 + {"negative value", -1, "unknown"}, + {"negative large", -1000, "unknown"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatSize(tt.bytes) + if result != tt.expected { + t.Errorf("FormatSize(%v) = %v, want %v", tt.bytes, result, tt.expected) + } + }) + } +} diff --git a/src/lastore-daemon/main.go b/src/lastore-daemon/main.go index d3d5a6230..130de133b 100644 --- a/src/lastore-daemon/main.go +++ b/src/lastore-daemon/main.go @@ -76,6 +76,8 @@ func main() { config := NewConfig(path.Join(system.VarLibDir, "config.json")) logger.Info("intranet update:", config.IntranetUpdate) system.IntranetUpdate = config.IntranetUpdate + logger.Info("incremental update:", config.IncrementalUpdate) + system.IncrementalUpdate = config.IncrementalUpdate if config.IntranetUpdate { go func() { out, err := exec.Command("/usr/bin/lastore-tools", "gatherinfo", "-type=post").CombinedOutput() diff --git a/src/lastore-daemon/manager_ifc.go b/src/lastore-daemon/manager_ifc.go index 8e521ada4..88664a332 100644 --- a/src/lastore-daemon/manager_ifc.go +++ b/src/lastore-daemon/manager_ifc.go @@ -26,7 +26,7 @@ import ( utils2 "github.com/linuxdeepin/go-lib/utils" "github.com/linuxdeepin/lastore-daemon/src/internal/config" "github.com/linuxdeepin/lastore-daemon/src/internal/system" - "github.com/linuxdeepin/lastore-daemon/src/internal/system/apt" + "github.com/linuxdeepin/lastore-daemon/src/internal/utils" ) /* @@ -214,10 +214,6 @@ func (m *Manager) PackagesDownloadSize(packages []string) (int64, *dbus.Error) { logger.Warningf("PackagesDownloadSize(%q)=%0.2f %v\n", strings.Join(packages, " "), size, err) } - if m.config.IncrementalUpdate && size > 0 && apt.IsIncrementalUpdateCached("") { - size = 0.0 - } - return int64(size), dbusutil.ToError(err) } @@ -485,9 +481,9 @@ func (m *Manager) QueryAllSizeWithSource(mode system.UpdateType) (int64, *dbus.E } _, allSize, err := system.QuerySourceDownloadSize(mode, pkgList) if err != nil || allSize == system.SizeUnknown { - logger.Warningf("failed to get %v source size:%v", strings.Join(sourcePathList, " and "), err) + logger.Warningf("failed to query %v all size: %v", strings.Join(sourcePathList, " and "), err) } else { - logger.Infof("%v size is:%v M", strings.Join(sourcePathList, " and "), int64(allSize/(1000*1000))) + logger.Infof("%v all size: %s", strings.Join(sourcePathList, " and "), utils.FormatSize(allSize)) } return int64(allSize), dbusutil.ToError(err) diff --git a/src/lastore-daemon/update_status.go b/src/lastore-daemon/update_status.go index 279a46d32..90ac73387 100644 --- a/src/lastore-daemon/update_status.go +++ b/src/lastore-daemon/update_status.go @@ -9,10 +9,9 @@ import ( "strings" "sync" - "github.com/linuxdeepin/go-lib/utils" "github.com/linuxdeepin/lastore-daemon/src/internal/config" "github.com/linuxdeepin/lastore-daemon/src/internal/system" - "github.com/linuxdeepin/lastore-daemon/src/internal/system/apt" + "github.com/linuxdeepin/lastore-daemon/src/internal/utils" ) type UpdateModeStatusManager struct { @@ -481,25 +480,11 @@ func (m *UpdateModeStatusManager) updateModeStatusBySize(mode system.UpdateType, changed = true } } else { - sourceList, ok := system.GetCategorySourceMap()[typ] - sourceArgs := "" - if ok && sourceList != "" { - if utils.IsDir(sourceList) { - sourceArgs = "-o Dir::Etc::sourcelist=/dev/null -o Dir::Etc::SourceParts=" + sourceList - } else { - sourceArgs = "-o Dir::Etc::sourcelist=" + sourceList + " -o Dir::Etc::SourceParts=/dev/null" - } - } - if m.lsConfig.IncrementalUpdate && needDownloadSize > 0 && apt.IsIncrementalUpdateCached(sourceArgs) { - needDownloadSize = 0.0 - } - m.updateModeDownloadSizeMapLock.Lock() m.updateModeDownloadSizeMap[currentMode.JobType()] = needDownloadSize m.updateModeDownloadSizeMapLock.Unlock() - logger.Infof("currentMode: %v, needDownloadSize: %v,"+ - " allPackageSize: %v, oldStatus: %v", - currentMode.JobType(), needDownloadSize, allPackageSize, oldStatus) + logger.Infof("currentMode: %v, needDownloadSize: %s, allPackageSize: %s, oldStatus: %v", + currentMode.JobType(), utils.FormatSize(needDownloadSize), utils.FormatSize(allPackageSize), oldStatus) // allPackageSize == 0 有两种情况:1.无需更新;2.更新完成需要重启; if allPackageSize == 0 { if oldStatus != system.Upgraded {