fix: use atomic operations for concurrent download progress counter
Co-Authored-By: OWL <noreply@anthropic.com>
This commit is contained in:
@@ -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,22 +65,19 @@ 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
|
||||
// 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(downloadedBytes, totalBytes)
|
||||
onProgress(atomic.LoadInt64(&downloadedBytes), totalBytes)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pool.Submit(func() {
|
||||
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)
|
||||
}
|
||||
_ = 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,24 +142,18 @@ 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,
|
||||
"classpath": classpathStr,
|
||||
"java_path": g.JavaBin,
|
||||
"game_directory": g.InstanceDir,
|
||||
"assets_root": filepath.Join(g.InstanceDir, "assets"),
|
||||
@@ -192,10 +170,9 @@ func (g *Game) BuildCommand() (*exec.Cmd, error) {
|
||||
"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
|
||||
|
||||
Reference in New Issue
Block a user