using System.Diagnostics;
using System.Text;
using ZA.CoreService.ESBCertificateManager.Models;
namespace ZA.CoreService.ESBCertificateManager.Services;
///
/// Startet den Container-Restart über SonicMfContainerTool
/// mit Java und den Sonic-MfApi-Clientbibliotheken.
///
public sealed class SonicMfApiExecutor
{
private readonly SonicConnection _connection;
public SonicMfApiExecutor(
SonicConnection connection)
{
_connection =
connection
?? throw new ArgumentNullException(
nameof(connection));
}
public (string SonicHome, string? JavaExe)
ResolveRuntimePaths()
{
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)
{
if (string.IsNullOrWhiteSpace(containerName))
{
return (
false,
null,
"Der Sonic-Containername fehlt.");
}
if (string.IsNullOrWhiteSpace(
_connection.Username)
|| string.IsNullOrEmpty(
_connection.Password))
{
return (
false,
null,
"Für die Sonic-Verbindung sind keine " +
"vollständigen Zugangsdaten eingerichtet. " +
"Bitte die Einstellungen öffnen und Zugangsdaten hinterlegen.");
}
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,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
WorkingDirectory = classDir!
};
startInfo.ArgumentList.Add("-cp");
startInfo.ArgumentList.Add(classpath!);
startInfo.ArgumentList.Add(
"SonicMfContainerTool");
startInfo.ArgumentList.Add("restart");
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.
*
* Das Kennwort stammt aus dbo.SonicCredential (Always Encrypted),
* wurde clientseitig entschlüsselt und als Laufzeitwert in
* SonicConnection.Password übernommen.
*/
startInfo.Environment[
"ESB_SONIC_PASSWORD"] =
_connection.Password;
using Process process = new()
{
StartInfo = startInfo
};
try
{
if (!process.Start())
{
return (
false,
null,
"Der Java-Prozess konnte nicht gestartet werden.");
}
}
catch (Exception ex)
{
return (
false,
null,
$"Der Java-Prozess konnte nicht gestartet werden: " +
$"{ex.Message}");
}
using CancellationTokenSource timeoutCts =
CancellationTokenSource
.CreateLinkedTokenSource(
cancellationToken);
int timeoutSeconds =
Math.Clamp(
_connection.TimeoutSeconds,
10,
600);
timeoutCts.CancelAfter(
TimeSpan.FromSeconds(
timeoutSeconds));
Task stdoutTask =
process.StandardOutput
.ReadToEndAsync();
Task stderrTask =
process.StandardError
.ReadToEndAsync();
try
{
await process.WaitForExitAsync(
timeoutCts.Token);
}
catch (OperationCanceledException)
when (!cancellationToken.IsCancellationRequested)
{
TryKillProcess(process);
return (
false,
null,
$"MfApi-Timeout nach {timeoutSeconds} Sekunden.");
}
catch (OperationCanceledException)
{
TryKillProcess(process);
throw;
}
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()
{
string? javaExe =
ResolveJavaExe();
if (javaExe is null)
{
return (
false,
null,
null,
null,
"Java wurde nicht gefunden. " +
"Runtime:JavaExecutablePath prüfen.");
}
(
string? libraryClasspath,
string? libraryError
) = ResolveSonicClasspath();
if (libraryClasspath is null)
{
return (
false,
null,
null,
null,
libraryError);
}
string? toolsDirectory =
ResolveToolsDir();
if (toolsDirectory is null)
{
return (
false,
null,
null,
null,
"Tools\\SonicMfContainerTool.class " +
"wurde nicht gefunden. " +
"Projekt vollständig neu bauen.");
}
string classpath =
toolsDirectory
+ Path.PathSeparator
+ libraryClasspath;
return (
true,
javaExe,
toolsDirectory,
classpath,
null);
}
private string? ResolveJavaExe()
{
// Explizite Verbindungs-Pfade zuerst, dann Java-8-Suche
// (Runtime\bin\java.exe fehlt oft; JAVA_HOME zeigt oft auf JDK 21).
string? fromConnection = JavaRuntimeLocator.Resolve(
!string.IsNullOrWhiteSpace(_connection.JavaPath)
? _connection.JavaPath
: null);
if (fromConnection is not null)
{
return fromConnection;
}
if (!string.IsNullOrWhiteSpace(_connection.JavaHome))
{
string fromHome = Path.Combine(
_connection.JavaHome.Trim(),
"bin",
"java.exe");
string? resolvedHome =
JavaRuntimeLocator.Resolve(fromHome);
if (resolvedHome is not null)
{
return resolvedHome;
}
}
return JavaRuntimeLocator.Resolve();
}
private (
string? Classpath,
string? Error) ResolveSonicClasspath()
{
List libraryDirectories = [];
if (!string.IsNullOrWhiteSpace(
_connection.MfClientLibPath))
{
libraryDirectories.Add(
ResolveDirectoryPath(
_connection.MfClientLibPath));
}
if (!string.IsNullOrWhiteSpace(
_connection.SonicHome))
{
string sonicHome =
ResolveDirectoryPath(
_connection.SonicHome);
libraryDirectories.Add(
Path.Combine(
sonicHome,
"lib"));
libraryDirectories.Add(
sonicHome);
}
HashSet jars =
new(
StringComparer.OrdinalIgnoreCase);
foreach (string directory
in libraryDirectories
.Where(Directory.Exists)
.Distinct(
StringComparer.OrdinalIgnoreCase))
{
foreach (string jar
in Directory.EnumerateFiles(
directory,
"*.jar",
SearchOption.TopDirectoryOnly))
{
jars.Add(
Path.GetFullPath(jar));
}
}
if (jars.Count == 0)
{
return (
null,
"Keine Sonic-Client-JARs gefunden. " +
"Runtime:SonicClientLibraryPath prüfen.");
}
string[] preferredJarNames =
[
"mgmt_client.jar",
"mfcontext.jar",
"sonic_Client.jar",
"sonic_Crypto.jar",
"mf_common.jar"
];
List orderedJars = [];
foreach (string preferredJarName
in preferredJarNames)
{
string? matchingJar =
jars.FirstOrDefault(
jar => string.Equals(
Path.GetFileName(jar),
preferredJarName,
StringComparison.OrdinalIgnoreCase));
if (matchingJar is not null)
{
orderedJars.Add(
matchingJar);
}
}
foreach (string jar
in jars.OrderBy(
Path.GetFileName,
StringComparer.OrdinalIgnoreCase))
{
if (!orderedJars.Contains(
jar,
StringComparer.OrdinalIgnoreCase))
{
orderedJars.Add(jar);
}
}
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[] directories =
[
Path.Combine(
AppContext.BaseDirectory,
"Tools"),
Path.Combine(
Directory.GetCurrentDirectory(),
"Tools"),
AppContext.BaseDirectory
];
foreach (string directory
in directories
.Where(Directory.Exists))
{
string toolPath =
Path.Combine(
directory,
"SonicMfContainerTool.class");
if (File.Exists(toolPath))
{
return directory;
}
}
return null;
}
private static string? ExtractError(
string combinedOutput)
{
foreach (string line
in combinedOutput.Split(
['\r', '\n'],
StringSplitOptions.RemoveEmptyEntries))
{
if (line.StartsWith(
"ERROR:",
StringComparison.OrdinalIgnoreCase))
{
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.
}
}
}