Java.cs 15 KB

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