1
0

JavaRuntimeService.cs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. using System.Diagnostics;
  2. using System.Runtime.InteropServices;
  3. using System.Text.Json.Nodes;
  4. using System.Text.RegularExpressions;
  5. namespace PCL.Communist.Core;
  6. public sealed record JavaRuntime(string Executable, int Major, string Architecture);
  7. public sealed class JavaRuntimeService(DownloadClient downloads, LauncherPaths paths)
  8. {
  9. public const string IndexUrl = "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json";
  10. public static string RuntimePlatform(PlatformInfo platform) => (platform.Kind, platform.Architecture) switch
  11. {
  12. (PlatformKind.Windows, Architecture.X64) => "windows-x64",
  13. (PlatformKind.Windows, Architecture.X86) => "windows-x86",
  14. (PlatformKind.Windows, Architecture.Arm64) => "windows-arm64",
  15. (PlatformKind.MacOS, Architecture.X64) => "mac-os",
  16. (PlatformKind.MacOS, Architecture.Arm64) => "mac-os-arm64",
  17. (PlatformKind.Linux, Architecture.X64) => "linux",
  18. (PlatformKind.Linux, Architecture.X86) => "linux-i386",
  19. _ => throw new PlatformNotSupportedException("此平台需要单独提供匹配架构的 Java 运行时。")
  20. };
  21. public async Task<JavaRuntime> ResolveAsync(JsonObject metadata, PlatformInfo platform, string? configuredJava,
  22. IProgress<InstallProgress>? progress = null, CancellationToken cancellationToken = default)
  23. {
  24. if (platform.IsMobile) throw new PlatformNotSupportedException("移动端 Java 需要原生运行时接入。" );
  25. var major = metadata["javaVersion"]?["majorVersion"]?.GetValue<int>() ?? 8;
  26. if (!string.IsNullOrWhiteSpace(configuredJava))
  27. {
  28. var configured = await ProbeAsync(configuredJava, cancellationToken);
  29. if (configured is null || configured.Major != major || !MatchesArchitecture(configured, platform))
  30. throw new InvalidOperationException($"所选 Java 需要为 {platform.MinecraftArch} 架构的 Java {major}。");
  31. return configured;
  32. }
  33. foreach (var candidate in Candidates(platform))
  34. {
  35. var found = await ProbeAsync(candidate, cancellationToken);
  36. if (found?.Major == major && MatchesArchitecture(found, platform)) return found;
  37. }
  38. progress?.Report(new($"准备 Java {major} · {platform.DisplayName}…", 0, 1));
  39. var runtimePlatform = RuntimePlatform(platform);
  40. var index = await downloads.GetJsonAsync(IndexUrl, cancellationToken);
  41. var component = metadata["javaVersion"]?["component"]?.GetValue<string>() ?? "jre-legacy";
  42. var entry = index[runtimePlatform]?[component]?.AsArray().FirstOrDefault()
  43. ?? throw new PlatformNotSupportedException($"官方没有 {platform.DisplayName} 的 Java {major}。请安装对应 Java 并在界面指定路径。");
  44. var root = LauncherPaths.SafeChild(paths.Runtimes, runtimePlatform + "/" + component);
  45. var manifestPath = Path.Combine(root, "runtime-manifest.json");
  46. await downloads.FetchAsync(MinecraftInstaller.Artifact(entry["manifest"]!, manifestPath), cancellationToken);
  47. var manifest = JsonNode.Parse(await File.ReadAllTextAsync(manifestPath, cancellationToken))!["files"]!.AsObject();
  48. var files = manifest.Where(f => f.Value?["type"]?.GetValue<string>() == "file").ToArray();
  49. var completed = 0;
  50. await Parallel.ForEachAsync(files, new ParallelOptions { MaxDegreeOfParallelism = 6, CancellationToken = cancellationToken },
  51. async (file, token) =>
  52. {
  53. var target = LauncherPaths.SafeChild(root, file.Key);
  54. await downloads.FetchAsync(MinecraftInstaller.Artifact(file.Value!["downloads"]!["raw"]!, target), token);
  55. if (!OperatingSystem.IsWindows() && file.Value["executable"]?.GetValue<bool>() == true)
  56. File.SetUnixFileMode(target, File.GetUnixFileMode(target) | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute);
  57. progress?.Report(new($"安装 Java {major}…", Interlocked.Increment(ref completed), files.Length));
  58. });
  59. // Resolve only links inside the verified runtime. Copying these small files also works without Windows symlink privileges.
  60. foreach (var file in manifest.Where(f => f.Value?["type"]?.GetValue<string>() == "link"))
  61. {
  62. var target = LauncherPaths.SafeChild(root, file.Key);
  63. var source = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(target)!, file.Value!["target"]!.GetValue<string>()));
  64. if (!source.StartsWith(Path.TrimEndingDirectorySeparator(root) + Path.DirectorySeparatorChar, StringComparison.Ordinal))
  65. throw new InvalidDataException("Java 运行时的链接超出安装目录。");
  66. Directory.CreateDirectory(Path.GetDirectoryName(target)!);
  67. if (Directory.Exists(source))
  68. {
  69. if (!Directory.Exists(target)) Directory.CreateSymbolicLink(target, source);
  70. }
  71. else File.Copy(source, target, true);
  72. }
  73. var result = await ProbeAsync(Path.Combine(root, "bin", platform.JavaExecutable), cancellationToken)
  74. ?? await ProbeAsync(Path.Combine(root, "jre.bundle", "Contents", "Home", "bin", platform.JavaExecutable), cancellationToken);
  75. if (result?.Major != major || !MatchesArchitecture(result, platform)) throw new IOException("下载的 Java 无法正常运行,或架构不匹配。" );
  76. return result;
  77. }
  78. public static bool MatchesArchitecture(JavaRuntime runtime, PlatformInfo platform) => platform.Architecture switch
  79. {
  80. Architecture.X64 => runtime.Architecture is "amd64" or "x86_64",
  81. Architecture.Arm64 => runtime.Architecture is "aarch64" or "arm64",
  82. Architecture.X86 => runtime.Architecture is "x86" or "i386" or "i686",
  83. _ => false
  84. };
  85. private IEnumerable<string> Candidates(PlatformInfo platform)
  86. {
  87. if (Directory.Exists(paths.Runtimes))
  88. foreach (var file in Directory.EnumerateFiles(paths.Runtimes, platform.JavaExecutable, SearchOption.AllDirectories)) yield return file;
  89. var javaHome = Environment.GetEnvironmentVariable("JAVA_HOME");
  90. if (!string.IsNullOrEmpty(javaHome)) yield return Path.Combine(javaHome, "bin", platform.JavaExecutable);
  91. yield return platform.JavaExecutable;
  92. var parent = platform.Kind == PlatformKind.MacOS ? "/Library/Java/JavaVirtualMachines" : "/usr/lib/jvm";
  93. if (!Directory.Exists(parent)) yield break;
  94. foreach (var directory in Directory.EnumerateDirectories(parent))
  95. yield return Path.Combine(directory, platform.Kind == PlatformKind.MacOS ? "Contents/Home/bin/java" : "bin/java");
  96. }
  97. public static async Task<JavaRuntime?> ProbeAsync(string executable, CancellationToken cancellationToken = default)
  98. {
  99. using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
  100. timeout.CancelAfter(TimeSpan.FromSeconds(10));
  101. Process? process = null;
  102. try
  103. {
  104. var start = new ProcessStartInfo(executable) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true };
  105. start.ArgumentList.Add("-XshowSettings:properties");
  106. start.ArgumentList.Add("-version");
  107. process = Process.Start(start)!;
  108. var stdout = process.StandardOutput.ReadToEndAsync(timeout.Token);
  109. var stderr = process.StandardError.ReadToEndAsync(timeout.Token);
  110. await process.WaitForExitAsync(timeout.Token);
  111. var text = await stdout + "\n" + await stderr;
  112. if (process.ExitCode != 0) return null;
  113. var version = Regex.Match(text, @"java\.version\s*=\s*(?:1\.)?(\d+)", RegexOptions.None, TimeSpan.FromSeconds(1));
  114. var arch = Regex.Match(text, @"os\.arch\s*=\s*(\S+)", RegexOptions.None, TimeSpan.FromSeconds(1));
  115. return version.Success && arch.Success ? new(executable, int.Parse(version.Groups[1].Value), arch.Groups[1].Value) : null;
  116. }
  117. catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { return null; }
  118. catch (System.ComponentModel.Win32Exception) { return null; }
  119. catch (IOException) { return null; }
  120. finally
  121. {
  122. if (process is not null)
  123. {
  124. if (!process.HasExited) process.Kill(true);
  125. process.Dispose();
  126. }
  127. }
  128. }
  129. }