Java.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. namespace PCLCS;
  2. /// <summary>
  3. /// 单个 Java 的实例。
  4. /// </summary>
  5. [Serializable] public class Java(string folder, Version? version = null) {
  6. /// <summary>
  7. /// java.exe 文件所在的文件夹路径,这通常是 bin 文件夹。以 \ 结尾。
  8. /// </summary>
  9. public readonly string Folder = PathUtils.AddSlashSuffix(PathUtils.ForCompare(folder));
  10. /// <summary>
  11. /// java.exe 文件的完整路径。
  12. /// </summary>
  13. [JsonIgnore] public string JavaExePath => Path.Combine(Folder, "java.exe");
  14. /// <summary>
  15. /// Java 的版本号(例如 16.0.1),保证 <see cref="Version.Major"/> 为其主版本。
  16. /// <para/> 若为 null,则还需要调用 <see cref="CheckAsync"/> 以获取版本号。
  17. /// </summary>
  18. public Version? Version = version;
  19. /// <summary>
  20. /// 检查该 Java 是否存在问题,并获取其版本号。
  21. /// 返回其是否通过了检查。
  22. /// </summary>
  23. public async Task<bool> CheckAsync(CancellationToken cancellationToken = default) {
  24. if (Interlocked.Exchange(ref isChecked, 1) == 1 && available != null) return available!.Value; // 已检查过
  25. string? output = null;
  26. try {
  27. if (!FileUtils.Exists(JavaExePath)) throw new FileNotFoundException("未找到 java.exe 文件", JavaExePath);
  28. // 运行 -version
  29. output = (await TaskUtils.RunProgramAsync(JavaExePath, "-version", 15000, cancellationToken: cancellationToken).NoCapture()).Output.Lower();
  30. if (output == "") throw new InvalidOperationException("尝试运行该 Java 失败");
  31. Logger.Trace($"Java 检查输出:{JavaExePath}{Environment.NewLine}{output}");
  32. if (output.Contains("/lib/ext exists")) throw new InvalidOperationException("无法运行该 Java,请在删除 Java 文件夹中的 /lib/ext 文件夹后再试");
  33. if (output.Contains("a fatal error") || output.Contains("error: ")) throw new InvalidOperationException("无法运行该 Java,该 Java 或系统存在问题");
  34. // 获取版本号
  35. var versionString = (output.RegexSeek(@"(?<=version "")[^""]+") ?? output.RegexSeek("(?<=openjdk )[0-9]+"))?.Replace("_", ".").Replace("+", ".").BeforeFirst("-");
  36. if (string.IsNullOrEmpty(versionString)) throw new InvalidOperationException($"未找到该 Java 的版本号{(output.Length < 500 ? $"\n原始输出为:\n{output}" : "")}");
  37. if (versionString.StartsWithF("1.")) versionString = versionString!.AfterFirst("."); // 去除开头的 1.
  38. var segments = versionString!.Split(".", true).ToList();
  39. while (segments.Count < 4) segments.Add("0");
  40. Version = new Version(segments.Take(4).Join("."));
  41. if (Version.Major is <= 4 or >= 100) throw new InvalidOperationException($"分析详细信息失败,获取的版本为 {Version}{(output.Length < 500 ? $"\n原始输出为:\n{output}" : "")}");
  42. if (!output.Contains("64-bit")) throw new InvalidOperationException("该 Java 为 32 位版本,请安装 64 位的 Java");
  43. Logger.Info($"检查 Java 成功:{this}");
  44. available = true;
  45. } catch (Exception ex) {
  46. if (ex.IsCanceled()) {
  47. isChecked = 0;
  48. throw;
  49. }
  50. Logger.Warn($"检查 Java 失败({JavaExePath}),其输出为:\n{output ?? "无程序输出"}");
  51. available = false;
  52. }
  53. return available!.Value;
  54. }
  55. [JsonIgnore] private int isChecked = 0;
  56. /// <summary>若已进行过检查,表示该检查是否成功。若未检查,则为 null。</summary>
  57. [JsonIgnore] internal bool? available = null;
  58. /// <summary>用户友好的描述文本。</summary>
  59. public override string ToString()
  60. => $"Java {Version?.Major.ToString() ?? "尚未检查"} ({Version}):{Folder}";
  61. }
  62. public static class JavaUtils {
  63. /// <summary>
  64. /// 检查所有指定的 Java 是否存在问题,并获取其版本号。
  65. /// </summary>
  66. /// <returns>所有通过检查的 Java。</returns>
  67. public static async Task<List<Java>> CheckAllAsync(IEnumerable<Java> javas, CancellationToken cancellationToken = default, ProgressProvider? progress = null) {
  68. await TaskUtils.WhenAll(javas.Where(j => j.available == null).Select(
  69. async java => await java.CheckAsync(cancellationToken).NoCapture()
  70. ), progress).NoCapture();
  71. return javas.Where(j => j.available == true).ToList();
  72. }
  73. /// <summary>
  74. /// 将 Java 列表进行排序。
  75. /// 这同时会检查所有未检查的 Java。
  76. /// </summary>
  77. public static async Task<List<Java>> SortAsync(List<Java> javas, CancellationToken cancellationToken = default, ProgressProvider? progress = null) {
  78. javas = await JavaUtils.CheckAllAsync(javas, cancellationToken, progress).NoCapture();
  79. javas.Sort((left, right) => {
  80. // 优先使用带完整 Java 的文件夹列表中的 Java
  81. var isLeftInCandidateFolder = JavaUtils.CandidateFolders.Any(folder => PathUtils.IsParentOf(folder, left.Folder));
  82. var isRightInCandidateFolder = JavaUtils.CandidateFolders.Any(folder => PathUtils.IsParentOf(folder, right.Folder));
  83. if (isLeftInCandidateFolder != isRightInCandidateFolder) return isLeftInCandidateFolder ? -1 : 1;
  84. // 其次优先使用主版本号接近 25 的 Java
  85. var leftDistance = left.Version is null ? int.MaxValue : Math.Abs(left.Version.Major - 25);
  86. var rightDistance = right.Version is null ? int.MaxValue : Math.Abs(right.Version.Major - 25);
  87. return leftDistance.CompareTo(rightDistance);
  88. });
  89. return javas;
  90. }
  91. #region 搜索
  92. /// <summary>
  93. /// 存在完整 Java 的文件夹列表。
  94. /// </summary>
  95. internal static readonly List<string> CandidateFolders = [
  96. Paths.AppData + @".minecraft\runtime\", // 这也是 PCL 下载 Java 的路径
  97. Paths.AppData + @".hmcl\java\",
  98. Paths.AppData + @"ATLauncher\runtimes\minecraft\",
  99. Paths.AppData + @"ModrinthApp\meta\java_versions\",
  100. Paths.AppData + @"PrismLauncher\java\",
  101. Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + @"\curseforge\minecraft\Install\runtime\",
  102. Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + @"\.jdks\",
  103. Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + @"\.sdkman\candidates\java\",
  104. Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\.ftba\bin\runtime\",
  105. Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime\",
  106. Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86) + @"\Minecraft Launcher\runtime\",
  107. Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86) + @"\Minecraft\runtime\",
  108. Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + @"\Curse\Minecraft\Install\runtime\",
  109. Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + @"\Java\",
  110. Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + @"\Eclipse Adoptium\",
  111. Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + @"\Amazon Corretto\",
  112. Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + @"\Zulu\",
  113. ..DirectoryUtils.EnumerateDirectories(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + @"\Microsoft\", searchPattern: "jdk-*"),
  114. // 环境变量
  115. ..(Environment.GetEnvironmentVariable("JDK_HOME") + ";" + Environment.GetEnvironmentVariable("JAVA_HOME")).
  116. Split(";", true).Select(path => path.Trim(' ', '"'))
  117. ];
  118. /// <summary>
  119. /// 从常见文件夹和环境变量中搜索 Java,并将搜索结果写入设置。
  120. /// </summary>
  121. public static async Task RefreshListAsync(CancellationToken c = default, ProgressProvider? p = null) {
  122. // 搜索
  123. List<Java> javaList = await SearchFoldersAsync(true, CandidateFolders, c, p?.SplitTo(0.5)).NoCapture();
  124. // 将新发现的 Java 排在最前,原有列表保留既有顺序排在后面
  125. var oldJavaList = Configs.JavaList.Get()!;
  126. oldJavaList = await JavaUtils.CheckAllAsync(oldJavaList, c, p?.SplitTo(0.9)).NoCapture(); // 检查原有列表中的 Java
  127. HashSet<string> oldJavaFolders = new(oldJavaList.Select(j => j.Folder), StringComparer.OrdinalIgnoreCase);
  128. javaList.RemoveIf(j => oldJavaFolders.Contains(j.Folder)); // 此时 javaList 中只剩下新发现的 Java
  129. javaList = await JavaUtils.SortAsync(javaList, c).NoCapture(); // 将所有新发现的 Java 按优先级排序
  130. javaList.AddRange(oldJavaList);
  131. javaList = javaList.DistinctBy(j => j.Folder.Lower()).ToList();
  132. // 删掉已被移除的 Java
  133. javaList.RemoveIf(j => Configs.JavaRemovedList.Get()!.Contains(j.Folder, StringComparer.OrdinalIgnoreCase));
  134. // 写入设置
  135. Configs.JavaList.Set([..javaList]);
  136. Logger.Info($"Java 搜索完成,发现 {javaList.Count} 个 Java");
  137. }
  138. /// <summary>
  139. /// 并行搜索所有指定的文件夹及其子文件夹中的 Java,并检查其有效性。
  140. /// </summary>
  141. public static async Task<List<Java>> SearchFoldersAsync(bool includeSubDirectories, IEnumerable<string> folders, CancellationToken cancellationToken = default, ProgressProvider? progress = null) {
  142. // 并行搜索
  143. ConcurrentBag<(string Folder, bool HasReparsePoint, bool IsSpecialPath)> results = [];
  144. await TaskUtils.ForEachAsync(folders.Distinct(StringComparer.OrdinalIgnoreCase), 4, async (folder, cp) => {
  145. await Task.Run(() => {
  146. try {
  147. foreach (var file in DirectoryUtils.EnumerateFiles(folder, includeSubDirectories, "java.exe")) {
  148. cancellationToken.ThrowIfCancellationRequested();
  149. string targetFolder = PathUtils.AddSlashSuffix(PathUtils.ForCompare(PathUtils.RemoveLastPart(file)));
  150. // 判断文件夹是否包含重解析点(例如符号链接)
  151. static bool HasReparsePoint(FileSystemInfo info) {
  152. do {
  153. if (info.Attributes.HasFlag(FileAttributes.ReparsePoint)) return true;
  154. info = info is FileInfo fileInfo ? fileInfo.Directory : ((DirectoryInfo) info).Parent;
  155. } while (info is not null);
  156. return false;
  157. }
  158. // 结果
  159. var hasReparsePoint = HasReparsePoint(FileUtils.GetInfo(Path.Combine(targetFolder, "java.exe")));
  160. var isSpecialPath = targetFolder.Contains("java8path_target_") || targetFolder.Contains("javapath_target_") || targetFolder.Contains("javatmp") || targetFolder.Contains("system32");
  161. results.Add((targetFolder, hasReparsePoint, isSpecialPath));
  162. }
  163. } catch (UnauthorizedAccessException) {
  164. Logger.Info($"快速查找 Java 时没有权限({folder})");
  165. } catch (Exception ex) {
  166. ex.ThrowIfCanceled();
  167. Logger.Warn(ex, $"快速查找 Java 时出错({folder})");
  168. }
  169. }, cancellationToken).NoCapture();
  170. }, cancellationToken, progress?.SplitTo(0.2)).NoCapture();
  171. if (!results.Any()) return [];
  172. // 筛除重解析点
  173. if (results.All(r => r.HasReparsePoint)) {
  174. Logger.Warn("找到的所有 Java 都包含重解析点,保留全部结果");
  175. } else if (results.Any(r => r.HasReparsePoint)) {
  176. Logger.Info($"找到以下的 Java 包含重解析点,将它们从列表中排除:{results.Where(r => r.HasReparsePoint).Select(r => r.Folder).Join('、')}");
  177. results = [..results.Where(r => !r.HasReparsePoint)];
  178. }
  179. // 筛除特殊路径
  180. if (results.All(r => r.IsSpecialPath)) {
  181. Logger.Warn("找到的所有 Java 都包含特殊路径,保留全部结果");
  182. } else if (results.Any(r => r.IsSpecialPath)) {
  183. Logger.Info($"找到以下的 Java 包含特殊路径,将它们从列表中排除:{results.Where(r => r.IsSpecialPath).Select(r => r.Folder).Join('、')}");
  184. results = [..results.Where(r => !r.IsSpecialPath)];
  185. }
  186. // 检查并返回
  187. List<Java> javaList = results.Select(r => r.Folder).Distinct(StringComparer.OrdinalIgnoreCase).Select(folder => new Java(folder)).ToList();
  188. javaList = (await JavaUtils.CheckAllAsync(javaList, cancellationToken, progress?.SplitTo(1)).NoCapture()).ToList();
  189. javaList = await JavaUtils.SortAsync(javaList, cancellationToken).NoCapture();
  190. Logger.Info($"快速查找 Java 完成,共有 {javaList.Count} 个候选:{javaList.Join('、')}");
  191. return javaList;
  192. }
  193. #endregion
  194. }