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:
@@ -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] + "…";
|
||||
}
|
||||
Reference in New Issue
Block a user