using System.Diagnostics; using System.Text; 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 readonly SonicConnection _connection; public SonicMfApiExecutor(SonicConnection connection) { _connection = connection; } 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) { string? javaExe = ResolveJavaExecutable(); if (javaExe is null) { string sonicHint = string.IsNullOrWhiteSpace(_connection.SonicHome) ? "" : $" Erwartet z.B. '{Path.Combine(_connection.SonicHome, "jre", "bin", "java.exe")}'."; return (false, null, null, null, "Java nicht gefunden. Gesucht: SonicConnection.JavaHome/JavaPath, JAVA_HOME, PATH, " + "SonicHome\\jre|jdk\\bin\\java.exe, Program Files\\Java|Eclipse Adoptium." + sonicHint + " Für MfApi JavaHome in appsettings setzen oder JRE unter SonicHome nutzen."); } (string? libDir, string? libError) = ResolveLibDirectoryDetailed(); if (libDir is null) { return (false, null, null, null, libError); } // Bevorzugt: vorcompilierte .jar/.class aus Tools\ (kein javac nötig). if (TryResolvePrebuiltTool(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); } // Fallback: zur Laufzeit aus .java kompilieren (braucht javac + Sonic-Libs). string? sourcePath = ResolveToolSourcePath(); if (sourcePath is null) { return (false, null, null, null, (prebuiltError ?? "Kein vorcompilierter SonicMfContainerTool gefunden.") + " Tools\\SonicMfContainerTool.java/.class/.jar fehlen im Ausgabeverzeichnis."); } string workDir = Path.Combine(Path.GetTempPath(), "esb-sonic-mf"); 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 needsCompile = !File.Exists(classFile) || File.GetLastWriteTimeUtc(classFile) < File.GetLastWriteTimeUtc(sourcePath); if (needsCompile) { string? javac = ResolveJavacExecutable(javaExe); if (javac is null) { return (false, null, null, null, "Vorcompilierter SonicMfContainerTool fehlt und javac wurde nicht gefunden. " + "JDK installieren oder Tools\\SonicMfContainerTool.jar/.class mit ausliefern. " + $"Gefundene java.exe: {javaExe}"); } ProcessStartInfo compilePsi = new() { FileName = javac, Arguments = $"-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); if (compile.ExitCode != 0 || !File.Exists(classFile)) { return (false, null, null, null, "Kompilieren von SonicMfContainerTool fehlgeschlagen.\n" + Truncate((cErr + "\n" + cOut).Trim()) + $"\nClasspath-Lib: {libDir}"); } } return (true, javaExe, workDir, runtimeClasspath, null); } private bool TryResolvePrebuiltTool(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 jar = Path.Combine(dir, "SonicMfContainerTool.jar"); if (File.Exists(jar)) { workDir = dir; classpathEntry = jar; return true; } string classFile = Path.Combine(dir, "SonicMfContainerTool.class"); if (File.Exists(classFile)) { workDir = dir; classpathEntry = dir; return true; } } error = "Tools\\SonicMfContainerTool.jar/.class nicht gefunden."; return false; } private (string? Dir, string? Error) ResolveLibDirectoryDetailed() { List<(string Path, string Label)> candidates = []; if (!string.IsNullOrWhiteSpace(_connection.MfClientLibPath)) { candidates.Add((_connection.MfClientLibPath, "MfClientLibPath")); } if (!string.IsNullOrWhiteSpace(_connection.SonicHome)) { candidates.Add((Path.Combine(_connection.SonicHome, "lib"), "SonicHome\\lib")); candidates.Add((_connection.SonicHome, "SonicHome")); } List checkedPaths = []; foreach ((string path, string label) in candidates) { checkedPaths.Add($"{label}='{path}'"); if (!Directory.Exists(path)) { continue; } if (Directory.EnumerateFiles(path, "*.jar", SearchOption.TopDirectoryOnly).Any()) { return (path, null); } } string sonicHome = _connection.SonicHome ?? string.Empty; bool sonicHomeExists = !string.IsNullOrWhiteSpace(sonicHome) && Directory.Exists(sonicHome); string libPath = string.IsNullOrWhiteSpace(sonicHome) ? "(SonicHome leer)" : Path.Combine(sonicHome, "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). " + "MfApi braucht u.a. mgmt_client.jar / mfcontext.jar / sonic_Client.jar. " + "MfClientLibPath auf den korrekten Lib-Ordner setzen."); } if (sonicHomeExists && !Directory.Exists(libPath)) { return (null, $"SonicHome='{sonicHome}' gefunden, aber Lib-Ordner fehlt: '{libPath}'. " + "Sonic-Client-Installation prüfen oder MfClientLibPath setzen " + "(erwartet *.jar, u.a. mgmt_client.jar)."); } return (null, "Sonic-Client-JARs nicht gefunden. SonicHome\\lib oder MfClientLibPath setzen " + $"(geprüft: {string.Join("; ", checkedPaths)}). " + "Benötigt u.a. mgmt_client.jar / mfcontext.jar / sonic_Client.jar."); } 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 string? ResolveJavaExecutable() { foreach (string? configured in new[] { _connection.JavaPath, _connection.JavaHome }) { string? fromConfig = ResolveJavaFromConfiguredPath(configured); if (fromConfig is not null) { return fromConfig; } } string? javaHome = Environment.GetEnvironmentVariable("JAVA_HOME"); string? fromEnv = ResolveJavaFromHomeDir(javaHome); if (fromEnv is not null) { return fromEnv; } string? fromPath = FindOnPath(JavaExeName) ?? FindOnPath("java"); if (fromPath is not null) { return fromPath; } if (!string.IsNullOrWhiteSpace(_connection.SonicHome)) { string sonicHome = _connection.SonicHome; string[] direct = [ Path.Combine(sonicHome, "jre", "bin", JavaExeName), Path.Combine(sonicHome, "jdk", "bin", JavaExeName), Path.Combine(sonicHome, "JRE", "bin", JavaExeName), Path.Combine(sonicHome, "JDK", "bin", JavaExeName) ]; foreach (string candidate in direct) { if (File.Exists(candidate)) { return candidate; } } // Häufige Layouts: SonicHome\MQ*\jre, SonicHome\jre64, SonicHome\Java\jre, ... string? nested = FindJavaUnderSonicHome(sonicHome); if (nested is not null) { return nested; } } return FindJavaInCommonInstallLocations(); } private string? ResolveJavacExecutable(string javaExe) { string javacName = OperatingSystem.IsWindows() ? "javac.exe" : "javac"; 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); if (fromConfig is not null) { return fromConfig; } } string? javaHome = Environment.GetEnvironmentVariable("JAVA_HOME"); if (!string.IsNullOrWhiteSpace(javaHome)) { string candidate = Path.Combine(javaHome, "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) ?? FindOnPath("javac"); } private static string? ResolveJavaFromConfiguredPath(string? configured) => ResolveToolFromConfiguredPath(configured, JavaExeName); private static string? ResolveToolFromConfiguredPath(string? configured, string exeName) { if (string.IsNullOrWhiteSpace(configured)) { return null; } string path = configured.Trim().Trim('"'); if (File.Exists(path)) { if (path.EndsWith(exeName, StringComparison.OrdinalIgnoreCase)) { return path; } // Konfiguriert: ...\bin\java.exe → sibling javac.exe (oder umgekehrt) string? parent = Path.GetDirectoryName(path); if (!string.IsNullOrWhiteSpace(parent)) { string sibling = Path.Combine(parent, exeName); if (File.Exists(sibling)) { return sibling; } } // java ohne Endung (Unix) oder exakter Treffer 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); return File.Exists(fromHome) ? fromHome : null; } private static string? ResolveJavaFromHomeDir(string? home) { if (string.IsNullOrWhiteSpace(home)) { return null; } string candidate = Path.Combine(home.Trim().Trim('"'), "bin", JavaExeName); return File.Exists(candidate) ? candidate : null; } private static string? FindJavaUnderSonicHome(string sonicHome) { if (!Directory.Exists(sonicHome)) { return null; } // Direkte Unterordner mit jre/jdk foreach (string root in EnumerateJdkRootsUnderSonicHome(sonicHome)) { string candidate = Path.Combine(root, "bin", JavaExeName); 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; } } } // {SonicHome}\*\jre und {SonicHome}\*\jdk IEnumerable children; try { children = Directory.EnumerateDirectories(sonicHome); } catch { yield break; } foreach (string child in children) { foreach (string leaf in new[] { "jre", "jdk", "JRE", "JDK" }) { string nested = Path.Combine(child, leaf); if (Directory.Exists(nested)) { yield return nested; } } // z.B. ...\jre1.8.0_xxx direkt als Kind string leafName = Path.GetFileName(child); if (leafName.StartsWith("jre", StringComparison.OrdinalIgnoreCase) || leafName.StartsWith("jdk", StringComparison.OrdinalIgnoreCase)) { yield return child; } } } private static string? FindJavaInCommonInstallLocations() { 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" ]; 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; } // vendor\bin\java.exe string direct = Path.Combine(vendorDir, "bin", JavaExeName); 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); if (File.Exists(candidate)) { return candidate; } string nestedJre = Path.Combine(sub, "jre", "bin", JavaExeName); if (File.Exists(nestedJre)) { return nestedJre; } } } catch { // ignore inaccessible vendor dirs } } } return null; } private static string JavaExeName => OperatingSystem.IsWindows() ? "java.exe" : "java"; private static string? FindOnPath(string fileName) { string? pathEnv = Environment.GetEnvironmentVariable("PATH"); if (string.IsNullOrWhiteSpace(pathEnv)) { return null; } foreach (string dir in pathEnv.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) { try { string candidate = Path.Combine(dir.Trim().Trim('"'), fileName); 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] + "…"; }