628 lines
16 KiB
C#
628 lines
16 KiB
C#
using System.Diagnostics;
|
|
using System.Text;
|
|
using ZA.CoreService.ESBCertificateManager.Models;
|
|
|
|
namespace ZA.CoreService.ESBCertificateManager.Services;
|
|
|
|
/// <summary>
|
|
/// 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)
|
|
{
|
|
_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<string> stdoutTask =
|
|
process.StandardOutput
|
|
.ReadToEndAsync();
|
|
|
|
Task<string> 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()
|
|
{
|
|
foreach (string? candidate
|
|
in EnumerateJavaCandidates())
|
|
{
|
|
if (string.IsNullOrWhiteSpace(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()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(
|
|
_connection.JavaPath))
|
|
{
|
|
yield return
|
|
_connection.JavaPath.Trim();
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(
|
|
_connection.JavaHome))
|
|
{
|
|
yield return Path.Combine(
|
|
_connection.JavaHome.Trim(),
|
|
"bin",
|
|
"java.exe");
|
|
}
|
|
|
|
string? javaHome =
|
|
Environment.GetEnvironmentVariable(
|
|
"JAVA_HOME");
|
|
|
|
if (!string.IsNullOrWhiteSpace(javaHome))
|
|
{
|
|
yield return Path.Combine(
|
|
javaHome.Trim(),
|
|
"bin",
|
|
"java.exe");
|
|
}
|
|
|
|
string? jreHome =
|
|
Environment.GetEnvironmentVariable(
|
|
"JRE_HOME");
|
|
|
|
if (!string.IsNullOrWhiteSpace(jreHome))
|
|
{
|
|
yield return Path.Combine(
|
|
jreHome.Trim(),
|
|
"bin",
|
|
"java.exe");
|
|
}
|
|
|
|
yield return "java.exe";
|
|
}
|
|
|
|
private (
|
|
string? Classpath,
|
|
string? Error) ResolveSonicClasspath()
|
|
{
|
|
List<string> 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<string> 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<string> 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.
|
|
}
|
|
}
|
|
} |