Switch default restart to Sonic MF Management API via Domain Manager.

Use ConnectionUrl and SMC credentials with IAgentProxy.restart; keep WinRM only as optional fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-24 08:54:53 +02:00
co-authored by Cursor
parent d71d6990ad
commit 11941a45f5
8 changed files with 962 additions and 31 deletions
@@ -9,13 +9,12 @@ namespace ZA.CoreService.ESBCertificateManager.Services;
/// <summary>
/// Verwaltet Sonic ESB Container über die Management Console.
///
/// Modus = WinRm (Standard):
/// PowerShell Remoting (Invoke-Command) → CMD stop/startcontainer auf dem Sonic-Server.
/// WinRM nutzt WinRmUsername/WinRmPassword (Windows); leer = aktueller Benutzer.
/// Username/Password bleiben Sonic-SMC-/Domain-Manager-Logins.
/// Modus = MfApi (Standard):
/// Offizielle Sonic MF Management Runtime API über ConnectionUrl + SMC-Logins
/// (JMSConnectorClient → MFProxyFactory.createAgentProxy → IAgentProxy.restart).
///
/// Modus = LocalCmd:
/// Dieselbe CMD/PowerShell-Logik lokal (App muss auf dem Sonic-PC laufen; kein WinRM).
/// Modus = WinRm / LocalCmd (optionaler Fallback):
/// PowerShell/CMD stopcontainer/startcontainer (nicht Domain-Manager-nativ).
///
/// Modus = HttpApi:
/// HTTP REST API mit automatischer Pfad-Erkennung.
@@ -25,6 +24,7 @@ public sealed class SonicManagementClient : IDisposable
private readonly SonicConnection _connection;
private readonly HttpClient? _http;
private readonly WinRmExecutor? _scriptRunner;
private readonly SonicMfApiExecutor? _mfApi;
// Gecachter HTTP-Basis-Pfad (nur im HttpApi-Modus)
private string? _resolvedContainerBasePath;
@@ -37,14 +37,22 @@ public sealed class SonicManagementClient : IDisposable
"/containers"
];
private SonicManagementMode Mode => _connection.EffectiveManagementMode;
private bool UsesMfApi => Mode == SonicManagementMode.MfApi;
private bool UsesScripts =>
_connection.ManagementMode is SonicManagementMode.WinRm or SonicManagementMode.LocalCmd;
Mode is SonicManagementMode.WinRm or SonicManagementMode.LocalCmd;
public SonicManagementClient(SonicConnection connection)
{
_connection = connection;
if (UsesScripts)
if (UsesMfApi)
{
_mfApi = new SonicMfApiExecutor(connection);
}
else if (UsesScripts)
{
_scriptRunner = new WinRmExecutor(connection);
}
@@ -77,16 +85,25 @@ public sealed class SonicManagementClient : IDisposable
/// <summary>
/// Prüft die Verbindung zur Management Console.
/// WinRm: TCP-Ping auf WinRM-Port + Test-PSSession.
/// Http: Probe gegen bekannte API-Pfade.
/// MfApi: Domain-Manager über ConnectionUrl + SMC-Credentials.
/// WinRm/LocalCmd: Script-Laufzeit.
/// Http: Probe gegen bekannte API-Pfade.
/// </summary>
public async Task<(bool Success, string? Error, string? ResolvedPath)> CheckConnectionAsync(
CancellationToken cancellationToken = default)
{
if (UsesMfApi)
{
(bool ok, string? error) = await _mfApi!.TestConnectionAsync(cancellationToken);
string modeLabel =
$"MfApi {_connection.ConnectionUrl} Domain={_connection.DomainName}";
return (ok, error, ok ? modeLabel : null);
}
if (UsesScripts)
{
(bool ok, string? error) = await _scriptRunner!.TestConnectionAsync(cancellationToken);
string modeLabel = _connection.ManagementMode == SonicManagementMode.LocalCmd
string modeLabel = Mode == SonicManagementMode.LocalCmd
? $"LocalCmd (SonicHome={_connection.SonicHome})"
: $"WinRM auf {ExtractHost(_connection.ConnectionUrl)}:{_connection.WinRmPort}";
return (ok, error, ok ? modeLabel : null);
@@ -100,7 +117,7 @@ public sealed class SonicManagementClient : IDisposable
return (false,
$"Keine Sonic HTTP-API unter {_http!.BaseAddress} gefunden.\n" +
$"Geprüfte Pfade: {string.Join(", ", GetCandidatePaths())}\n" +
$"Tipp: ManagementMode auf 'WinRm' setzen falls kein HTTP-API vorhanden.",
$"Tipp: ManagementMode auf 'MfApi' setzen (ConnectionUrl + SMC-Logins).",
null);
}
@@ -110,7 +127,7 @@ public sealed class SonicManagementClient : IDisposable
{
return (false,
$"HTTP-Timeout nach {_connection.TimeoutSeconds}s Port {_connection.ManagementHttpPort} nicht erreichbar.\n" +
$"Tipp: ManagementMode auf 'WinRm' setzen.",
$"Tipp: ManagementMode auf 'MfApi' setzen.",
null);
}
catch (Exception ex)
@@ -125,6 +142,11 @@ public sealed class SonicManagementClient : IDisposable
public async Task<(bool Success, IReadOnlyList<string> ContainerNames, string? Error)> GetContainersAsync(
CancellationToken cancellationToken = default)
{
if (UsesMfApi)
{
return await GetContainersViaMfApiAsync(cancellationToken);
}
if (UsesScripts)
{
return await GetContainersViaScriptAsync(cancellationToken);
@@ -140,6 +162,11 @@ public sealed class SonicManagementClient : IDisposable
string containerName,
CancellationToken cancellationToken = default)
{
if (UsesMfApi)
{
return await RestartViaMfApiAsync(containerName, cancellationToken);
}
if (UsesScripts)
{
return await RestartViaScriptAsync(containerName, cancellationToken);
@@ -156,6 +183,41 @@ public sealed class SonicManagementClient : IDisposable
string xapiSourcePath,
CancellationToken cancellationToken = default)
{
if (UsesMfApi)
{
// XApi-Import bleibt script/HTTP; Neustart danach über MfApi.
if (!string.IsNullOrWhiteSpace(_connection.WinRmXapiImportScript))
{
WinRmExecutor local = new(CloneAsLocalCmd(_connection));
string script = WinRmExecutor.ApplyScriptTemplate(
_connection.WinRmXapiImportScript,
containerName,
_connection.DomainName,
_connection.SonicHome,
xapiSourcePath,
_connection.ConnectionUrl,
_connection.Username,
_connection.Password);
(bool importOk, string? importOut, string? importErr) =
await local.RunScriptAsync(script, cancellationToken);
if (!importOk)
{
return (false, "XApi-Import fehlgeschlagen", importErr ?? importOut);
}
(bool restartOk, string restartStatus, string? restartDetail) =
await RestartViaMfApiAsync(containerName, cancellationToken);
return restartOk
? (true, "XApi importiert + Container neugestartet (MfApi)",
$"Import: {importOut} | Restart: {restartDetail}")
: (false, restartStatus, restartDetail);
}
return (false, "XApi-Import fehlgeschlagen",
"Im MfApi-Modus ist WinRmXapiImportScript für den Import nötig, " +
"oder ManagementMode vorübergehend auf LocalCmd/WinRm setzen.");
}
if (UsesScripts)
{
return await ImportXapiViaScriptAsync(containerName, xapiSourcePath, cancellationToken);
@@ -164,6 +226,86 @@ public sealed class SonicManagementClient : IDisposable
return await ImportXapiViaHttpAsync(containerName, xapiSourcePath, cancellationToken);
}
// ---------------------------------------------------------------
// MfApi (Sonic Domain Manager / IAgentProxy.restart)
// ---------------------------------------------------------------
private async Task<(bool, IReadOnlyList<string>, string?)> GetContainersViaMfApiAsync(
CancellationToken cancellationToken)
{
(bool ok, IReadOnlyList<string> names, string? error) =
await _mfApi!.ListContainersAsync(cancellationToken);
if (!ok)
{
return (false, [], $"Container-Liste fehlgeschlagen (MfApi): {error}");
}
if (names.Count == 0 && _connection.KnownContainers.Count > 0)
{
List<string> withHint =
[
..names,
$"INFO:MfApiListeLeer FallbackKnownContainers={_connection.KnownContainers.Count}"
];
return (true, withHint, null);
}
return (true, names, null);
}
private async Task<(bool, string, string?)> RestartViaMfApiAsync(
string containerName, CancellationToken cancellationToken)
{
(bool ok, string? output, string? error) =
await _mfApi!.RestartAsync(containerName, cancellationToken);
if (!ok)
{
return (false, "Neustart fehlgeschlagen (MfApi)",
$"IAgentProxy.restart für '{containerName}' (Domain '{_connection.DomainName}') " +
$"über {_connection.ConnectionUrl} fehlgeschlagen.\n" +
$"Fehler: {error}\nAusgabe: {output}\n" +
"Voraussetzungen: Domain Manager erreichbar, SMC-Logins korrekt, " +
"Java/JDK + Sonic-Client-JARs (SonicHome\\lib), Container online.");
}
await Task.Delay(
TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)),
cancellationToken);
return (true, $"Container '{containerName}' neugestartet (MfApi)",
$"Domain={_connection.DomainName}; ConnectionUrl={_connection.ConnectionUrl}\n{output}");
}
private static SonicConnection CloneAsLocalCmd(SonicConnection source)
=> new()
{
Name = source.Name,
DomainName = source.DomainName,
ConnectionUrl = source.ConnectionUrl,
Username = source.Username,
Password = source.Password,
WinRmUsername = source.WinRmUsername,
WinRmPassword = source.WinRmPassword,
ManagementMode = SonicManagementMode.LocalCmd,
SonicHome = source.SonicHome,
MfClientLibPath = source.MfClientLibPath,
KnownContainers = source.KnownContainers,
ManagementHttpPort = source.ManagementHttpPort,
ApiBasePath = source.ApiBasePath,
ContainerListPath = source.ContainerListPath,
ContainerRestartPath = source.ContainerRestartPath,
ContainerStopPath = source.ContainerStopPath,
ContainerStartPath = source.ContainerStartPath,
WinRmPort = source.WinRmPort,
WinRmRestartScript = source.WinRmRestartScript,
WinRmContainerListScript = source.WinRmContainerListScript,
WinRmXapiImportScript = source.WinRmXapiImportScript,
TimeoutSeconds = source.TimeoutSeconds,
PostRestartDelaySeconds = source.PostRestartDelaySeconds
};
// ---------------------------------------------------------------
// Script-Implementierungen (WinRM / LocalCmd)
// ---------------------------------------------------------------
@@ -185,7 +327,7 @@ public sealed class SonicManagementClient : IDisposable
if (!ok)
{
return (false, [], $"Container-Liste fehlgeschlagen ({_connection.ManagementMode}): {error}");
return (false, [], $"Container-Liste fehlgeschlagen ({Mode}): {error}");
}
List<string> names = (output ?? string.Empty)
@@ -213,7 +355,7 @@ public sealed class SonicManagementClient : IDisposable
(bool ok, string? output, string? error) =
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
string mode = _connection.ManagementMode.ToString();
string mode = Mode.ToString();
bool verified = (output ?? string.Empty)
.Contains("OK:ContainerRestartVerified", StringComparison.OrdinalIgnoreCase);
@@ -0,0 +1,392 @@
using System.Diagnostics;
using System.Text;
using ZA.CoreService.ESBCertificateManager.Models;
namespace ZA.CoreService.ESBCertificateManager.Services;
/// <summary>
/// Führt Sonic-Container-Operationen über die offizielle MF Management Runtime API aus
/// (JMSConnectorClient + MFProxyFactory + IAgentProxy.restart), per Java-Hilfsprogramm
/// und Client-JARs unter SonicHome/lib bzw. MfClientLibPath.
/// Nutzt ConnectionUrl + Username/Password aus appsettings (SMC / Domain Manager).
/// </summary>
public sealed class SonicMfApiExecutor
{
private readonly SonicConnection _connection;
public SonicMfApiExecutor(SonicConnection connection)
{
_connection = connection;
}
public Task<(bool Success, string? Error)> TestConnectionAsync(CancellationToken cancellationToken = default)
=> RunAsync("ping", container: null, cancellationToken);
public async Task<(bool Success, IReadOnlyList<string> Containers, string? Error)> ListContainersAsync(
CancellationToken cancellationToken = default)
{
(bool ok, string? output, string? error) = await RunRawAsync("list", null, cancellationToken);
if (!ok)
{
return (false, [], error ?? output);
}
List<string> names = (output ?? string.Empty)
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(l => !l.StartsWith("OK:", StringComparison.OrdinalIgnoreCase))
.Where(l => !l.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase))
.Where(l => !l.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase))
.Where(l => !l.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase))
.Where(l => !l.StartsWith("Usage:", StringComparison.OrdinalIgnoreCase))
.ToList();
return (true, names, null);
}
public async Task<(bool Success, string? Output, string? Error)> RestartAsync(
string containerName,
CancellationToken cancellationToken = default)
{
(bool ok, string? output, string? error) = await RunRawAsync("restart", containerName, cancellationToken);
if (!ok)
{
return (false, output, error);
}
bool invoked = (output ?? string.Empty)
.Contains("OK:RestartInvoked", StringComparison.OrdinalIgnoreCase);
return invoked
? (true, output, error)
: (false, output, error ?? "Kein OK:RestartInvoked von SonicMfContainerTool.");
}
private async Task<(bool Success, string? Error)> RunAsync(
string command,
string? container,
CancellationToken cancellationToken)
{
(bool ok, string? output, string? error) = await RunRawAsync(command, container, cancellationToken);
if (ok)
{
return (true, null);
}
return (false, error ?? output);
}
private async Task<(bool Success, string? Output, string? Error)> RunRawAsync(
string command,
string? container,
CancellationToken cancellationToken)
{
(bool prepared, string? javaExe, string? classDir, string? classpath, string? prepareError) =
await PrepareToolAsync(cancellationToken);
if (!prepared)
{
return (false, null, prepareError);
}
List<string> args =
[
"-cp", Quote(classpath!),
"SonicMfContainerTool",
command,
"--domain", _connection.DomainName,
"--url", _connection.ConnectionUrl,
"--user", _connection.Username,
"--timeout", Math.Clamp(_connection.TimeoutSeconds, 5, 600).ToString()
];
if (!string.IsNullOrWhiteSpace(container))
{
args.Add("--container");
args.Add(container);
}
ProcessStartInfo psi = new()
{
FileName = javaExe!,
Arguments = string.Join(" ", args.Select(EscapeArg)),
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
WorkingDirectory = classDir!
};
psi.Environment["ESB_SONIC_PASSWORD"] = _connection.Password ?? string.Empty;
using Process process = new() { StartInfo = psi };
if (!process.Start())
{
return (false, null, "Java-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, $"MfApi Timeout nach {_connection.TimeoutSeconds}s.");
}
string stdout = (await stdoutTask).Trim();
string stderr = (await stderrTask).Trim();
string combined = string.Join("\n", new[] { stdout, stderr }.Where(s => s.Length > 0));
if (process.ExitCode == 0
&& !combined.Contains("ERROR:", StringComparison.OrdinalIgnoreCase))
{
return (true, combined, null);
}
string err = ExtractError(combined)
?? $"SonicMfContainerTool ExitCode={process.ExitCode}";
return (false, combined.Length > 0 ? combined : null, err);
}
private async Task<(bool Ok, string? JavaExe, string? ClassDir, string? Classpath, string? Error)>
PrepareToolAsync(CancellationToken cancellationToken)
{
string? javaExe = ResolveJavaExecutable();
if (javaExe is null)
{
return (false, null, null, null,
"Java nicht gefunden (JAVA_HOME/bin/java.exe oder PATH). " +
"Für MfApi wird ein JRE/JDK benötigt.");
}
string? libDir = ResolveLibDirectory();
if (libDir is null)
{
return (false, null, null, null,
"Sonic-Client-JARs nicht gefunden. SonicHome\\lib oder MfClientLibPath setzen " +
$"(aktuell SonicHome='{_connection.SonicHome}', MfClientLibPath='{_connection.MfClientLibPath}'). " +
"Benötigt u.a. mgmt_client.jar / mfcontext.jar / sonic_Client.jar.");
}
string? sourcePath = ResolveToolSourcePath();
if (sourcePath is null)
{
return (false, null, null, null,
"Tools\\SonicMfContainerTool.java nicht gefunden (App-Ausgabeverzeichnis prüfen).");
}
string workDir = Path.Combine(Path.GetTempPath(), "esb-sonic-mf");
Directory.CreateDirectory(workDir);
string javaTarget = Path.Combine(workDir, "SonicMfContainerTool.java");
string classFile = Path.Combine(workDir, "SonicMfContainerTool.class");
File.Copy(sourcePath, javaTarget, overwrite: true);
string classpath = BuildClasspath(libDir, workDir);
bool needsCompile = !File.Exists(classFile)
|| File.GetLastWriteTimeUtc(classFile) < File.GetLastWriteTimeUtc(sourcePath);
if (needsCompile)
{
string? javac = ResolveJavacExecutable(javaExe);
if (javac is null)
{
return (false, null, null, null,
"javac nicht gefunden. JDK installieren (nicht nur JRE), " +
"damit SonicMfContainerTool.java kompiliert werden kann.");
}
ProcessStartInfo compilePsi = new()
{
FileName = javac,
Arguments = $"-encoding UTF-8 -cp {Quote(BuildClasspath(libDir, null))} -d {Quote(workDir)} {Quote(javaTarget)}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
using Process compile = new() { StartInfo = compilePsi };
if (!compile.Start())
{
return (false, null, null, null, "javac konnte nicht gestartet werden.");
}
string cOut = await compile.StandardOutput.ReadToEndAsync(cancellationToken);
string cErr = await compile.StandardError.ReadToEndAsync(cancellationToken);
await compile.WaitForExitAsync(cancellationToken);
if (compile.ExitCode != 0 || !File.Exists(classFile))
{
return (false, null, null, null,
"Kompilieren von SonicMfContainerTool fehlgeschlagen.\n" +
Truncate((cErr + "\n" + cOut).Trim()) +
$"\nClasspath-Lib: {libDir}");
}
}
return (true, javaExe, workDir, classpath, null);
}
private string? ResolveLibDirectory()
{
List<string> candidates = [];
if (!string.IsNullOrWhiteSpace(_connection.MfClientLibPath))
{
candidates.Add(_connection.MfClientLibPath);
}
if (!string.IsNullOrWhiteSpace(_connection.SonicHome))
{
candidates.Add(Path.Combine(_connection.SonicHome, "lib"));
candidates.Add(_connection.SonicHome);
}
foreach (string dir in candidates.Where(Directory.Exists))
{
if (Directory.EnumerateFiles(dir, "*.jar", SearchOption.TopDirectoryOnly).Any())
{
return dir;
}
}
return null;
}
private static string BuildClasspath(string libDir, string? extraDir)
{
// Java-Classpath-Wildcard für alle JARs im Lib-Ordner
string jars = Path.Combine(libDir, "*");
return string.IsNullOrWhiteSpace(extraDir)
? jars
: jars + Path.PathSeparator + extraDir;
}
private static string? ResolveToolSourcePath()
{
string[] candidates =
[
Path.Combine(AppContext.BaseDirectory, "Tools", "SonicMfContainerTool.java"),
Path.Combine(Directory.GetCurrentDirectory(), "Tools", "SonicMfContainerTool.java"),
Path.Combine(AppContext.BaseDirectory, "SonicMfContainerTool.java")
];
return candidates.FirstOrDefault(File.Exists);
}
private static string? ResolveJavaExecutable()
{
string? javaHome = Environment.GetEnvironmentVariable("JAVA_HOME");
if (!string.IsNullOrWhiteSpace(javaHome))
{
string candidate = Path.Combine(javaHome, "bin", "java.exe");
if (File.Exists(candidate))
{
return candidate;
}
}
return FindOnPath("java.exe") ?? FindOnPath("java");
}
private static string? ResolveJavacExecutable(string javaExe)
{
string? dir = Path.GetDirectoryName(javaExe);
if (!string.IsNullOrWhiteSpace(dir))
{
string sibling = Path.Combine(dir, OperatingSystem.IsWindows() ? "javac.exe" : "javac");
if (File.Exists(sibling))
{
return sibling;
}
}
string? javaHome = Environment.GetEnvironmentVariable("JAVA_HOME");
if (!string.IsNullOrWhiteSpace(javaHome))
{
string candidate = Path.Combine(javaHome, "bin",
OperatingSystem.IsWindows() ? "javac.exe" : "javac");
if (File.Exists(candidate))
{
return candidate;
}
}
return FindOnPath("javac.exe") ?? FindOnPath("javac");
}
private static string? FindOnPath(string fileName)
{
string? pathEnv = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrWhiteSpace(pathEnv))
{
return null;
}
foreach (string dir in pathEnv.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
{
try
{
string candidate = Path.Combine(dir.Trim('"'), fileName);
if (File.Exists(candidate))
{
return candidate;
}
}
catch
{
// ignore bad PATH entries
}
}
return null;
}
private static string? ExtractError(string combined)
{
foreach (string line in combined.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
{
if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase))
{
return line["ERROR:".Length..].Trim();
}
}
return string.IsNullOrWhiteSpace(combined) ? null : Truncate(combined);
}
private static string Quote(string value)
=> "\"" + value.Replace("\"", "\\\"") + "\"";
private static string EscapeArg(string value)
{
if (value.Length == 0)
{
return "\"\"";
}
bool needsQuotes = value.Contains(' ') || value.Contains('\t') || value.Contains('"')
|| value.Contains('*') || value.Contains(';');
if (!needsQuotes)
{
return value;
}
return "\"" + value.Replace("\"", "\\\"") + "\"";
}
private static string Truncate(string s, int max = 800)
=> s.Length <= max ? s : s[..max] + "…";
}
@@ -27,7 +27,7 @@ public sealed class WinRmExecutor
public async Task<(bool Success, string? Error)> TestConnectionAsync(
CancellationToken cancellationToken = default)
{
if (_connection.ManagementMode == SonicManagementMode.LocalCmd)
if (_connection.EffectiveManagementMode == SonicManagementMode.LocalCmd)
{
(bool ok, string? output, string? error) = await RunScriptAsync(
"$env:COMPUTERNAME", cancellationToken);
@@ -66,7 +66,7 @@ public sealed class WinRmExecutor
string scriptBlock,
CancellationToken cancellationToken = default)
{
string fullScript = _connection.ManagementMode == SonicManagementMode.LocalCmd
string fullScript = _connection.EffectiveManagementMode == SonicManagementMode.LocalCmd
? BuildLocalScript(scriptBlock)
: BuildRemoteScript(ExtractHost(_connection.ConnectionUrl), scriptBlock);
@@ -126,7 +126,7 @@ public sealed class WinRmExecutor
? stderr
: $"PowerShell ExitCode={process.ExitCode}";
string error = _connection.ManagementMode == SonicManagementMode.WinRm
string error = _connection.EffectiveManagementMode == SonicManagementMode.WinRm
? FormatWinRmFailure(rawError)
: Truncate(rawError);