using System.Diagnostics; using System.Text; using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; /// /// Startet den Container-Restart über SonicMfContainerTool /// mit Java und den Sonic-MfApi-Clientbibliotheken. /// public sealed class SonicMfApiExecutor { private readonly SonicConnection _connection; public SonicMfApiExecutor( SonicConnection connection) { _connection = connection ?? throw new ArgumentNullException( nameof(connection)); } public (string SonicHome, string? JavaExe) ResolveRuntimePaths() { string sonicHome = string.IsNullOrWhiteSpace( _connection.SonicHome) ? "(leer)" : _connection.SonicHome.Trim(); return ( sonicHome, ResolveJavaExe()); } public async Task<( bool Success, string? Output, string? Error)> RestartAsync( string containerName, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(containerName)) { return ( false, null, "Der Sonic-Containername fehlt."); } if (string.IsNullOrWhiteSpace( _connection.Username) || string.IsNullOrEmpty( _connection.Password)) { return ( false, null, "Für die Sonic-Verbindung sind keine " + "vollständigen Zugangsdaten eingerichtet. " + "Bitte die Einstellungen öffnen und Zugangsdaten hinterlegen."); } if (string.IsNullOrWhiteSpace( _connection.DomainName)) { return ( false, null, "Für die Sonic-Verbindung fehlt DomainName."); } if (string.IsNullOrWhiteSpace( _connection.ConnectionUrl)) { return ( false, null, "Für die Sonic-Verbindung fehlt ConnectionUrl."); } ( bool prepared, string? javaExe, string? classDir, string? classpath, string? prepareError ) = PrepareTool(); if (!prepared) { return ( false, null, prepareError); } ProcessStartInfo startInfo = new() { FileName = javaExe!, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8, WorkingDirectory = classDir! }; startInfo.ArgumentList.Add("-cp"); startInfo.ArgumentList.Add(classpath!); startInfo.ArgumentList.Add( "SonicMfContainerTool"); startInfo.ArgumentList.Add("restart"); startInfo.ArgumentList.Add("--domain"); startInfo.ArgumentList.Add( _connection.DomainName); startInfo.ArgumentList.Add("--url"); startInfo.ArgumentList.Add( _connection.ConnectionUrl); startInfo.ArgumentList.Add("--user"); startInfo.ArgumentList.Add( _connection.Username); startInfo.ArgumentList.Add("--container"); startInfo.ArgumentList.Add( containerName); startInfo.ArgumentList.Add("--timeout"); startInfo.ArgumentList.Add( Math.Clamp( _connection.TimeoutSeconds, 5, 600) .ToString()); /* * Das Kennwort wird nicht als Kommandozeilenargument * übergeben. Dadurch erscheint das Kennwort nicht in * der Prozessargumentliste. * * Das Kennwort stammt aus dbo.SonicCredential (Always Encrypted), * wurde clientseitig entschlüsselt und als Laufzeitwert in * SonicConnection.Password übernommen. */ startInfo.Environment[ "ESB_SONIC_PASSWORD"] = _connection.Password; using Process process = new() { StartInfo = startInfo }; try { if (!process.Start()) { return ( false, null, "Der Java-Prozess konnte nicht gestartet werden."); } } catch (Exception ex) { return ( false, null, $"Der Java-Prozess konnte nicht gestartet werden: " + $"{ex.Message}"); } using CancellationTokenSource timeoutCts = CancellationTokenSource .CreateLinkedTokenSource( cancellationToken); int timeoutSeconds = Math.Clamp( _connection.TimeoutSeconds, 10, 600); timeoutCts.CancelAfter( TimeSpan.FromSeconds( timeoutSeconds)); Task stdoutTask = process.StandardOutput .ReadToEndAsync(); Task stderrTask = process.StandardError .ReadToEndAsync(); try { await process.WaitForExitAsync( timeoutCts.Token); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { TryKillProcess(process); return ( false, null, $"MfApi-Timeout nach {timeoutSeconds} Sekunden."); } catch (OperationCanceledException) { TryKillProcess(process); throw; } string stdout = (await stdoutTask).Trim(); string stderr = (await stderrTask).Trim(); string combined = string.Join( Environment.NewLine, new[] { stdout, stderr } .Where(output => !string.IsNullOrWhiteSpace(output))); bool succeeded = process.ExitCode == 0 && combined.Contains( "OK:RestartInvoked", StringComparison.OrdinalIgnoreCase) && !combined.Contains( "ERROR:", StringComparison.OrdinalIgnoreCase); if (succeeded) { return ( true, string.IsNullOrWhiteSpace(combined) ? null : combined, null); } string error = ExtractError(combined) ?? $"SonicMfContainerTool ExitCode=" + $"{process.ExitCode}"; if (error.Contains( "ClassNotFoundException", StringComparison.OrdinalIgnoreCase) || combined.Contains( "JMSConnectorAddress", StringComparison.OrdinalIgnoreCase)) { error += Environment.NewLine + Environment.NewLine + "Der Classpath ist vermutlich unvollständig. " + "Runtime:SonicClientLibraryPath muss auf den " + "Ordner mit den benötigten Sonic-JARs zeigen."; } string detail = string.IsNullOrWhiteSpace(combined) ? error : error + Environment.NewLine + Environment.NewLine + "--- Tool-Output ---" + Environment.NewLine + combined; return ( false, string.IsNullOrWhiteSpace(combined) ? null : combined, detail); } private ( bool Ok, string? JavaExe, string? ClassDir, string? Classpath, string? Error) PrepareTool() { string? javaExe = ResolveJavaExe(); if (javaExe is null) { return ( false, null, null, null, "Java wurde nicht gefunden. " + "Runtime:JavaExecutablePath prüfen."); } ( string? libraryClasspath, string? libraryError ) = ResolveSonicClasspath(); if (libraryClasspath is null) { return ( false, null, null, null, libraryError); } string? toolsDirectory = ResolveToolsDir(); if (toolsDirectory is null) { return ( false, null, null, null, "Tools\\SonicMfContainerTool.class " + "wurde nicht gefunden. " + "Projekt vollständig neu bauen."); } string classpath = toolsDirectory + Path.PathSeparator + libraryClasspath; return ( true, javaExe, toolsDirectory, classpath, null); } private string? ResolveJavaExe() { // Explizite Verbindungs-Pfade zuerst, dann Java-8-Suche // (Runtime\bin\java.exe fehlt oft; JAVA_HOME zeigt oft auf JDK 21). string? fromConnection = JavaRuntimeLocator.Resolve( !string.IsNullOrWhiteSpace(_connection.JavaPath) ? _connection.JavaPath : null); if (fromConnection is not null) { return fromConnection; } if (!string.IsNullOrWhiteSpace(_connection.JavaHome)) { string fromHome = Path.Combine( _connection.JavaHome.Trim(), "bin", "java.exe"); string? resolvedHome = JavaRuntimeLocator.Resolve(fromHome); if (resolvedHome is not null) { return resolvedHome; } } return JavaRuntimeLocator.Resolve(); } private ( string? Classpath, string? Error) ResolveSonicClasspath() { List libraryDirectories = []; if (!string.IsNullOrWhiteSpace( _connection.MfClientLibPath)) { libraryDirectories.Add( ResolveDirectoryPath( _connection.MfClientLibPath)); } if (!string.IsNullOrWhiteSpace( _connection.SonicHome)) { string sonicHome = ResolveDirectoryPath( _connection.SonicHome); libraryDirectories.Add( Path.Combine( sonicHome, "lib")); libraryDirectories.Add( sonicHome); } HashSet jars = new( StringComparer.OrdinalIgnoreCase); foreach (string directory in libraryDirectories .Where(Directory.Exists) .Distinct( StringComparer.OrdinalIgnoreCase)) { foreach (string jar in Directory.EnumerateFiles( directory, "*.jar", SearchOption.TopDirectoryOnly)) { jars.Add( Path.GetFullPath(jar)); } } if (jars.Count == 0) { return ( null, "Keine Sonic-Client-JARs gefunden. " + "Runtime:SonicClientLibraryPath prüfen."); } string[] preferredJarNames = [ "mgmt_client.jar", "mfcontext.jar", "sonic_Client.jar", "sonic_Crypto.jar", "mf_common.jar" ]; List orderedJars = []; foreach (string preferredJarName in preferredJarNames) { string? matchingJar = jars.FirstOrDefault( jar => string.Equals( Path.GetFileName(jar), preferredJarName, StringComparison.OrdinalIgnoreCase)); if (matchingJar is not null) { orderedJars.Add( matchingJar); } } foreach (string jar in jars.OrderBy( Path.GetFileName, StringComparer.OrdinalIgnoreCase)) { if (!orderedJars.Contains( jar, StringComparer.OrdinalIgnoreCase)) { orderedJars.Add(jar); } } return ( string.Join( Path.PathSeparator, orderedJars), null); } private static string ResolveDirectoryPath( string configuredPath) { return Path.IsPathRooted(configuredPath) ? Path.GetFullPath(configuredPath) : Path.GetFullPath( Path.Combine( AppContext.BaseDirectory, configuredPath)); } private static string? ResolveToolsDir() { string[] directories = [ Path.Combine( AppContext.BaseDirectory, "Tools"), Path.Combine( Directory.GetCurrentDirectory(), "Tools"), AppContext.BaseDirectory ]; foreach (string directory in directories .Where(Directory.Exists)) { string toolPath = Path.Combine( directory, "SonicMfContainerTool.class"); if (File.Exists(toolPath)) { return directory; } } return null; } private static string? ExtractError( string combinedOutput) { foreach (string line in combinedOutput.Split( ['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) { if (line.StartsWith( "ERROR:", StringComparison.OrdinalIgnoreCase)) { return line[ "ERROR:".Length..] .Trim(); } } return null; } private static void TryKillProcess( Process process) { try { if (!process.HasExited) { process.Kill( entireProcessTree: true); } } catch { // Der ursprüngliche Fehler oder Timeout // darf nicht verdeckt werden. } } }