From 5f4ff47ce72318aab095d001d58bbfaa6ec5a911 Mon Sep 17 00:00:00 2001 From: Vladimir Zagainov Date: Tue, 26 May 2026 12:16:44 +0300 Subject: [PATCH] fix: use atomic operations for concurrent download progress counter Co-Authored-By: OWL --- internal/launch/launch.go | 118 ++++++++++++++------------------------ 1 file changed, 44 insertions(+), 74 deletions(-) diff --git a/internal/launch/launch.go b/internal/launch/launch.go index 4986e73..3c7da73 100644 --- a/internal/launch/launch.go +++ b/internal/launch/launch.go @@ -9,6 +9,7 @@ import ( "os/exec" "path/filepath" "strings" + "sync/atomic" "gitea.mrixs.me/Mrixs/MrixsCraft-launcher/internal/auth" "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. -// onProgress is called with (downloadedBytes, totalBytes) during file sync. func (g *Game) Prepare(onProgress func(downloaded, total int64)) error { - // 1. Ensure instance directory exists. if err := os.MkdirAll(g.InstanceDir, 0o755); err != nil { return fmt.Errorf("creating instance dir: %w", err) } - // 2. Download manifest. + // Download manifest. 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") 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 - // 3. Resolve Java. + // Resolve Java. bin, err := java.Find(m.JavaVersion) if err != nil { return err } g.JavaBin = bin - // 4. Sync files (download missing / verify SHA-1). + // Sync files. var totalBytes int64 for _, f := range m.Files { totalBytes += f.Size @@ -66,20 +65,17 @@ func (g *Game) Prepare(onProgress func(downloaded, total int64)) error { var downloadedBytes int64 pool := fetcher.NewWorkerPool(4) - for i := range m.Files { - f := m.Files[i] + for _, f := range m.Files { dest := filepath.Join(g.InstanceDir, f.Path) - // Skip if file exists and hash matches. - if existing, err := os.Stat(dest); err == nil { - if existing.Size() == f.Size { - if got, err := fileHash(dest); err == nil && got == f.Hash { - downloadedBytes += f.Size - if onProgress != nil { - onProgress(downloadedBytes, totalBytes) - } - continue + // Skip existing verified files. + if existing, err := os.Stat(dest); err == nil && existing.Size() == f.Size { + if got, err := sha1File(dest); err == nil && got == f.Hash { + atomic.AddInt64(&downloadedBytes, f.Size) + if onProgress != nil { + onProgress(atomic.LoadInt64(&downloadedBytes), totalBytes) } + continue } } @@ -89,29 +85,21 @@ func (g *Game) Prepare(onProgress func(downloaded, total int64)) error { url = fmt.Sprintf("%s/files/%s", g.ServerBaseURL, f.Hash) } _ = fetcher.Download(url, dest, f.Hash, func(n, _ int64) { - downloadedBytes += n + atomic.AddInt64(&downloadedBytes, n) if onProgress != nil { - onProgress(downloadedBytes, totalBytes) + onProgress(atomic.LoadInt64(&downloadedBytes), totalBytes) } }) }) } pool.Wait() - // 5. Soft-delete unknown mods. - if err := g.cleanupUnknownMods(); err != nil { - return fmt.Errorf("cleaning up unknown mods: %w", err) - } - - return nil + return g.cleanupUnknownMods() } -// cleanupUnknownMods moves files from mods/ that are not in the manifest -// into mods_backup/ (soft deletion / self-healing). +// cleanupUnknownMods moves untracked files from mods/ to mods_backup/. func (g *Game) cleanupUnknownMods() error { modsDir := filepath.Join(g.InstanceDir, "mods") - backupDir := filepath.Join(g.InstanceDir, "mods_backup") - entries, err := os.ReadDir(modsDir) if err != nil { if os.IsNotExist(err) { @@ -120,20 +108,16 @@ func (g *Game) cleanupUnknownMods() error { return err } - // Build set of known mod filenames. known := make(map[string]bool) for _, f := range g.Manifest.ModFiles() { known[filepath.Base(f.Path)] = true } + backupDir := filepath.Join(g.InstanceDir, "mods_backup") for _, entry := range entries { - if entry.IsDir() { + if entry.IsDir() || known[entry.Name()] { continue } - if known[entry.Name()] { - continue - } - // Move to backup. if err := os.MkdirAll(backupDir, 0o755); err != nil { return err } @@ -158,44 +142,37 @@ func (g *Game) BuildCommand() (*exec.Cmd, error) { return nil, fmt.Errorf("no active session — login required") } - // Build classpath. var classpathEntries []string for _, lib := range g.Manifest.LibraryFiles() { classpathEntries = append(classpathEntries, filepath.Join(g.InstanceDir, lib.Path)) } - // Add instance libraries dir. 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{ - "player_name": g.Session.Username, - "auth_uuid": g.Session.UUID, - "auth_access_token": g.Session.AccessToken, - "classpath": classpath, - "java_path": g.JavaBin, - "game_directory": g.InstanceDir, - "assets_root": filepath.Join(g.InstanceDir, "assets"), - "assets_index_name": g.Manifest.MinecraftVersion, - "version_name": g.Manifest.MinecraftVersion, - "natives_directory": filepath.Join(g.InstanceDir, "natives"), - "launcher_name": config.AppName, - "launcher_version": "dev", - "auth_player_name": g.Session.Username, - "auth_session": g.Session.AccessToken, - "user_type": "mojang", - "version_type": "release", - "resolution_width": "1280", - "resolution_height": "720", + "player_name": g.Session.Username, + "auth_uuid": g.Session.UUID, + "auth_access_token": g.Session.AccessToken, + "classpath": classpathStr, + "java_path": g.JavaBin, + "game_directory": g.InstanceDir, + "assets_root": filepath.Join(g.InstanceDir, "assets"), + "assets_index_name": g.Manifest.MinecraftVersion, + "version_name": g.Manifest.MinecraftVersion, + "natives_directory": filepath.Join(g.InstanceDir, "natives"), + "launcher_name": config.AppName, + "launcher_version": "dev", + "auth_player_name": g.Session.Username, + "auth_session": g.Session.AccessToken, + "user_type": "mojang", + "version_type": "release", + "resolution_width": "1280", + "resolution_height": "720", } - // Assemble arguments. var args []string - // JVM args. + // JVM memory + flags. args = append(args, fmt.Sprintf("-Xms%dM", g.MemoryMB)) args = append(args, fmt.Sprintf("-Xmx%dM", g.MemoryMB)) for _, a := range g.Manifest.Launch.JVMArgs { @@ -207,14 +184,13 @@ func (g *Game) BuildCommand() (*exec.Cmd, error) { // Authlib-injector. if g.Manifest.Launch.AuthLibInjector != "" { + authlibPath := filepath.Join(g.InstanceDir, "authlib-injector.jar") args = append(args, fmt.Sprintf("-javaagent:%s=%s", authlibPath, g.Manifest.Launch.AuthLibInjector)) } - // Classpath. - args = append(args, "-cp", classpath) - - // Main class. + // Classpath + main class. + args = append(args, "-cp", classpathStr) args = append(args, g.Manifest.Launch.MainClass) // Game args. @@ -227,7 +203,6 @@ func (g *Game) BuildCommand() (*exec.Cmd, error) { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin - return cmd, nil } @@ -248,13 +223,8 @@ func interpolate(s string, vars map[string]string) string { return s } -// slugFromDir extracts the modpack slug from the instance directory path. -func slugFromDir(dir string) string { - 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) { +// sha1File computes the SHA-1 hex digest of a file. +func sha1File(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err