using System.Diagnostics;
using System.Text;
using ZA.CoreService.ESBCertificateManager.Models;
namespace ZA.CoreService.ESBCertificateManager.Services;
///
/// 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).
///
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 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 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 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 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, $"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)
{
string sonicHint = string.IsNullOrWhiteSpace(_connection.SonicHome)
? ""
: $" Erwartet z.B. '{Path.Combine(_connection.SonicHome, "jre", "bin", "java.exe")}'.";
return (false, null, null, null,
"Java nicht gefunden. Gesucht: SonicConnection.JavaHome/JavaPath, JAVA_HOME, PATH, " +
"SonicHome\\jre|jdk\\bin\\java.exe, Program Files\\Java|Eclipse Adoptium." +
sonicHint +
" Für MfApi JavaHome in appsettings setzen oder JRE unter SonicHome nutzen.");
}
(string? libDir, string? libError) = ResolveLibDirectoryDetailed();
if (libDir is null)
{
return (false, null, null, null, libError);
}
// Bevorzugt: vorcompilierte .jar/.class aus Tools\ (kein javac nötig).
if (TryResolvePrebuiltTool(out string? prebuiltDir, out string? prebuiltEntry, out string? prebuiltError)
&& prebuiltDir is not null && prebuiltEntry is not null)
{
string classpath = BuildClasspath(libDir, prebuiltEntry);
return (true, javaExe, prebuiltDir, classpath, null);
}
// Fallback: zur Laufzeit aus .java kompilieren (braucht javac + Sonic-Libs).
string? sourcePath = ResolveToolSourcePath();
if (sourcePath is null)
{
return (false, null, null, null,
(prebuiltError ?? "Kein vorcompilierter SonicMfContainerTool gefunden.") +
" Tools\\SonicMfContainerTool.java/.class/.jar fehlen im Ausgabeverzeichnis.");
}
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 compileClasspath = BuildClasspath(libDir, null);
string runtimeClasspath = 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,
"Vorcompilierter SonicMfContainerTool fehlt und javac wurde nicht gefunden. " +
"JDK installieren oder Tools\\SonicMfContainerTool.jar/.class mit ausliefern. " +
$"Gefundene java.exe: {javaExe}");
}
ProcessStartInfo compilePsi = new()
{
FileName = javac,
Arguments = $"-encoding UTF-8 -cp {Quote(compileClasspath)} -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, runtimeClasspath, null);
}
private bool TryResolvePrebuiltTool(out string? workDir, out string? classpathEntry, out string? error)
{
workDir = null;
classpathEntry = null;
error = null;
string[] searchDirs =
[
Path.Combine(AppContext.BaseDirectory, "Tools"),
Path.Combine(Directory.GetCurrentDirectory(), "Tools"),
AppContext.BaseDirectory
];
foreach (string dir in searchDirs.Where(Directory.Exists))
{
string jar = Path.Combine(dir, "SonicMfContainerTool.jar");
if (File.Exists(jar))
{
workDir = dir;
classpathEntry = jar;
return true;
}
string classFile = Path.Combine(dir, "SonicMfContainerTool.class");
if (File.Exists(classFile))
{
workDir = dir;
classpathEntry = dir;
return true;
}
}
error = "Tools\\SonicMfContainerTool.jar/.class nicht gefunden.";
return false;
}
private (string? Dir, string? Error) ResolveLibDirectoryDetailed()
{
List<(string Path, string Label)> candidates = [];
if (!string.IsNullOrWhiteSpace(_connection.MfClientLibPath))
{
candidates.Add((_connection.MfClientLibPath, "MfClientLibPath"));
}
if (!string.IsNullOrWhiteSpace(_connection.SonicHome))
{
candidates.Add((Path.Combine(_connection.SonicHome, "lib"), "SonicHome\\lib"));
candidates.Add((_connection.SonicHome, "SonicHome"));
}
List checkedPaths = [];
foreach ((string path, string label) in candidates)
{
checkedPaths.Add($"{label}='{path}'");
if (!Directory.Exists(path))
{
continue;
}
if (Directory.EnumerateFiles(path, "*.jar", SearchOption.TopDirectoryOnly).Any())
{
return (path, null);
}
}
string sonicHome = _connection.SonicHome ?? string.Empty;
bool sonicHomeExists = !string.IsNullOrWhiteSpace(sonicHome) && Directory.Exists(sonicHome);
string libPath = string.IsNullOrWhiteSpace(sonicHome)
? "(SonicHome leer)"
: Path.Combine(sonicHome, "lib");
if (sonicHomeExists && Directory.Exists(libPath)
&& !Directory.EnumerateFiles(libPath, "*.jar", SearchOption.TopDirectoryOnly).Any())
{
return (null,
$"Sonic-Client-JARs fehlen unter '{libPath}' (Ordner existiert, aber keine *.jar). " +
"MfApi braucht u.a. mgmt_client.jar / mfcontext.jar / sonic_Client.jar. " +
"MfClientLibPath auf den korrekten Lib-Ordner setzen.");
}
if (sonicHomeExists && !Directory.Exists(libPath))
{
return (null,
$"SonicHome='{sonicHome}' gefunden, aber Lib-Ordner fehlt: '{libPath}'. " +
"Sonic-Client-Installation prüfen oder MfClientLibPath setzen " +
"(erwartet *.jar, u.a. mgmt_client.jar).");
}
return (null,
"Sonic-Client-JARs nicht gefunden. SonicHome\\lib oder MfClientLibPath setzen " +
$"(geprüft: {string.Join("; ", checkedPaths)}). " +
"Benötigt u.a. mgmt_client.jar / mfcontext.jar / sonic_Client.jar.");
}
private static string BuildClasspath(string libDir, string? extraEntry)
{
// Java-Classpath-Wildcard für alle JARs im Lib-Ordner
string jars = Path.Combine(libDir, "*");
return string.IsNullOrWhiteSpace(extraEntry)
? jars
: jars + Path.PathSeparator + extraEntry;
}
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 string? ResolveJavaExecutable()
{
foreach (string? configured in new[] { _connection.JavaPath, _connection.JavaHome })
{
string? fromConfig = ResolveJavaFromConfiguredPath(configured);
if (fromConfig is not null)
{
return fromConfig;
}
}
string? javaHome = Environment.GetEnvironmentVariable("JAVA_HOME");
string? fromEnv = ResolveJavaFromHomeDir(javaHome);
if (fromEnv is not null)
{
return fromEnv;
}
string? fromPath = FindOnPath(JavaExeName) ?? FindOnPath("java");
if (fromPath is not null)
{
return fromPath;
}
if (!string.IsNullOrWhiteSpace(_connection.SonicHome))
{
string sonicHome = _connection.SonicHome;
string[] direct =
[
Path.Combine(sonicHome, "jre", "bin", JavaExeName),
Path.Combine(sonicHome, "jdk", "bin", JavaExeName),
Path.Combine(sonicHome, "JRE", "bin", JavaExeName),
Path.Combine(sonicHome, "JDK", "bin", JavaExeName)
];
foreach (string candidate in direct)
{
if (File.Exists(candidate))
{
return candidate;
}
}
// Häufige Layouts: SonicHome\MQ*\jre, SonicHome\jre64, SonicHome\Java\jre, ...
string? nested = FindJavaUnderSonicHome(sonicHome);
if (nested is not null)
{
return nested;
}
}
return FindJavaInCommonInstallLocations();
}
private string? ResolveJavacExecutable(string javaExe)
{
string javacName = OperatingSystem.IsWindows() ? "javac.exe" : "javac";
string? dir = Path.GetDirectoryName(javaExe);
if (!string.IsNullOrWhiteSpace(dir))
{
string sibling = Path.Combine(dir, javacName);
if (File.Exists(sibling))
{
return sibling;
}
}
foreach (string? configured in new[] { _connection.JavaPath, _connection.JavaHome })
{
string? fromConfig = ResolveToolFromConfiguredPath(configured, javacName);
if (fromConfig is not null)
{
return fromConfig;
}
}
string? javaHome = Environment.GetEnvironmentVariable("JAVA_HOME");
if (!string.IsNullOrWhiteSpace(javaHome))
{
string candidate = Path.Combine(javaHome, "bin", javacName);
if (File.Exists(candidate))
{
return candidate;
}
}
if (!string.IsNullOrWhiteSpace(_connection.SonicHome))
{
foreach (string root in EnumerateJdkRootsUnderSonicHome(_connection.SonicHome))
{
string candidate = Path.Combine(root, "bin", javacName);
if (File.Exists(candidate))
{
return candidate;
}
}
}
return FindOnPath(javacName) ?? FindOnPath("javac");
}
private static string? ResolveJavaFromConfiguredPath(string? configured)
=> ResolveToolFromConfiguredPath(configured, JavaExeName);
private static string? ResolveToolFromConfiguredPath(string? configured, string exeName)
{
if (string.IsNullOrWhiteSpace(configured))
{
return null;
}
string path = configured.Trim().Trim('"');
if (File.Exists(path))
{
if (path.EndsWith(exeName, StringComparison.OrdinalIgnoreCase))
{
return path;
}
// Konfiguriert: ...\bin\java.exe → sibling javac.exe (oder umgekehrt)
string? parent = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(parent))
{
string sibling = Path.Combine(parent, exeName);
if (File.Exists(sibling))
{
return sibling;
}
}
// java ohne Endung (Unix) oder exakter Treffer
string fileName = Path.GetFileName(path);
if (fileName.Equals(Path.GetFileNameWithoutExtension(exeName), StringComparison.OrdinalIgnoreCase)
|| fileName.Equals(exeName, StringComparison.OrdinalIgnoreCase))
{
return path;
}
}
string fromHome = Path.Combine(path, "bin", exeName);
return File.Exists(fromHome) ? fromHome : null;
}
private static string? ResolveJavaFromHomeDir(string? home)
{
if (string.IsNullOrWhiteSpace(home))
{
return null;
}
string candidate = Path.Combine(home.Trim().Trim('"'), "bin", JavaExeName);
return File.Exists(candidate) ? candidate : null;
}
private static string? FindJavaUnderSonicHome(string sonicHome)
{
if (!Directory.Exists(sonicHome))
{
return null;
}
// Direkte Unterordner mit jre/jdk
foreach (string root in EnumerateJdkRootsUnderSonicHome(sonicHome))
{
string candidate = Path.Combine(root, "bin", JavaExeName);
if (File.Exists(candidate))
{
return candidate;
}
}
return null;
}
private static IEnumerable EnumerateJdkRootsUnderSonicHome(string sonicHome)
{
if (!Directory.Exists(sonicHome))
{
yield break;
}
string[] preferredNames =
[
"jre", "jdk", "JRE", "JDK", "jre64", "jdk64", "Java", "java"
];
foreach (string name in preferredNames)
{
string path = Path.Combine(sonicHome, name);
if (Directory.Exists(path))
{
yield return path;
string nestedJre = Path.Combine(path, "jre");
if (Directory.Exists(nestedJre))
{
yield return nestedJre;
}
}
}
// {SonicHome}\*\jre und {SonicHome}\*\jdk
IEnumerable children;
try
{
children = Directory.EnumerateDirectories(sonicHome);
}
catch
{
yield break;
}
foreach (string child in children)
{
foreach (string leaf in new[] { "jre", "jdk", "JRE", "JDK" })
{
string nested = Path.Combine(child, leaf);
if (Directory.Exists(nested))
{
yield return nested;
}
}
// z.B. ...\jre1.8.0_xxx direkt als Kind
string leafName = Path.GetFileName(child);
if (leafName.StartsWith("jre", StringComparison.OrdinalIgnoreCase)
|| leafName.StartsWith("jdk", StringComparison.OrdinalIgnoreCase))
{
yield return child;
}
}
}
private static string? FindJavaInCommonInstallLocations()
{
string[] roots =
[
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
@"C:\Program Files",
@"C:\Program Files (x86)"
];
string[] vendorFolders =
[
"Java",
"Eclipse Adoptium",
"AdoptOpenJDK",
"Microsoft",
"Amazon Corretto",
"Zulu",
"BellSoft",
"Semeru"
];
foreach (string root in roots.Where(r => !string.IsNullOrWhiteSpace(r)).Distinct(StringComparer.OrdinalIgnoreCase))
{
foreach (string vendor in vendorFolders)
{
string vendorDir = Path.Combine(root, vendor);
if (!Directory.Exists(vendorDir))
{
continue;
}
// vendor\bin\java.exe
string direct = Path.Combine(vendorDir, "bin", JavaExeName);
if (File.Exists(direct))
{
return direct;
}
try
{
foreach (string sub in Directory.EnumerateDirectories(vendorDir)
.OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase))
{
string candidate = Path.Combine(sub, "bin", JavaExeName);
if (File.Exists(candidate))
{
return candidate;
}
string nestedJre = Path.Combine(sub, "jre", "bin", JavaExeName);
if (File.Exists(nestedJre))
{
return nestedJre;
}
}
}
catch
{
// ignore inaccessible vendor dirs
}
}
}
return null;
}
private static string JavaExeName => OperatingSystem.IsWindows() ? "java.exe" : "java";
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().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] + "…";
}