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:
@@ -6,9 +6,14 @@ namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
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<string> stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
||||
Task<string> 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<string> stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
||||
Task<string> 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;
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user