using System.Diagnostics; namespace BrassAndSigil.Server.Services; /// /// Downloads NeoForge's official server installer JAR and runs it with --installServer /// to produce run.sh/run.bat + the server library tree. Handles Java invocation and /// streams installer output via a progress callback. /// public sealed class NeoForgeInstaller { private static readonly HttpClient _http = new() { Timeout = TimeSpan.FromMinutes(10) }; public bool IsAlreadyInstalled(string serverDir) { return File.Exists(Path.Combine(serverDir, OperatingSystem.IsWindows() ? "run.bat" : "run.sh")); } public async Task InstallAsync(string version, string serverDir, string javaPath, IProgress? progress, CancellationToken ct) { Directory.CreateDirectory(serverDir); // 1. Download installer var installerName = $"neoforge-{version}-installer.jar"; var installerPath = Path.Combine(serverDir, installerName); var url = $"https://maven.neoforged.net/releases/net/neoforged/neoforge/{version}/{installerName}"; if (!File.Exists(installerPath)) { progress?.Report($"Downloading NeoForge {version} installer..."); var bytes = await _http.GetByteArrayAsync(url, ct); await File.WriteAllBytesAsync(installerPath, bytes, ct); progress?.Report($" Saved {bytes.Length:N0} bytes to {installerName}"); } else { progress?.Report($"NeoForge installer already present, skipping download."); } // 2. Run installer progress?.Report("Running NeoForge installer (java -jar ... --installServer)..."); var psi = new ProcessStartInfo { FileName = javaPath, WorkingDirectory = serverDir, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }; psi.ArgumentList.Add("-jar"); psi.ArgumentList.Add(installerName); psi.ArgumentList.Add("--installServer"); Process? proc; try { proc = Process.Start(psi); } catch (Exception ex) { progress?.Report($" [error] Could not start java: {ex.Message}"); return false; } if (proc is null) { progress?.Report(" [error] Failed to start java."); return false; } var stdoutTask = StreamLines(proc.StandardOutput, line => progress?.Report($" {line}"), ct); var stderrTask = StreamLines(proc.StandardError, line => progress?.Report($" [err] {line}"), ct); await proc.WaitForExitAsync(ct); await Task.WhenAll(stdoutTask, stderrTask); if (proc.ExitCode != 0) { progress?.Report($" [error] NeoForge installer exited with code {proc.ExitCode}"); return false; } // 3. Verify run script exists if (!IsAlreadyInstalled(serverDir)) { progress?.Report(" [error] NeoForge installer ran but run.sh/run.bat is missing."); return false; } progress?.Report($"NeoForge {version} installed."); // 4. Clean up the installer JAR (large, no longer needed) try { File.Delete(installerPath); } catch { /* best-effort */ } return true; } private static async Task StreamLines(StreamReader reader, Action onLine, CancellationToken ct) { try { while (!reader.EndOfStream) { var line = await reader.ReadLineAsync(ct); if (line is null) break; onLine(line); } } catch { } } }