diff --git a/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs b/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs index cdd0249..62305e6 100644 --- a/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs +++ b/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs @@ -60,8 +60,10 @@ public sealed class SonicConnection : WinRmContainerListScript; /// - /// Echter Neustart mit Prozess-Verifikation (sichtbar in SMC): - /// alte PIDs müssen weg, neue PIDs müssen erscheinen – sonst Fehler. + /// Offizieller MF-Container-Neustart laut Aurea CX Messenger Doku: + /// SonicHome\bin\stopcontainer.bat Domain.Container + /// SonicHome\bin\startcontainer.bat Domain.Container + /// Danach Prozess-Verifikation (alte PIDs weg, neue PIDs da). /// public const string DefaultRestartScript = """ @@ -69,14 +71,10 @@ public sealed class SonicConnection $sonicHome = '{sonicHome}' $domain = '{domain}' $container = '{container}' - $connectionUrl = '{connectionUrl}' - $username = '{username}' - $password = '{password}' $bin = Join-Path $sonicHome 'bin' $stopBat = Join-Path $bin 'stopcontainer.bat' $startBat = Join-Path $bin 'startcontainer.bat' - $esbAdmin = Join-Path $bin 'esbadmin.bat' # 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 } @@ -120,46 +118,40 @@ public sealed class SonicConnection return ,@() } + function Invoke-ContainerBat([string]$bat, [string]$name) { + if (-not (Test-Path -LiteralPath $bat)) { + throw "Sonic BAT fehlt: $bat (SonicHome pruefen)" + } + $arg = '/c "' + $bat + '" "' + $name + '"' + $p = Start-Process -FilePath 'cmd.exe' -ArgumentList $arg -Wait -PassThru -NoNewWindow + return [int]$p.ExitCode + } + Write-Output "INFO:Domain=$domain Container=$shortName Full=$fullName" + Write-Output "INFO:RestartVia=stopcontainer/startcontainer (MF Container)" $before = @(Get-ContainerPids) - Write-Output "INFO:PIDsVorher=$($before -join ',')" + Write-Output "INFO:PIDsBefore=$($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." + Write-Output "WARN:No running Java/Sonic process for '$shortName' - starting anyway" } - # 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 + # 1) Offiziell: stopcontainer.bat Domain.Container (danach Kurzname als Fallback) + $stopOk = $false + foreach ($n in @($fullName, $shortName)) { + Write-Output "INFO:stopcontainer $n" + $code = Invoke-ContainerBat -bat $stopBat -name $n + Write-Output "INFO:stopcontainer ExitCode=$code Name=$n" + if ($code -eq 0) { $stopOk = $true; break } } - - # 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" + if (-not $stopOk) { + Write-Output "WARN:stopcontainer non-zero; will force-stop remaining PIDs if any" } Start-Sleep -Seconds 3 $still = @(Get-ContainerPids) - # 3) Harter Stop der alten PIDs – sonst sieht SMC oft keinen Neustart + # 2) Force-Stop falls BAT den Prozess nicht beendet (sonst kein echter Restart in SMC) if ($still.Count -gt 0) { Write-Output "INFO:Force-Stop PIDs=$($still -join ',')" foreach ($procId in $still) { @@ -169,41 +161,36 @@ public sealed class SonicConnection 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." + throw "Container process still running after stop (PIDs=$($before -join ','))" } - Write-Output "INFO:Alte PIDs beendet" + Write-Output "INFO:Old PIDs gone" } Start-Sleep -Seconds 3 - # 4) Start - if (-not (Test-Path -LiteralPath $startBat)) { - throw "startcontainer.bat nicht gefunden: $startBat (SonicHome prüfen)" - } - + # 3) Offiziell: startcontainer.bat Domain.Container $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 } + $code = Invoke-ContainerBat -bat $startBat -name $n + Write-Output "INFO:startcontainer ExitCode=$code Name=$n" + if ($code -eq 0) { $started = $true; break } } if (-not $started) { - throw "startcontainer fehlgeschlagen für $fullName / $shortName" + throw "startcontainer failed for $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." + throw "After start no process for container '$shortName'. Check SMC / SonicHome / ContainerName." } - Write-Output "INFO:PIDsNachher=$($any -join ',')" + Write-Output "INFO:PIDsAfter=$($any -join ',')" } else { - Write-Output "INFO:PIDsNachher=$($after -join ',')" + Write-Output "INFO:PIDsAfter=$($after -join ',')" } Write-Output "OK:ContainerRestartVerified Domain=$domain Container=$shortName" diff --git a/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs b/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs index d486325..7cbd386 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs @@ -6,9 +6,14 @@ namespace ZA.CoreService.ESBCertificateManager.Services; /// /// Führt PowerShell-Befehle lokal oder via WinRM (Invoke-Command) auf dem Sonic-Server aus. +/// Schreibt Skripte als .ps1 (UTF-8 ohne BOM) und startet sie mit -File, +/// um den bekannten stdin/BOM-Fehler zu vermeiden +/// ("$ErrorActionPreference wurde nicht als Name eines Cmdlet erkannt"). /// public sealed class WinRmExecutor { + private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + private readonly SonicConnection _connection; public WinRmExecutor(SonicConnection connection) @@ -62,73 +67,91 @@ public sealed class WinRmExecutor ? BuildLocalScript(scriptBlock) : BuildRemoteScript(ExtractHost(_connection.ConnectionUrl), scriptBlock); - ProcessStartInfo psi = new() - { - FileName = "powershell.exe", - Arguments = "-NonInteractive -NoProfile -ExecutionPolicy Bypass -Command -", - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - StandardInputEncoding = Encoding.UTF8 - }; + string tempFile = Path.Combine( + Path.GetTempPath(), + $"esb-winrm-{Guid.NewGuid():N}.ps1"); - using Process process = new() { StartInfo = psi }; - - if (!process.Start()) - { - return (false, null, "PowerShell-Prozess konnte nicht gestartet werden."); - } - - await process.StandardInput.WriteAsync(fullScript.AsMemory(), cancellationToken); - process.StandardInput.Close(); - - 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); + await File.WriteAllTextAsync(tempFile, fullScript, Utf8NoBom, cancellationToken); try { - await process.WaitForExitAsync(timeoutCts.Token); + ProcessStartInfo psi = new() + { + FileName = "powershell.exe", + Arguments = "-NonInteractive -NoProfile -ExecutionPolicy Bypass -File \"" + tempFile + "\"", + 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, null, "PowerShell-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, $"Ausführung Timeout nach {_connection.TimeoutSeconds}s."); + } + + string stdout = (await stdoutTask).Trim(); + string stderr = (await stderrTask).Trim(); + + if (process.ExitCode == 0) + { + return (true, stdout, stderr.Length > 0 ? stderr : null); + } + + return (false, stdout.Length > 0 ? stdout : null, + stderr.Length > 0 ? Truncate(stderr) : $"PowerShell ExitCode={process.ExitCode}"); } - catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + finally { - try { process.Kill(entireProcessTree: true); } catch { /* ignore */ } - return (false, null, $"Ausführung Timeout nach {_connection.TimeoutSeconds}s."); + try { File.Delete(tempFile); } catch { /* ignore */ } } - - string stdout = (await stdoutTask).Trim(); - string stderr = (await stderrTask).Trim(); - - if (process.ExitCode == 0) - { - return (true, stdout, stderr.Length > 0 ? stderr : null); - } - - return (false, stdout.Length > 0 ? stdout : null, - stderr.Length > 0 ? Truncate(stderr) : $"PowerShell ExitCode={process.ExitCode}"); } private static string BuildLocalScript(string scriptBlock) => "$ErrorActionPreference = 'Stop'\n" + scriptBlock; + /// + /// Remote-Payload als Base64 einbetten und per [scriptblock]::Create ausführen. + /// Vermeidet verschachteltes ScriptBlock-Brace-Nesting und lokale $-Expansion. + /// private string BuildRemoteScript(string host, string scriptBlock) { string escapedPwd = _connection.Password.Replace("'", "''"); string escapedUser = _connection.Username.Replace("'", "''"); string escapedHost = host.Replace("'", "''"); + string remoteB64 = Convert.ToBase64String(Encoding.Unicode.GetBytes(scriptBlock)); return "$ErrorActionPreference = 'Stop'\n" + $"$secPwd = ConvertTo-SecureString '{escapedPwd}' -AsPlainText -Force\n" + $"$cred = New-Object System.Management.Automation.PSCredential('{escapedUser}', $secPwd)\n" + - $"Invoke-Command -ComputerName '{escapedHost}' -Port {_connection.WinRmPort} -Credential $cred -ScriptBlock {{\n" + - $" {scriptBlock}\n" + - "} -ErrorAction Stop"; + $"$remoteB64 = '{remoteB64}'\n" + + "$remoteScript = [System.Text.Encoding]::Unicode.GetString(" + + "[System.Convert]::FromBase64String($remoteB64))\n" + + "$sb = [scriptblock]::Create($remoteScript)\n" + + $"Invoke-Command -ComputerName '{escapedHost}' -Port {_connection.WinRmPort} " + + "-Credential $cred -ScriptBlock $sb -ErrorAction Stop"; } public static string ApplyScriptTemplate(