big changes

This commit is contained in:
Mike Gisler
2026-07-28 08:03:12 +02:00
parent c73f53d740
commit 4d19873776
179 changed files with 14953 additions and 494 deletions
@@ -5,37 +5,99 @@ using ZA.CoreService.ESBCertificateManager.Models;
namespace ZA.CoreService.ESBCertificateManager.Services;
/// <summary>
/// Startet nur den Container-Restart über SonicMfContainerTool (Java + MF-Client-JARs).
/// Startet den Container-Restart über SonicMfContainerTool
/// mit Java und den Sonic-MfApi-Clientbibliotheken.
/// </summary>
public sealed class SonicMfApiExecutor
{
private readonly SonicConnection _connection;
public SonicMfApiExecutor(SonicConnection connection)
public SonicMfApiExecutor(
SonicConnection connection)
{
_connection = connection;
_connection =
connection
?? throw new ArgumentNullException(
nameof(connection));
}
public (string SonicHome, string? JavaExe) ResolveRuntimePaths()
public (string SonicHome, string? JavaExe)
ResolveRuntimePaths()
{
string sonicHome = string.IsNullOrWhiteSpace(_connection.SonicHome)
? "(leer)"
: _connection.SonicHome.Trim();
return (sonicHome, ResolveJavaExe());
string sonicHome =
string.IsNullOrWhiteSpace(
_connection.SonicHome)
? "(leer)"
: _connection.SonicHome.Trim();
return (
sonicHome,
ResolveJavaExe());
}
public async Task<(bool Success, string? Output, string? Error)> RestartAsync(
string containerName,
CancellationToken cancellationToken = default)
public async Task<(
bool Success,
string? Output,
string? Error)> RestartAsync(
string containerName,
CancellationToken cancellationToken = default)
{
(bool prepared, string? javaExe, string? classDir, string? classpath, string? prepareError) =
PrepareTool();
if (!prepared)
if (string.IsNullOrWhiteSpace(containerName))
{
return (false, null, prepareError);
return (
false,
null,
"Der Sonic-Containername fehlt.");
}
ProcessStartInfo psi = new()
if (string.IsNullOrWhiteSpace(
_connection.Username)
|| string.IsNullOrEmpty(
_connection.Password))
{
return (
false,
null,
"Für die Sonic-Verbindung sind keine " +
"vollständigen Zugangsdaten eingerichtet. " +
"Bitte das Setup ausführen.");
}
if (string.IsNullOrWhiteSpace(
_connection.DomainName))
{
return (
false,
null,
"Für die Sonic-Verbindung fehlt DomainName.");
}
if (string.IsNullOrWhiteSpace(
_connection.ConnectionUrl))
{
return (
false,
null,
"Für die Sonic-Verbindung fehlt ConnectionUrl.");
}
(
bool prepared,
string? javaExe,
string? classDir,
string? classpath,
string? prepareError
) = PrepareTool();
if (!prepared)
{
return (
false,
null,
prepareError);
}
ProcessStartInfo startInfo = new()
{
FileName = javaExe!,
UseShellExecute = false,
@@ -47,173 +109,393 @@ public sealed class SonicMfApiExecutor
WorkingDirectory = classDir!
};
psi.ArgumentList.Add("-cp");
psi.ArgumentList.Add(classpath!);
psi.ArgumentList.Add("SonicMfContainerTool");
psi.ArgumentList.Add("restart");
psi.ArgumentList.Add("--domain");
psi.ArgumentList.Add(_connection.DomainName);
psi.ArgumentList.Add("--url");
psi.ArgumentList.Add(_connection.ConnectionUrl);
psi.ArgumentList.Add("--user");
psi.ArgumentList.Add(_connection.Username);
psi.ArgumentList.Add("--container");
psi.ArgumentList.Add(containerName);
psi.ArgumentList.Add("--timeout");
psi.ArgumentList.Add(Math.Clamp(_connection.TimeoutSeconds, 5, 600).ToString());
startInfo.ArgumentList.Add("-cp");
startInfo.ArgumentList.Add(classpath!);
startInfo.ArgumentList.Add(
"SonicMfContainerTool");
psi.Environment["ESB_SONIC_PASSWORD"] = _connection.Password ?? string.Empty;
startInfo.ArgumentList.Add("restart");
using Process process = new() { StartInfo = psi };
if (!process.Start())
startInfo.ArgumentList.Add("--domain");
startInfo.ArgumentList.Add(
_connection.DomainName);
startInfo.ArgumentList.Add("--url");
startInfo.ArgumentList.Add(
_connection.ConnectionUrl);
startInfo.ArgumentList.Add("--user");
startInfo.ArgumentList.Add(
_connection.Username);
startInfo.ArgumentList.Add("--container");
startInfo.ArgumentList.Add(
containerName);
startInfo.ArgumentList.Add("--timeout");
startInfo.ArgumentList.Add(
Math.Clamp(
_connection.TimeoutSeconds,
5,
600)
.ToString());
/*
* Das Kennwort wird nicht als Kommandozeilenargument
* übergeben. Dadurch erscheint das Kennwort nicht in
* der Prozessargumentliste.
*
* CredentialSecret wurde zuvor durch Microsoft.Data.SqlClient
* clientseitig entschlüsselt und als Laufzeitwert in
* SonicConnection.Password übernommen.
*/
startInfo.Environment[
"ESB_SONIC_PASSWORD"] =
_connection.Password;
using Process process = new()
{
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);
StartInfo = startInfo
};
try
{
await process.WaitForExitAsync(timeoutCts.Token);
if (!process.Start())
{
return (
false,
null,
"Der Java-Prozess konnte nicht gestartet werden.");
}
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
catch (Exception ex)
{
try { process.Kill(entireProcessTree: true); } catch { /* ignore */ }
return (false, null, $"MfApi Timeout nach {_connection.TimeoutSeconds}s.");
return (
false,
null,
$"Der Java-Prozess konnte nicht gestartet werden: " +
$"{ex.Message}");
}
string stdout = (await stdoutTask).Trim();
string stderr = (await stderrTask).Trim();
string combined = string.Join("\n", new[] { stdout, stderr }.Where(s => s.Length > 0));
using CancellationTokenSource timeoutCts =
CancellationTokenSource
.CreateLinkedTokenSource(
cancellationToken);
bool ok = process.ExitCode == 0
&& combined.Contains("OK:RestartInvoked", StringComparison.OrdinalIgnoreCase)
&& !combined.Contains("ERROR:", StringComparison.OrdinalIgnoreCase);
int timeoutSeconds =
Math.Clamp(
_connection.TimeoutSeconds,
10,
600);
if (ok)
timeoutCts.CancelAfter(
TimeSpan.FromSeconds(
timeoutSeconds));
Task<string> stdoutTask =
process.StandardOutput
.ReadToEndAsync();
Task<string> stderrTask =
process.StandardError
.ReadToEndAsync();
try
{
return (true, combined, null);
await process.WaitForExitAsync(
timeoutCts.Token);
}
string err = ExtractError(combined)
?? $"SonicMfContainerTool ExitCode={process.ExitCode}";
if (err.Contains("ClassNotFoundException", StringComparison.OrdinalIgnoreCase)
|| combined.Contains("JMSConnectorAddress", StringComparison.OrdinalIgnoreCase))
catch (OperationCanceledException)
when (!cancellationToken.IsCancellationRequested)
{
err +=
"\n\nClasspath unvollständig. MfClientLibPath auf Ordner mit mgmt_client.jar / mfcontext.jar setzen.";
TryKillProcess(process);
return (
false,
null,
$"MfApi-Timeout nach {timeoutSeconds} Sekunden.");
}
catch (OperationCanceledException)
{
TryKillProcess(process);
throw;
}
string detail = string.IsNullOrWhiteSpace(combined) ? err : err + "\n\n--- Tool-Output ---\n" + combined;
return (false, combined.Length > 0 ? combined : null, detail);
string stdout =
(await stdoutTask).Trim();
string stderr =
(await stderrTask).Trim();
string combined = string.Join(
Environment.NewLine,
new[] { stdout, stderr }
.Where(output =>
!string.IsNullOrWhiteSpace(output)));
bool succeeded =
process.ExitCode == 0
&& combined.Contains(
"OK:RestartInvoked",
StringComparison.OrdinalIgnoreCase)
&& !combined.Contains(
"ERROR:",
StringComparison.OrdinalIgnoreCase);
if (succeeded)
{
return (
true,
string.IsNullOrWhiteSpace(combined)
? null
: combined,
null);
}
string error =
ExtractError(combined)
?? $"SonicMfContainerTool ExitCode=" +
$"{process.ExitCode}";
if (error.Contains(
"ClassNotFoundException",
StringComparison.OrdinalIgnoreCase)
|| combined.Contains(
"JMSConnectorAddress",
StringComparison.OrdinalIgnoreCase))
{
error +=
Environment.NewLine +
Environment.NewLine +
"Der Classpath ist vermutlich unvollständig. " +
"Runtime:SonicClientLibraryPath muss auf den " +
"Ordner mit den benötigten Sonic-JARs zeigen.";
}
string detail =
string.IsNullOrWhiteSpace(combined)
? error
: error
+ Environment.NewLine
+ Environment.NewLine
+ "--- Tool-Output ---"
+ Environment.NewLine
+ combined;
return (
false,
string.IsNullOrWhiteSpace(combined)
? null
: combined,
detail);
}
private (bool Ok, string? JavaExe, string? ClassDir, string? Classpath, string? Error) PrepareTool()
private (
bool Ok,
string? JavaExe,
string? ClassDir,
string? Classpath,
string? Error) PrepareTool()
{
string? javaExe = ResolveJavaExe();
string? javaExe =
ResolveJavaExe();
if (javaExe is null)
{
return (false, null, null, null,
"Java nicht gefunden. In appsettings JavaPath auf java.exe (Java 8+) setzen.");
return (
false,
null,
null,
null,
"Java wurde nicht gefunden. " +
"Runtime:JavaExecutablePath prüfen.");
}
(string? libClasspath, string? libError) = ResolveSonicClasspath();
if (libClasspath is null)
(
string? libraryClasspath,
string? libraryError
) = ResolveSonicClasspath();
if (libraryClasspath is null)
{
return (false, null, null, null, libError);
return (
false,
null,
null,
null,
libraryError);
}
string? toolsDir = ResolveToolsDir();
if (toolsDir is null)
string? toolsDirectory =
ResolveToolsDir();
if (toolsDirectory is null)
{
return (false, null, null, null,
"Tools\\SonicMfContainerTool.class nicht gefunden. Projekt neu bauen.");
return (
false,
null,
null,
null,
"Tools\\SonicMfContainerTool.class " +
"wurde nicht gefunden. " +
"Projekt vollständig neu bauen.");
}
string classpath = toolsDir + Path.PathSeparator + libClasspath;
return (true, javaExe, toolsDir, classpath, null);
string classpath =
toolsDirectory
+ Path.PathSeparator
+ libraryClasspath;
return (
true,
javaExe,
toolsDirectory,
classpath,
null);
}
private string? ResolveJavaExe()
{
foreach (string? candidate in EnumerateJavaCandidates())
foreach (string? candidate
in EnumerateJavaCandidates())
{
if (!string.IsNullOrWhiteSpace(candidate) && File.Exists(candidate))
if (string.IsNullOrWhiteSpace(candidate))
{
return Path.GetFullPath(candidate);
continue;
}
string resolvedCandidate;
try
{
resolvedCandidate =
Path.IsPathRooted(candidate)
? Path.GetFullPath(candidate)
: Path.GetFullPath(
Path.Combine(
AppContext.BaseDirectory,
candidate));
}
catch
{
continue;
}
if (File.Exists(resolvedCandidate))
{
return resolvedCandidate;
}
}
return null;
}
private IEnumerable<string?> EnumerateJavaCandidates()
private IEnumerable<string?>
EnumerateJavaCandidates()
{
if (!string.IsNullOrWhiteSpace(_connection.JavaPath))
if (!string.IsNullOrWhiteSpace(
_connection.JavaPath))
{
yield return _connection.JavaPath.Trim();
yield return
_connection.JavaPath.Trim();
}
if (!string.IsNullOrWhiteSpace(_connection.JavaHome))
if (!string.IsNullOrWhiteSpace(
_connection.JavaHome))
{
yield return Path.Combine(_connection.JavaHome.Trim(), "bin", "java.exe");
yield return Path.Combine(
_connection.JavaHome.Trim(),
"bin",
"java.exe");
}
string? javaHome = Environment.GetEnvironmentVariable("JAVA_HOME");
string? javaHome =
Environment.GetEnvironmentVariable(
"JAVA_HOME");
if (!string.IsNullOrWhiteSpace(javaHome))
{
yield return Path.Combine(javaHome.Trim(), "bin", "java.exe");
yield return Path.Combine(
javaHome.Trim(),
"bin",
"java.exe");
}
string? jreHome = Environment.GetEnvironmentVariable("JRE_HOME");
string? jreHome =
Environment.GetEnvironmentVariable(
"JRE_HOME");
if (!string.IsNullOrWhiteSpace(jreHome))
{
yield return Path.Combine(jreHome.Trim(), "bin", "java.exe");
yield return Path.Combine(
jreHome.Trim(),
"bin",
"java.exe");
}
yield return "java.exe"; // PATH
yield return "java.exe";
}
private (string? Classpath, string? Error) ResolveSonicClasspath()
private (
string? Classpath,
string? Error) ResolveSonicClasspath()
{
List<string> libDirs = [];
List<string> libraryDirectories = [];
if (!string.IsNullOrWhiteSpace(_connection.MfClientLibPath))
if (!string.IsNullOrWhiteSpace(
_connection.MfClientLibPath))
{
libDirs.Add(_connection.MfClientLibPath.Trim());
libraryDirectories.Add(
ResolveDirectoryPath(
_connection.MfClientLibPath));
}
if (!string.IsNullOrWhiteSpace(_connection.SonicHome))
if (!string.IsNullOrWhiteSpace(
_connection.SonicHome))
{
string home = _connection.SonicHome.Trim();
libDirs.Add(Path.Combine(home, "lib"));
libDirs.Add(home);
string sonicHome =
ResolveDirectoryPath(
_connection.SonicHome);
libraryDirectories.Add(
Path.Combine(
sonicHome,
"lib"));
libraryDirectories.Add(
sonicHome);
}
HashSet<string> jars = new(StringComparer.OrdinalIgnoreCase);
foreach (string dir in libDirs.Where(Directory.Exists))
HashSet<string> jars =
new(
StringComparer.OrdinalIgnoreCase);
foreach (string directory
in libraryDirectories
.Where(Directory.Exists)
.Distinct(
StringComparer.OrdinalIgnoreCase))
{
foreach (string jar in Directory.EnumerateFiles(dir, "*.jar", SearchOption.TopDirectoryOnly))
foreach (string jar
in Directory.EnumerateFiles(
directory,
"*.jar",
SearchOption.TopDirectoryOnly))
{
jars.Add(jar);
jars.Add(
Path.GetFullPath(jar));
}
}
if (jars.Count == 0)
{
return (null,
"Keine Sonic-Client-JARs gefunden. MfClientLibPath oder SonicHome\\lib setzen "
+ "(mgmt_client.jar, mfcontext.jar, …).");
return (
null,
"Keine Sonic-Client-JARs gefunden. " +
"Runtime:SonicClientLibraryPath prüfen.");
}
// Explizite Kern-JARs zuerst, falls vorhanden
string[] preferred =
string[] preferredJarNames =
[
"mgmt_client.jar",
"mfcontext.jar",
@@ -222,58 +504,125 @@ public sealed class SonicMfApiExecutor
"mf_common.jar"
];
List<string> ordered = [];
foreach (string name in preferred)
List<string> orderedJars = [];
foreach (string preferredJarName
in preferredJarNames)
{
string? hit = jars.FirstOrDefault(j =>
string.Equals(Path.GetFileName(j), name, StringComparison.OrdinalIgnoreCase));
if (hit is not null)
string? matchingJar =
jars.FirstOrDefault(
jar => string.Equals(
Path.GetFileName(jar),
preferredJarName,
StringComparison.OrdinalIgnoreCase));
if (matchingJar is not null)
{
ordered.Add(hit);
orderedJars.Add(
matchingJar);
}
}
foreach (string jar in jars.OrderBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase))
foreach (string jar
in jars.OrderBy(
Path.GetFileName,
StringComparer.OrdinalIgnoreCase))
{
if (!ordered.Contains(jar, StringComparer.OrdinalIgnoreCase))
if (!orderedJars.Contains(
jar,
StringComparer.OrdinalIgnoreCase))
{
ordered.Add(jar);
orderedJars.Add(jar);
}
}
return (string.Join(Path.PathSeparator, ordered), null);
return (
string.Join(
Path.PathSeparator,
orderedJars),
null);
}
private static string ResolveDirectoryPath(
string configuredPath)
{
return Path.IsPathRooted(configuredPath)
? Path.GetFullPath(configuredPath)
: Path.GetFullPath(
Path.Combine(
AppContext.BaseDirectory,
configuredPath));
}
private static string? ResolveToolsDir()
{
string[] dirs =
string[] directories =
[
Path.Combine(AppContext.BaseDirectory, "Tools"),
Path.Combine(Directory.GetCurrentDirectory(), "Tools"),
Path.Combine(
AppContext.BaseDirectory,
"Tools"),
Path.Combine(
Directory.GetCurrentDirectory(),
"Tools"),
AppContext.BaseDirectory
];
foreach (string dir in dirs.Where(Directory.Exists))
foreach (string directory
in directories
.Where(Directory.Exists))
{
if (File.Exists(Path.Combine(dir, "SonicMfContainerTool.class")))
string toolPath =
Path.Combine(
directory,
"SonicMfContainerTool.class");
if (File.Exists(toolPath))
{
return dir;
return directory;
}
}
return null;
}
private static string? ExtractError(string combined)
private static string? ExtractError(
string combinedOutput)
{
foreach (string line in combined.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
foreach (string line
in combinedOutput.Split(
['\r', '\n'],
StringSplitOptions.RemoveEmptyEntries))
{
if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase))
if (line.StartsWith(
"ERROR:",
StringComparison.OrdinalIgnoreCase))
{
return line["ERROR:".Length..].Trim();
return line[
"ERROR:".Length..]
.Trim();
}
}
return null;
}
}
private static void TryKillProcess(
Process process)
{
try
{
if (!process.HasExited)
{
process.Kill(
entireProcessTree: true);
}
}
catch
{
// Der ursprüngliche Fehler oder Timeout
// darf nicht verdeckt werden.
}
}
}