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"
|
"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 untracked files from mods/ to mods_backup/.
|
||||||
}
|
|
||||||
|
|
||||||
// cleanupUnknownMods moves files from mods/ that are not in the manifest
|
|
||||||
// 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
|
||||||
|
|||||||
Reference in New Issue
Block a user