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); } List args = [ "-cp", Quote(classpath!), "SonicMfContainerTool", command, "--domain", _connection.DomainName, "--url", _connection.ConnectionUrl, "--user", _connection.Username, "--timeout", Math.Clamp(_connection.TimeoutSeconds, 5, 600).ToString() ]; if (!string.IsNullOrWhiteSpace(container)) { args.Add("--container"); args.Add(container); } ProcessStartInfo psi = new() { FileName = javaExe!, Arguments = string.Join(" ", args.Select(EscapeArg)), UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8, WorkingDirectory = classDir! }; 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}"; return (false, combined.Length > 0 ? combined : null, err); } private async Task<(bool Ok, string? JavaExe, string? ClassDir, string? Classpath, string? Error)> PrepareToolAsync(CancellationToken cancellationToken) { JavaDiscoveryResult javaResult = DiscoverJava(); string? javaExe = javaResult.JavaExe; 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)); if (javaResult.CheckedPaths.Count > MaxCheckedPathsInError) { checkedList += $"\n - … (+{javaResult.CheckedPaths.Count - MaxCheckedPathsInError} weitere)"; } return (false, null, null, null, "Java nicht gefunden.\n" + $"Verwendetes SonicHome: '{sonicHome}'\n" + $"SonicHome existiert: {(Directory.Exists(sonicHome) ? "ja" : "nein")}\n" + "Konfiguration: SonicConnection.JavaHome / JavaPath, sonst Auto-Suche " + "(JAVA_HOME/JRE_HOME, setenv.bat, rekursiv unter SonicHome, MQ/ESB-Nachbarn, Registry, where).\n" + "Geprüfte Pfade:\n - " + checkedList + "\n" + "Tipp: JavaHome oder JavaPath in appsettings auf die echte java.exe setzen " + "(z.B. C:\\Sonic\\MQ10.0\\jre\\bin\\java.exe)."); } (string? libDir, string? libError) = ResolveLibDirectoryDetailed(); if (libDir is null) { return (false, null, null, null, libError); } int runtimeMajor = await GetJavaClassMajorAsync(javaExe, cancellationToken); // Sonic/SMC nutzt typisch Java 8 (major 52). Nie mit höherer Class-Version starten. // Vorcompilierte .class/.jar nur nutzen, wenn Class-Version zur Runtime passt. if (TryResolvePrebuiltTool(runtimeMajor, out string? prebuiltDir, out string? prebuiltEntry, out string? prebuiltError) && prebuiltDir is not null && prebuiltEntry is not null) { string classpath = BuildClasspath(libDir, prebuiltEntry); 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})."); } 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 = BuildClasspath(libDir, null); string runtimeClasspath = BuildClasspath(libDir, workDir); 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, Arguments = $"{releaseArgs} -encoding UTF-8 -cp {Quote(compileClasspath)} -d {Quote(workDir)} {Quote(javaTarget)}", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; 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-Lib: {libDir}"); } } 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); if (major is not null && major <= runtimeMajor) { workDir = dir; classpathEntry = dir; return true; } 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 52; // Sonic-Default Java 8 } string err = await p.StandardError.ReadToEndAsync(cancellationToken); string output = await p.StandardOutput.ReadToEndAsync(cancellationToken); await p.WaitForExitAsync(cancellationToken); string text = (err + "\n" + output).ToLowerInvariant(); if (text.Contains("version \"1.8") || text.Contains("version \"8")) { return 52; } Match m = Regex.Match(text, @"version ""(\d+)"); if (m.Success && int.TryParse(m.Groups[1].Value, out int major) && major >= 9) { // Java 9+ class major = 44 + version => 9→53, 11→55, 17→61, 21→65 return 44 + major; } } catch { // ignore } return 52; } 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; } } private (string? Dir, string? Error) ResolveLibDirectoryDetailed() { List<(string Path, string Label)> candidates = []; if (!string.IsNullOrWhiteSpace(_connection.MfClientLibPath)) { candidates.Add((_connection.MfClientLibPath.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)) { candidates.Add((Path.Combine(mqHome.Trim().Trim('"'), "lib"), "MQ_HOME\\lib")); } if (!string.IsNullOrWhiteSpace(esbHome)) { string esb = esbHome.Trim().Trim('"'); candidates.Add((Path.Combine(esb, "lib"), "ESB_HOME\\lib")); candidates.Add((Path.Combine(esb, "MQ", "lib"), "ESB_HOME\\MQ\\lib")); candidates.Add((Path.Combine(esb, "mq", "lib"), "ESB_HOME\\mq\\lib")); } if (!string.IsNullOrWhiteSpace(_connection.SonicHome)) { string sonicHome = _connection.SonicHome.Trim(); candidates.Add((Path.Combine(sonicHome, "lib"), "SonicHome\\lib")); candidates.Add((sonicHome, "SonicHome")); // ESB-Home zeigt oft auf Broker/ESB; Client-JARs liegen unter MQ*/lib oder sibling MQ. foreach (string nested in EnumerateLikelyLibRoots(sonicHome)) { candidates.Add((nested, "SonicHome-nested\\lib")); } string? parent = Directory.GetParent(sonicHome)?.FullName; if (!string.IsNullOrWhiteSpace(parent)) { candidates.Add((Path.Combine(parent, "lib"), "SonicHome-Parent\\lib")); foreach (string nested in EnumerateLikelyLibRoots(parent)) { candidates.Add((nested, "SonicHome-Parent-nested\\lib")); } } } List checkedPaths = []; HashSet seen = new(StringComparer.OrdinalIgnoreCase); foreach ((string path, string label) in candidates) { if (string.IsNullOrWhiteSpace(path) || !seen.Add(path)) { continue; } checkedPaths.Add($"{label}='{path}'"); if (!Directory.Exists(path)) { continue; } if (DirectoryHasSonicClientJars(path)) { return (path, null); } } string sonicHomeVal = _connection.SonicHome ?? string.Empty; bool sonicHomeExists = !string.IsNullOrWhiteSpace(sonicHomeVal) && Directory.Exists(sonicHomeVal); string libPath = string.IsNullOrWhiteSpace(sonicHomeVal) ? "(SonicHome leer)" : Path.Combine(sonicHomeVal, "lib"); if (sonicHomeExists && Directory.Exists(libPath) && !Directory.EnumerateFiles(libPath, "*.jar", SearchOption.TopDirectoryOnly).Any()) { return (null, $"Sonic-Client-JARs fehlen unter '{libPath}' (Ordner existiert, aber keine *.jar).\n" + $"Verwendetes SonicHome: '{sonicHomeVal}'\n" + "MfApi braucht u.a. mgmt_client.jar / mfcontext.jar / sonic_Client.jar.\n" + "Falls SonicHome auf ESB zeigt: auf MQ-Home setzen oder MfClientLibPath auf den MQ\\lib-Ordner.\n" + $"Geprüft: {string.Join("; ", checkedPaths)}"); } if (sonicHomeExists && !Directory.Exists(libPath)) { return (null, $"SonicHome='{sonicHomeVal}' gefunden, aber Lib-Ordner fehlt: '{libPath}'.\n" + "Sonic-Client-Installation prüfen oder MfClientLibPath setzen " + "(erwartet *.jar, u.a. mgmt_client.jar).\n" + $"Geprüft: {string.Join("; ", checkedPaths)}"); } return (null, "Sonic-Client-JARs nicht gefunden. SonicHome\\lib, MQ_HOME\\lib, ESB_HOME\\lib oder MfClientLibPath setzen.\n" + $"Verwendetes SonicHome: '{(string.IsNullOrWhiteSpace(sonicHomeVal) ? "(leer)" : sonicHomeVal)}'\n" + $"Geprüft:\n - {string.Join("\n - ", checkedPaths)}\n" + "Benötigt u.a. mgmt_client.jar / mfcontext.jar / sonic_Client.jar."); } 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; } } } } private static bool DirectoryHasSonicClientJars(string path) { try { string[] jars = Directory.EnumerateFiles(path, "*.jar", SearchOption.TopDirectoryOnly).ToArray(); if (jars.Length == 0) { return false; } // Bevorzugt echte Sonic-Client-Libs; sonst jeder JAR-Ordner als Fallback. string[] markers = [ "mgmt_client", "mfcontext", "sonic_Client", "sonic_client", "mf_client", "Sonic" ]; if (jars.Any(j => markers.Any(m => Path.GetFileName(j).Contains(m, StringComparison.OrdinalIgnoreCase)))) { return true; } return jars.Length > 0; } catch { return false; } } private static string BuildClasspath(string libDir, string? extraEntry) { // Java-Classpath-Wildcard für alle JARs im Lib-Ordner string jars = Path.Combine(libDir, "*"); return string.IsNullOrWhiteSpace(extraEntry) ? jars : jars + Path.PathSeparator + extraEntry; } 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] + "…"; }