using System.Diagnostics; using System.Text; using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; /// /// Führt PowerShell-Befehle lokal oder via WinRM (Invoke-Command) auf dem Sonic-Server aus. /// public sealed class WinRmExecutor { private readonly SonicConnection _connection; public WinRmExecutor(SonicConnection connection) { _connection = connection; } 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); try { using System.Net.Sockets.TcpClient tcp = new(); using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 5, 30))); await tcp.ConnectAsync(host, _connection.WinRmPort, cts.Token); } catch (Exception ex) when (ex is not OperationCanceledException) { return (false, $"WinRM-Port {_connection.WinRmPort} auf '{host}' nicht erreichbar: {ex.Message}\n" + "Auf dem Zielrechner: Enable-PSRemoting -Force\n" + "Oder ManagementMode=LocalCmd setzen und die App auf dem Sonic-PC starten."); } (bool sessionOk, _, string? sessionError) = await RunScriptAsync( "$env:COMPUTERNAME", cancellationToken); return sessionOk ? (true, null) : (false, $"WinRM-Verbindung fehlgeschlagen: {sessionError}"); } public async Task<(bool Success, string? Output, string? Error)> RunScriptAsync( string scriptBlock, CancellationToken cancellationToken = default) { string fullScript = _connection.ManagementMode == SonicManagementMode.LocalCmd ? 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 }; 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); 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}"); } private static string BuildLocalScript(string scriptBlock) => "$ErrorActionPreference = 'Stop'\n" + scriptBlock; private string BuildRemoteScript(string host, string scriptBlock) { string escapedPwd = _connection.Password.Replace("'", "''"); string escapedUser = _connection.Username.Replace("'", "''"); string escapedHost = host.Replace("'", "''"); 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"; } 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("{sonicHome}", sonicHome.Replace("'", "''"), StringComparison.OrdinalIgnoreCase) .Replace("{xapiPath}", xapiPath.Replace("'", "''"), StringComparison.OrdinalIgnoreCase); private static string ExtractHost(string connectionUrl) { try { return new Uri(connectionUrl).Host; } catch { return connectionUrl; } } private static string Truncate(string s, int max = 600) => s.Length <= max ? s : s[..max] + "…"; }