using System.Diagnostics; using System.Text; using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; public sealed class SonicContainerScanner { private readonly RuntimeSettings _runtimeSettings; public SonicContainerScanner(RuntimeSettings runtimeSettings) { _runtimeSettings = runtimeSettings ?? throw new ArgumentNullException(nameof(runtimeSettings)); } public async Task ScanAsync( SonicSystemOption system, string userName, string secret, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(system); if (string.IsNullOrWhiteSpace(userName)) { return ContainerScanResult.Failed( "Der Sonic-Benutzername fehlt."); } if (string.IsNullOrEmpty(secret)) { return ContainerScanResult.Failed( "Das Sonic-Kennwort fehlt."); } string javaPath = PathResolver.ResolvePath( _runtimeSettings.JavaExecutablePath); string libraryPath = PathResolver.ResolvePath( _runtimeSettings.SonicClientLibraryPath); string? toolsPath = ResolveToolsDirectory(); if (!File.Exists(javaPath)) { return ContainerScanResult.Failed( $"Java wurde nicht gefunden: {javaPath}"); } if (!Directory.Exists(libraryPath)) { return ContainerScanResult.Failed( $"Der Sonic-JAR-Ordner wurde nicht gefunden: {libraryPath}"); } if (toolsPath is null) { return ContainerScanResult.Failed( "SonicMfContainerTool.class wurde nicht gefunden."); } string[] jars = Directory.GetFiles( libraryPath, "*.jar", SearchOption.TopDirectoryOnly); if (jars.Length == 0) { return ContainerScanResult.Failed( "Im Sonic-Client-Ordner wurden keine JAR-Dateien gefunden."); } string classpath = toolsPath + Path.PathSeparator + string.Join( Path.PathSeparator, jars.OrderBy( Path.GetFileName, StringComparer.OrdinalIgnoreCase)); ProcessStartInfo startInfo = new() { FileName = javaPath, WorkingDirectory = toolsPath, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8, CreateNoWindow = true }; startInfo.ArgumentList.Add("-cp"); startInfo.ArgumentList.Add(classpath); startInfo.ArgumentList.Add("SonicMfContainerTool"); startInfo.ArgumentList.Add("list"); startInfo.ArgumentList.Add("--domain"); startInfo.ArgumentList.Add(system.DomainName); startInfo.ArgumentList.Add("--url"); startInfo.ArgumentList.Add(system.ConnectionUrl); startInfo.ArgumentList.Add("--user"); startInfo.ArgumentList.Add(userName.Trim()); startInfo.ArgumentList.Add("--timeout"); startInfo.ArgumentList.Add("45"); startInfo.Environment["ESB_SONIC_PASSWORD"] = secret; using Process process = new() { StartInfo = startInfo }; try { if (!process.Start()) { return ContainerScanResult.Failed( "Der Java-Prozess konnte nicht gestartet werden."); } } catch (Exception ex) { return ContainerScanResult.Failed( $"Java konnte nicht gestartet werden: {ex.Message}"); } Task standardOutputTask = process.StandardOutput.ReadToEndAsync(); Task standardErrorTask = process.StandardError.ReadToEndAsync(); using CancellationTokenSource timeoutSource = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken); timeoutSource.CancelAfter(TimeSpan.FromSeconds(50)); try { await process.WaitForExitAsync(timeoutSource.Token); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { TryKill(process); return ContainerScanResult.Failed( "Der Container-Scan hat das Zeitlimit überschritten."); } catch (OperationCanceledException) { TryKill(process); throw; } string standardOutput = (await standardOutputTask).Trim(); string standardError = (await standardErrorTask).Trim(); string combined = string.Join( Environment.NewLine, new[] { standardOutput, standardError } .Where(value => !string.IsNullOrWhiteSpace(value))); bool success = process.ExitCode == 0 && combined.Contains( "OK:ContainerListEnd", StringComparison.OrdinalIgnoreCase) && !combined.Contains( "ERROR:", StringComparison.OrdinalIgnoreCase); if (!success) { return ContainerScanResult.Failed( ExtractError(combined) ?? $"Container-Scan fehlgeschlagen, ExitCode={process.ExitCode}."); } List containers = []; foreach (string line in standardOutput.Split( ['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) { const string prefix = "CONTAINER="; if (!line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { continue; } string name = line[prefix.Length..].Trim(); if (name.Length > 0 && !containers.Contains(name, StringComparer.OrdinalIgnoreCase)) { containers.Add(name); } } containers.Sort(StringComparer.OrdinalIgnoreCase); return ContainerScanResult.Successful(containers); } private static string? ResolveToolsDirectory() { string baseDirectory = AppContext.BaseDirectory; string[] candidates = [ Path.Combine(baseDirectory, "Tools"), Path.Combine(baseDirectory, "..", "..", "..", "Tools"), Path.Combine( Directory.GetCurrentDirectory(), "Tools") ]; foreach (string candidate in candidates) { string fullPath = Path.GetFullPath(candidate); string classFile = Path.Combine( fullPath, "SonicMfContainerTool.class"); if (File.Exists(classFile)) { return fullPath; } } return null; } private static string? ExtractError(string output) { foreach (string line in output.Split( ['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) { if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase)) { return line["ERROR:".Length..].Trim(); } } return string.IsNullOrWhiteSpace(output) ? null : output; } private static void TryKill(Process process) { try { if (!process.HasExited) { process.Kill(entireProcessTree: true); } } catch { } } } public sealed class ContainerScanResult { public bool Success { get; init; } public string Message { get; init; } = string.Empty; public IReadOnlyList Containers { get; init; } = []; public static ContainerScanResult Successful( IReadOnlyList containers) { return new ContainerScanResult { Success = true, Message = $"{containers.Count} Container gefunden.", Containers = containers }; } public static ContainerScanResult Failed(string message) { return new ContainerScanResult { Success = false, Message = message }; } }