using System.Diagnostics; using Phantom.Common.Data.Java; using Phantom.Utils.IO; using Phantom.Utils.Logging; using Serilog; namespace Phantom.Agent.Minecraft.Java; public sealed class JavaRuntimeDiscovery { private static readonly ILogger Logger = PhantomLogger.Create(nameof(JavaRuntimeDiscovery)); public static string? GetSystemSearchPath() { const string LinuxJavaPath = "/usr/lib/jvm"; if (OperatingSystem.IsLinux() && Directory.Exists(LinuxJavaPath)) { return LinuxJavaPath; } return null; } public static IAsyncEnumerable<JavaRuntimeExecutable> Scan(string folderPath) { return new JavaRuntimeDiscovery().ScanInternal(folderPath); } private readonly Dictionary<string, int> duplicateDisplayNames = new (); private async IAsyncEnumerable<JavaRuntimeExecutable> ScanInternal(string folderPath) { Logger.Information("Starting Java runtime scan in: {FolderPath}", folderPath); string javaExecutableName = OperatingSystem.IsWindows() ? "java.exe" : "java"; foreach (var binFolderPath in Directory.EnumerateDirectories(Paths.ExpandTilde(folderPath), "bin", new EnumerationOptions { MatchType = MatchType.Simple, RecurseSubdirectories = true, ReturnSpecialDirectories = false, IgnoreInaccessible = true, AttributesToSkip = FileAttributes.Hidden | FileAttributes.ReparsePoint | FileAttributes.System }).Order()) { var javaExecutablePath = Paths.NormalizeSlashes(Path.Combine(binFolderPath, javaExecutableName)); FileAttributes javaExecutableAttributes; try { javaExecutableAttributes = File.GetAttributes(javaExecutablePath); } catch (Exception) { continue; } if (javaExecutableAttributes.HasFlag(FileAttributes.ReparsePoint)) { continue; } Logger.Information("Found candidate Java executable: {JavaExecutablePath}", javaExecutablePath); JavaRuntime? foundRuntime; try { foundRuntime = await TryReadJavaRuntimeInformationFromProcess(javaExecutablePath); } catch (OperationCanceledException) { Logger.Error("Java process did not exit in time."); continue; } catch (Exception e) { Logger.Error(e, "Caught exception while reading Java version information."); continue; } if (foundRuntime == null) { Logger.Error("Java executable did not output version information."); continue; } Logger.Information("Found Java {DisplayName} at: {Path}", foundRuntime.DisplayName, javaExecutablePath); yield return new JavaRuntimeExecutable(javaExecutablePath, foundRuntime); } } private async Task<JavaRuntime?> TryReadJavaRuntimeInformationFromProcess(string javaExecutablePath) { var startInfo = new ProcessStartInfo { FileName = javaExecutablePath, WorkingDirectory = Path.GetDirectoryName(javaExecutablePath), Arguments = "-XshowSettings:properties -version", RedirectStandardInput = false, RedirectStandardOutput = false, RedirectStandardError = true, UseShellExecute = false }; var process = new Process { StartInfo = startInfo }; var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)); try { process.Start(); JavaRuntimeBuilder runtimeBuilder = new (); while (await process.StandardError.ReadLineAsync(cancellationTokenSource.Token) is {} line) { ExtractJavaVersionPropertiesFromLine(line, runtimeBuilder); JavaRuntime? runtime = runtimeBuilder.TryBuild(duplicateDisplayNames); if (runtime != null) { return runtime; } } await process.WaitForExitAsync(cancellationTokenSource.Token); return null; } finally { process.Dispose(); cancellationTokenSource.Dispose(); } } private static void ExtractJavaVersionPropertiesFromLine(ReadOnlySpan<char> line, JavaRuntimeBuilder runtimeBuilder) { line = line.TrimStart(); int separatorIndex = line.IndexOf('='); if (separatorIndex == -1) { return; } var propertyName = line[..separatorIndex].TrimEnd(); if (propertyName.Equals("java.specification.version", StringComparison.Ordinal)) { runtimeBuilder.MainVersion = ExtractValue(line, separatorIndex); } else if (propertyName.Equals("java.version", StringComparison.Ordinal)) { runtimeBuilder.FullVersion = ExtractValue(line, separatorIndex); } else if (propertyName.Equals("java.vm.vendor", StringComparison.Ordinal)) { runtimeBuilder.Vendor = ExtractValue(line, separatorIndex); } } private static string ExtractValue(ReadOnlySpan<char> line, int separatorIndex) { return line[(separatorIndex + 1)..].Trim().ToString(); } private sealed class JavaRuntimeBuilder { public string? MainVersion { get; set; } = null; public string? FullVersion { get; set; } = null; public string? Vendor { get; set; } = null; public JavaRuntime? TryBuild(Dictionary<string, int> duplicateDisplayNames) { if (MainVersion == null || FullVersion == null || Vendor == null) { return null; } else { string displayName = $"{FullVersion} ({Vendor})"; if (duplicateDisplayNames.TryGetValue(displayName, out int usedCount)) { ++usedCount; displayName += " (" + usedCount + ")"; duplicateDisplayNames[displayName] = usedCount; } else { duplicateDisplayNames[displayName] = 1; } return new JavaRuntime(MainVersion, FullVersion, displayName); } } } }