diff --git a/ZA.CoreService.ESBCertificateManager/Data/targets.sample.json b/ZA.CoreService.ESBCertificateManager/Data/targets.sample.json index 2fb5864..d12639f 100644 --- a/ZA.CoreService.ESBCertificateManager/Data/targets.sample.json +++ b/ZA.CoreService.ESBCertificateManager/Data/targets.sample.json @@ -1,16 +1,16 @@ [ { "Id": 1, - "Name": "DE-Test ESB Container", + "Name": "DE-Test Container", "Environment": "TEST", "IsActive": true, "TargetDirectory": "DeploySandbox/target-esb", "CertificateFileName": "esb-cert.cer", - "ContainerName": "ESB", + "ContainerName": "DE-Test", "RestartType": "SonicContainer", "RestartCommand": "", "RestartArguments": "", - "RestartTimeoutSeconds": 90, + "RestartTimeoutSeconds": 120, "SonicConnectionName": "DE-Test", "XapiSourcePath": "", "TlsHost": "", @@ -23,7 +23,7 @@ "Id": 2, "Name": "Lokaler CMD-Test (Echo)", "Environment": "DEV", - "IsActive": true, + "IsActive": false, "TargetDirectory": "DeploySandbox/target-cmd", "CertificateFileName": "esb-cert.cer", "ContainerName": "", diff --git a/ZA.CoreService.ESBCertificateManager/Form1.cs b/ZA.CoreService.ESBCertificateManager/Form1.cs index 8b29338..51b7936 100644 --- a/ZA.CoreService.ESBCertificateManager/Form1.cs +++ b/ZA.CoreService.ESBCertificateManager/Form1.cs @@ -1500,11 +1500,15 @@ namespace ZA.CoreService.ESBCertificateManager return; } + string targetNames = string.Join(", ", selected.Select(t => + string.IsNullOrWhiteSpace(t.ContainerName) ? t.Name : t.ContainerName)); + DialogResult confirm = MessageBox.Show( this, - $"ESB/Container für {selected.Count} Ziel(e) wirklich neu starten?\n\n" + - "Es wird stopcontainer/startcontainer über die Sonic-Verbindung ausgeführt\n" + - "(WinRM Remote-CMD oder LocalCmd laut appsettings).", + $"Container wirklich neu starten?\n\n" + + $"Ziele: {targetNames}\n" + + "Domain bleibt aus appsettings (z.B. proalpha-test).\n" + + "Erfolg nur bei verifiziertem Prozess-Neustart (sichtbar in SMC).", "ESB neu starten", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); @@ -1538,11 +1542,24 @@ namespace ZA.CoreService.ESBCertificateManager SetTargetRowStatus(targetResult.TargetId, targetResult.StatusText); } + string details = string.Join( + Environment.NewLine + Environment.NewLine, + runResult.TargetResults.Select(r => + $"{r.TargetName}: {r.StatusText}" + + (string.IsNullOrWhiteSpace(r.Detail) ? string.Empty : Environment.NewLine + r.Detail))); + SetStatus( runResult.OverallSuccess - ? $"Neustart erfolgreich ({runResult.TargetResults.Count} Ziel(e))." - : "Neustart mit Fehlern beendet. Details in Status-Spalte / Log.", + ? $"Neustart verifiziert ({runResult.TargetResults.Count} Ziel(e))." + : "Neustart fehlgeschlagen oder nicht verifiziert.", isError: !runResult.OverallSuccess); + + MessageBox.Show( + this, + details, + runResult.OverallSuccess ? "Neustart verifiziert" : "Neustart fehlgeschlagen", + MessageBoxButtons.OK, + runResult.OverallSuccess ? MessageBoxIcon.Information : MessageBoxIcon.Warning); } catch (OperationCanceledException) { diff --git a/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs b/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs index 8dc20b0..cdd0249 100644 --- a/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs +++ b/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs @@ -2,53 +2,38 @@ namespace ZA.CoreService.ESBCertificateManager.Models; /// /// Verbindungskonfiguration für eine Progress Sonic ESB Management Instanz. +/// Name = Verbindungs-Alias (hier oft gleich ContainerName, z.B. "DE-Test"). +/// DomainName = Sonic-Domain (z.B. "proalpha-test"). +/// Container wird in KnownContainers / DeploymentTarget.ContainerName geführt (z.B. "DE-Test"). /// public sealed class SonicConnection { /// - /// Eindeutiger Bezeichner; wird in referenziert. + /// Alias der Verbindung; referenziert von . /// public required string Name { get; init; } - /// Sonic-Domain-Name, z.B. "proalpha-test". + /// Sonic-Domain, z.B. "proalpha-test" (nicht der Containername). public required string DomainName { get; init; } /// - /// Sonic-Broker-/Management-URL im Sonic-Format, z.B. "tcp://dekun-painwbdet:13070". - /// Der Hostname wird daraus extrahiert (WinRM-Ziel / HTTP-Basis). + /// Domain-Manager-/Broker-URL, z.B. "tcp://dekun-painwbdet:13070". /// public required string ConnectionUrl { get; init; } - /// Benutzername für WinRM / Management-Konsole. public required string Username { get; init; } - - /// Passwort für WinRM / Management-Konsole. public required string Password { get; init; } - /// - /// Management-Modus: - /// - WinRm: PowerShell Remoting auf dem Sonic-Server (Standard) - /// - LocalCmd: CMD/PowerShell lokal (zum Testen direkt auf dem Sonic-PC) - /// - HttpApi: REST, falls vorhanden - /// public SonicManagementMode ManagementMode { get; init; } = SonicManagementMode.WinRm; - /// - /// Installationsroot von Sonic MQ / Management Console, z.B. "C:\Sonic\MQ10.0". - /// Wird für Default-Scripts (stopcontainer/startcontainer) benötigt. - /// + /// Sonic-Installationsroot, z.B. "C:\Sonic\MQ10.0". public string SonicHome { get; init; } = @"C:\Sonic\MQ10.0"; /// - /// Bekannte Container-Namen dieser Domain (Fallback, wenn Remote-Discovery nichts findet). - /// z.B. [ "ESB", "DomainManager" ] oder vollqualifiziert [ "proalpha-test.ESB" ]. + /// Container-Namen (kurz), z.B. ["DE-Test"]. Nicht die Domain. /// public List KnownContainers { get; init; } = []; - // --------------------------------------------------------------- - // HTTP REST API (ManagementMode = HttpApi) - // --------------------------------------------------------------- - public int ManagementHttpPort { get; init; } = 8080; public string ApiBasePath { get; init; } = "/api/v1"; public string ContainerListPath { get; init; } = string.Empty; @@ -56,43 +41,27 @@ public sealed class SonicConnection public string ContainerStopPath { get; init; } = string.Empty; public string ContainerStartPath { get; init; } = string.Empty; - // --------------------------------------------------------------- - // WinRM / LocalCmd - // --------------------------------------------------------------- - public int WinRmPort { get; init; } = 5985; - - /// - /// PowerShell-Script zum Neustarten. Leer = Default über Sonic bin\stop/startcontainer.bat. - /// Platzhalter: {container}, {domain}, {sonicHome} - /// public string WinRmRestartScript { get; init; } = string.Empty; - - /// - /// PowerShell-Script zum Auflisten der Container. Leer = Default (Services + SonicHome). - /// public string WinRmContainerListScript { get; init; } = string.Empty; - public string WinRmXapiImportScript { get; init; } = string.Empty; - public int TimeoutSeconds { get; init; } = 60; - public int PostRestartDelaySeconds { get; init; } = 15; + public int TimeoutSeconds { get; init; } = 120; + public int PostRestartDelaySeconds { get; init; } = 20; - /// Effektives Restart-Script inkl. Default, wenn leer. public string ResolveRestartScript() => string.IsNullOrWhiteSpace(WinRmRestartScript) ? DefaultRestartScript : WinRmRestartScript; - /// Effektives List-Script inkl. Default, wenn leer. public string ResolveContainerListScript() => string.IsNullOrWhiteSpace(WinRmContainerListScript) ? DefaultContainerListScript : WinRmContainerListScript; /// - /// Default: Sonic Management Console Tools per CMD auf dem Ziel-PC. - /// stopcontainer.bat / startcontainer.bat unter SonicHome\bin. + /// Echter Neustart mit Prozess-Verifikation (sichtbar in SMC): + /// alte PIDs müssen weg, neue PIDs müssen erscheinen – sonst Fehler. /// public const string DefaultRestartScript = """ @@ -100,33 +69,144 @@ public sealed class SonicConnection $sonicHome = '{sonicHome}' $domain = '{domain}' $container = '{container}' + $connectionUrl = '{connectionUrl}' + $username = '{username}' + $password = '{password}' + $bin = Join-Path $sonicHome 'bin' - $stop = Join-Path $bin 'stopcontainer.bat' - $start = Join-Path $bin 'startcontainer.bat' - $fullName = if ($container -like '*.*') { $container } else { "$domain.$container" } + $stopBat = Join-Path $bin 'stopcontainer.bat' + $startBat = Join-Path $bin 'startcontainer.bat' + $esbAdmin = Join-Path $bin 'esbadmin.bat' - if (-not (Test-Path -LiteralPath $stop)) { - throw "Sonic stopcontainer.bat nicht gefunden: $stop (SonicHome prüfen)" - } - if (-not (Test-Path -LiteralPath $start)) { - throw "Sonic startcontainer.bat nicht gefunden: $start (SonicHome prüfen)" + # Kurzname = Container (z.B. DE-Test), Full = Domain.Container (z.B. proalpha-test.DE-Test) + $shortName = if ($container -like '*.*') { ($container -split '\.', 2)[1] } else { $container } + $fullName = if ($container -like '*.*') { $container } else { "$domain.$container" } + $markers = @($shortName, $fullName, "$domain.$shortName") | Select-Object -Unique + + function Get-ContainerPids { + $pids = @() + Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | + Where-Object { + $cmd = $_.CommandLine + if (-not $cmd) { return $false } + if ($_.Name -notmatch 'java|javaw|sonic') { return $false } + foreach ($m in $markers) { + if ($cmd -like "*$m*") { return $true } + } + return $false + } | + ForEach-Object { $pids += [int]$_.ProcessId } + return @($pids | Select-Object -Unique) } - Write-Output "STOP $fullName via $stop" - $stopProc = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', "`"$stop`" `"$fullName`"") -Wait -PassThru -NoNewWindow - if ($stopProc.ExitCode -ne 0) { - Write-Warning "stopcontainer ExitCode=$($stopProc.ExitCode) – starte trotzdem neu" + function Wait-PidsGone([int[]]$pids, [int]$seconds) { + $deadline = (Get-Date).AddSeconds($seconds) + while ((Get-Date) -lt $deadline) { + $alive = @($pids | Where-Object { Get-Process -Id $_ -ErrorAction SilentlyContinue }) + if ($alive.Count -eq 0) { return $true } + Start-Sleep -Seconds 2 + } + return $false } - Start-Sleep -Seconds 5 - - Write-Output "START $fullName via $start" - $startProc = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', "`"$start`" `"$fullName`"") -Wait -PassThru -NoNewWindow - if ($startProc.ExitCode -ne 0) { - throw "startcontainer fehlgeschlagen, ExitCode=$($startProc.ExitCode)" + function Wait-NewPids([int[]]$oldPids, [int]$seconds) { + $deadline = (Get-Date).AddSeconds($seconds) + while ((Get-Date) -lt $deadline) { + $now = @(Get-ContainerPids) + $fresh = @($now | Where-Object { $oldPids -notcontains $_ }) + if ($fresh.Count -gt 0) { return ,$fresh } + Start-Sleep -Seconds 2 + } + return ,@() } - Write-Output "Neustart ok: $fullName" + Write-Output "INFO:Domain=$domain Container=$shortName Full=$fullName" + $before = @(Get-ContainerPids) + Write-Output "INFO:PIDsVorher=$($before -join ',')" + + if ($before.Count -eq 0) { + Write-Output "WARN:Kein laufender Java/Sonic-Prozess für '$shortName' gefunden – Stop ggf. schon offline; starte trotzdem." + } + + # 1) Optional: esbadmin (SMC-kompatibel über Domain Manager) + if (Test-Path -LiteralPath $esbAdmin) { + Write-Output "INFO:Versuche esbadmin Stop/Start" + $scriptText = "connect $domain $connectionUrl $username $password`r`nstop container $shortName`r`nstart container $shortName`r`nexit`r`n" + $tmp = [System.IO.Path]::GetTempFileName() + '.txt' + Set-Content -LiteralPath $tmp -Value $scriptText -Encoding ASCII + try { + $p = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', "`"$esbAdmin`" < `"$tmp`"") -Wait -PassThru -NoNewWindow + Write-Output "INFO:esbadmin ExitCode=$($p.ExitCode)" + } + finally { + Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue + } + Start-Sleep -Seconds 5 + } + + # 2) stopcontainer.bat + if (Test-Path -LiteralPath $stopBat) { + foreach ($n in @($fullName, $shortName)) { + Write-Output "INFO:stopcontainer $n" + $sp = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', "`"$stopBat`" `"$n`"") -Wait -PassThru -NoNewWindow + Write-Output "INFO:stopcontainer ExitCode=$($sp.ExitCode) Name=$n" + } + } + else { + Write-Output "WARN:stopcontainer.bat fehlt: $stopBat" + } + + Start-Sleep -Seconds 3 + $still = @(Get-ContainerPids) + + # 3) Harter Stop der alten PIDs – sonst sieht SMC oft keinen Neustart + if ($still.Count -gt 0) { + Write-Output "INFO:Force-Stop PIDs=$($still -join ',')" + foreach ($procId in $still) { + Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue + } + } + + if ($before.Count -gt 0) { + if (-not (Wait-PidsGone -pids $before -seconds 45)) { + throw "Container-Prozess läuft noch nach Stop (PIDs=$($before -join ',')). SMC würde keinen Stop sehen." + } + Write-Output "INFO:Alte PIDs beendet" + } + + Start-Sleep -Seconds 3 + + # 4) Start + if (-not (Test-Path -LiteralPath $startBat)) { + throw "startcontainer.bat nicht gefunden: $startBat (SonicHome prüfen)" + } + + $started = $false + foreach ($n in @($fullName, $shortName)) { + Write-Output "INFO:startcontainer $n" + $st = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', "`"$startBat`" `"$n`"") -Wait -PassThru -NoNewWindow + Write-Output "INFO:startcontainer ExitCode=$($st.ExitCode) Name=$n" + if ($st.ExitCode -eq 0) { $started = $true; break } + } + + if (-not $started) { + throw "startcontainer fehlgeschlagen für $fullName / $shortName" + } + + $after = @(Wait-NewPids -oldPids $before -seconds 60) + if ($after.Count -eq 0) { + # falls Prozess mit gleicher PID-Liste zurückkam: mindestens irgendeinen Treffer verlangen + $any = @(Get-ContainerPids) + if ($any.Count -eq 0) { + throw "Nach Start kein Prozess für Container '$shortName' sichtbar. In SMC prüfen / SonicHome & ContainerName prüfen." + } + Write-Output "INFO:PIDsNachher=$($any -join ',')" + } + else { + Write-Output "INFO:PIDsNachher=$($after -join ',')" + } + + Write-Output "OK:ContainerRestartVerified Domain=$domain Container=$shortName" """; public const string DefaultContainerListScript = @@ -146,9 +226,8 @@ public sealed class SonicConnection Write-Output "WARN:SonicHomeNichtGefunden:$sonicHome" } else { - Write-Output "INFO:SonicHomeOk:$sonicHome" + Write-Output "INFO:SonicHomeOk:$sonicHome Domain=$domain" - # *.cache Ordner (Domain.Container.cache) – 1–2 Ebenen Get-ChildItem -LiteralPath $sonicHome -Directory -ErrorAction SilentlyContinue | ForEach-Object { if ($_.Name -like '*.cache') { @@ -159,33 +238,33 @@ public sealed class SonicConnection ForEach-Object { Add-Name ($_.Name -replace '\.cache$', '') } } - # container.xml / Containers-Verzeichnisse Get-ChildItem -LiteralPath $sonicHome -Recurse -Filter 'container.xml' -File -ErrorAction SilentlyContinue | Select-Object -First 40 | ForEach-Object { - $parent = Split-Path $_.DirectoryName -Leaf - Add-Name $parent + Add-Name (Split-Path $_.DirectoryName -Leaf) } } - # Windows-Dienste - Get-Service -ErrorAction SilentlyContinue | - Where-Object { $_.DisplayName -match 'Sonic|ESB|MQ|Aurea' -or $_.Name -match 'Sonic|ESB|MQ|Aurea' } | - ForEach-Object { - Add-Name $_.Name - Add-Name $_.DisplayName - } - - # Prozess-Hinweise (laufende Container) Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match 'sonic|mf\.framework|container\.xml' } | ForEach-Object { if ($_.CommandLine -match '([A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+)\.cache') { Add-Name $Matches[1] } + elseif ($_.CommandLine -match '\\([A-Za-z0-9_\-]+)\\container\.xml') { + Add-Name $Matches[1] + } } - if ($names.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($domain)) { + # Kurzform Domain.Container → Container + foreach ($n in @($names.ToArray())) { + if ($n -like '*.*') { + $parts = $n -split '\.', 2 + if ($parts.Count -eq 2) { Add-Name $parts[1] } + } + } + + if ($names.Count -eq 0) { Write-Output "WARN:KeineContainerGefunden Domain=$domain SonicHome=$sonicHome" } @@ -195,16 +274,7 @@ public sealed class SonicConnection public enum SonicManagementMode { - /// HTTP REST API (wenn vom Sonic-Server bereitgestellt). HttpApi = 0, - - /// - /// PowerShell Remoting (WinRM) – führt Scripts auf dem Sonic-Server aus. - /// WinRm = 1, - - /// - /// CMD/PowerShell lokal auf diesem PC – zum Testen direkt auf dem Sonic-Rechner. - /// LocalCmd = 2 } diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs index 23e0f2d..11f633b 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs @@ -173,7 +173,10 @@ public sealed class SonicManagementClient : IDisposable _connection.ResolveContainerListScript(), containerName: string.Empty, domainName: _connection.DomainName, - sonicHome: _connection.SonicHome); + sonicHome: _connection.SonicHome, + connectionUrl: _connection.ConnectionUrl, + username: _connection.Username, + password: _connection.Password); (bool ok, string? output, string? error) = await _scriptRunner!.RunScriptAsync(script, cancellationToken); @@ -200,16 +203,23 @@ public sealed class SonicManagementClient : IDisposable _connection.ResolveRestartScript(), containerName, _connection.DomainName, - _connection.SonicHome); + _connection.SonicHome, + connectionUrl: _connection.ConnectionUrl, + username: _connection.Username, + password: _connection.Password); (bool ok, string? output, string? error) = await _scriptRunner!.RunScriptAsync(script, cancellationToken); string mode = _connection.ManagementMode.ToString(); + bool verified = (output ?? string.Empty) + .Contains("OK:ContainerRestartVerified", StringComparison.OrdinalIgnoreCase); - if (!ok) + if (!ok || !verified) { return (false, $"Neustart fehlgeschlagen ({mode})", + $"Kein verifizierter Prozess-Neustart für Container '{containerName}' " + + $"(Domain '{_connection.DomainName}').\n" + $"Fehler: {error}\nAusgabe: {output}\nSonicHome={_connection.SonicHome}"); } @@ -217,8 +227,8 @@ public sealed class SonicManagementClient : IDisposable TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)), cancellationToken); - return (true, $"ESB-Container neugestartet ({mode})", - $"Ausgabe: {output ?? "(keine)"}"); + return (true, $"Container '{containerName}' neugestartet ({mode})", + $"Domain={_connection.DomainName}; verifiziert.\n{output}"); } private async Task<(bool, string, string?)> ImportXapiViaScriptAsync( @@ -235,7 +245,10 @@ public sealed class SonicManagementClient : IDisposable containerName, _connection.DomainName, _connection.SonicHome, - xapiSourcePath); + xapiSourcePath, + _connection.ConnectionUrl, + _connection.Username, + _connection.Password); (bool importOk, string? importOut, string? importErr) = await _scriptRunner!.RunScriptAsync(script, cancellationToken); diff --git a/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs b/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs index dd74c63..d486325 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs @@ -136,12 +136,18 @@ public sealed class WinRmExecutor string containerName, string domainName = "", string sonicHome = "", - string xapiPath = "") + string xapiPath = "", + string connectionUrl = "", + string username = "", + string password = "") => template .Replace("{container}", containerName.Replace("'", "''"), StringComparison.OrdinalIgnoreCase) .Replace("{domain}", domainName.Replace("'", "''"), StringComparison.OrdinalIgnoreCase) .Replace("{sonicHome}", sonicHome.Replace("'", "''"), StringComparison.OrdinalIgnoreCase) - .Replace("{xapiPath}", xapiPath.Replace("'", "''"), StringComparison.OrdinalIgnoreCase); + .Replace("{xapiPath}", xapiPath.Replace("'", "''"), StringComparison.OrdinalIgnoreCase) + .Replace("{connectionUrl}", connectionUrl.Replace("'", "''"), StringComparison.OrdinalIgnoreCase) + .Replace("{username}", username.Replace("'", "''"), StringComparison.OrdinalIgnoreCase) + .Replace("{password}", password.Replace("'", "''"), StringComparison.OrdinalIgnoreCase); private static string ExtractHost(string connectionUrl) { diff --git a/ZA.CoreService.ESBCertificateManager/appsettings.json b/ZA.CoreService.ESBCertificateManager/appsettings.json index f3c1560..5d7e440 100644 --- a/ZA.CoreService.ESBCertificateManager/appsettings.json +++ b/ZA.CoreService.ESBCertificateManager/appsettings.json @@ -15,12 +15,11 @@ "ManagementMode": "WinRm", "SonicHome": "C:\\Sonic\\MQ10.0", "KnownContainers": [ - "ESB", - "DomainManager" + "DE-Test" ], "WinRmPort": 5985, - "TimeoutSeconds": 90, - "PostRestartDelaySeconds": 15, + "TimeoutSeconds": 120, + "PostRestartDelaySeconds": 20, "ManagementHttpPort": 8080, "ApiBasePath": "/api/v1", "ContainerListPath": "",