using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; using Microsoft.Win32; using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; /// /// Führt Sonic-Container-Operationen über die offizielle MF Management Runtime API aus /// (JMSConnectorClient + MFProxyFactory + IAgentProxy.restart), per Java-Hilfsprogramm /// und Client-JARs unter SonicHome/lib bzw. MfClientLibPath. /// Nutzt ConnectionUrl + Username/Password aus appsettings (SMC / Domain Manager). /// public sealed class SonicMfApiExecutor { private const int MaxJavaSearchDepth = 6; private const int MaxJavaFilesToScan = 80; private const int MaxCheckedPathsInError = 40; private readonly SonicConnection _connection; public SonicMfApiExecutor(SonicConnection connection) { _connection = connection; } /// /// Liefert den konfigurierten SonicHome-Pfad und die gefundene java.exe (oder null). /// Für UI-Status und Diagnose. /// public (string SonicHome, string? JavaExe) ResolveRuntimePaths() { JavaDiscoveryResult result = DiscoverJava(); string sonicHome = string.IsNullOrWhiteSpace(_connection.SonicHome) ? "(leer)" : _connection.SonicHome.Trim(); return (sonicHome, result.JavaExe); } public Task<(bool Success, string? Error)> TestConnectionAsync(CancellationToken cancellationToken = default) => RunAsync("ping", container: null, cancellationToken); public async Task<(bool Success, IReadOnlyList Containers, string? Error)> ListContainersAsync( CancellationToken cancellationToken = default) { (bool ok, string? output, string? error) = await RunRawAsync("list", null, cancellationToken); if (!ok) { return (false, [], error ?? output); } List names = (output ?? string.Empty) .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(l => !l.StartsWith("OK:", StringComparison.OrdinalIgnoreCase)) .Where(l => !l.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase)) .Where(l => !l.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase)) .Where(l => !l.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase)) .Where(l => !l.StartsWith("Usage:", StringComparison.OrdinalIgnoreCase)) .ToList(); return (true, names, null); } public async Task<(bool Success, string? Output, string? Error)> RestartAsync( string containerName, CancellationToken cancellationToken = default) { (bool ok, string? output, string? error) = await RunRawAsync("restart", containerName, cancellationToken); if (!ok) { return (false, output, error); } bool invoked = (output ?? string.Empty) .Contains("OK:RestartInvoked", StringComparison.OrdinalIgnoreCase); return invoked ? (true, output, error) : (false, output, error ?? "Kein OK:RestartInvoked von SonicMfContainerTool."); } private async Task<(bool Success, string? Error)> RunAsync( string command, string? container, CancellationToken cancellationToken) { (bool ok, string? output, string? error) = await RunRawAsync(command, container, cancellationToken); if (ok) { return (true, null); } return (false, error ?? output); } private async Task<(bool Success, string? Output, string? Error)> RunRawAsync( string command, string? container, CancellationToken cancellationToken) { (bool prepared, string? javaExe, string? classDir, string? classpath, string? prepareError) = await PrepareToolAsync(cancellationToken); if (!prepared) { return (false, null, prepareError); } // ArgumentList statt manueller Quotes: sonst landet \"...\" im Classpath // und Java findet JMSConnectorAddress trotz vorhandener JARs nicht. ProcessStartInfo psi = new() { FileName = javaExe!, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8, WorkingDirectory = classDir! }; psi.ArgumentList.Add("-cp"); psi.ArgumentList.Add(classpath!); psi.ArgumentList.Add("SonicMfContainerTool"); psi.ArgumentList.Add(command); psi.ArgumentList.Add("--domain"); psi.ArgumentList.Add(_connection.DomainName); psi.ArgumentList.Add("--url"); psi.ArgumentList.Add(_connection.ConnectionUrl); psi.ArgumentList.Add("--user"); psi.ArgumentList.Add(_connection.Username); psi.ArgumentList.Add("--timeout"); psi.ArgumentList.Add(Math.Clamp(_connection.TimeoutSeconds, 5, 600).ToString()); if (!string.IsNullOrWhiteSpace(container)) { psi.ArgumentList.Add("--container"); psi.ArgumentList.Add(container); } psi.Environment["ESB_SONIC_PASSWORD"] = _connection.Password ?? string.Empty; using Process process = new() { StartInfo = psi }; if (!process.Start()) { return (false, null, "Java-Prozess konnte nicht gestartet werden."); } using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 10, 600))); Task stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken); Task stderrTask = process.StandardError.ReadToEndAsync(cancellationToken); try { await process.WaitForExitAsync(timeoutCts.Token); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { try { process.Kill(entireProcessTree: true); } catch { /* ignore */ } return (false, null, $"MfApi Timeout nach {_connection.TimeoutSeconds}s."); } string stdout = (await stdoutTask).Trim(); string stderr = (await stderrTask).Trim(); string combined = string.Join("\n", new[] { stdout, stderr }.Where(s => s.Length > 0)); if (process.ExitCode == 0 && !combined.Contains("ERROR:", StringComparison.OrdinalIgnoreCase)) { return (true, combined, null); } string err = ExtractError(combined) ?? $"SonicMfContainerTool ExitCode={process.ExitCode}"; if (err.Contains("ClassNotFoundException", StringComparison.OrdinalIgnoreCase) || combined.Contains("JMSConnectorAddress", StringComparison.OrdinalIgnoreCase)) { err += "\n\nClasspath unvollständig: com.sonicsw.mf.jmx.client.JMSConnectorAddress fehlt.\n" + "Bitte in appsettings MfClientLibPath auf den Ordner mit mgmt_client.jar / mfcontext.jar / sonic_Client.jar setzen\n" + $"(aktuell SonicHome='{_connection.SonicHome}').\n" + "Hinweis: Container-Kurzname laut SMC ist 'ct-ZADBService' (nicht der Verbindungs-Alias DE-Test)."; } if (!combined.Contains("ToolVersion=", StringComparison.OrdinalIgnoreCase)) { err += "\n\nWARN: Ausgabe enthält kein INFO:ToolVersion=… – vermutlich alte SonicMfContainerTool.class.\n" + "App neu bauen (Debug+Release), Tools-Ordner im Output prüfen, Temp esb-sonic-mf* löschen."; } // Volle Ausgabe mitliefern (INFO/WARN), nicht nur die ERROR-Zeile string detail = string.IsNullOrWhiteSpace(combined) ? err : err + "\n\n--- Tool-Output ---\n" + combined; return (false, combined.Length > 0 ? combined : null, detail); } private static void TryDeleteTempToolCache() { try { string dir = Path.Combine(Path.GetTempPath(), "esb-sonic-mf-j8"); if (Directory.Exists(dir)) { Directory.Delete(dir, recursive: true); } } catch { /* ignore */ } } private async Task<(bool Ok, string? JavaExe, string? ClassDir, string? Classpath, string? Error)> PrepareToolAsync(CancellationToken cancellationToken) { // Tool-Bytecode ist Java 8 (major 52). Sonic-mitgelieferte JREs sind oft Java 6/7 → unbrauchbar. // Deshalb: Runtime mit major>=52 bevorzugen (PATH/JAVA_HOME/Registry), Libs weiter aus SonicHome. const int minClassMajor = 52; JavaDiscoveryResult javaResult = DiscoverJava(); string? javaExe = javaResult.JavaExe; int runtimeMajor = javaExe is null ? 0 : await GetJavaClassMajorAsync(javaExe, cancellationToken); if (javaExe is null || runtimeMajor < minClassMajor) { (string? newerJava, int newerMajor, List probed) = await FindJavaWithMinimumMajorAsync(minClassMajor, cancellationToken); if (newerJava is not null) { javaExe = newerJava; runtimeMajor = newerMajor; javaResult.CheckedPaths.Add($"UPGRADE:RuntimeJava8+={newerJava} major={newerMajor}"); } } if (javaExe is null) { string sonicHome = string.IsNullOrWhiteSpace(_connection.SonicHome) ? "(SonicHome leer / nicht gesetzt)" : _connection.SonicHome.Trim(); string checkedList = javaResult.CheckedPaths.Count == 0 ? "(keine Kandidaten gesammelt)" : string.Join("\n - ", javaResult.CheckedPaths.Take(MaxCheckedPathsInError)); return (false, null, null, null, "Java nicht gefunden.\n" + $"Verwendetes SonicHome: '{sonicHome}'\n" + "Konfiguration: JavaPath/JavaHome auf eine Java-8+-Runtime setzen.\n" + "Geprüfte Pfade:\n - " + checkedList); } if (runtimeMajor < minClassMajor) { return (false, null, null, null, "Gefundene Java-Runtime ist zu alt für SonicMfContainerTool (braucht Java 8 / major 52).\n" + $"Aktuell: {javaExe} (class major={runtimeMajor}).\n" + "Die JRE unter SonicHome ist oft Java 6/7 – bitte Java 8+ installieren und in appsettings setzen:\n" + " \"JavaPath\": \"C:\\\\Program Files\\\\Java\\\\jre1.8.0_xxx\\\\bin\\\\java.exe\"\n" + "Sonic-Client-JARs bleiben unter SonicHome\\lib."); } (string? libClasspath, string? libError) = ResolveSonicClasspathDetailed(); if (libClasspath is null) { return (false, null, null, null, libError); } // Vorcompilierte .class nur nutzen, wenn Class-Version passt. // Tool-Verzeichnis ZUERST im Classpath, damit keine alte Klasse aus Temp/JAR gewinnt. if (TryResolvePrebuiltTool(runtimeMajor, out string? prebuiltDir, out string? prebuiltEntry, out string? prebuiltError) && prebuiltDir is not null && prebuiltEntry is not null) { // Veraltete Temp-Kopien entfernen (früheres Compile-Cache). TryDeleteTempToolCache(); string classpath = prebuiltEntry + Path.PathSeparator + libClasspath; return (true, javaExe, prebuiltDir, classpath, null); } // Zur Laufzeit aus .java für Java 8 kompilieren (--release 8 / -source 1.8 -target 1.8). string? sourcePath = ResolveToolSourcePath(); if (sourcePath is null) { return (false, null, null, null, (prebuiltError ?? "Kein SonicMfContainerTool gefunden.") + " Tools\\SonicMfContainerTool.java/.class fehlen.\n" + $"Runtime-Java: {javaExe} (class major max={runtimeMajor})."); } TryDeleteTempToolCache(); string workDir = Path.Combine(Path.GetTempPath(), "esb-sonic-mf-j8"); Directory.CreateDirectory(workDir); string javaTarget = Path.Combine(workDir, "SonicMfContainerTool.java"); string classFile = Path.Combine(workDir, "SonicMfContainerTool.class"); File.Copy(sourcePath, javaTarget, overwrite: true); string compileClasspath = libClasspath; // workDir zuerst string runtimeClasspath = workDir + Path.PathSeparator + libClasspath; bool classOk = File.Exists(classFile) && ReadClassFileMajorVersion(classFile) is int existingMajor && existingMajor <= runtimeMajor && File.GetLastWriteTimeUtc(classFile) >= File.GetLastWriteTimeUtc(sourcePath); if (!classOk) { string? javac = ResolveJavacExecutable(javaExe); if (javac is null) { return (false, null, null, null, "SonicMfContainerTool muss für Java 8 gebaut werden, aber javac fehlt.\n" + "Tools\\SonicMfContainerTool.class (major<=52) mit ausliefern oder JDK installieren.\n" + $"Gefundene java.exe: {javaExe}"); } // Wichtig: ohne --release 8 erzeugt modernes javac major 65 → UnsupportedClassVersionError auf Sonic-JRE 8. string releaseArgs = SupportsJavacRelease8(javac) ? "--release 8" : "-source 1.8 -target 1.8"; ProcessStartInfo compilePsi = new() { FileName = javac, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; if (releaseArgs.StartsWith("--release", StringComparison.Ordinal)) { compilePsi.ArgumentList.Add("--release"); compilePsi.ArgumentList.Add("8"); } else { compilePsi.ArgumentList.Add("-source"); compilePsi.ArgumentList.Add("1.8"); compilePsi.ArgumentList.Add("-target"); compilePsi.ArgumentList.Add("1.8"); } compilePsi.ArgumentList.Add("-encoding"); compilePsi.ArgumentList.Add("UTF-8"); compilePsi.ArgumentList.Add("-cp"); compilePsi.ArgumentList.Add(compileClasspath); compilePsi.ArgumentList.Add("-d"); compilePsi.ArgumentList.Add(workDir); compilePsi.ArgumentList.Add(javaTarget); using Process compile = new() { StartInfo = compilePsi }; if (!compile.Start()) { return (false, null, null, null, "javac konnte nicht gestartet werden."); } string cOut = await compile.StandardOutput.ReadToEndAsync(cancellationToken); string cErr = await compile.StandardError.ReadToEndAsync(cancellationToken); await compile.WaitForExitAsync(cancellationToken); int? builtMajor = File.Exists(classFile) ? ReadClassFileMajorVersion(classFile) : null; if (compile.ExitCode != 0 || builtMajor is null || builtMajor > runtimeMajor) { return (false, null, null, null, "Kompilieren von SonicMfContainerTool für Java 8 fehlgeschlagen.\n" + Truncate((cErr + "\n" + cOut).Trim()) + $"\nErzeugt major={builtMajor?.ToString() ?? "?"}, Runtime erlaubt <={runtimeMajor}.\n" + $"Classpath: {Truncate(compileClasspath, 240)}"); } } return (true, javaExe, workDir, runtimeClasspath, null); } private bool TryResolvePrebuiltTool( int runtimeMajor, out string? workDir, out string? classpathEntry, out string? error) { workDir = null; classpathEntry = null; error = null; string[] searchDirs = [ Path.Combine(AppContext.BaseDirectory, "Tools"), Path.Combine(Directory.GetCurrentDirectory(), "Tools"), AppContext.BaseDirectory ]; foreach (string dir in searchDirs.Where(Directory.Exists)) { string classFile = Path.Combine(dir, "SonicMfContainerTool.class"); if (File.Exists(classFile)) { int? major = ReadClassFileMajorVersion(classFile); // Alte Builds waren ~7KB; MBean-Stop-Tool ist >10KB. long len = new FileInfo(classFile).Length; if (major is not null && major <= runtimeMajor && len >= 10_000) { workDir = dir; classpathEntry = dir; return true; } if (major is not null && major <= runtimeMajor && len < 10_000) { error = $"Tools\\SonicMfContainerTool.class ist veraltet ({len} Bytes). Bitte neu bauen/pullen (erwartet >=10000)."; continue; } error = $"Tools\\SonicMfContainerTool.class ist zu neu (major={major}, braucht <={runtimeMajor} / Java 8)."; continue; } // JAR absichtlich nicht blind nutzen – oft noch mit Java 21 gebaut. // Nur .class mit geprüftem major<=runtimeMajor. } error ??= "Tools\\SonicMfContainerTool.class (Java 8) nicht gefunden."; return false; } private static int? ReadClassFileMajorVersion(string classFile) { try { byte[] header = new byte[8]; using FileStream fs = File.OpenRead(classFile); if (fs.Read(header, 0, 8) < 8) { return null; } // CA FE BA BE | minor | major if (header[0] != 0xCA || header[1] != 0xFE || header[2] != 0xBA || header[3] != 0xBE) { return null; } return (header[6] << 8) | header[7]; } catch { return null; } } private static async Task GetJavaClassMajorAsync(string javaExe, CancellationToken cancellationToken) { try { ProcessStartInfo psi = new() { FileName = javaExe, Arguments = "-version", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; using Process p = new() { StartInfo = psi }; if (!p.Start()) { return 0; } string err = await p.StandardError.ReadToEndAsync(cancellationToken); string output = await p.StandardOutput.ReadToEndAsync(cancellationToken); await p.WaitForExitAsync(cancellationToken); return ParseJavaVersionToClassMajor(err + "\n" + output); } catch { return 0; } } private static int ParseJavaVersionToClassMajor(string versionText) { string text = versionText.ToLowerInvariant(); // 1.8.x → 52, 1.7.x → 51, 1.6.x → 50 Match legacy = Regex.Match(text, @"version\s+""1\.(\d+)"); if (legacy.Success && int.TryParse(legacy.Groups[1].Value, out int minor)) { return 44 + minor; // 1.6→50, 1.7→51, 1.8→52 } Match modern = Regex.Match(text, @"version\s+""(\d+)"); if (modern.Success && int.TryParse(modern.Groups[1].Value, out int major) && major >= 9) { return 44 + major; // 9→53 … 21→65 } return 0; } /// /// Sucht eine java.exe mit mindestens der gewünschten Class-Major-Version /// (für Tool-Bytecode Java 8 = 52), unabhängig von der alten Sonic-JRE. /// private async Task<(string? JavaExe, int Major, List Probed)> FindJavaWithMinimumMajorAsync( int minMajor, CancellationToken cancellationToken) { List probed = []; List candidates = []; void AddCandidate(string? path) { if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) { return; } string full = Path.GetFullPath(path); if (!candidates.Contains(full, StringComparer.OrdinalIgnoreCase)) { candidates.Add(full); } } foreach (string? configured in new[] { _connection.JavaPath, _connection.JavaHome }) { if (string.IsNullOrWhiteSpace(configured)) continue; AddCandidate(ResolveToolFromConfiguredPath(configured, JavaExeName, probed)); } foreach (string envName in new[] { "JAVA_HOME", "JRE_HOME", "JDK_HOME" }) { AddCandidate(ResolveJavaFromHomeDir(Environment.GetEnvironmentVariable(envName), probed, envName)); } AddCandidate(FindOnPath(JavaExeName, probed)); AddCandidate(FindOnPath("java", probed)); AddCandidate(FindJavaInWindowsRegistry(probed)); AddCandidate(FindJavaViaWhereExe(probed)); AddCandidate(FindJavaInCommonInstallLocations(probed)); // Program Files Java/Adoptium/Temurin grob scannen foreach (string root in new[] { Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"C:\Program Files\Eclipse Adoptium", @"C:\Program Files\Java", @"C:\Program Files\Microsoft", @"C:\Program Files\Amazon Corretto" }) { if (!Directory.Exists(root)) continue; try { foreach (string javaExe in Directory.EnumerateFiles(root, "java.exe", SearchOption.AllDirectories) .Take(40)) { AddCandidate(javaExe); } } catch { // ignore } } foreach (string candidate in candidates) { int major = await GetJavaClassMajorAsync(candidate, cancellationToken); probed.Add($"{candidate} => major={major}"); if (major >= minMajor) { return (candidate, major, probed); } } return (null, 0, probed); } private static bool SupportsJavacRelease8(string javacPath) { try { using Process p = Process.Start(new ProcessStartInfo { FileName = javacPath, Arguments = "-version", UseShellExecute = false, RedirectStandardError = true, RedirectStandardOutput = true, CreateNoWindow = true })!; string text = (p.StandardError.ReadToEnd() + p.StandardOutput.ReadToEnd()).ToLowerInvariant(); p.WaitForExit(5000); // javac 9+ supports --release return !text.Contains("1.8") && !text.Contains("1.7") && !text.Contains("1.6"); } catch { return false; } } /// /// Baut einen Classpath mit echten Sonic-Management-Client-JARs /// (inkl. com.sonicsw.mf.jmx.client.JMSConnectorAddress). /// private (string? Classpath, string? Error) ResolveSonicClasspathDetailed() { List<(string Path, string Label)> searchRoots = []; if (!string.IsNullOrWhiteSpace(_connection.MfClientLibPath)) { searchRoots.Add((_connection.MfClientLibPath.Trim().Trim('"'), "MfClientLibPath")); } string? mqHome = Environment.GetEnvironmentVariable("MQ_HOME"); string? esbHome = Environment.GetEnvironmentVariable("ESB_HOME") ?? Environment.GetEnvironmentVariable("SONIC_HOME") ?? Environment.GetEnvironmentVariable("SONICMQ_HOME"); if (!string.IsNullOrWhiteSpace(mqHome)) { searchRoots.Add((mqHome.Trim().Trim('"'), "MQ_HOME")); } if (!string.IsNullOrWhiteSpace(esbHome)) { searchRoots.Add((esbHome.Trim().Trim('"'), "ESB_HOME/SONIC_HOME")); } if (!string.IsNullOrWhiteSpace(_connection.SonicHome)) { string sonicHome = _connection.SonicHome.Trim().Trim('"'); searchRoots.Add((sonicHome, "SonicHome")); string? parent = Directory.GetParent(sonicHome)?.FullName; if (!string.IsNullOrWhiteSpace(parent)) { searchRoots.Add((parent, "SonicHome-Parent")); } } List checkedPaths = []; HashSet seenDirs = new(StringComparer.OrdinalIgnoreCase); List markerHits = []; foreach ((string root, string label) in searchRoots) { if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root)) { checkedPaths.Add($"{label}='{root}' (fehlt)"); continue; } checkedPaths.Add($"{label}='{root}'"); // 1) Bekannte Lib-Ordner List libCandidates = [root, Path.Combine(root, "lib"), .. EnumerateLikelyLibRoots(root)]; // 2) Rekursiv nach Marker-JARs (SMC-only oft unter MQ*/lib oder Client/lib) foreach (string jar in EnumerateMarkerJars(root, maxDepth: 5, maxFiles: 400)) { string? dir = Path.GetDirectoryName(jar); if (!string.IsNullOrWhiteSpace(dir)) { libCandidates.Add(dir); markerHits.Add(jar); } } foreach (string libDir in libCandidates) { if (string.IsNullOrWhiteSpace(libDir) || !seenDirs.Add(libDir) || !Directory.Exists(libDir)) { continue; } if (!DirectoryHasRequiredSonicClientJars(libDir, out string? detail)) { if (!string.IsNullOrWhiteSpace(detail)) { checkedPaths.Add($" skip '{libDir}': {detail}"); } continue; } string? classpath = BuildClasspathFromLibDirs(CollectRelatedLibDirs(libDir, root)); if (!string.IsNullOrWhiteSpace(classpath)) { return (classpath, null); } } } string sonicHomeVal = _connection.SonicHome ?? string.Empty; string markerHint = markerHits.Count == 0 ? "Keine Marker-JARs (mgmt_client/mfcontext/sonic_Client) gefunden." : "Marker-Treffer (unvollständig):\n - " + string.Join("\n - ", markerHits.Distinct(StringComparer.OrdinalIgnoreCase).Take(12)); return (null, "Sonic-Client-JARs für MfApi fehlen (Class JMSConnectorAddress).\n" + $"Verwendetes SonicHome: '{(string.IsNullOrWhiteSpace(sonicHomeVal) ? "(leer)" : sonicHomeVal)}'\n" + "Benötigt u.a.: mgmt_client.jar, mfcontext.jar, sonic_Client.jar (Ordner mit *.jar).\n" + "Lösung: MfClientLibPath auf den Ordner setzen, in dem diese JARs liegen\n" + " (oft ...\\MQ10.0\\lib oder SMC-Client\\lib – nicht nur das JRE).\n" + markerHint + "\n" + "Geprüft:\n - " + string.Join("\n - ", checkedPaths.Take(40))); } private static IEnumerable EnumerateMarkerJars(string root, int maxDepth, int maxFiles) { if (!Directory.Exists(root)) { yield break; } string[] markers = [ "mgmt_client", "mfcontext", "sonic_Client", "sonic_client", "mf_client", "mf-client", "mgmt-client", "sonic_Crypto", "sonic_XA" ]; Queue<(string Dir, int Depth)> queue = new(); queue.Enqueue((root, 0)); int yielded = 0; HashSet seen = new(StringComparer.OrdinalIgnoreCase); while (queue.Count > 0 && yielded < maxFiles) { (string dir, int depth) = queue.Dequeue(); if (!seen.Add(dir)) { continue; } IEnumerable files; try { files = Directory.EnumerateFiles(dir, "*.jar", SearchOption.TopDirectoryOnly); } catch { continue; } foreach (string file in files) { string name = Path.GetFileNameWithoutExtension(file); if (markers.Any(m => name.Contains(m, StringComparison.OrdinalIgnoreCase))) { yielded++; yield return file; if (yielded >= maxFiles) { yield break; } } } if (depth >= maxDepth) { continue; } IEnumerable children; try { children = Directory.EnumerateDirectories(dir); } catch { continue; } foreach (string child in children) { string leaf = Path.GetFileName(child); // Skip große / irrelevante Bäume if (leaf.Equals("docs", StringComparison.OrdinalIgnoreCase) || leaf.Equals("documentation", StringComparison.OrdinalIgnoreCase) || leaf.Equals("samples", StringComparison.OrdinalIgnoreCase) || leaf.Equals("log", StringComparison.OrdinalIgnoreCase) || leaf.Equals("logs", StringComparison.OrdinalIgnoreCase) || leaf.Equals("tmp", StringComparison.OrdinalIgnoreCase) || leaf.Equals("temp", StringComparison.OrdinalIgnoreCase) || leaf.StartsWith("jre", StringComparison.OrdinalIgnoreCase) || leaf.Equals("jdk", StringComparison.OrdinalIgnoreCase)) { continue; } queue.Enqueue((child, depth + 1)); } } } private static List CollectRelatedLibDirs(string primaryLibDir, string searchRoot) { HashSet dirs = new(StringComparer.OrdinalIgnoreCase) { primaryLibDir }; // Sibling-Libs oft: MQ/lib + ESB/lib + common/lib string? parent = Directory.GetParent(primaryLibDir)?.FullName; if (!string.IsNullOrWhiteSpace(parent)) { foreach (string sibling in new[] { Path.Combine(parent, "lib"), Path.Combine(parent, "common", "lib"), Path.Combine(parent, "client", "lib") }) { if (Directory.Exists(sibling)) { dirs.Add(sibling); } } string? grand = Directory.GetParent(parent)?.FullName; if (!string.IsNullOrWhiteSpace(grand)) { foreach (string nested in EnumerateLikelyLibRoots(grand)) { dirs.Add(nested); } } } if (Directory.Exists(searchRoot)) { foreach (string nested in EnumerateLikelyLibRoots(searchRoot)) { dirs.Add(nested); } } return dirs.Where(Directory.Exists).ToList(); } private static string? BuildClasspathFromLibDirs(IEnumerable libDirs) { HashSet jarFiles = new(StringComparer.OrdinalIgnoreCase); foreach (string dir in libDirs) { if (!Directory.Exists(dir)) { continue; } try { foreach (string jar in Directory.EnumerateFiles(dir, "*.jar", SearchOption.TopDirectoryOnly)) { jarFiles.Add(jar); } } catch { // ignore unreadable dirs } } List entries = jarFiles .OrderBy(j => j, StringComparer.OrdinalIgnoreCase) .ToList(); if (entries.Count == 0) { return null; } // Classpath-Länge begrenzen: zu viele Einträge → CreateProcess-Fehler if (entries.Count > 250) { entries = entries.Take(250).ToList(); } return string.Join(Path.PathSeparator, entries); } private static IEnumerable EnumerateLikelyLibRoots(string root) { if (!Directory.Exists(root)) { yield break; } string[] preferred = [ Path.Combine(root, "MQ", "lib"), Path.Combine(root, "mq", "lib"), Path.Combine(root, "ESB", "lib"), Path.Combine(root, "esb", "lib"), Path.Combine(root, "client", "lib"), Path.Combine(root, "Client", "lib") ]; foreach (string path in preferred) { if (Directory.Exists(path)) { yield return path; } } IEnumerable children; try { children = Directory.EnumerateDirectories(root); } catch { yield break; } foreach (string child in children) { string name = Path.GetFileName(child); if (name.StartsWith("MQ", StringComparison.OrdinalIgnoreCase) || name.StartsWith("ESB", StringComparison.OrdinalIgnoreCase) || name.Contains("Sonic", StringComparison.OrdinalIgnoreCase)) { string lib = Path.Combine(child, "lib"); if (Directory.Exists(lib)) { yield return lib; } } } } /// /// Strikt: mindestens ein MF/Management-Client-JAR (nicht irgendein JAR-Ordner). /// private static bool DirectoryHasRequiredSonicClientJars(string path, out string? detail) { detail = null; try { string[] jars = Directory.EnumerateFiles(path, "*.jar", SearchOption.TopDirectoryOnly).ToArray(); if (jars.Length == 0) { detail = "keine *.jar"; return false; } string[] strong = [ "mgmt_client", "mfcontext", "sonic_Client", "sonic_client", "mf_client", "mf-client" ]; bool hasStrong = jars.Any(j => strong.Any(m => Path.GetFileName(j).Contains(m, StringComparison.OrdinalIgnoreCase))); if (!hasStrong) { detail = $"{jars.Length} JARs, aber ohne mgmt_client/mfcontext/sonic_Client"; return false; } return true; } catch (Exception ex) { detail = ex.Message; return false; } } private static string? ResolveToolSourcePath() { string[] candidates = [ Path.Combine(AppContext.BaseDirectory, "Tools", "SonicMfContainerTool.java"), Path.Combine(Directory.GetCurrentDirectory(), "Tools", "SonicMfContainerTool.java"), Path.Combine(AppContext.BaseDirectory, "SonicMfContainerTool.java") ]; return candidates.FirstOrDefault(File.Exists); } private sealed class JavaDiscoveryResult { public string? JavaExe { get; set; } public List CheckedPaths { get; } = []; } private string? ResolveJavaExecutable() => DiscoverJava().JavaExe; private JavaDiscoveryResult DiscoverJava() { JavaDiscoveryResult result = new(); List checkedPaths = result.CheckedPaths; foreach (string? configured in new[] { _connection.JavaPath, _connection.JavaHome }) { if (string.IsNullOrWhiteSpace(configured)) { continue; } string? fromConfig = ResolveToolFromConfiguredPath(configured, JavaExeName, checkedPaths); if (fromConfig is not null) { result.JavaExe = fromConfig; return result; } } foreach (string envName in new[] { "JAVA_HOME", "JRE_HOME", "JDK_HOME" }) { string? home = Environment.GetEnvironmentVariable(envName); string? fromEnv = ResolveJavaFromHomeDir(home, checkedPaths, envName); if (fromEnv is not null) { result.JavaExe = fromEnv; return result; } } string? fromPath = FindOnPath(JavaExeName, checkedPaths) ?? FindOnPath("java", checkedPaths); if (fromPath is not null) { result.JavaExe = fromPath; return result; } string? sonicHome = string.IsNullOrWhiteSpace(_connection.SonicHome) ? null : _connection.SonicHome.Trim(); if (!string.IsNullOrWhiteSpace(sonicHome)) { string? fromScripts = FindJavaFromSonicScripts(sonicHome, checkedPaths); if (fromScripts is not null) { result.JavaExe = fromScripts; return result; } string? direct = TryKnownJavaLayouts(sonicHome, checkedPaths); if (direct is not null) { result.JavaExe = direct; return result; } string? nested = FindJavaUnderSonicHome(sonicHome, checkedPaths); if (nested is not null) { result.JavaExe = nested; return result; } string? recursive = FindJavaRecursive(sonicHome, checkedPaths, MaxJavaSearchDepth); if (recursive is not null) { result.JavaExe = recursive; return result; } // Parent / sibling MQ / ESB Layouts string? parent = Directory.GetParent(sonicHome)?.FullName; if (!string.IsNullOrWhiteSpace(parent)) { string? fromParent = TryKnownJavaLayouts(parent, checkedPaths) ?? FindJavaUnderSonicHome(parent, checkedPaths) ?? FindJavaRecursive(parent, checkedPaths, depth: 3); if (fromParent is not null) { result.JavaExe = fromParent; return result; } foreach (string siblingRoot in EnumerateSiblingMqEsbRoots(parent, sonicHome)) { string? fromSibling = TryKnownJavaLayouts(siblingRoot, checkedPaths) ?? FindJavaUnderSonicHome(siblingRoot, checkedPaths); if (fromSibling is not null) { result.JavaExe = fromSibling; return result; } } } } foreach (string commonRoot in EnumerateCommonSonicRoots()) { string? found = TryKnownJavaLayouts(commonRoot, checkedPaths) ?? FindJavaUnderSonicHome(commonRoot, checkedPaths); if (found is not null) { result.JavaExe = found; return result; } } string? fromRegistry = FindJavaInWindowsRegistry(checkedPaths); if (fromRegistry is not null) { result.JavaExe = fromRegistry; return result; } string? fromWhere = FindJavaViaWhereExe(checkedPaths); if (fromWhere is not null) { result.JavaExe = fromWhere; return result; } string? fromCommon = FindJavaInCommonInstallLocations(checkedPaths); if (fromCommon is not null) { result.JavaExe = fromCommon; return result; } return result; } private string? ResolveJavacExecutable(string javaExe) { string javacName = OperatingSystem.IsWindows() ? "javac.exe" : "javac"; List ignored = []; string? dir = Path.GetDirectoryName(javaExe); if (!string.IsNullOrWhiteSpace(dir)) { string sibling = Path.Combine(dir, javacName); if (File.Exists(sibling)) { return sibling; } } foreach (string? configured in new[] { _connection.JavaPath, _connection.JavaHome }) { string? fromConfig = ResolveToolFromConfiguredPath(configured, javacName, ignored); if (fromConfig is not null) { return fromConfig; } } foreach (string envName in new[] { "JAVA_HOME", "JDK_HOME" }) { string? javaHome = Environment.GetEnvironmentVariable(envName); if (!string.IsNullOrWhiteSpace(javaHome)) { string candidate = Path.Combine(javaHome.Trim().Trim('"'), "bin", javacName); if (File.Exists(candidate)) { return candidate; } } } if (!string.IsNullOrWhiteSpace(_connection.SonicHome)) { foreach (string root in EnumerateJdkRootsUnderSonicHome(_connection.SonicHome)) { string candidate = Path.Combine(root, "bin", javacName); if (File.Exists(candidate)) { return candidate; } } } return FindOnPath(javacName, ignored) ?? FindOnPath("javac", ignored); } private static string? ResolveToolFromConfiguredPath( string? configured, string exeName, List checkedPaths) { if (string.IsNullOrWhiteSpace(configured)) { return null; } string path = configured.Trim().Trim('"'); NoteChecked(checkedPaths, $"config:{path}"); if (File.Exists(path)) { if (path.EndsWith(exeName, StringComparison.OrdinalIgnoreCase)) { return path; } string? parent = Path.GetDirectoryName(path); if (!string.IsNullOrWhiteSpace(parent)) { string sibling = Path.Combine(parent, exeName); NoteChecked(checkedPaths, sibling); if (File.Exists(sibling)) { return sibling; } } string fileName = Path.GetFileName(path); if (fileName.Equals(Path.GetFileNameWithoutExtension(exeName), StringComparison.OrdinalIgnoreCase) || fileName.Equals(exeName, StringComparison.OrdinalIgnoreCase)) { return path; } } string fromHome = Path.Combine(path, "bin", exeName); NoteChecked(checkedPaths, fromHome); return File.Exists(fromHome) ? fromHome : null; } private static string? ResolveJavaFromHomeDir(string? home, List checkedPaths, string label) { if (string.IsNullOrWhiteSpace(home)) { NoteChecked(checkedPaths, $"{label}=(leer)"); return null; } string trimmed = home.Trim().Trim('"'); string candidate = Path.Combine(trimmed, "bin", JavaExeName); NoteChecked(checkedPaths, $"{label}:{candidate}"); if (File.Exists(candidate)) { return candidate; } // Manchmal zeigt JAVA_HOME bereits auf ...\bin string asBin = Path.Combine(trimmed, JavaExeName); NoteChecked(checkedPaths, $"{label}:{asBin}"); return File.Exists(asBin) ? asBin : null; } private static string? TryKnownJavaLayouts(string root, List checkedPaths) { if (string.IsNullOrWhiteSpace(root)) { return null; } string[] relatives = [ Path.Combine("jre", "bin", JavaExeName), Path.Combine("jdk", "bin", JavaExeName), Path.Combine("JRE", "bin", JavaExeName), Path.Combine("JDK", "bin", JavaExeName), Path.Combine("jre64", "bin", JavaExeName), Path.Combine("jdk64", "bin", JavaExeName), Path.Combine("Java", "jre", "bin", JavaExeName), Path.Combine("Java", "jdk", "bin", JavaExeName), Path.Combine("java", "bin", JavaExeName), Path.Combine("bin", JavaExeName) ]; foreach (string rel in relatives) { string candidate = Path.Combine(root, rel); NoteChecked(checkedPaths, candidate); if (File.Exists(candidate)) { return candidate; } } return null; } private static string? FindJavaUnderSonicHome(string sonicHome, List checkedPaths) { if (!Directory.Exists(sonicHome)) { NoteChecked(checkedPaths, $"SonicHome-missing:{sonicHome}"); return null; } foreach (string root in EnumerateJdkRootsUnderSonicHome(sonicHome)) { string candidate = Path.Combine(root, "bin", JavaExeName); NoteChecked(checkedPaths, candidate); if (File.Exists(candidate)) { return candidate; } } return null; } private static IEnumerable EnumerateJdkRootsUnderSonicHome(string sonicHome) { if (!Directory.Exists(sonicHome)) { yield break; } string[] preferredNames = [ "jre", "jdk", "JRE", "JDK", "jre64", "jdk64", "Java", "java" ]; foreach (string name in preferredNames) { string path = Path.Combine(sonicHome, name); if (Directory.Exists(path)) { yield return path; string nestedJre = Path.Combine(path, "jre"); if (Directory.Exists(nestedJre)) { yield return nestedJre; } } } IEnumerable children; try { children = Directory.EnumerateDirectories(sonicHome); } catch { yield break; } foreach (string child in children) { foreach (string leaf in new[] { "jre", "jdk", "JRE", "JDK", "jre64", "jdk64" }) { string nested = Path.Combine(child, leaf); if (Directory.Exists(nested)) { yield return nested; } } string leafName = Path.GetFileName(child); if (leafName.StartsWith("jre", StringComparison.OrdinalIgnoreCase) || leafName.StartsWith("jdk", StringComparison.OrdinalIgnoreCase)) { yield return child; } } } private static string? FindJavaFromSonicScripts(string sonicHome, List checkedPaths) { if (!Directory.Exists(sonicHome)) { return null; } string[] scriptDirs = [ Path.Combine(sonicHome, "bin"), sonicHome, Path.Combine(sonicHome, "MQ", "bin"), Path.Combine(sonicHome, "mq", "bin") ]; string[] scriptNames = [ "setenv.bat", "setenv.sh", "setenv.cmd", "sonic_env.bat", "sonic_env.sh", "sonic_env.cmd", "startcontainer.bat", "startcontainer.sh", "startmc.bat", "startmc.sh", "startbroker.bat", "startbroker.sh" ]; List scripts = []; foreach (string dir in scriptDirs.Where(Directory.Exists)) { foreach (string name in scriptNames) { string path = Path.Combine(dir, name); if (File.Exists(path)) { scripts.Add(path); } } try { foreach (string path in Directory.EnumerateFiles(dir, "sonic_env*", SearchOption.TopDirectoryOnly)) { scripts.Add(path); } foreach (string path in Directory.EnumerateFiles(dir, "setenv*", SearchOption.TopDirectoryOnly)) { scripts.Add(path); } } catch { // ignore } } foreach (string script in scripts.Distinct(StringComparer.OrdinalIgnoreCase)) { NoteChecked(checkedPaths, $"script:{script}"); string? found = ParseJavaFromEnvScript(script, sonicHome, checkedPaths); if (found is not null) { return found; } } return null; } private static string? ParseJavaFromEnvScript(string scriptPath, string sonicHome, List checkedPaths) { string text; try { text = File.ReadAllText(scriptPath); } catch { return null; } // JAVA_HOME / JRE_HOME / JDK_HOME Assignments Regex homeAssign = new( @"(?im)(?:set\s+)?(?JAVA_HOME|JRE_HOME|JDK_HOME)\s*=\s*(?[^\r\n]+)"); foreach (Match match in homeAssign.Matches(text)) { string raw = ExpandScriptPath(match.Groups["val"].Value, sonicHome); string? resolved = ResolveJavaFromHomeDir(raw, checkedPaths, Path.GetFileName(scriptPath)); if (resolved is not null) { return resolved; } } // Direkte java.exe-Pfade in Scripten Regex javaExeRef = new( @"(?i)(?(?:[A-Za-z]:\\|/)[^\s""']+?[/\\]java(?:\.exe)?)"); foreach (Match match in javaExeRef.Matches(text)) { string raw = ExpandScriptPath(match.Groups["path"].Value, sonicHome); NoteChecked(checkedPaths, $"script-java:{raw}"); if (File.Exists(raw)) { return raw; } } return null; } private static string ExpandScriptPath(string raw, string sonicHome) { string value = raw.Trim().Trim('"', '\'', '`'); // Zuerst Variablen expandieren, danach Rauschen (Kommentare/Operatoren) abschneiden. value = Regex.Replace(value, "%~dp0", sonicHome + Path.DirectorySeparatorChar, RegexOptions.IgnoreCase); value = Regex.Replace(value, "%SONIC_HOME%", sonicHome, RegexOptions.IgnoreCase); value = Regex.Replace(value, "%MQ_HOME%", Environment.GetEnvironmentVariable("MQ_HOME") ?? sonicHome, RegexOptions.IgnoreCase); value = Regex.Replace(value, "%ESB_HOME%", Environment.GetEnvironmentVariable("ESB_HOME") ?? sonicHome, RegexOptions.IgnoreCase); value = Regex.Replace(value, "%JAVA_HOME%", Environment.GetEnvironmentVariable("JAVA_HOME") ?? string.Empty, RegexOptions.IgnoreCase); value = Regex.Replace(value, "%JRE_HOME%", Environment.GetEnvironmentVariable("JRE_HOME") ?? string.Empty, RegexOptions.IgnoreCase); value = Regex.Replace(value, @"\$\{?SONIC_HOME\}?", sonicHome, RegexOptions.IgnoreCase); value = Regex.Replace(value, @"\$\{?MQ_HOME\}?", Environment.GetEnvironmentVariable("MQ_HOME") ?? sonicHome, RegexOptions.IgnoreCase); value = Regex.Replace(value, @"\$\{?JAVA_HOME\}?", Environment.GetEnvironmentVariable("JAVA_HOME") ?? string.Empty, RegexOptions.IgnoreCase); value = Regex.Replace(value, @"\$\{?JRE_HOME\}?", Environment.GetEnvironmentVariable("JRE_HOME") ?? string.Empty, RegexOptions.IgnoreCase); // trailing " & rem ..." / " && ..." entfernen int cut = value.IndexOfAny(['&', '|', '>', '<', '`']); if (cut >= 0) { value = value[..cut]; } int rem = value.IndexOf(" rem ", StringComparison.OrdinalIgnoreCase); if (rem >= 0) { value = value[..rem]; } if (value.StartsWith('.') || value.StartsWith("..")) { try { value = Path.GetFullPath(Path.Combine(sonicHome, value)); } catch { // keep as-is } } return value.Trim().Trim('"', '\''); } private static string? FindJavaRecursive(string root, List checkedPaths, int depth) { if (!Directory.Exists(root) || depth < 1) { return null; } List preferred = []; List others = []; int scanned = 0; void Walk(string dir, int remaining) { if (scanned >= MaxJavaFilesToScan || remaining < 0) { return; } try { string candidate = Path.Combine(dir, JavaExeName); if (File.Exists(candidate)) { scanned++; string normalized = candidate.Replace('/', Path.DirectorySeparatorChar); if (normalized.Contains($"{Path.DirectorySeparatorChar}jre{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase) || normalized.Contains($"{Path.DirectorySeparatorChar}jdk{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase)) { preferred.Add(candidate); } else { others.Add(candidate); } } if (remaining == 0 || scanned >= MaxJavaFilesToScan) { return; } foreach (string child in Directory.EnumerateDirectories(dir)) { string name = Path.GetFileName(child); if (name.Equals("tmp", StringComparison.OrdinalIgnoreCase) || name.Equals("temp", StringComparison.OrdinalIgnoreCase) || name.Equals("logs", StringComparison.OrdinalIgnoreCase) || name.Equals("log", StringComparison.OrdinalIgnoreCase) || name.Equals(".git", StringComparison.OrdinalIgnoreCase) || name.Equals("node_modules", StringComparison.OrdinalIgnoreCase)) { continue; } Walk(child, remaining - 1); if (scanned >= MaxJavaFilesToScan) { return; } } } catch { // inaccessible directory } } Walk(root, depth); string? best = preferred.FirstOrDefault() ?? others.FirstOrDefault(); if (best is not null) { NoteChecked(checkedPaths, $"recursive-hit:{best}"); return best; } NoteChecked(checkedPaths, $"recursive-miss:{root} (depth<={depth}, scanned={scanned})"); return null; } private static IEnumerable EnumerateSiblingMqEsbRoots(string parent, string currentSonicHome) { if (!Directory.Exists(parent)) { yield break; } IEnumerable children; try { children = Directory.EnumerateDirectories(parent); } catch { yield break; } foreach (string child in children) { if (string.Equals(child, currentSonicHome, StringComparison.OrdinalIgnoreCase)) { continue; } string name = Path.GetFileName(child); if (name.StartsWith("MQ", StringComparison.OrdinalIgnoreCase) || name.StartsWith("ESB", StringComparison.OrdinalIgnoreCase) || name.Contains("Sonic", StringComparison.OrdinalIgnoreCase)) { yield return child; } } } private static IEnumerable EnumerateCommonSonicRoots() { string[] roots = [ @"C:\Sonic", @"C:\Program Files\Progress", @"C:\Program Files\Aurea", @"C:\Program Files (x86)\Progress", @"C:\Program Files (x86)\Aurea", @"D:\Sonic", Environment.GetEnvironmentVariable("MQ_HOME") ?? string.Empty, Environment.GetEnvironmentVariable("ESB_HOME") ?? string.Empty, Environment.GetEnvironmentVariable("SONIC_HOME") ?? string.Empty ]; foreach (string root in roots.Where(r => !string.IsNullOrWhiteSpace(r)).Distinct(StringComparer.OrdinalIgnoreCase)) { if (Directory.Exists(root)) { yield return root; } string? parentExists = Directory.Exists(root) ? root : null; if (parentExists is null) { continue; } IEnumerable children; try { children = Directory.EnumerateDirectories(parentExists); } catch { continue; } foreach (string child in children) { string name = Path.GetFileName(child); if (name.StartsWith("MQ", StringComparison.OrdinalIgnoreCase) || name.StartsWith("ESB", StringComparison.OrdinalIgnoreCase) || name.Contains("Sonic", StringComparison.OrdinalIgnoreCase)) { yield return child; } } } } private static string? FindJavaInWindowsRegistry(List checkedPaths) { if (!OperatingSystem.IsWindows()) { return null; } string[] keyPaths = [ @"SOFTWARE\JavaSoft\Java Runtime Environment", @"SOFTWARE\JavaSoft\JDK", @"SOFTWARE\JavaSoft\Java Development Kit", @"SOFTWARE\Eclipse Adoptium\JRE", @"SOFTWARE\Eclipse Adoptium\JDK", @"SOFTWARE\WOW6432Node\JavaSoft\Java Runtime Environment", @"SOFTWARE\WOW6432Node\JavaSoft\JDK" ]; foreach (string keyPath in keyPaths) { try { using RegistryKey? baseKey = Registry.LocalMachine.OpenSubKey(keyPath); if (baseKey is null) { NoteChecked(checkedPaths, $"registry-miss:HKLM\\{keyPath}"); continue; } NoteChecked(checkedPaths, $"registry:HKLM\\{keyPath}"); string? current = baseKey.GetValue("CurrentVersion") as string; if (!string.IsNullOrWhiteSpace(current)) { using RegistryKey? verKey = baseKey.OpenSubKey(current); string? javaHome = verKey?.GetValue("JavaHome") as string; string? resolved = ResolveJavaFromHomeDir(javaHome, checkedPaths, $"registry:{current}"); if (resolved is not null) { return resolved; } } foreach (string subName in baseKey.GetSubKeyNames().OrderByDescending(s => s)) { using RegistryKey? verKey = baseKey.OpenSubKey(subName); string? javaHome = verKey?.GetValue("JavaHome") as string ?? verKey?.GetValue("Path") as string; string? resolved = ResolveJavaFromHomeDir(javaHome, checkedPaths, $"registry:{subName}"); if (resolved is not null) { return resolved; } } } catch { NoteChecked(checkedPaths, $"registry-error:HKLM\\{keyPath}"); } } return null; } private static string? FindJavaViaWhereExe(List checkedPaths) { if (!OperatingSystem.IsWindows()) { return null; } try { ProcessStartInfo psi = new() { FileName = "where.exe", Arguments = "java", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; using Process process = new() { StartInfo = psi }; if (!process.Start()) { NoteChecked(checkedPaths, "where.exe:start-failed"); return null; } string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(3000); foreach (string line in output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { NoteChecked(checkedPaths, $"where:{line}"); if (File.Exists(line) && Path.GetFileName(line).Equals(JavaExeName, StringComparison.OrdinalIgnoreCase)) { return line; } } } catch (Exception ex) { NoteChecked(checkedPaths, $"where.exe:error:{ex.GetType().Name}"); } return null; } private static string? FindJavaInCommonInstallLocations(List checkedPaths) { string[] roots = [ Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"C:\Program Files", @"C:\Program Files (x86)" ]; string[] vendorFolders = [ "Java", "Eclipse Adoptium", "AdoptOpenJDK", "Microsoft", "Amazon Corretto", "Zulu", "BellSoft", "Semeru", "Progress", "Aurea" ]; foreach (string root in roots.Where(r => !string.IsNullOrWhiteSpace(r)).Distinct(StringComparer.OrdinalIgnoreCase)) { foreach (string vendor in vendorFolders) { string vendorDir = Path.Combine(root, vendor); if (!Directory.Exists(vendorDir)) { continue; } string direct = Path.Combine(vendorDir, "bin", JavaExeName); NoteChecked(checkedPaths, direct); if (File.Exists(direct)) { return direct; } try { foreach (string sub in Directory.EnumerateDirectories(vendorDir) .OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase)) { string candidate = Path.Combine(sub, "bin", JavaExeName); NoteChecked(checkedPaths, candidate); if (File.Exists(candidate)) { return candidate; } string nestedJre = Path.Combine(sub, "jre", "bin", JavaExeName); NoteChecked(checkedPaths, nestedJre); if (File.Exists(nestedJre)) { return nestedJre; } } } catch { // ignore inaccessible vendor dirs } } } return null; } private static void NoteChecked(List checkedPaths, string path) { if (checkedPaths.Count >= MaxCheckedPathsInError * 2) { return; } if (!checkedPaths.Contains(path, StringComparer.OrdinalIgnoreCase)) { checkedPaths.Add(path); } } private static string JavaExeName => OperatingSystem.IsWindows() ? "java.exe" : "java"; private static string? FindOnPath(string fileName, List checkedPaths) { string? pathEnv = Environment.GetEnvironmentVariable("PATH"); if (string.IsNullOrWhiteSpace(pathEnv)) { NoteChecked(checkedPaths, "PATH=(leer)"); return null; } foreach (string dir in pathEnv.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) { try { string candidate = Path.Combine(dir.Trim().Trim('"'), fileName); NoteChecked(checkedPaths, $"PATH:{candidate}"); if (File.Exists(candidate)) { return candidate; } } catch { // ignore bad PATH entries } } return null; } private static string? ExtractError(string combined) { foreach (string line in combined.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) { if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase)) { return line["ERROR:".Length..].Trim(); } } return string.IsNullOrWhiteSpace(combined) ? null : Truncate(combined); } private static string Quote(string value) => "\"" + value.Replace("\"", "\\\"") + "\""; private static string EscapeArg(string value) { if (value.Length == 0) { return "\"\""; } bool needsQuotes = value.Contains(' ') || value.Contains('\t') || value.Contains('"') || value.Contains('*') || value.Contains(';'); if (!needsQuotes) { return value; } return "\"" + value.Replace("\"", "\\\"") + "\""; } private static string Truncate(string s, int max = 800) => s.Length <= max ? s : s[..max] + "…"; }