Fix app startup and enable Sonic ESB restart via remote CMD.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -26,6 +26,89 @@ public sealed class DeploymentOrchestrator
|
||||
IReadOnlyList<DeploymentTarget> selectedTargets)
|
||||
=> _preflightValidator.Validate(certificatePath, certificateInfo, selectedTargets);
|
||||
|
||||
public PreflightValidationResult ValidateRestartOnly(IReadOnlyList<DeploymentTarget> selectedTargets)
|
||||
=> _preflightValidator.ValidateRestartOnly(selectedTargets);
|
||||
|
||||
/// <summary>
|
||||
/// Nur ESB-/Container-Neustart – ohne Zertifikatskopieren und ohne TLS-Probe.
|
||||
/// </summary>
|
||||
public async Task<DeploymentRunResult> RestartOnlyAsync(
|
||||
IReadOnlyList<DeploymentTarget> selectedTargets,
|
||||
IProgress<TargetProgressUpdate>? progress,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
DateTimeOffset startedAt = DateTimeOffset.Now;
|
||||
List<TargetStepResult> results = [];
|
||||
|
||||
using RunLogger logger = new(_settings.LogDirectory);
|
||||
logger.Write($"Neustart-only gestartet für {selectedTargets.Count} Ziel(e).");
|
||||
|
||||
foreach (DeploymentTarget target in selectedTargets)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
results.Add(await RestartTargetOnlyAsync(target, logger, progress, cancellationToken));
|
||||
}
|
||||
|
||||
DateTimeOffset finishedAt = DateTimeOffset.Now;
|
||||
DeploymentRunResult runResult = new()
|
||||
{
|
||||
TargetResults = results,
|
||||
StartedAt = startedAt,
|
||||
FinishedAt = finishedAt,
|
||||
CertificateFilePath = string.Empty,
|
||||
CertificateFingerprint = string.Empty
|
||||
};
|
||||
|
||||
logger.Write(
|
||||
$"Neustart-only beendet. Erfolg={runResult.OverallSuccess}; Dauer={(finishedAt - startedAt).TotalSeconds:F1}s; Log={logger.LogFilePath}");
|
||||
|
||||
try
|
||||
{
|
||||
await _sqlRunLogger.PersistRunResultAsync(runResult, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Write($"SQL-Protokollierung fehlgeschlagen (nicht kritisch): {ex.Message}");
|
||||
}
|
||||
|
||||
return runResult;
|
||||
}
|
||||
|
||||
private async Task<TargetStepResult> RestartTargetOnlyAsync(
|
||||
DeploymentTarget target,
|
||||
RunLogger logger,
|
||||
IProgress<TargetProgressUpdate>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<string> steps = [];
|
||||
DateTimeOffset targetStart = DateTimeOffset.Now;
|
||||
progress?.Report(new TargetProgressUpdate(target.Id, "Neustart…", false));
|
||||
|
||||
(bool restartOk, string restartStatus, string? restartDetail) =
|
||||
await _restartExecutor.ExecuteAsync(target, cancellationToken);
|
||||
|
||||
RecordStep(steps, logger, target.Name, "Restart", restartStatus, restartDetail);
|
||||
|
||||
TargetStepResult targetResult = new()
|
||||
{
|
||||
TargetId = target.Id,
|
||||
TargetName = target.Name,
|
||||
Success = restartOk,
|
||||
StatusText = restartStatus,
|
||||
Detail = restartDetail,
|
||||
Steps = steps,
|
||||
StartedAt = targetStart,
|
||||
FinishedAt = DateTimeOffset.Now,
|
||||
CopySucceeded = true,
|
||||
RestartSucceeded = restartOk,
|
||||
TlsSucceeded = true,
|
||||
ObservedFingerprint = null
|
||||
};
|
||||
|
||||
progress?.Report(new TargetProgressUpdate(target.Id, restartStatus, restartOk));
|
||||
return targetResult;
|
||||
}
|
||||
|
||||
public async Task<DeploymentRunResult> RunAsync(
|
||||
string certificatePath,
|
||||
CertificateInfo certificateInfo,
|
||||
|
||||
@@ -32,22 +32,55 @@ public sealed class PreflightValidator
|
||||
|
||||
foreach (DeploymentTarget target in selectedTargets)
|
||||
{
|
||||
ValidateTarget(result, target);
|
||||
ValidateTarget(result, target, requireDeployPaths: true);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ValidateTarget(PreflightValidationResult result, DeploymentTarget target)
|
||||
/// <summary>
|
||||
/// Vorabprüfung nur für ESB-Neustart (ohne Zertifikat / Kopierpfade).
|
||||
/// </summary>
|
||||
public PreflightValidationResult ValidateRestartOnly(IReadOnlyList<DeploymentTarget> selectedTargets)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(target.TargetDirectory))
|
||||
PreflightValidationResult result = new();
|
||||
|
||||
if (selectedTargets.Count == 0)
|
||||
{
|
||||
AddIssue(result, $"Ziel '{target.Name}': TargetDirectory fehlt.", target.Id);
|
||||
AddIssue(result, "Bitte mindestens ein Ziel anhaken.");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(target.CertificateFileName))
|
||||
foreach (DeploymentTarget target in selectedTargets)
|
||||
{
|
||||
AddIssue(result, $"Ziel '{target.Name}': CertificateFileName fehlt.", target.Id);
|
||||
if (target.RestartType is RestartType.None)
|
||||
{
|
||||
AddIssue(result, $"Ziel '{target.Name}': RestartType=None – kein Neustart konfiguriert.", target.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
ValidateTarget(result, target, requireDeployPaths: false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ValidateTarget(
|
||||
PreflightValidationResult result,
|
||||
DeploymentTarget target,
|
||||
bool requireDeployPaths)
|
||||
{
|
||||
if (requireDeployPaths)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(target.TargetDirectory))
|
||||
{
|
||||
AddIssue(result, $"Ziel '{target.Name}': TargetDirectory fehlt.", target.Id);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(target.CertificateFileName))
|
||||
{
|
||||
AddIssue(result, $"Ziel '{target.Name}': CertificateFileName fehlt.", target.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (target.RestartType == RestartType.Command
|
||||
@@ -75,7 +108,7 @@ public sealed class PreflightValidator
|
||||
AddIssue(result, $"Ziel '{target.Name}': XapiSourcePath fehlt bei RestartType=SonicContainerWithXapi.", target.Id);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(target.TargetDirectory))
|
||||
if (!requireDeployPaths || string.IsNullOrWhiteSpace(target.TargetDirectory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,19 +9,20 @@ namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
/// <summary>
|
||||
/// Verwaltet Sonic ESB Container über die Management Console.
|
||||
///
|
||||
/// Modus = WinRm (Standard, Sonic 10.x):
|
||||
/// PowerShell Remoting (Invoke-Command) auf dem Sonic-Server.
|
||||
/// Erfordert WinRM auf dem Zielrechner: Enable-PSRemoting -Force
|
||||
/// Modus = WinRm (Standard):
|
||||
/// PowerShell Remoting (Invoke-Command) → CMD stop/startcontainer auf dem Sonic-Server.
|
||||
///
|
||||
/// Modus = LocalCmd:
|
||||
/// Dieselbe CMD/PowerShell-Logik lokal (Test direkt auf dem Sonic-PC).
|
||||
///
|
||||
/// Modus = HttpApi:
|
||||
/// HTTP REST API mit automatischer Pfad-Erkennung.
|
||||
/// Probiert: /mf/rest/v1, /api/v1, /sonic/management, /containers
|
||||
/// </summary>
|
||||
public sealed class SonicManagementClient : IDisposable
|
||||
{
|
||||
private readonly SonicConnection _connection;
|
||||
private readonly HttpClient? _http;
|
||||
private readonly WinRmExecutor? _winRm;
|
||||
private readonly WinRmExecutor? _scriptRunner;
|
||||
|
||||
// Gecachter HTTP-Basis-Pfad (nur im HttpApi-Modus)
|
||||
private string? _resolvedContainerBasePath;
|
||||
@@ -34,13 +35,16 @@ public sealed class SonicManagementClient : IDisposable
|
||||
"/containers"
|
||||
];
|
||||
|
||||
private bool UsesScripts =>
|
||||
_connection.ManagementMode is SonicManagementMode.WinRm or SonicManagementMode.LocalCmd;
|
||||
|
||||
public SonicManagementClient(SonicConnection connection)
|
||||
{
|
||||
_connection = connection;
|
||||
|
||||
if (connection.ManagementMode == SonicManagementMode.WinRm)
|
||||
if (UsesScripts)
|
||||
{
|
||||
_winRm = new WinRmExecutor(connection);
|
||||
_scriptRunner = new WinRmExecutor(connection);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -77,10 +81,13 @@ public sealed class SonicManagementClient : IDisposable
|
||||
public async Task<(bool Success, string? Error, string? ResolvedPath)> CheckConnectionAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_connection.ManagementMode == SonicManagementMode.WinRm)
|
||||
if (UsesScripts)
|
||||
{
|
||||
(bool ok, string? error) = await _winRm!.TestConnectionAsync(cancellationToken);
|
||||
return (ok, error, ok ? $"WinRM auf {ExtractHost(_connection.ConnectionUrl)}:{_connection.WinRmPort}" : null);
|
||||
(bool ok, string? error) = await _scriptRunner!.TestConnectionAsync(cancellationToken);
|
||||
string modeLabel = _connection.ManagementMode == SonicManagementMode.LocalCmd
|
||||
? $"LocalCmd (SonicHome={_connection.SonicHome})"
|
||||
: $"WinRM auf {ExtractHost(_connection.ConnectionUrl)}:{_connection.WinRmPort}";
|
||||
return (ok, error, ok ? modeLabel : null);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -116,9 +123,9 @@ public sealed class SonicManagementClient : IDisposable
|
||||
public async Task<(bool Success, IReadOnlyList<string> ContainerNames, string? Error)> GetContainersAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_connection.ManagementMode == SonicManagementMode.WinRm)
|
||||
if (UsesScripts)
|
||||
{
|
||||
return await GetContainersViaWinRmAsync(cancellationToken);
|
||||
return await GetContainersViaScriptAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return await GetContainersViaHttpAsync(cancellationToken);
|
||||
@@ -131,9 +138,9 @@ public sealed class SonicManagementClient : IDisposable
|
||||
string containerName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_connection.ManagementMode == SonicManagementMode.WinRm)
|
||||
if (UsesScripts)
|
||||
{
|
||||
return await RestartViaWinRmAsync(containerName, cancellationToken);
|
||||
return await RestartViaScriptAsync(containerName, cancellationToken);
|
||||
}
|
||||
|
||||
return await RestartViaHttpAsync(containerName, cancellationToken);
|
||||
@@ -147,83 +154,72 @@ public sealed class SonicManagementClient : IDisposable
|
||||
string xapiSourcePath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_connection.ManagementMode == SonicManagementMode.WinRm)
|
||||
if (UsesScripts)
|
||||
{
|
||||
return await ImportXapiViaWinRmAsync(containerName, xapiSourcePath, cancellationToken);
|
||||
return await ImportXapiViaScriptAsync(containerName, xapiSourcePath, cancellationToken);
|
||||
}
|
||||
|
||||
return await ImportXapiViaHttpAsync(containerName, xapiSourcePath, cancellationToken);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// WinRM-Implementierungen
|
||||
// Script-Implementierungen (WinRM / LocalCmd)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private async Task<(bool, IReadOnlyList<string>, string?)> GetContainersViaWinRmAsync(
|
||||
private async Task<(bool, IReadOnlyList<string>, string?)> GetContainersViaScriptAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_connection.WinRmContainerListScript))
|
||||
{
|
||||
return (false, [],
|
||||
"WinRmContainerListScript ist nicht konfiguriert.\n" +
|
||||
"Beispiel: \"Get-Service -DisplayName 'Sonic*' | Select-Object -ExpandProperty DisplayName\"");
|
||||
}
|
||||
|
||||
string script = WinRmExecutor.ApplyScriptTemplate(
|
||||
_connection.WinRmContainerListScript,
|
||||
_connection.ResolveContainerListScript(),
|
||||
containerName: string.Empty,
|
||||
domainName: _connection.DomainName);
|
||||
domainName: _connection.DomainName,
|
||||
sonicHome: _connection.SonicHome);
|
||||
|
||||
(bool ok, string? output, string? error) =
|
||||
await _winRm!.RunScriptAsync(script, cancellationToken);
|
||||
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
return (false, [], $"WinRM Container-Liste fehlgeschlagen: {error}");
|
||||
return (false, [], $"Container-Liste fehlgeschlagen ({_connection.ManagementMode}): {error}");
|
||||
}
|
||||
|
||||
List<string> names = (output ?? string.Empty)
|
||||
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(l => l.Length > 0)
|
||||
.Where(l => l.Length > 0 && !l.StartsWith("WARNING", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
return (true, names, null);
|
||||
}
|
||||
|
||||
private async Task<(bool, string, string?)> RestartViaWinRmAsync(
|
||||
private async Task<(bool, string, string?)> RestartViaScriptAsync(
|
||||
string containerName, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_connection.WinRmRestartScript))
|
||||
{
|
||||
return (false, "Neustart fehlgeschlagen",
|
||||
"WinRmRestartScript ist nicht konfiguriert.\n" +
|
||||
"Beispiel für Windows-Service: \"Restart-Service -Name 'CT-ZADBService' -Force\"\n" +
|
||||
"Platzhalter {container} wird durch den Container-Namen ersetzt.");
|
||||
}
|
||||
|
||||
string script = WinRmExecutor.ApplyScriptTemplate(
|
||||
_connection.WinRmRestartScript,
|
||||
_connection.ResolveRestartScript(),
|
||||
containerName,
|
||||
_connection.DomainName);
|
||||
_connection.DomainName,
|
||||
_connection.SonicHome);
|
||||
|
||||
(bool ok, string? output, string? error) =
|
||||
await _winRm!.RunScriptAsync(script, cancellationToken);
|
||||
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
|
||||
|
||||
string mode = _connection.ManagementMode.ToString();
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
return (false, "Neustart fehlgeschlagen (WinRM)",
|
||||
$"Fehler: {error}\nAusgabe: {output}");
|
||||
return (false, $"Neustart fehlgeschlagen ({mode})",
|
||||
$"Fehler: {error}\nAusgabe: {output}\nSonicHome={_connection.SonicHome}");
|
||||
}
|
||||
|
||||
await Task.Delay(
|
||||
TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)),
|
||||
cancellationToken);
|
||||
|
||||
return (true, "Container neugestartet (WinRM)",
|
||||
return (true, $"ESB-Container neugestartet ({mode})",
|
||||
$"Ausgabe: {output ?? "(keine)"}");
|
||||
}
|
||||
|
||||
private async Task<(bool, string, string?)> ImportXapiViaWinRmAsync(
|
||||
private async Task<(bool, string, string?)> ImportXapiViaScriptAsync(
|
||||
string containerName, string xapiSourcePath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_connection.WinRmXapiImportScript))
|
||||
@@ -236,21 +232,22 @@ public sealed class SonicManagementClient : IDisposable
|
||||
_connection.WinRmXapiImportScript,
|
||||
containerName,
|
||||
_connection.DomainName,
|
||||
_connection.SonicHome,
|
||||
xapiSourcePath);
|
||||
|
||||
(bool importOk, string? importOut, string? importErr) =
|
||||
await _winRm!.RunScriptAsync(script, cancellationToken);
|
||||
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
|
||||
|
||||
if (!importOk)
|
||||
{
|
||||
return (false, "XApi-Import fehlgeschlagen (WinRM)", importErr ?? importOut);
|
||||
return (false, "XApi-Import fehlgeschlagen", importErr ?? importOut);
|
||||
}
|
||||
|
||||
(bool restartOk, string restartStatus, string? restartDetail) =
|
||||
await RestartViaWinRmAsync(containerName, cancellationToken);
|
||||
await RestartViaScriptAsync(containerName, cancellationToken);
|
||||
|
||||
return restartOk
|
||||
? (true, "XApi importiert + Container neugestartet (WinRM)",
|
||||
? (true, "XApi importiert + Container neugestartet",
|
||||
$"Import: {importOut} | Restart: {restartDetail}")
|
||||
: (false, restartStatus, restartDetail);
|
||||
}
|
||||
@@ -473,8 +470,14 @@ public sealed class SonicManagementClient : IDisposable
|
||||
? root.EnumerateArray()
|
||||
: root.ValueKind == JsonValueKind.Object
|
||||
? new[] { "containers", "data", "items", "result" }
|
||||
.Where(root.TryGetProperty)
|
||||
.SelectMany(k => { root.TryGetProperty(k, out JsonElement a); return a.EnumerateArray(); })
|
||||
.Where(k => root.TryGetProperty(k, out _))
|
||||
.SelectMany(k =>
|
||||
{
|
||||
root.TryGetProperty(k, out JsonElement a);
|
||||
return a.ValueKind == JsonValueKind.Array
|
||||
? a.EnumerateArray()
|
||||
: Enumerable.Empty<JsonElement>();
|
||||
})
|
||||
: [];
|
||||
|
||||
foreach (JsonElement el in elements)
|
||||
|
||||
@@ -5,14 +5,7 @@ using ZA.CoreService.ESBCertificateManager.Models;
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Führt PowerShell-Befehle via WinRM (Invoke-Command) auf dem Sonic-Server aus.
|
||||
///
|
||||
/// Voraussetzungen auf dem Zielrechner:
|
||||
/// - WinRM muss aktiviert sein: Enable-PSRemoting -Force
|
||||
/// - Ausführungsrichtlinie: Set-ExecutionPolicy RemoteSigned
|
||||
///
|
||||
/// Voraussetzungen auf dem App-Rechner (einmalig, als Admin):
|
||||
/// - Set-Item WSMan:\localhost\Client\TrustedHosts -Value "dekun-painwbdet"
|
||||
/// Führt PowerShell-Befehle lokal oder via WinRM (Invoke-Command) auf dem Sonic-Server aus.
|
||||
/// </summary>
|
||||
public sealed class WinRmExecutor
|
||||
{
|
||||
@@ -23,15 +16,20 @@ public sealed class WinRmExecutor
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prüft die WinRM-Konnektivität und ob der Sonic-Server per TCP erreichbar ist.
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error)> TestConnectionAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_connection.ManagementMode == SonicManagementMode.LocalCmd)
|
||||
{
|
||||
(bool ok, string? output, string? error) = await RunScriptAsync(
|
||||
"$env:COMPUTERNAME", cancellationToken);
|
||||
return ok
|
||||
? (true, null)
|
||||
: (false, $"LocalCmd fehlgeschlagen: {error ?? output}");
|
||||
}
|
||||
|
||||
string host = ExtractHost(_connection.ConnectionUrl);
|
||||
|
||||
// TCP-Ping auf WinRM-Port
|
||||
try
|
||||
{
|
||||
using System.Net.Sockets.TcpClient tcp = new();
|
||||
@@ -44,30 +42,25 @@ public sealed class WinRmExecutor
|
||||
{
|
||||
return (false,
|
||||
$"WinRM-Port {_connection.WinRmPort} auf '{host}' nicht erreichbar: {ex.Message}\n" +
|
||||
$"Auf dem Zielrechner ausführen: Enable-PSRemoting -Force");
|
||||
"Auf dem Zielrechner: Enable-PSRemoting -Force\n" +
|
||||
"Oder ManagementMode=LocalCmd setzen und die App auf dem Sonic-PC starten.");
|
||||
}
|
||||
|
||||
// Kurztest: Hostname zurückgeben
|
||||
(bool ok, string? output, string? error) = await RunScriptAsync(
|
||||
(bool sessionOk, _, string? sessionError) = await RunScriptAsync(
|
||||
"$env:COMPUTERNAME", cancellationToken);
|
||||
|
||||
return ok
|
||||
return sessionOk
|
||||
? (true, null)
|
||||
: (false, $"WinRM-Verbindung fehlgeschlagen: {error}");
|
||||
: (false, $"WinRM-Verbindung fehlgeschlagen: {sessionError}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Führt einen Scriptblock auf dem Remote-Rechner aus und gibt Stdout zurück.
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Output, string? Error)> RunScriptAsync(
|
||||
string scriptBlock,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
string host = ExtractHost(_connection.ConnectionUrl);
|
||||
|
||||
// Passwort als SecureString – bleibt im PowerShell-Prozess, wird nicht als Argument übergeben
|
||||
// Stattdessen: Scriptblock über stdin senden
|
||||
string fullScript = BuildScript(host, scriptBlock);
|
||||
string fullScript = _connection.ManagementMode == SonicManagementMode.LocalCmd
|
||||
? BuildLocalScript(scriptBlock)
|
||||
: BuildRemoteScript(ExtractHost(_connection.ConnectionUrl), scriptBlock);
|
||||
|
||||
ProcessStartInfo psi = new()
|
||||
{
|
||||
@@ -88,8 +81,7 @@ public sealed class WinRmExecutor
|
||||
return (false, null, "PowerShell-Prozess konnte nicht gestartet werden.");
|
||||
}
|
||||
|
||||
// Skript über stdin – Credentials gehen NICHT als sichtbares Argument durch
|
||||
await process.StandardInput.WriteAsync(fullScript);
|
||||
await process.StandardInput.WriteAsync(fullScript.AsMemory(), cancellationToken);
|
||||
process.StandardInput.Close();
|
||||
|
||||
using CancellationTokenSource timeoutCts =
|
||||
@@ -106,7 +98,7 @@ public sealed class WinRmExecutor
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try { process.Kill(entireProcessTree: true); } catch { /* ignore */ }
|
||||
return (false, null, $"WinRM-Ausführung Timeout nach {_connection.TimeoutSeconds}s.");
|
||||
return (false, null, $"Ausführung Timeout nach {_connection.TimeoutSeconds}s.");
|
||||
}
|
||||
|
||||
string stdout = (await stdoutTask).Trim();
|
||||
@@ -121,9 +113,11 @@ public sealed class WinRmExecutor
|
||||
stderr.Length > 0 ? Truncate(stderr) : $"PowerShell ExitCode={process.ExitCode}");
|
||||
}
|
||||
|
||||
private string BuildScript(string host, string scriptBlock)
|
||||
private static string BuildLocalScript(string scriptBlock)
|
||||
=> "$ErrorActionPreference = 'Stop'\n" + scriptBlock;
|
||||
|
||||
private string BuildRemoteScript(string host, string scriptBlock)
|
||||
{
|
||||
// Passwort über Variable, nicht als Argument – verhindert Sichtbarkeit in Prozessliste
|
||||
string escapedPwd = _connection.Password.Replace("'", "''");
|
||||
string escapedUser = _connection.Username.Replace("'", "''");
|
||||
string escapedHost = host.Replace("'", "''");
|
||||
@@ -137,21 +131,17 @@ public sealed class WinRmExecutor
|
||||
"} -ErrorAction Stop";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ersetzt Platzhalter in einem konfigurierten WinRM-Script.
|
||||
/// {container} → Container-Name (einfache Hochkommas werden verdoppelt)
|
||||
/// {domain} → Domain-Name
|
||||
/// {xapiPath} → Pfad zur XApi-Quelldatei
|
||||
/// </summary>
|
||||
public static string ApplyScriptTemplate(string template, string containerName,
|
||||
string domainName = "", string xapiPath = "")
|
||||
public static string ApplyScriptTemplate(
|
||||
string template,
|
||||
string containerName,
|
||||
string domainName = "",
|
||||
string sonicHome = "",
|
||||
string xapiPath = "")
|
||||
=> template
|
||||
.Replace("{container}", containerName.Replace("'", "''"),
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{domain}", domainName.Replace("'", "''"),
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{xapiPath}", xapiPath.Replace("'", "''"),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
.Replace("{container}", containerName.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{domain}", domainName.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{sonicHome}", sonicHome.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{xapiPath}", xapiPath.Replace("'", "''"), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string ExtractHost(string connectionUrl)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user