1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2025-09-30 23:02:48 +02:00
Files
Minecraft-Phantom-Panel/Agent/Phantom.Agent.Minecraft/Java/JavaRuntimeDiscovery.cs

164 lines
5.7 KiB
C#

using System.Diagnostics;
using System.Runtime.CompilerServices;
using Phantom.Common.Data.Java;
using Phantom.Utils.Collections;
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 async Task<JavaRuntimeRepository> Scan(string folderPath, CancellationToken cancellationToken) {
var runtimes = await new JavaRuntimeDiscovery().ScanInternal(folderPath, cancellationToken).ToImmutableArrayAsync(cancellationToken);
return new JavaRuntimeRepository(runtimes);
}
private readonly Dictionary<string, int> duplicateDisplayNames = new ();
private async IAsyncEnumerable<JavaRuntimeExecutable> ScanInternal(string folderPath, [EnumeratorCancellation] CancellationToken cancellationToken) {
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()) {
cancellationToken.ThrowIfCancellationRequested();
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, cancellationToken);
} 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, CancellationToken cancellationToken) {
var startInfo = new ProcessStartInfo {
FileName = javaExecutablePath,
WorkingDirectory = Path.GetDirectoryName(javaExecutablePath),
Arguments = "-XshowSettings:properties -version",
RedirectStandardInput = false,
RedirectStandardOutput = false,
RedirectStandardError = true,
UseShellExecute = false,
};
using var timeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken);
using var process = new Process();
process.StartInfo = startInfo;
process.Start();
JavaRuntimeBuilder runtimeBuilder = new ();
while (await process.StandardError.ReadLineAsync(combinedCancellationTokenSource.Token) is {} line) {
ExtractJavaVersionPropertiesFromLine(line, runtimeBuilder);
JavaRuntime? runtime = runtimeBuilder.TryBuild(duplicateDisplayNames);
if (runtime != null) {
return runtime;
}
}
await process.WaitForExitAsync(combinedCancellationTokenSource.Token);
return null;
}
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);
}
}
}
}