Fix WinRM restart: avoid UTF-8 BOM PowerShell parse error.

Run scripts via -File (UTF-8 no BOM) and Base64 ScriptBlock, using official stopcontainer/startcontainer for MF restart.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-24 08:36:10 +02:00
co-authored by Cursor
parent fe1988cdc7
commit cfbf80ecbf
2 changed files with 104 additions and 94 deletions
@@ -60,8 +60,10 @@ public sealed class SonicConnection
: WinRmContainerListScript; : WinRmContainerListScript;
/// <summary> /// <summary>
/// Echter Neustart mit Prozess-Verifikation (sichtbar in SMC): /// Offizieller MF-Container-Neustart laut Aurea CX Messenger Doku:
/// alte PIDs müssen weg, neue PIDs müssen erscheinen sonst Fehler. /// SonicHome\bin\stopcontainer.bat Domain.Container
/// SonicHome\bin\startcontainer.bat Domain.Container
/// Danach Prozess-Verifikation (alte PIDs weg, neue PIDs da).
/// </summary> /// </summary>
public const string DefaultRestartScript = public const string DefaultRestartScript =
""" """
@@ -69,14 +71,10 @@ public sealed class SonicConnection
$sonicHome = '{sonicHome}' $sonicHome = '{sonicHome}'
$domain = '{domain}' $domain = '{domain}'
$container = '{container}' $container = '{container}'
$connectionUrl = '{connectionUrl}'
$username = '{username}'
$password = '{password}'
$bin = Join-Path $sonicHome 'bin' $bin = Join-Path $sonicHome 'bin'
$stopBat = Join-Path $bin 'stopcontainer.bat' $stopBat = Join-Path $bin 'stopcontainer.bat'
$startBat = Join-Path $bin 'startcontainer.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) # 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 } $shortName = if ($container -like '*.*') { ($container -split '\.', 2)[1] } else { $container }
@@ -120,46 +118,40 @@ public sealed class SonicConnection
return ,@() 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:Domain=$domain Container=$shortName Full=$fullName"
Write-Output "INFO:RestartVia=stopcontainer/startcontainer (MF Container)"
$before = @(Get-ContainerPids) $before = @(Get-ContainerPids)
Write-Output "INFO:PIDsVorher=$($before -join ',')" Write-Output "INFO:PIDsBefore=$($before -join ',')"
if ($before.Count -eq 0) { 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) # 1) Offiziell: stopcontainer.bat Domain.Container (danach Kurzname als Fallback)
if (Test-Path -LiteralPath $esbAdmin) { $stopOk = $false
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)) { foreach ($n in @($fullName, $shortName)) {
Write-Output "INFO:stopcontainer $n" Write-Output "INFO:stopcontainer $n"
$sp = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', "`"$stopBat`" `"$n`"") -Wait -PassThru -NoNewWindow $code = Invoke-ContainerBat -bat $stopBat -name $n
Write-Output "INFO:stopcontainer ExitCode=$($sp.ExitCode) Name=$n" Write-Output "INFO:stopcontainer ExitCode=$code Name=$n"
if ($code -eq 0) { $stopOk = $true; break }
} }
} if (-not $stopOk) {
else { Write-Output "WARN:stopcontainer non-zero; will force-stop remaining PIDs if any"
Write-Output "WARN:stopcontainer.bat fehlt: $stopBat"
} }
Start-Sleep -Seconds 3 Start-Sleep -Seconds 3
$still = @(Get-ContainerPids) $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) { if ($still.Count -gt 0) {
Write-Output "INFO:Force-Stop PIDs=$($still -join ',')" Write-Output "INFO:Force-Stop PIDs=$($still -join ',')"
foreach ($procId in $still) { foreach ($procId in $still) {
@@ -169,41 +161,36 @@ public sealed class SonicConnection
if ($before.Count -gt 0) { if ($before.Count -gt 0) {
if (-not (Wait-PidsGone -pids $before -seconds 45)) { 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 Start-Sleep -Seconds 3
# 4) Start # 3) Offiziell: startcontainer.bat Domain.Container
if (-not (Test-Path -LiteralPath $startBat)) {
throw "startcontainer.bat nicht gefunden: $startBat (SonicHome prüfen)"
}
$started = $false $started = $false
foreach ($n in @($fullName, $shortName)) { foreach ($n in @($fullName, $shortName)) {
Write-Output "INFO:startcontainer $n" Write-Output "INFO:startcontainer $n"
$st = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', "`"$startBat`" `"$n`"") -Wait -PassThru -NoNewWindow $code = Invoke-ContainerBat -bat $startBat -name $n
Write-Output "INFO:startcontainer ExitCode=$($st.ExitCode) Name=$n" Write-Output "INFO:startcontainer ExitCode=$code Name=$n"
if ($st.ExitCode -eq 0) { $started = $true; break } if ($code -eq 0) { $started = $true; break }
} }
if (-not $started) { if (-not $started) {
throw "startcontainer fehlgeschlagen für $fullName / $shortName" throw "startcontainer failed for $fullName / $shortName"
} }
$after = @(Wait-NewPids -oldPids $before -seconds 60) $after = @(Wait-NewPids -oldPids $before -seconds 60)
if ($after.Count -eq 0) { if ($after.Count -eq 0) {
# falls Prozess mit gleicher PID-Liste zurückkam: mindestens irgendeinen Treffer verlangen
$any = @(Get-ContainerPids) $any = @(Get-ContainerPids)
if ($any.Count -eq 0) { 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 { else {
Write-Output "INFO:PIDsNachher=$($after -join ',')" Write-Output "INFO:PIDsAfter=$($after -join ',')"
} }
Write-Output "OK:ContainerRestartVerified Domain=$domain Container=$shortName" Write-Output "OK:ContainerRestartVerified Domain=$domain Container=$shortName"
@@ -6,9 +6,14 @@ namespace ZA.CoreService.ESBCertificateManager.Services;
/// <summary> /// <summary>
/// Führt PowerShell-Befehle lokal oder via WinRM (Invoke-Command) auf dem Sonic-Server aus. /// 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").
/// </summary> /// </summary>
public sealed class WinRmExecutor public sealed class WinRmExecutor
{ {
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private readonly SonicConnection _connection; private readonly SonicConnection _connection;
public WinRmExecutor(SonicConnection connection) public WinRmExecutor(SonicConnection connection)
@@ -62,16 +67,24 @@ public sealed class WinRmExecutor
? BuildLocalScript(scriptBlock) ? BuildLocalScript(scriptBlock)
: BuildRemoteScript(ExtractHost(_connection.ConnectionUrl), scriptBlock); : BuildRemoteScript(ExtractHost(_connection.ConnectionUrl), scriptBlock);
string tempFile = Path.Combine(
Path.GetTempPath(),
$"esb-winrm-{Guid.NewGuid():N}.ps1");
await File.WriteAllTextAsync(tempFile, fullScript, Utf8NoBom, cancellationToken);
try
{
ProcessStartInfo psi = new() ProcessStartInfo psi = new()
{ {
FileName = "powershell.exe", FileName = "powershell.exe",
Arguments = "-NonInteractive -NoProfile -ExecutionPolicy Bypass -Command -", Arguments = "-NonInteractive -NoProfile -ExecutionPolicy Bypass -File \"" + tempFile + "\"",
UseShellExecute = false, UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
CreateNoWindow = true, CreateNoWindow = true,
StandardInputEncoding = Encoding.UTF8 StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
}; };
using Process process = new() { StartInfo = psi }; using Process process = new() { StartInfo = psi };
@@ -81,9 +94,6 @@ public sealed class WinRmExecutor
return (false, null, "PowerShell-Prozess konnte nicht gestartet werden."); return (false, null, "PowerShell-Prozess konnte nicht gestartet werden.");
} }
await process.StandardInput.WriteAsync(fullScript.AsMemory(), cancellationToken);
process.StandardInput.Close();
using CancellationTokenSource timeoutCts = using CancellationTokenSource timeoutCts =
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 10, 600))); timeoutCts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 10, 600)));
@@ -112,23 +122,36 @@ public sealed class WinRmExecutor
return (false, stdout.Length > 0 ? stdout : null, return (false, stdout.Length > 0 ? stdout : null,
stderr.Length > 0 ? Truncate(stderr) : $"PowerShell ExitCode={process.ExitCode}"); stderr.Length > 0 ? Truncate(stderr) : $"PowerShell ExitCode={process.ExitCode}");
} }
finally
{
try { File.Delete(tempFile); } catch { /* ignore */ }
}
}
private static string BuildLocalScript(string scriptBlock) private static string BuildLocalScript(string scriptBlock)
=> "$ErrorActionPreference = 'Stop'\n" + scriptBlock; => "$ErrorActionPreference = 'Stop'\n" + scriptBlock;
/// <summary>
/// Remote-Payload als Base64 einbetten und per [scriptblock]::Create ausführen.
/// Vermeidet verschachteltes ScriptBlock-Brace-Nesting und lokale $-Expansion.
/// </summary>
private string BuildRemoteScript(string host, string scriptBlock) private string BuildRemoteScript(string host, string scriptBlock)
{ {
string escapedPwd = _connection.Password.Replace("'", "''"); string escapedPwd = _connection.Password.Replace("'", "''");
string escapedUser = _connection.Username.Replace("'", "''"); string escapedUser = _connection.Username.Replace("'", "''");
string escapedHost = host.Replace("'", "''"); string escapedHost = host.Replace("'", "''");
string remoteB64 = Convert.ToBase64String(Encoding.Unicode.GetBytes(scriptBlock));
return return
"$ErrorActionPreference = 'Stop'\n" + "$ErrorActionPreference = 'Stop'\n" +
$"$secPwd = ConvertTo-SecureString '{escapedPwd}' -AsPlainText -Force\n" + $"$secPwd = ConvertTo-SecureString '{escapedPwd}' -AsPlainText -Force\n" +
$"$cred = New-Object System.Management.Automation.PSCredential('{escapedUser}', $secPwd)\n" + $"$cred = New-Object System.Management.Automation.PSCredential('{escapedUser}', $secPwd)\n" +
$"Invoke-Command -ComputerName '{escapedHost}' -Port {_connection.WinRmPort} -Credential $cred -ScriptBlock {{\n" + $"$remoteB64 = '{remoteB64}'\n" +
$" {scriptBlock}\n" + "$remoteScript = [System.Text.Encoding]::Unicode.GetString(" +
"} -ErrorAction Stop"; "[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( public static string ApplyScriptTemplate(