diff --git a/ZA.CoreService.ESBCertificateManager/Form1.cs b/ZA.CoreService.ESBCertificateManager/Form1.cs index ede2546..4b1ecbe 100644 --- a/ZA.CoreService.ESBCertificateManager/Form1.cs +++ b/ZA.CoreService.ESBCertificateManager/Form1.cs @@ -1827,17 +1827,18 @@ namespace ZA.CoreService.ESBCertificateManager .FirstOrDefault(c => string.Equals(c.Name, selectedName, StringComparison.OrdinalIgnoreCase)) ?? _sonicDiscovery.Connections[0]; - (string sonicHome, string? javaExe) = new SonicMfApiExecutor(conn).ResolveRuntimePaths(); - string javaDisplay = string.IsNullOrWhiteSpace(javaExe) ? "java=?" : javaExe; + string sonicHome = string.IsNullOrWhiteSpace(conn.SonicHome) ? "(leer)" : conn.SonicHome.Trim(); + (bool binOk, string? binErr, _) = new SonicBinRestartExecutor(conn).Probe(); + string binDisplay = binOk ? "stop/startcontainer=ok" : $"bin=fehlt ({binErr})"; string baseText = - $"{conn.ManagementModeDisplay} | SonicHome={sonicHome} | {javaDisplay}"; + $"{conn.ManagementModeDisplay} | SonicHome={sonicHome} | {binDisplay}"; if (!string.IsNullOrWhiteSpace(suffix)) { baseText = $"{suffix} | {baseText}"; } lblSonicStatus.Text = baseText; - lblSonicStatus.ForeColor = string.IsNullOrWhiteSpace(javaExe) ? GoldColor : GreenColor; + lblSonicStatus.ForeColor = binOk ? GreenColor : GoldColor; } private static string TruncateStatus(string? text, int max = 120) diff --git a/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs b/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs index e85fadc..cd58cff 100644 --- a/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs +++ b/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs @@ -42,7 +42,11 @@ public sealed class SonicConnection /// WinRm / LocalCmd = optionaler Fallback über stopcontainer/startcontainer; /// HttpApi = REST (falls vorhanden). /// - public SonicManagementMode ManagementMode { get; init; } = SonicManagementMode.MfApi; + /// + /// Standard: LocalCmd = stopcontainer/startcontainer unter SonicHome (laut Sonic-Doku, ohne extra Java). + /// Optional: MfApi (Domain Manager via Java-Client) oder WinRm. + /// + public SonicManagementMode ManagementMode { get; init; } = SonicManagementMode.LocalCmd; /// /// Effektiver Modus: bei WinRm und lokalem ConnectionUrl-Host wird LocalCmd erzwungen. diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicBinRestartExecutor.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicBinRestartExecutor.cs new file mode 100644 index 0000000..8c7b04c --- /dev/null +++ b/ZA.CoreService.ESBCertificateManager/Services/SonicBinRestartExecutor.cs @@ -0,0 +1,150 @@ +using System.Diagnostics; +using System.Text; +using ZA.CoreService.ESBCertificateManager.Models; + +namespace ZA.CoreService.ESBCertificateManager.Services; + +/// +/// Neustart über die offiziellen Sonic-Bin-Scripts (laut Doku): +/// SonicHome\bin\stopcontainer.bat Domain.Container +/// SonicHome\bin\startcontainer.bat Domain.Container +/// Die Scripts setzen selbst JAVA_HOME – kein separates System-Java nötig. +/// +public sealed class SonicBinRestartExecutor +{ + private readonly SonicConnection _connection; + + public SonicBinRestartExecutor(SonicConnection connection) + { + _connection = connection; + } + + public (bool Ok, string? Error, string? Detail) Probe() + { + string sonicHome = _connection.SonicHome?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(sonicHome)) + { + return (false, "SonicHome ist leer – in appsettings setzen.", null); + } + + if (!Directory.Exists(sonicHome)) + { + return (false, $"SonicHome existiert nicht: '{sonicHome}'", null); + } + + string stop = Path.Combine(sonicHome, "bin", "stopcontainer.bat"); + string start = Path.Combine(sonicHome, "bin", "startcontainer.bat"); + if (!File.Exists(stop) || !File.Exists(start)) + { + return (false, + $"stopcontainer.bat/startcontainer.bat fehlen unter '{Path.Combine(sonicHome, "bin")}'.", + $"stop={File.Exists(stop)}; start={File.Exists(start)}"); + } + + return (true, null, $"SonicHome={sonicHome}"); + } + + public async Task<(bool Success, string Status, string? Detail)> RestartAsync( + string containerName, + CancellationToken cancellationToken = default) + { + (bool probeOk, string? probeError, string? probeDetail) = Probe(); + if (!probeOk) + { + return (false, "Neustart fehlgeschlagen (SonicBin)", + $"{probeError}\n{probeDetail}"); + } + + string sonicHome = _connection.SonicHome.Trim(); + string bin = Path.Combine(sonicHome, "bin"); + string stopBat = Path.Combine(bin, "stopcontainer.bat"); + string startBat = Path.Combine(bin, "startcontainer.bat"); + + string shortName = containerName.Contains('.') + ? containerName[(containerName.IndexOf('.') + 1)..] + : containerName; + string fullName = containerName.Contains('.') + ? containerName + : $"{_connection.DomainName}.{containerName}"; + + StringBuilder log = new(); + log.AppendLine($"SonicHome={sonicHome}"); + log.AppendLine($"Container={fullName} (kurz={shortName})"); + + (bool stopOk, string stopOut, string stopErr, int stopCode) = + await RunBatAsync(stopBat, fullName, bin, cancellationToken); + log.AppendLine($"STOP ExitCode={stopCode}"); + if (stopOut.Length > 0) log.AppendLine("STOP out: " + Truncate(stopOut)); + if (stopErr.Length > 0) log.AppendLine("STOP err: " + Truncate(stopErr)); + + // stopcontainer kann ExitCode!=0 liefern wenn Container schon down – trotzdem starten + await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); + + (bool startOk, string startOut, string startErr, int startCode) = + await RunBatAsync(startBat, fullName, bin, cancellationToken); + log.AppendLine($"START ExitCode={startCode}"); + if (startOut.Length > 0) log.AppendLine("START out: " + Truncate(startOut)); + if (startErr.Length > 0) log.AppendLine("START err: " + Truncate(startErr)); + + if (!startOk || startCode != 0) + { + // Fallback: Kurzname ohne Domain + if (!string.Equals(shortName, fullName, StringComparison.OrdinalIgnoreCase)) + { + log.AppendLine($"Retry START mit Kurzname '{shortName}'…"); + (startOk, startOut, startErr, startCode) = + await RunBatAsync(startBat, shortName, bin, cancellationToken); + log.AppendLine($"START(short) ExitCode={startCode}"); + if (startOut.Length > 0) log.AppendLine("START out: " + Truncate(startOut)); + if (startErr.Length > 0) log.AppendLine("START err: " + Truncate(startErr)); + } + } + + await Task.Delay( + TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)), + cancellationToken); + + if (startCode != 0) + { + return (false, "Neustart fehlgeschlagen (SonicBin)", + $"startcontainer ExitCode={startCode} (stop ExitCode={stopCode}).\n{log}"); + } + + return (true, $"Container '{fullName}' neugestartet (SonicBin)", log.ToString()); + } + + private static async Task<(bool Started, string StdOut, string StdErr, int ExitCode)> RunBatAsync( + string batPath, + string argument, + string workingDirectory, + CancellationToken cancellationToken) + { + ProcessStartInfo psi = new() + { + FileName = "cmd.exe", + Arguments = $"/c \"\"{batPath}\" \"{argument}\"\"", + WorkingDirectory = workingDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8 + }; + + using Process process = new() { StartInfo = psi }; + if (!process.Start()) + { + return (false, string.Empty, "cmd.exe konnte nicht gestartet werden.", -1); + } + + Task stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken); + Task stderrTask = process.StandardError.ReadToEndAsync(cancellationToken); + + await process.WaitForExitAsync(cancellationToken); + return (true, (await stdoutTask).Trim(), (await stderrTask).Trim(), process.ExitCode); + } + + private static string Truncate(string value, int max = 500) + => value.Length <= max ? value : value[..max] + "…"; +} diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicContainerDiscovery.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicContainerDiscovery.cs index edcdb81..d03fe4b 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/SonicContainerDiscovery.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/SonicContainerDiscovery.cs @@ -35,34 +35,47 @@ public sealed class SonicContainerDiscovery using SonicManagementClient client = new(connection); (bool reachable, string? pingError, string? resolvedPath) = await client.CheckConnectionAsync(cancellationToken); - if (!reachable) - { - return SonicDiscoveryResult.Failed(connectionName, - $"Management-Konsole nicht erreichbar: {pingError}"); - } - - (bool listOk, IReadOnlyList remoteNames, string? listError) = - await client.GetContainersAsync(cancellationToken); List diagnostics = []; List remoteContainers = []; + bool listOk = false; + string? listError = null; - foreach (string line in remoteNames) + if (!reachable) { - if (line.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase) - || line.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase)) + // Trotzdem KnownContainers nutzen – Neustart kann über SonicBin (stop/startcontainer) gehen. + diagnostics.Add($"Management-Ping: {pingError}"); + if (connection.KnownContainers.Count == 0 + && !knownTargets.Any(t => + string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase) + && !string.IsNullOrWhiteSpace(t.ContainerName))) { - diagnostics.Add(line); - continue; + return SonicDiscoveryResult.Failed(connectionName, + $"Management-Konsole nicht erreichbar: {pingError}"); + } + } + else + { + (listOk, IReadOnlyList remoteNames, listError) = + await client.GetContainersAsync(cancellationToken); + + foreach (string line in remoteNames) + { + if (line.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase) + || line.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase)) + { + diagnostics.Add(line); + continue; + } + + remoteContainers.Add(line); } - remoteContainers.Add(line); - } - - if (!listOk) - { - diagnostics.Add($"Listen-Fehler: {listError}"); - remoteContainers = []; + if (!listOk) + { + diagnostics.Add($"Listen-Fehler: {listError}"); + remoteContainers = []; + } } // Fallback: explizit in appsettings hinterlegte Container diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs index 0160e5a..6144902 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs @@ -95,18 +95,49 @@ public sealed class SonicManagementClient : IDisposable if (UsesMfApi) { (bool ok, string? error) = await _mfApi!.TestConnectionAsync(cancellationToken); - string modeLabel = - $"MfApi {_connection.ConnectionUrl} Domain={_connection.DomainName}"; - return (ok, error, ok ? modeLabel : null); + if (ok) + { + return (true, null, + $"MfApi {_connection.ConnectionUrl} Domain={_connection.DomainName}"); + } + + // Ohne lokales Java: Domain-Manager-TCP + KnownContainers reichen für Discovery. + bool javaMissing = error?.Contains("Java nicht gefunden", StringComparison.OrdinalIgnoreCase) == true; + if (javaMissing) + { + SonicBinRestartExecutor bin = new(_connection); + (bool binOk, _, string? binDetail) = bin.Probe(); + if (binOk) + { + return (true, null, + $"SonicBin Fallback (kein Java für MfApi) {binDetail}; {_connection.ConnectionUrl}"); + } + + if (_connection.KnownContainers.Count > 0 + || await IsTcpReachableAsync(_connection.ConnectionUrl, cancellationToken)) + { + return (true, null, + $"MfApi ohne Java – KnownContainers/TCP; Hinweis: {Truncate(error ?? string.Empty, 180)}"); + } + } + + return (false, error, null); } if (UsesScripts) { + if (Mode == SonicManagementMode.LocalCmd) + { + (bool binOk, string? binError, string? binDetail) = new SonicBinRestartExecutor(_connection).Probe(); + return binOk + ? (true, null, $"LocalCmd/SonicBin {binDetail}") + : (false, binError, null); + } + (bool ok, string? error) = await _scriptRunner!.TestConnectionAsync(cancellationToken); - string modeLabel = Mode == SonicManagementMode.LocalCmd - ? $"LocalCmd (SonicHome={_connection.SonicHome})" - : $"WinRM auf {ExtractHost(_connection.ConnectionUrl)}:{_connection.WinRmPort}"; - return (ok, error, ok ? modeLabel : null); + return ok + ? (true, null, $"WinRM auf {ExtractHost(_connection.ConnectionUrl)}:{_connection.WinRmPort}") + : (false, error, null); } try @@ -169,6 +200,13 @@ public sealed class SonicManagementClient : IDisposable if (UsesScripts) { + // LocalCmd / WinRm: bevorzugter Doku-Weg über die .bat-Dateien (ohne extra Java-Suche). + if (Mode == SonicManagementMode.LocalCmd) + { + return await new SonicBinRestartExecutor(_connection) + .RestartAsync(containerName, cancellationToken); + } + return await RestartViaScriptAsync(containerName, cancellationToken); } @@ -260,22 +298,60 @@ public sealed class SonicManagementClient : IDisposable (bool ok, string? output, string? error) = await _mfApi!.RestartAsync(containerName, cancellationToken); - if (!ok) + if (ok) { - return (false, "Neustart fehlgeschlagen (MfApi)", - $"IAgentProxy.restart für '{containerName}' (Domain '{_connection.DomainName}') " + - $"über {_connection.ConnectionUrl} fehlgeschlagen.\n" + - $"Fehler: {error}\nAusgabe: {output}\n" + - "Voraussetzungen: Domain Manager erreichbar, SMC-Logins korrekt, " + - "JRE (JavaHome/SonicHome\\jre/JAVA_HOME) + Sonic-Client-JARs (SonicHome\\lib), Container online."); + await Task.Delay( + TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)), + cancellationToken); + + return (true, $"Container '{containerName}' neugestartet (MfApi)", + $"Domain={_connection.DomainName}; ConnectionUrl={_connection.ConnectionUrl}\n{output}"); } - await Task.Delay( - TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)), - cancellationToken); + // Kein Java / keine Client-JARs → offizieller Doku-Weg über stop/startcontainer.bat + bool softFail = error?.Contains("Java nicht gefunden", StringComparison.OrdinalIgnoreCase) == true + || error?.Contains("Client-JARs", StringComparison.OrdinalIgnoreCase) == true + || error?.Contains("lib", StringComparison.OrdinalIgnoreCase) == true; - return (true, $"Container '{containerName}' neugestartet (MfApi)", - $"Domain={_connection.DomainName}; ConnectionUrl={_connection.ConnectionUrl}\n{output}"); + if (softFail) + { + SonicBinRestartExecutor bin = new(_connection); + (bool binOk, string binStatus, string? binDetail) = + await bin.RestartAsync(containerName, cancellationToken); + if (binOk) + { + return (true, binStatus, + $"MfApi nicht nutzbar ({Truncate(error ?? string.Empty, 160)}) – Fallback SonicBin:\n{binDetail}"); + } + + return (false, "Neustart fehlgeschlagen (MfApi→SonicBin)", + $"MfApi: {error}\n\nSonicBin-Fallback: {binDetail}\n" + + $"SonicHome='{_connection.SonicHome}' muss stopcontainer.bat enthalten."); + } + + return (false, "Neustart fehlgeschlagen (MfApi)", + $"IAgentProxy.restart für '{containerName}' (Domain '{_connection.DomainName}') " + + $"über {_connection.ConnectionUrl} fehlgeschlagen.\n" + + $"Fehler: {error}\nAusgabe: {output}"); + } + + private static async Task IsTcpReachableAsync(string connectionUrl, CancellationToken cancellationToken) + { + try + { + Uri uri = new(connectionUrl); + string host = uri.Host; + int port = uri.Port > 0 ? uri.Port : 2506; + using System.Net.Sockets.TcpClient tcp = new(); + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(TimeSpan.FromSeconds(5)); + await tcp.ConnectAsync(host, port, cts.Token); + return true; + } + catch + { + return false; + } } private static SonicConnection CloneAsLocalCmd(SonicConnection source) diff --git a/ZA.CoreService.ESBCertificateManager/appsettings.json b/ZA.CoreService.ESBCertificateManager/appsettings.json index 0e5a457..3b446f7 100644 --- a/ZA.CoreService.ESBCertificateManager/appsettings.json +++ b/ZA.CoreService.ESBCertificateManager/appsettings.json @@ -10,15 +10,9 @@ "Name": "DE-Test", "DomainName": "proalpha-test", "ConnectionUrl": "tcp://dekun-painwbdet:13070", - // Sonic SMC / Domain Manager (für MfApi-Neustart): "Username": "Administrator", "Password": "Administrator", - // Standard: Sonic MF Management API (IAgentProxy.restart) – kein WinRM. - // Benötigt JRE + Client-JARs unter SonicHome\\lib (oder MfClientLibPath / MQ_HOME). - // Java-Suche: JavaPath/JavaHome, JAVA_HOME/JRE_HOME, setenv.bat, rekursiv unter SonicHome - // (nicht nur SonicHome\\jre\\bin\\java.exe), Registry, where.exe. Optional JavaHome/JavaPath setzen. - // Optional-Fallback: "WinRm" / "LocalCmd" (stopcontainer/startcontainer). - "ManagementMode": "MfApi", + "ManagementMode": "LocalCmd", "SonicHome": "C:\\Sonic\\MQ10.0", "JavaHome": "", "JavaPath": "", @@ -26,7 +20,6 @@ "KnownContainers": [ "DE-Test" ], - // Nur für ManagementMode=WinRm relevant; leer lassen wenn nicht genutzt: "WinRmUsername": "", "WinRmPassword": "", "WinRmPort": 5985,