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(