fix: use atomic operations for concurrent download progress counter

Co-Authored-By: OWL <noreply@anthropic.com>
This commit is contained in:
2026-05-26 12:16:44 +03:00
parent c06c205b7b
commit 5f4ff47ce7

View File

@@ -9,6 +9,7 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"sync/atomic"
"gitea.mrixs.me/Mrixs/MrixsCraft-launcher/internal/auth" "gitea.mrixs.me/Mrixs/MrixsCraft-launcher/internal/auth"
"gitea.mrixs.me/Mrixs/MrixsCraft-launcher/internal/config" "gitea.mrixs.me/Mrixs/MrixsCraft-launcher/internal/config"
@@ -28,16 +29,14 @@ type Game struct {
} }
// Prepare downloads the manifest, syncs files, and resolves the Java binary. // Prepare downloads the manifest, syncs files, and resolves the Java binary.
// onProgress is called with (downloadedBytes, totalBytes) during file sync.
func (g *Game) Prepare(onProgress func(downloaded, total int64)) error { func (g *Game) Prepare(onProgress func(downloaded, total int64)) error {
// 1. Ensure instance directory exists.
if err := os.MkdirAll(g.InstanceDir, 0o755); err != nil { if err := os.MkdirAll(g.InstanceDir, 0o755); err != nil {
return fmt.Errorf("creating instance dir: %w", err) return fmt.Errorf("creating instance dir: %w", err)
} }
// 2. Download manifest. // Download manifest.
manifestURL := fmt.Sprintf("%s/api/instances/%s/manifest.json", manifestURL := fmt.Sprintf("%s/api/instances/%s/manifest.json",
g.ServerBaseURL, slugFromDir(g.InstanceDir)) g.ServerBaseURL, filepath.Base(g.InstanceDir))
manifestPath := filepath.Join(g.InstanceDir, "manifest.json") manifestPath := filepath.Join(g.InstanceDir, "manifest.json")
if err := fetcher.Download(manifestURL, manifestPath, "", nil); err != nil { if err := fetcher.Download(manifestURL, manifestPath, "", nil); err != nil {
@@ -50,14 +49,14 @@ func (g *Game) Prepare(onProgress func(downloaded, total int64)) error {
} }
g.Manifest = m g.Manifest = m
// 3. Resolve Java. // Resolve Java.
bin, err := java.Find(m.JavaVersion) bin, err := java.Find(m.JavaVersion)
if err != nil { if err != nil {
return err return err
} }
g.JavaBin = bin g.JavaBin = bin
// 4. Sync files (download missing / verify SHA-1). // Sync files.
var totalBytes int64 var totalBytes int64
for _, f := range m.Files { for _, f := range m.Files {
totalBytes += f.Size totalBytes += f.Size
@@ -66,22 +65,19 @@ func (g *Game) Prepare(onProgress func(downloaded, total int64)) error {
var downloadedBytes int64 var downloadedBytes int64
pool := fetcher.NewWorkerPool(4) pool := fetcher.NewWorkerPool(4)
for i := range m.Files { for _, f := range m.Files {
f := m.Files[i]
dest := filepath.Join(g.InstanceDir, f.Path) dest := filepath.Join(g.InstanceDir, f.Path)
// Skip if file exists and hash matches. // Skip existing verified files.
if existing, err := os.Stat(dest); err == nil { if existing, err := os.Stat(dest); err == nil && existing.Size() == f.Size {
if existing.Size() == f.Size { if got, err := sha1File(dest); err == nil && got == f.Hash {
if got, err := fileHash(dest); err == nil && got == f.Hash { atomic.AddInt64(&downloadedBytes, f.Size)
downloadedBytes += f.Size
if onProgress != nil { if onProgress != nil {
onProgress(downloadedBytes, totalBytes) onProgress(atomic.LoadInt64(&downloadedBytes), totalBytes)
} }
continue continue
} }
} }
}
pool.Submit(func() { pool.Submit(func() {
url := f.URL url := f.URL
@@ -89,29 +85,21 @@ func (g *Game) Prepare(onProgress func(downloaded, total int64)) error {
url = fmt.Sprintf("%s/files/%s", g.ServerBaseURL, f.Hash) url = fmt.Sprintf("%s/files/%s", g.ServerBaseURL, f.Hash)
} }
_ = fetcher.Download(url, dest, f.Hash, func(n, _ int64) { _ = fetcher.Download(url, dest, f.Hash, func(n, _ int64) {
downloadedBytes += n atomic.AddInt64(&downloadedBytes, n)
if onProgress != nil { if onProgress != nil {
onProgress(downloadedBytes, totalBytes) onProgress(atomic.LoadInt64(&downloadedBytes), totalBytes)
} }
}) })
}) })
} }
pool.Wait() pool.Wait()
// 5. Soft-delete unknown mods. return g.cleanupUnknownMods()
if err := g.cleanupUnknownMods(); err != nil {
return fmt.Errorf("cleaning up unknown mods: %w", err)
}
return nil
} }
// cleanupUnknownMods moves files from mods/ that are not in the manifest // cleanupUnknownMods moves untracked files from mods/ to mods_backup/.
// into mods_backup/ (soft deletion / self-healing).
func (g *Game) cleanupUnknownMods() error { func (g *Game) cleanupUnknownMods() error {
modsDir := filepath.Join(g.InstanceDir, "mods") modsDir := filepath.Join(g.InstanceDir, "mods")
backupDir := filepath.Join(g.InstanceDir, "mods_backup")
entries, err := os.ReadDir(modsDir) entries, err := os.ReadDir(modsDir)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@@ -120,20 +108,16 @@ func (g *Game) cleanupUnknownMods() error {
return err return err
} }
// Build set of known mod filenames.
known := make(map[string]bool) known := make(map[string]bool)
for _, f := range g.Manifest.ModFiles() { for _, f := range g.Manifest.ModFiles() {
known[filepath.Base(f.Path)] = true known[filepath.Base(f.Path)] = true
} }
backupDir := filepath.Join(g.InstanceDir, "mods_backup")
for _, entry := range entries { for _, entry := range entries {
if entry.IsDir() { if entry.IsDir() || known[entry.Name()] {
continue continue
} }
if known[entry.Name()] {
continue
}
// Move to backup.
if err := os.MkdirAll(backupDir, 0o755); err != nil { if err := os.MkdirAll(backupDir, 0o755); err != nil {
return err return err
} }
@@ -158,24 +142,18 @@ func (g *Game) BuildCommand() (*exec.Cmd, error) {
return nil, fmt.Errorf("no active session — login required") return nil, fmt.Errorf("no active session — login required")
} }
// Build classpath.
var classpathEntries []string var classpathEntries []string
for _, lib := range g.Manifest.LibraryFiles() { for _, lib := range g.Manifest.LibraryFiles() {
classpathEntries = append(classpathEntries, filepath.Join(g.InstanceDir, lib.Path)) classpathEntries = append(classpathEntries, filepath.Join(g.InstanceDir, lib.Path))
} }
// Add instance libraries dir.
classpathEntries = append(classpathEntries, filepath.Join(g.InstanceDir, "libraries", "*")) classpathEntries = append(classpathEntries, filepath.Join(g.InstanceDir, "libraries", "*"))
classpath := strings.Join(classpathEntries, string(os.PathListSeparator)) classpathStr := strings.Join(classpathEntries, string(os.PathListSeparator))
// Authlib-injector path.
authlibPath := filepath.Join(g.InstanceDir, "authlib-injector.jar")
// Interpolation context.
vars := map[string]string{ vars := map[string]string{
"player_name": g.Session.Username, "player_name": g.Session.Username,
"auth_uuid": g.Session.UUID, "auth_uuid": g.Session.UUID,
"auth_access_token": g.Session.AccessToken, "auth_access_token": g.Session.AccessToken,
"classpath": classpath, "classpath": classpathStr,
"java_path": g.JavaBin, "java_path": g.JavaBin,
"game_directory": g.InstanceDir, "game_directory": g.InstanceDir,
"assets_root": filepath.Join(g.InstanceDir, "assets"), "assets_root": filepath.Join(g.InstanceDir, "assets"),
@@ -192,10 +170,9 @@ func (g *Game) BuildCommand() (*exec.Cmd, error) {
"resolution_height": "720", "resolution_height": "720",
} }
// Assemble arguments.
var args []string var args []string
// JVM args. // JVM memory + flags.
args = append(args, fmt.Sprintf("-Xms%dM", g.MemoryMB)) args = append(args, fmt.Sprintf("-Xms%dM", g.MemoryMB))
args = append(args, fmt.Sprintf("-Xmx%dM", g.MemoryMB)) args = append(args, fmt.Sprintf("-Xmx%dM", g.MemoryMB))
for _, a := range g.Manifest.Launch.JVMArgs { for _, a := range g.Manifest.Launch.JVMArgs {
@@ -207,14 +184,13 @@ func (g *Game) BuildCommand() (*exec.Cmd, error) {
// Authlib-injector. // Authlib-injector.
if g.Manifest.Launch.AuthLibInjector != "" { if g.Manifest.Launch.AuthLibInjector != "" {
authlibPath := filepath.Join(g.InstanceDir, "authlib-injector.jar")
args = append(args, fmt.Sprintf("-javaagent:%s=%s", args = append(args, fmt.Sprintf("-javaagent:%s=%s",
authlibPath, g.Manifest.Launch.AuthLibInjector)) authlibPath, g.Manifest.Launch.AuthLibInjector))
} }
// Classpath. // Classpath + main class.
args = append(args, "-cp", classpath) args = append(args, "-cp", classpathStr)
// Main class.
args = append(args, g.Manifest.Launch.MainClass) args = append(args, g.Manifest.Launch.MainClass)
// Game args. // Game args.
@@ -227,7 +203,6 @@ func (g *Game) BuildCommand() (*exec.Cmd, error) {
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
return cmd, nil return cmd, nil
} }
@@ -248,13 +223,8 @@ func interpolate(s string, vars map[string]string) string {
return s return s
} }
// slugFromDir extracts the modpack slug from the instance directory path. // sha1File computes the SHA-1 hex digest of a file.
func slugFromDir(dir string) string { func sha1File(path string) (string, error) {
return filepath.Base(dir)
}
// fileHash is a local alias to avoid import cycle — computes SHA-1 hex of a file.
func fileHash(path string) (string, error) {
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
return "", err return "", err