Fix app startup and enable Sonic ESB restart via remote CMD.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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