Use ct-ZADBService and explicit MfClientLibPath so SMC restart finds client JARs. Co-authored-by: Cursor <cursoragent@cursor.com>
1917 lines
65 KiB
C#
1917 lines
65 KiB
C#
using System.Diagnostics;
|
||
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
using Microsoft.Win32;
|
||
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 const int MaxJavaSearchDepth = 6;
|
||
private const int MaxJavaFilesToScan = 80;
|
||
private const int MaxCheckedPathsInError = 40;
|
||
|
||
private readonly SonicConnection _connection;
|
||
|
||
public SonicMfApiExecutor(SonicConnection connection)
|
||
{
|
||
_connection = connection;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Liefert den konfigurierten SonicHome-Pfad und die gefundene java.exe (oder null).
|
||
/// Für UI-Status und Diagnose.
|
||
/// </summary>
|
||
public (string SonicHome, string? JavaExe) ResolveRuntimePaths()
|
||
{
|
||
JavaDiscoveryResult result = DiscoverJava();
|
||
string sonicHome = string.IsNullOrWhiteSpace(_connection.SonicHome)
|
||
? "(leer)"
|
||
: _connection.SonicHome.Trim();
|
||
return (sonicHome, result.JavaExe);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
// ArgumentList statt manueller Quotes: sonst landet \"...\" im Classpath
|
||
// und Java findet JMSConnectorAddress trotz vorhandener JARs nicht.
|
||
ProcessStartInfo psi = new()
|
||
{
|
||
FileName = javaExe!,
|
||
UseShellExecute = false,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
CreateNoWindow = true,
|
||
StandardOutputEncoding = Encoding.UTF8,
|
||
StandardErrorEncoding = Encoding.UTF8,
|
||
WorkingDirectory = classDir!
|
||
};
|
||
|
||
psi.ArgumentList.Add("-cp");
|
||
psi.ArgumentList.Add(classpath!);
|
||
psi.ArgumentList.Add("SonicMfContainerTool");
|
||
psi.ArgumentList.Add(command);
|
||
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("--timeout");
|
||
psi.ArgumentList.Add(Math.Clamp(_connection.TimeoutSeconds, 5, 600).ToString());
|
||
|
||
if (!string.IsNullOrWhiteSpace(container))
|
||
{
|
||
psi.ArgumentList.Add("--container");
|
||
psi.ArgumentList.Add(container);
|
||
}
|
||
|
||
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}";
|
||
|
||
if (err.Contains("ClassNotFoundException", StringComparison.OrdinalIgnoreCase)
|
||
|| combined.Contains("JMSConnectorAddress", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
err +=
|
||
"\n\nClasspath unvollständig: com.sonicsw.mf.jmx.client.JMSConnectorAddress fehlt.\n" +
|
||
"Bitte in appsettings MfClientLibPath auf den Ordner mit mgmt_client.jar / mfcontext.jar / sonic_Client.jar setzen\n" +
|
||
$"(aktuell SonicHome='{_connection.SonicHome}').\n" +
|
||
"Hinweis: Container-Kurzname laut SMC ist 'ct-ZADBService' (nicht der Verbindungs-Alias DE-Test).";
|
||
}
|
||
|
||
return (false, combined.Length > 0 ? combined : null, err);
|
||
}
|
||
|
||
private async Task<(bool Ok, string? JavaExe, string? ClassDir, string? Classpath, string? Error)>
|
||
PrepareToolAsync(CancellationToken cancellationToken)
|
||
{
|
||
// Tool-Bytecode ist Java 8 (major 52). Sonic-mitgelieferte JREs sind oft Java 6/7 → unbrauchbar.
|
||
// Deshalb: Runtime mit major>=52 bevorzugen (PATH/JAVA_HOME/Registry), Libs weiter aus SonicHome.
|
||
const int minClassMajor = 52;
|
||
|
||
JavaDiscoveryResult javaResult = DiscoverJava();
|
||
string? javaExe = javaResult.JavaExe;
|
||
int runtimeMajor = javaExe is null
|
||
? 0
|
||
: await GetJavaClassMajorAsync(javaExe, cancellationToken);
|
||
|
||
if (javaExe is null || runtimeMajor < minClassMajor)
|
||
{
|
||
(string? newerJava, int newerMajor, List<string> probed) =
|
||
await FindJavaWithMinimumMajorAsync(minClassMajor, cancellationToken);
|
||
if (newerJava is not null)
|
||
{
|
||
javaExe = newerJava;
|
||
runtimeMajor = newerMajor;
|
||
javaResult.CheckedPaths.Add($"UPGRADE:RuntimeJava8+={newerJava} major={newerMajor}");
|
||
}
|
||
}
|
||
|
||
if (javaExe is null)
|
||
{
|
||
string sonicHome = string.IsNullOrWhiteSpace(_connection.SonicHome)
|
||
? "(SonicHome leer / nicht gesetzt)"
|
||
: _connection.SonicHome.Trim();
|
||
string checkedList = javaResult.CheckedPaths.Count == 0
|
||
? "(keine Kandidaten gesammelt)"
|
||
: string.Join("\n - ", javaResult.CheckedPaths.Take(MaxCheckedPathsInError));
|
||
|
||
return (false, null, null, null,
|
||
"Java nicht gefunden.\n" +
|
||
$"Verwendetes SonicHome: '{sonicHome}'\n" +
|
||
"Konfiguration: JavaPath/JavaHome auf eine Java-8+-Runtime setzen.\n" +
|
||
"Geprüfte Pfade:\n - " + checkedList);
|
||
}
|
||
|
||
if (runtimeMajor < minClassMajor)
|
||
{
|
||
return (false, null, null, null,
|
||
"Gefundene Java-Runtime ist zu alt für SonicMfContainerTool (braucht Java 8 / major 52).\n" +
|
||
$"Aktuell: {javaExe} (class major={runtimeMajor}).\n" +
|
||
"Die JRE unter SonicHome ist oft Java 6/7 – bitte Java 8+ installieren und in appsettings setzen:\n" +
|
||
" \"JavaPath\": \"C:\\\\Program Files\\\\Java\\\\jre1.8.0_xxx\\\\bin\\\\java.exe\"\n" +
|
||
"Sonic-Client-JARs bleiben unter SonicHome\\lib.");
|
||
}
|
||
|
||
(string? libClasspath, string? libError) = ResolveSonicClasspathDetailed();
|
||
if (libClasspath is null)
|
||
{
|
||
return (false, null, null, null, libError);
|
||
}
|
||
|
||
// Vorcompilierte .class/.jar nur nutzen, wenn Class-Version zur Runtime passt.
|
||
if (TryResolvePrebuiltTool(runtimeMajor, out string? prebuiltDir, out string? prebuiltEntry, out string? prebuiltError)
|
||
&& prebuiltDir is not null && prebuiltEntry is not null)
|
||
{
|
||
string classpath = libClasspath + Path.PathSeparator + prebuiltEntry;
|
||
return (true, javaExe, prebuiltDir, classpath, null);
|
||
}
|
||
|
||
// Zur Laufzeit aus .java für Java 8 kompilieren (--release 8 / -source 1.8 -target 1.8).
|
||
string? sourcePath = ResolveToolSourcePath();
|
||
if (sourcePath is null)
|
||
{
|
||
return (false, null, null, null,
|
||
(prebuiltError ?? "Kein SonicMfContainerTool gefunden.") +
|
||
" Tools\\SonicMfContainerTool.java/.class fehlen.\n" +
|
||
$"Runtime-Java: {javaExe} (class major max={runtimeMajor}).");
|
||
}
|
||
|
||
string workDir = Path.Combine(Path.GetTempPath(), "esb-sonic-mf-j8");
|
||
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 = libClasspath;
|
||
string runtimeClasspath = libClasspath + Path.PathSeparator + workDir;
|
||
|
||
bool classOk = File.Exists(classFile)
|
||
&& ReadClassFileMajorVersion(classFile) is int existingMajor
|
||
&& existingMajor <= runtimeMajor
|
||
&& File.GetLastWriteTimeUtc(classFile) >= File.GetLastWriteTimeUtc(sourcePath);
|
||
|
||
if (!classOk)
|
||
{
|
||
string? javac = ResolveJavacExecutable(javaExe);
|
||
if (javac is null)
|
||
{
|
||
return (false, null, null, null,
|
||
"SonicMfContainerTool muss für Java 8 gebaut werden, aber javac fehlt.\n" +
|
||
"Tools\\SonicMfContainerTool.class (major<=52) mit ausliefern oder JDK installieren.\n" +
|
||
$"Gefundene java.exe: {javaExe}");
|
||
}
|
||
|
||
// Wichtig: ohne --release 8 erzeugt modernes javac major 65 → UnsupportedClassVersionError auf Sonic-JRE 8.
|
||
string releaseArgs = SupportsJavacRelease8(javac)
|
||
? "--release 8"
|
||
: "-source 1.8 -target 1.8";
|
||
|
||
ProcessStartInfo compilePsi = new()
|
||
{
|
||
FileName = javac,
|
||
UseShellExecute = false,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
CreateNoWindow = true,
|
||
StandardOutputEncoding = Encoding.UTF8,
|
||
StandardErrorEncoding = Encoding.UTF8
|
||
};
|
||
|
||
if (releaseArgs.StartsWith("--release", StringComparison.Ordinal))
|
||
{
|
||
compilePsi.ArgumentList.Add("--release");
|
||
compilePsi.ArgumentList.Add("8");
|
||
}
|
||
else
|
||
{
|
||
compilePsi.ArgumentList.Add("-source");
|
||
compilePsi.ArgumentList.Add("1.8");
|
||
compilePsi.ArgumentList.Add("-target");
|
||
compilePsi.ArgumentList.Add("1.8");
|
||
}
|
||
|
||
compilePsi.ArgumentList.Add("-encoding");
|
||
compilePsi.ArgumentList.Add("UTF-8");
|
||
compilePsi.ArgumentList.Add("-cp");
|
||
compilePsi.ArgumentList.Add(compileClasspath);
|
||
compilePsi.ArgumentList.Add("-d");
|
||
compilePsi.ArgumentList.Add(workDir);
|
||
compilePsi.ArgumentList.Add(javaTarget);
|
||
|
||
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);
|
||
|
||
int? builtMajor = File.Exists(classFile) ? ReadClassFileMajorVersion(classFile) : null;
|
||
if (compile.ExitCode != 0 || builtMajor is null || builtMajor > runtimeMajor)
|
||
{
|
||
return (false, null, null, null,
|
||
"Kompilieren von SonicMfContainerTool für Java 8 fehlgeschlagen.\n" +
|
||
Truncate((cErr + "\n" + cOut).Trim()) +
|
||
$"\nErzeugt major={builtMajor?.ToString() ?? "?"}, Runtime erlaubt <={runtimeMajor}.\n" +
|
||
$"Classpath: {Truncate(compileClasspath, 240)}");
|
||
}
|
||
}
|
||
|
||
return (true, javaExe, workDir, runtimeClasspath, null);
|
||
}
|
||
|
||
private bool TryResolvePrebuiltTool(
|
||
int runtimeMajor,
|
||
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 classFile = Path.Combine(dir, "SonicMfContainerTool.class");
|
||
if (File.Exists(classFile))
|
||
{
|
||
int? major = ReadClassFileMajorVersion(classFile);
|
||
if (major is not null && major <= runtimeMajor)
|
||
{
|
||
workDir = dir;
|
||
classpathEntry = dir;
|
||
return true;
|
||
}
|
||
|
||
error =
|
||
$"Tools\\SonicMfContainerTool.class ist zu neu (major={major}, braucht <={runtimeMajor} / Java 8).";
|
||
continue;
|
||
}
|
||
|
||
// JAR absichtlich nicht blind nutzen – oft noch mit Java 21 gebaut.
|
||
// Nur .class mit geprüftem major<=runtimeMajor.
|
||
}
|
||
|
||
error ??= "Tools\\SonicMfContainerTool.class (Java 8) nicht gefunden.";
|
||
return false;
|
||
}
|
||
|
||
private static int? ReadClassFileMajorVersion(string classFile)
|
||
{
|
||
try
|
||
{
|
||
byte[] header = new byte[8];
|
||
using FileStream fs = File.OpenRead(classFile);
|
||
if (fs.Read(header, 0, 8) < 8)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
// CA FE BA BE | minor | major
|
||
if (header[0] != 0xCA || header[1] != 0xFE || header[2] != 0xBA || header[3] != 0xBE)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return (header[6] << 8) | header[7];
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private static async Task<int> GetJavaClassMajorAsync(string javaExe, CancellationToken cancellationToken)
|
||
{
|
||
try
|
||
{
|
||
ProcessStartInfo psi = new()
|
||
{
|
||
FileName = javaExe,
|
||
Arguments = "-version",
|
||
UseShellExecute = false,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
CreateNoWindow = true
|
||
};
|
||
|
||
using Process p = new() { StartInfo = psi };
|
||
if (!p.Start())
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
string err = await p.StandardError.ReadToEndAsync(cancellationToken);
|
||
string output = await p.StandardOutput.ReadToEndAsync(cancellationToken);
|
||
await p.WaitForExitAsync(cancellationToken);
|
||
return ParseJavaVersionToClassMajor(err + "\n" + output);
|
||
}
|
||
catch
|
||
{
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
private static int ParseJavaVersionToClassMajor(string versionText)
|
||
{
|
||
string text = versionText.ToLowerInvariant();
|
||
|
||
// 1.8.x → 52, 1.7.x → 51, 1.6.x → 50
|
||
Match legacy = Regex.Match(text, @"version\s+""1\.(\d+)");
|
||
if (legacy.Success && int.TryParse(legacy.Groups[1].Value, out int minor))
|
||
{
|
||
return 44 + minor; // 1.6→50, 1.7→51, 1.8→52
|
||
}
|
||
|
||
Match modern = Regex.Match(text, @"version\s+""(\d+)");
|
||
if (modern.Success && int.TryParse(modern.Groups[1].Value, out int major) && major >= 9)
|
||
{
|
||
return 44 + major; // 9→53 … 21→65
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sucht eine java.exe mit mindestens der gewünschten Class-Major-Version
|
||
/// (für Tool-Bytecode Java 8 = 52), unabhängig von der alten Sonic-JRE.
|
||
/// </summary>
|
||
private async Task<(string? JavaExe, int Major, List<string> Probed)> FindJavaWithMinimumMajorAsync(
|
||
int minMajor,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
List<string> probed = [];
|
||
List<string> candidates = [];
|
||
|
||
void AddCandidate(string? path)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||
{
|
||
return;
|
||
}
|
||
|
||
string full = Path.GetFullPath(path);
|
||
if (!candidates.Contains(full, StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
candidates.Add(full);
|
||
}
|
||
}
|
||
|
||
foreach (string? configured in new[] { _connection.JavaPath, _connection.JavaHome })
|
||
{
|
||
if (string.IsNullOrWhiteSpace(configured)) continue;
|
||
AddCandidate(ResolveToolFromConfiguredPath(configured, JavaExeName, probed));
|
||
}
|
||
|
||
foreach (string envName in new[] { "JAVA_HOME", "JRE_HOME", "JDK_HOME" })
|
||
{
|
||
AddCandidate(ResolveJavaFromHomeDir(Environment.GetEnvironmentVariable(envName), probed, envName));
|
||
}
|
||
|
||
AddCandidate(FindOnPath(JavaExeName, probed));
|
||
AddCandidate(FindOnPath("java", probed));
|
||
AddCandidate(FindJavaInWindowsRegistry(probed));
|
||
AddCandidate(FindJavaViaWhereExe(probed));
|
||
AddCandidate(FindJavaInCommonInstallLocations(probed));
|
||
|
||
// Program Files Java/Adoptium/Temurin grob scannen
|
||
foreach (string root in new[]
|
||
{
|
||
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
|
||
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
|
||
@"C:\Program Files\Eclipse Adoptium",
|
||
@"C:\Program Files\Java",
|
||
@"C:\Program Files\Microsoft",
|
||
@"C:\Program Files\Amazon Corretto"
|
||
})
|
||
{
|
||
if (!Directory.Exists(root)) continue;
|
||
try
|
||
{
|
||
foreach (string javaExe in Directory.EnumerateFiles(root, "java.exe", SearchOption.AllDirectories)
|
||
.Take(40))
|
||
{
|
||
AddCandidate(javaExe);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
foreach (string candidate in candidates)
|
||
{
|
||
int major = await GetJavaClassMajorAsync(candidate, cancellationToken);
|
||
probed.Add($"{candidate} => major={major}");
|
||
if (major >= minMajor)
|
||
{
|
||
return (candidate, major, probed);
|
||
}
|
||
}
|
||
|
||
return (null, 0, probed);
|
||
}
|
||
|
||
private static bool SupportsJavacRelease8(string javacPath)
|
||
{
|
||
try
|
||
{
|
||
using Process p = Process.Start(new ProcessStartInfo
|
||
{
|
||
FileName = javacPath,
|
||
Arguments = "-version",
|
||
UseShellExecute = false,
|
||
RedirectStandardError = true,
|
||
RedirectStandardOutput = true,
|
||
CreateNoWindow = true
|
||
})!;
|
||
string text = (p.StandardError.ReadToEnd() + p.StandardOutput.ReadToEnd()).ToLowerInvariant();
|
||
p.WaitForExit(5000);
|
||
// javac 9+ supports --release
|
||
return !text.Contains("1.8") && !text.Contains("1.7") && !text.Contains("1.6");
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Baut einen Classpath mit echten Sonic-Management-Client-JARs
|
||
/// (inkl. com.sonicsw.mf.jmx.client.JMSConnectorAddress).
|
||
/// </summary>
|
||
private (string? Classpath, string? Error) ResolveSonicClasspathDetailed()
|
||
{
|
||
List<(string Path, string Label)> searchRoots = [];
|
||
|
||
if (!string.IsNullOrWhiteSpace(_connection.MfClientLibPath))
|
||
{
|
||
searchRoots.Add((_connection.MfClientLibPath.Trim().Trim('"'), "MfClientLibPath"));
|
||
}
|
||
|
||
string? mqHome = Environment.GetEnvironmentVariable("MQ_HOME");
|
||
string? esbHome = Environment.GetEnvironmentVariable("ESB_HOME")
|
||
?? Environment.GetEnvironmentVariable("SONIC_HOME")
|
||
?? Environment.GetEnvironmentVariable("SONICMQ_HOME");
|
||
|
||
if (!string.IsNullOrWhiteSpace(mqHome))
|
||
{
|
||
searchRoots.Add((mqHome.Trim().Trim('"'), "MQ_HOME"));
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(esbHome))
|
||
{
|
||
searchRoots.Add((esbHome.Trim().Trim('"'), "ESB_HOME/SONIC_HOME"));
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(_connection.SonicHome))
|
||
{
|
||
string sonicHome = _connection.SonicHome.Trim().Trim('"');
|
||
searchRoots.Add((sonicHome, "SonicHome"));
|
||
string? parent = Directory.GetParent(sonicHome)?.FullName;
|
||
if (!string.IsNullOrWhiteSpace(parent))
|
||
{
|
||
searchRoots.Add((parent, "SonicHome-Parent"));
|
||
}
|
||
}
|
||
|
||
List<string> checkedPaths = [];
|
||
HashSet<string> seenDirs = new(StringComparer.OrdinalIgnoreCase);
|
||
List<string> markerHits = [];
|
||
|
||
foreach ((string root, string label) in searchRoots)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root))
|
||
{
|
||
checkedPaths.Add($"{label}='{root}' (fehlt)");
|
||
continue;
|
||
}
|
||
|
||
checkedPaths.Add($"{label}='{root}'");
|
||
|
||
// 1) Bekannte Lib-Ordner
|
||
List<string> libCandidates = [root, Path.Combine(root, "lib"), .. EnumerateLikelyLibRoots(root)];
|
||
|
||
// 2) Rekursiv nach Marker-JARs (SMC-only oft unter MQ*/lib oder Client/lib)
|
||
foreach (string jar in EnumerateMarkerJars(root, maxDepth: 5, maxFiles: 400))
|
||
{
|
||
string? dir = Path.GetDirectoryName(jar);
|
||
if (!string.IsNullOrWhiteSpace(dir))
|
||
{
|
||
libCandidates.Add(dir);
|
||
markerHits.Add(jar);
|
||
}
|
||
}
|
||
|
||
foreach (string libDir in libCandidates)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(libDir) || !seenDirs.Add(libDir) || !Directory.Exists(libDir))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!DirectoryHasRequiredSonicClientJars(libDir, out string? detail))
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(detail))
|
||
{
|
||
checkedPaths.Add($" skip '{libDir}': {detail}");
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
string? classpath = BuildClasspathFromLibDirs(CollectRelatedLibDirs(libDir, root));
|
||
if (!string.IsNullOrWhiteSpace(classpath))
|
||
{
|
||
return (classpath, null);
|
||
}
|
||
}
|
||
}
|
||
|
||
string sonicHomeVal = _connection.SonicHome ?? string.Empty;
|
||
string markerHint = markerHits.Count == 0
|
||
? "Keine Marker-JARs (mgmt_client/mfcontext/sonic_Client) gefunden."
|
||
: "Marker-Treffer (unvollständig):\n - " + string.Join("\n - ", markerHits.Distinct(StringComparer.OrdinalIgnoreCase).Take(12));
|
||
|
||
return (null,
|
||
"Sonic-Client-JARs für MfApi fehlen (Class JMSConnectorAddress).\n" +
|
||
$"Verwendetes SonicHome: '{(string.IsNullOrWhiteSpace(sonicHomeVal) ? "(leer)" : sonicHomeVal)}'\n" +
|
||
"Benötigt u.a.: mgmt_client.jar, mfcontext.jar, sonic_Client.jar (Ordner mit *.jar).\n" +
|
||
"Lösung: MfClientLibPath auf den Ordner setzen, in dem diese JARs liegen\n" +
|
||
" (oft ...\\MQ10.0\\lib oder SMC-Client\\lib – nicht nur das JRE).\n" +
|
||
markerHint + "\n" +
|
||
"Geprüft:\n - " + string.Join("\n - ", checkedPaths.Take(40)));
|
||
}
|
||
|
||
private static IEnumerable<string> EnumerateMarkerJars(string root, int maxDepth, int maxFiles)
|
||
{
|
||
if (!Directory.Exists(root))
|
||
{
|
||
yield break;
|
||
}
|
||
|
||
string[] markers =
|
||
[
|
||
"mgmt_client", "mfcontext", "sonic_Client", "sonic_client",
|
||
"mf_client", "mf-client", "mgmt-client", "sonic_Crypto", "sonic_XA"
|
||
];
|
||
|
||
Queue<(string Dir, int Depth)> queue = new();
|
||
queue.Enqueue((root, 0));
|
||
int yielded = 0;
|
||
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
|
||
|
||
while (queue.Count > 0 && yielded < maxFiles)
|
||
{
|
||
(string dir, int depth) = queue.Dequeue();
|
||
if (!seen.Add(dir))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
IEnumerable<string> files;
|
||
try
|
||
{
|
||
files = Directory.EnumerateFiles(dir, "*.jar", SearchOption.TopDirectoryOnly);
|
||
}
|
||
catch
|
||
{
|
||
continue;
|
||
}
|
||
|
||
foreach (string file in files)
|
||
{
|
||
string name = Path.GetFileNameWithoutExtension(file);
|
||
if (markers.Any(m => name.Contains(m, StringComparison.OrdinalIgnoreCase)))
|
||
{
|
||
yielded++;
|
||
yield return file;
|
||
if (yielded >= maxFiles)
|
||
{
|
||
yield break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (depth >= maxDepth)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
IEnumerable<string> children;
|
||
try
|
||
{
|
||
children = Directory.EnumerateDirectories(dir);
|
||
}
|
||
catch
|
||
{
|
||
continue;
|
||
}
|
||
|
||
foreach (string child in children)
|
||
{
|
||
string leaf = Path.GetFileName(child);
|
||
// Skip große / irrelevante Bäume
|
||
if (leaf.Equals("docs", StringComparison.OrdinalIgnoreCase)
|
||
|| leaf.Equals("documentation", StringComparison.OrdinalIgnoreCase)
|
||
|| leaf.Equals("samples", StringComparison.OrdinalIgnoreCase)
|
||
|| leaf.Equals("log", StringComparison.OrdinalIgnoreCase)
|
||
|| leaf.Equals("logs", StringComparison.OrdinalIgnoreCase)
|
||
|| leaf.Equals("tmp", StringComparison.OrdinalIgnoreCase)
|
||
|| leaf.Equals("temp", StringComparison.OrdinalIgnoreCase)
|
||
|| leaf.StartsWith("jre", StringComparison.OrdinalIgnoreCase)
|
||
|| leaf.Equals("jdk", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
queue.Enqueue((child, depth + 1));
|
||
}
|
||
}
|
||
}
|
||
|
||
private static List<string> CollectRelatedLibDirs(string primaryLibDir, string searchRoot)
|
||
{
|
||
HashSet<string> dirs = new(StringComparer.OrdinalIgnoreCase) { primaryLibDir };
|
||
|
||
// Sibling-Libs oft: MQ/lib + ESB/lib + common/lib
|
||
string? parent = Directory.GetParent(primaryLibDir)?.FullName;
|
||
if (!string.IsNullOrWhiteSpace(parent))
|
||
{
|
||
foreach (string sibling in new[]
|
||
{
|
||
Path.Combine(parent, "lib"),
|
||
Path.Combine(parent, "common", "lib"),
|
||
Path.Combine(parent, "client", "lib")
|
||
})
|
||
{
|
||
if (Directory.Exists(sibling))
|
||
{
|
||
dirs.Add(sibling);
|
||
}
|
||
}
|
||
|
||
string? grand = Directory.GetParent(parent)?.FullName;
|
||
if (!string.IsNullOrWhiteSpace(grand))
|
||
{
|
||
foreach (string nested in EnumerateLikelyLibRoots(grand))
|
||
{
|
||
dirs.Add(nested);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (Directory.Exists(searchRoot))
|
||
{
|
||
foreach (string nested in EnumerateLikelyLibRoots(searchRoot))
|
||
{
|
||
dirs.Add(nested);
|
||
}
|
||
}
|
||
|
||
return dirs.Where(Directory.Exists).ToList();
|
||
}
|
||
|
||
private static string? BuildClasspathFromLibDirs(IEnumerable<string> libDirs)
|
||
{
|
||
HashSet<string> jarFiles = new(StringComparer.OrdinalIgnoreCase);
|
||
|
||
foreach (string dir in libDirs)
|
||
{
|
||
if (!Directory.Exists(dir))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
try
|
||
{
|
||
foreach (string jar in Directory.EnumerateFiles(dir, "*.jar", SearchOption.TopDirectoryOnly))
|
||
{
|
||
jarFiles.Add(jar);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore unreadable dirs
|
||
}
|
||
}
|
||
|
||
List<string> entries = jarFiles
|
||
.OrderBy(j => j, StringComparer.OrdinalIgnoreCase)
|
||
.ToList();
|
||
|
||
if (entries.Count == 0)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
// Classpath-Länge begrenzen: zu viele Einträge → CreateProcess-Fehler
|
||
if (entries.Count > 250)
|
||
{
|
||
entries = entries.Take(250).ToList();
|
||
}
|
||
|
||
return string.Join(Path.PathSeparator, entries);
|
||
}
|
||
|
||
private static IEnumerable<string> EnumerateLikelyLibRoots(string root)
|
||
{
|
||
if (!Directory.Exists(root))
|
||
{
|
||
yield break;
|
||
}
|
||
|
||
string[] preferred =
|
||
[
|
||
Path.Combine(root, "MQ", "lib"),
|
||
Path.Combine(root, "mq", "lib"),
|
||
Path.Combine(root, "ESB", "lib"),
|
||
Path.Combine(root, "esb", "lib"),
|
||
Path.Combine(root, "client", "lib"),
|
||
Path.Combine(root, "Client", "lib")
|
||
];
|
||
|
||
foreach (string path in preferred)
|
||
{
|
||
if (Directory.Exists(path))
|
||
{
|
||
yield return path;
|
||
}
|
||
}
|
||
|
||
IEnumerable<string> children;
|
||
try
|
||
{
|
||
children = Directory.EnumerateDirectories(root);
|
||
}
|
||
catch
|
||
{
|
||
yield break;
|
||
}
|
||
|
||
foreach (string child in children)
|
||
{
|
||
string name = Path.GetFileName(child);
|
||
if (name.StartsWith("MQ", StringComparison.OrdinalIgnoreCase)
|
||
|| name.StartsWith("ESB", StringComparison.OrdinalIgnoreCase)
|
||
|| name.Contains("Sonic", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
string lib = Path.Combine(child, "lib");
|
||
if (Directory.Exists(lib))
|
||
{
|
||
yield return lib;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Strikt: mindestens ein MF/Management-Client-JAR (nicht irgendein JAR-Ordner).
|
||
/// </summary>
|
||
private static bool DirectoryHasRequiredSonicClientJars(string path, out string? detail)
|
||
{
|
||
detail = null;
|
||
try
|
||
{
|
||
string[] jars = Directory.EnumerateFiles(path, "*.jar", SearchOption.TopDirectoryOnly).ToArray();
|
||
if (jars.Length == 0)
|
||
{
|
||
detail = "keine *.jar";
|
||
return false;
|
||
}
|
||
|
||
string[] strong =
|
||
[
|
||
"mgmt_client", "mfcontext", "sonic_Client", "sonic_client", "mf_client", "mf-client"
|
||
];
|
||
|
||
bool hasStrong = jars.Any(j =>
|
||
strong.Any(m => Path.GetFileName(j).Contains(m, StringComparison.OrdinalIgnoreCase)));
|
||
|
||
if (!hasStrong)
|
||
{
|
||
detail = $"{jars.Length} JARs, aber ohne mgmt_client/mfcontext/sonic_Client";
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
detail = ex.Message;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
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 sealed class JavaDiscoveryResult
|
||
{
|
||
public string? JavaExe { get; set; }
|
||
public List<string> CheckedPaths { get; } = [];
|
||
}
|
||
|
||
private string? ResolveJavaExecutable() => DiscoverJava().JavaExe;
|
||
|
||
private JavaDiscoveryResult DiscoverJava()
|
||
{
|
||
JavaDiscoveryResult result = new();
|
||
List<string> checkedPaths = result.CheckedPaths;
|
||
|
||
foreach (string? configured in new[] { _connection.JavaPath, _connection.JavaHome })
|
||
{
|
||
if (string.IsNullOrWhiteSpace(configured))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
string? fromConfig = ResolveToolFromConfiguredPath(configured, JavaExeName, checkedPaths);
|
||
if (fromConfig is not null)
|
||
{
|
||
result.JavaExe = fromConfig;
|
||
return result;
|
||
}
|
||
}
|
||
|
||
foreach (string envName in new[] { "JAVA_HOME", "JRE_HOME", "JDK_HOME" })
|
||
{
|
||
string? home = Environment.GetEnvironmentVariable(envName);
|
||
string? fromEnv = ResolveJavaFromHomeDir(home, checkedPaths, envName);
|
||
if (fromEnv is not null)
|
||
{
|
||
result.JavaExe = fromEnv;
|
||
return result;
|
||
}
|
||
}
|
||
|
||
string? fromPath = FindOnPath(JavaExeName, checkedPaths) ?? FindOnPath("java", checkedPaths);
|
||
if (fromPath is not null)
|
||
{
|
||
result.JavaExe = fromPath;
|
||
return result;
|
||
}
|
||
|
||
string? sonicHome = string.IsNullOrWhiteSpace(_connection.SonicHome)
|
||
? null
|
||
: _connection.SonicHome.Trim();
|
||
|
||
if (!string.IsNullOrWhiteSpace(sonicHome))
|
||
{
|
||
string? fromScripts = FindJavaFromSonicScripts(sonicHome, checkedPaths);
|
||
if (fromScripts is not null)
|
||
{
|
||
result.JavaExe = fromScripts;
|
||
return result;
|
||
}
|
||
|
||
string? direct = TryKnownJavaLayouts(sonicHome, checkedPaths);
|
||
if (direct is not null)
|
||
{
|
||
result.JavaExe = direct;
|
||
return result;
|
||
}
|
||
|
||
string? nested = FindJavaUnderSonicHome(sonicHome, checkedPaths);
|
||
if (nested is not null)
|
||
{
|
||
result.JavaExe = nested;
|
||
return result;
|
||
}
|
||
|
||
string? recursive = FindJavaRecursive(sonicHome, checkedPaths, MaxJavaSearchDepth);
|
||
if (recursive is not null)
|
||
{
|
||
result.JavaExe = recursive;
|
||
return result;
|
||
}
|
||
|
||
// Parent / sibling MQ / ESB Layouts
|
||
string? parent = Directory.GetParent(sonicHome)?.FullName;
|
||
if (!string.IsNullOrWhiteSpace(parent))
|
||
{
|
||
string? fromParent = TryKnownJavaLayouts(parent, checkedPaths)
|
||
?? FindJavaUnderSonicHome(parent, checkedPaths)
|
||
?? FindJavaRecursive(parent, checkedPaths, depth: 3);
|
||
if (fromParent is not null)
|
||
{
|
||
result.JavaExe = fromParent;
|
||
return result;
|
||
}
|
||
|
||
foreach (string siblingRoot in EnumerateSiblingMqEsbRoots(parent, sonicHome))
|
||
{
|
||
string? fromSibling = TryKnownJavaLayouts(siblingRoot, checkedPaths)
|
||
?? FindJavaUnderSonicHome(siblingRoot, checkedPaths);
|
||
if (fromSibling is not null)
|
||
{
|
||
result.JavaExe = fromSibling;
|
||
return result;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach (string commonRoot in EnumerateCommonSonicRoots())
|
||
{
|
||
string? found = TryKnownJavaLayouts(commonRoot, checkedPaths)
|
||
?? FindJavaUnderSonicHome(commonRoot, checkedPaths);
|
||
if (found is not null)
|
||
{
|
||
result.JavaExe = found;
|
||
return result;
|
||
}
|
||
}
|
||
|
||
string? fromRegistry = FindJavaInWindowsRegistry(checkedPaths);
|
||
if (fromRegistry is not null)
|
||
{
|
||
result.JavaExe = fromRegistry;
|
||
return result;
|
||
}
|
||
|
||
string? fromWhere = FindJavaViaWhereExe(checkedPaths);
|
||
if (fromWhere is not null)
|
||
{
|
||
result.JavaExe = fromWhere;
|
||
return result;
|
||
}
|
||
|
||
string? fromCommon = FindJavaInCommonInstallLocations(checkedPaths);
|
||
if (fromCommon is not null)
|
||
{
|
||
result.JavaExe = fromCommon;
|
||
return result;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private string? ResolveJavacExecutable(string javaExe)
|
||
{
|
||
string javacName = OperatingSystem.IsWindows() ? "javac.exe" : "javac";
|
||
List<string> ignored = [];
|
||
|
||
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, ignored);
|
||
if (fromConfig is not null)
|
||
{
|
||
return fromConfig;
|
||
}
|
||
}
|
||
|
||
foreach (string envName in new[] { "JAVA_HOME", "JDK_HOME" })
|
||
{
|
||
string? javaHome = Environment.GetEnvironmentVariable(envName);
|
||
if (!string.IsNullOrWhiteSpace(javaHome))
|
||
{
|
||
string candidate = Path.Combine(javaHome.Trim().Trim('"'), "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, ignored) ?? FindOnPath("javac", ignored);
|
||
}
|
||
|
||
private static string? ResolveToolFromConfiguredPath(
|
||
string? configured,
|
||
string exeName,
|
||
List<string> checkedPaths)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(configured))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
string path = configured.Trim().Trim('"');
|
||
NoteChecked(checkedPaths, $"config:{path}");
|
||
|
||
if (File.Exists(path))
|
||
{
|
||
if (path.EndsWith(exeName, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return path;
|
||
}
|
||
|
||
string? parent = Path.GetDirectoryName(path);
|
||
if (!string.IsNullOrWhiteSpace(parent))
|
||
{
|
||
string sibling = Path.Combine(parent, exeName);
|
||
NoteChecked(checkedPaths, sibling);
|
||
if (File.Exists(sibling))
|
||
{
|
||
return sibling;
|
||
}
|
||
}
|
||
|
||
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);
|
||
NoteChecked(checkedPaths, fromHome);
|
||
return File.Exists(fromHome) ? fromHome : null;
|
||
}
|
||
|
||
private static string? ResolveJavaFromHomeDir(string? home, List<string> checkedPaths, string label)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(home))
|
||
{
|
||
NoteChecked(checkedPaths, $"{label}=(leer)");
|
||
return null;
|
||
}
|
||
|
||
string trimmed = home.Trim().Trim('"');
|
||
string candidate = Path.Combine(trimmed, "bin", JavaExeName);
|
||
NoteChecked(checkedPaths, $"{label}:{candidate}");
|
||
if (File.Exists(candidate))
|
||
{
|
||
return candidate;
|
||
}
|
||
|
||
// Manchmal zeigt JAVA_HOME bereits auf ...\bin
|
||
string asBin = Path.Combine(trimmed, JavaExeName);
|
||
NoteChecked(checkedPaths, $"{label}:{asBin}");
|
||
return File.Exists(asBin) ? asBin : null;
|
||
}
|
||
|
||
private static string? TryKnownJavaLayouts(string root, List<string> checkedPaths)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(root))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
string[] relatives =
|
||
[
|
||
Path.Combine("jre", "bin", JavaExeName),
|
||
Path.Combine("jdk", "bin", JavaExeName),
|
||
Path.Combine("JRE", "bin", JavaExeName),
|
||
Path.Combine("JDK", "bin", JavaExeName),
|
||
Path.Combine("jre64", "bin", JavaExeName),
|
||
Path.Combine("jdk64", "bin", JavaExeName),
|
||
Path.Combine("Java", "jre", "bin", JavaExeName),
|
||
Path.Combine("Java", "jdk", "bin", JavaExeName),
|
||
Path.Combine("java", "bin", JavaExeName),
|
||
Path.Combine("bin", JavaExeName)
|
||
];
|
||
|
||
foreach (string rel in relatives)
|
||
{
|
||
string candidate = Path.Combine(root, rel);
|
||
NoteChecked(checkedPaths, candidate);
|
||
if (File.Exists(candidate))
|
||
{
|
||
return candidate;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static string? FindJavaUnderSonicHome(string sonicHome, List<string> checkedPaths)
|
||
{
|
||
if (!Directory.Exists(sonicHome))
|
||
{
|
||
NoteChecked(checkedPaths, $"SonicHome-missing:{sonicHome}");
|
||
return null;
|
||
}
|
||
|
||
foreach (string root in EnumerateJdkRootsUnderSonicHome(sonicHome))
|
||
{
|
||
string candidate = Path.Combine(root, "bin", JavaExeName);
|
||
NoteChecked(checkedPaths, candidate);
|
||
if (File.Exists(candidate))
|
||
{
|
||
return candidate;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static IEnumerable<string> 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;
|
||
}
|
||
}
|
||
}
|
||
|
||
IEnumerable<string> children;
|
||
try
|
||
{
|
||
children = Directory.EnumerateDirectories(sonicHome);
|
||
}
|
||
catch
|
||
{
|
||
yield break;
|
||
}
|
||
|
||
foreach (string child in children)
|
||
{
|
||
foreach (string leaf in new[] { "jre", "jdk", "JRE", "JDK", "jre64", "jdk64" })
|
||
{
|
||
string nested = Path.Combine(child, leaf);
|
||
if (Directory.Exists(nested))
|
||
{
|
||
yield return nested;
|
||
}
|
||
}
|
||
|
||
string leafName = Path.GetFileName(child);
|
||
if (leafName.StartsWith("jre", StringComparison.OrdinalIgnoreCase)
|
||
|| leafName.StartsWith("jdk", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
yield return child;
|
||
}
|
||
}
|
||
}
|
||
|
||
private static string? FindJavaFromSonicScripts(string sonicHome, List<string> checkedPaths)
|
||
{
|
||
if (!Directory.Exists(sonicHome))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
string[] scriptDirs =
|
||
[
|
||
Path.Combine(sonicHome, "bin"),
|
||
sonicHome,
|
||
Path.Combine(sonicHome, "MQ", "bin"),
|
||
Path.Combine(sonicHome, "mq", "bin")
|
||
];
|
||
|
||
string[] scriptNames =
|
||
[
|
||
"setenv.bat", "setenv.sh", "setenv.cmd",
|
||
"sonic_env.bat", "sonic_env.sh", "sonic_env.cmd",
|
||
"startcontainer.bat", "startcontainer.sh",
|
||
"startmc.bat", "startmc.sh",
|
||
"startbroker.bat", "startbroker.sh"
|
||
];
|
||
|
||
List<string> scripts = [];
|
||
foreach (string dir in scriptDirs.Where(Directory.Exists))
|
||
{
|
||
foreach (string name in scriptNames)
|
||
{
|
||
string path = Path.Combine(dir, name);
|
||
if (File.Exists(path))
|
||
{
|
||
scripts.Add(path);
|
||
}
|
||
}
|
||
|
||
try
|
||
{
|
||
foreach (string path in Directory.EnumerateFiles(dir, "sonic_env*", SearchOption.TopDirectoryOnly))
|
||
{
|
||
scripts.Add(path);
|
||
}
|
||
|
||
foreach (string path in Directory.EnumerateFiles(dir, "setenv*", SearchOption.TopDirectoryOnly))
|
||
{
|
||
scripts.Add(path);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
foreach (string script in scripts.Distinct(StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
NoteChecked(checkedPaths, $"script:{script}");
|
||
string? found = ParseJavaFromEnvScript(script, sonicHome, checkedPaths);
|
||
if (found is not null)
|
||
{
|
||
return found;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static string? ParseJavaFromEnvScript(string scriptPath, string sonicHome, List<string> checkedPaths)
|
||
{
|
||
string text;
|
||
try
|
||
{
|
||
text = File.ReadAllText(scriptPath);
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
|
||
// JAVA_HOME / JRE_HOME / JDK_HOME Assignments
|
||
Regex homeAssign = new(
|
||
@"(?im)(?:set\s+)?(?<key>JAVA_HOME|JRE_HOME|JDK_HOME)\s*=\s*(?<val>[^\r\n]+)");
|
||
foreach (Match match in homeAssign.Matches(text))
|
||
{
|
||
string raw = ExpandScriptPath(match.Groups["val"].Value, sonicHome);
|
||
string? resolved = ResolveJavaFromHomeDir(raw, checkedPaths, Path.GetFileName(scriptPath));
|
||
if (resolved is not null)
|
||
{
|
||
return resolved;
|
||
}
|
||
}
|
||
|
||
// Direkte java.exe-Pfade in Scripten
|
||
Regex javaExeRef = new(
|
||
@"(?i)(?<path>(?:[A-Za-z]:\\|/)[^\s""']+?[/\\]java(?:\.exe)?)");
|
||
foreach (Match match in javaExeRef.Matches(text))
|
||
{
|
||
string raw = ExpandScriptPath(match.Groups["path"].Value, sonicHome);
|
||
NoteChecked(checkedPaths, $"script-java:{raw}");
|
||
if (File.Exists(raw))
|
||
{
|
||
return raw;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static string ExpandScriptPath(string raw, string sonicHome)
|
||
{
|
||
string value = raw.Trim().Trim('"', '\'', '`');
|
||
|
||
// Zuerst Variablen expandieren, danach Rauschen (Kommentare/Operatoren) abschneiden.
|
||
value = Regex.Replace(value, "%~dp0", sonicHome + Path.DirectorySeparatorChar,
|
||
RegexOptions.IgnoreCase);
|
||
value = Regex.Replace(value, "%SONIC_HOME%", sonicHome, RegexOptions.IgnoreCase);
|
||
value = Regex.Replace(value, "%MQ_HOME%",
|
||
Environment.GetEnvironmentVariable("MQ_HOME") ?? sonicHome, RegexOptions.IgnoreCase);
|
||
value = Regex.Replace(value, "%ESB_HOME%",
|
||
Environment.GetEnvironmentVariable("ESB_HOME") ?? sonicHome, RegexOptions.IgnoreCase);
|
||
value = Regex.Replace(value, "%JAVA_HOME%",
|
||
Environment.GetEnvironmentVariable("JAVA_HOME") ?? string.Empty, RegexOptions.IgnoreCase);
|
||
value = Regex.Replace(value, "%JRE_HOME%",
|
||
Environment.GetEnvironmentVariable("JRE_HOME") ?? string.Empty, RegexOptions.IgnoreCase);
|
||
|
||
value = Regex.Replace(value, @"\$\{?SONIC_HOME\}?", sonicHome, RegexOptions.IgnoreCase);
|
||
value = Regex.Replace(value, @"\$\{?MQ_HOME\}?",
|
||
Environment.GetEnvironmentVariable("MQ_HOME") ?? sonicHome, RegexOptions.IgnoreCase);
|
||
value = Regex.Replace(value, @"\$\{?JAVA_HOME\}?",
|
||
Environment.GetEnvironmentVariable("JAVA_HOME") ?? string.Empty, RegexOptions.IgnoreCase);
|
||
value = Regex.Replace(value, @"\$\{?JRE_HOME\}?",
|
||
Environment.GetEnvironmentVariable("JRE_HOME") ?? string.Empty, RegexOptions.IgnoreCase);
|
||
|
||
// trailing " & rem ..." / " && ..." entfernen
|
||
int cut = value.IndexOfAny(['&', '|', '>', '<', '`']);
|
||
if (cut >= 0)
|
||
{
|
||
value = value[..cut];
|
||
}
|
||
|
||
int rem = value.IndexOf(" rem ", StringComparison.OrdinalIgnoreCase);
|
||
if (rem >= 0)
|
||
{
|
||
value = value[..rem];
|
||
}
|
||
|
||
if (value.StartsWith('.') || value.StartsWith(".."))
|
||
{
|
||
try
|
||
{
|
||
value = Path.GetFullPath(Path.Combine(sonicHome, value));
|
||
}
|
||
catch
|
||
{
|
||
// keep as-is
|
||
}
|
||
}
|
||
|
||
return value.Trim().Trim('"', '\'');
|
||
}
|
||
|
||
private static string? FindJavaRecursive(string root, List<string> checkedPaths, int depth)
|
||
{
|
||
if (!Directory.Exists(root) || depth < 1)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
List<string> preferred = [];
|
||
List<string> others = [];
|
||
int scanned = 0;
|
||
|
||
void Walk(string dir, int remaining)
|
||
{
|
||
if (scanned >= MaxJavaFilesToScan || remaining < 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
string candidate = Path.Combine(dir, JavaExeName);
|
||
if (File.Exists(candidate))
|
||
{
|
||
scanned++;
|
||
string normalized = candidate.Replace('/', Path.DirectorySeparatorChar);
|
||
if (normalized.Contains($"{Path.DirectorySeparatorChar}jre{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}",
|
||
StringComparison.OrdinalIgnoreCase)
|
||
|| normalized.Contains($"{Path.DirectorySeparatorChar}jdk{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}",
|
||
StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
preferred.Add(candidate);
|
||
}
|
||
else
|
||
{
|
||
others.Add(candidate);
|
||
}
|
||
}
|
||
|
||
if (remaining == 0 || scanned >= MaxJavaFilesToScan)
|
||
{
|
||
return;
|
||
}
|
||
|
||
foreach (string child in Directory.EnumerateDirectories(dir))
|
||
{
|
||
string name = Path.GetFileName(child);
|
||
if (name.Equals("tmp", StringComparison.OrdinalIgnoreCase)
|
||
|| name.Equals("temp", StringComparison.OrdinalIgnoreCase)
|
||
|| name.Equals("logs", StringComparison.OrdinalIgnoreCase)
|
||
|| name.Equals("log", StringComparison.OrdinalIgnoreCase)
|
||
|| name.Equals(".git", StringComparison.OrdinalIgnoreCase)
|
||
|| name.Equals("node_modules", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
Walk(child, remaining - 1);
|
||
if (scanned >= MaxJavaFilesToScan)
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// inaccessible directory
|
||
}
|
||
}
|
||
|
||
Walk(root, depth);
|
||
|
||
string? best = preferred.FirstOrDefault() ?? others.FirstOrDefault();
|
||
if (best is not null)
|
||
{
|
||
NoteChecked(checkedPaths, $"recursive-hit:{best}");
|
||
return best;
|
||
}
|
||
|
||
NoteChecked(checkedPaths, $"recursive-miss:{root} (depth<={depth}, scanned={scanned})");
|
||
return null;
|
||
}
|
||
|
||
private static IEnumerable<string> EnumerateSiblingMqEsbRoots(string parent, string currentSonicHome)
|
||
{
|
||
if (!Directory.Exists(parent))
|
||
{
|
||
yield break;
|
||
}
|
||
|
||
IEnumerable<string> children;
|
||
try
|
||
{
|
||
children = Directory.EnumerateDirectories(parent);
|
||
}
|
||
catch
|
||
{
|
||
yield break;
|
||
}
|
||
|
||
foreach (string child in children)
|
||
{
|
||
if (string.Equals(child, currentSonicHome, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
string name = Path.GetFileName(child);
|
||
if (name.StartsWith("MQ", StringComparison.OrdinalIgnoreCase)
|
||
|| name.StartsWith("ESB", StringComparison.OrdinalIgnoreCase)
|
||
|| name.Contains("Sonic", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
yield return child;
|
||
}
|
||
}
|
||
}
|
||
|
||
private static IEnumerable<string> EnumerateCommonSonicRoots()
|
||
{
|
||
string[] roots =
|
||
[
|
||
@"C:\Sonic",
|
||
@"C:\Program Files\Progress",
|
||
@"C:\Program Files\Aurea",
|
||
@"C:\Program Files (x86)\Progress",
|
||
@"C:\Program Files (x86)\Aurea",
|
||
@"D:\Sonic",
|
||
Environment.GetEnvironmentVariable("MQ_HOME") ?? string.Empty,
|
||
Environment.GetEnvironmentVariable("ESB_HOME") ?? string.Empty,
|
||
Environment.GetEnvironmentVariable("SONIC_HOME") ?? string.Empty
|
||
];
|
||
|
||
foreach (string root in roots.Where(r => !string.IsNullOrWhiteSpace(r)).Distinct(StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
if (Directory.Exists(root))
|
||
{
|
||
yield return root;
|
||
}
|
||
|
||
string? parentExists = Directory.Exists(root) ? root : null;
|
||
if (parentExists is null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
IEnumerable<string> children;
|
||
try
|
||
{
|
||
children = Directory.EnumerateDirectories(parentExists);
|
||
}
|
||
catch
|
||
{
|
||
continue;
|
||
}
|
||
|
||
foreach (string child in children)
|
||
{
|
||
string name = Path.GetFileName(child);
|
||
if (name.StartsWith("MQ", StringComparison.OrdinalIgnoreCase)
|
||
|| name.StartsWith("ESB", StringComparison.OrdinalIgnoreCase)
|
||
|| name.Contains("Sonic", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
yield return child;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private static string? FindJavaInWindowsRegistry(List<string> checkedPaths)
|
||
{
|
||
if (!OperatingSystem.IsWindows())
|
||
{
|
||
return null;
|
||
}
|
||
|
||
string[] keyPaths =
|
||
[
|
||
@"SOFTWARE\JavaSoft\Java Runtime Environment",
|
||
@"SOFTWARE\JavaSoft\JDK",
|
||
@"SOFTWARE\JavaSoft\Java Development Kit",
|
||
@"SOFTWARE\Eclipse Adoptium\JRE",
|
||
@"SOFTWARE\Eclipse Adoptium\JDK",
|
||
@"SOFTWARE\WOW6432Node\JavaSoft\Java Runtime Environment",
|
||
@"SOFTWARE\WOW6432Node\JavaSoft\JDK"
|
||
];
|
||
|
||
foreach (string keyPath in keyPaths)
|
||
{
|
||
try
|
||
{
|
||
using RegistryKey? baseKey = Registry.LocalMachine.OpenSubKey(keyPath);
|
||
if (baseKey is null)
|
||
{
|
||
NoteChecked(checkedPaths, $"registry-miss:HKLM\\{keyPath}");
|
||
continue;
|
||
}
|
||
|
||
NoteChecked(checkedPaths, $"registry:HKLM\\{keyPath}");
|
||
|
||
string? current = baseKey.GetValue("CurrentVersion") as string;
|
||
if (!string.IsNullOrWhiteSpace(current))
|
||
{
|
||
using RegistryKey? verKey = baseKey.OpenSubKey(current);
|
||
string? javaHome = verKey?.GetValue("JavaHome") as string;
|
||
string? resolved = ResolveJavaFromHomeDir(javaHome, checkedPaths, $"registry:{current}");
|
||
if (resolved is not null)
|
||
{
|
||
return resolved;
|
||
}
|
||
}
|
||
|
||
foreach (string subName in baseKey.GetSubKeyNames().OrderByDescending(s => s))
|
||
{
|
||
using RegistryKey? verKey = baseKey.OpenSubKey(subName);
|
||
string? javaHome = verKey?.GetValue("JavaHome") as string
|
||
?? verKey?.GetValue("Path") as string;
|
||
string? resolved = ResolveJavaFromHomeDir(javaHome, checkedPaths, $"registry:{subName}");
|
||
if (resolved is not null)
|
||
{
|
||
return resolved;
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
NoteChecked(checkedPaths, $"registry-error:HKLM\\{keyPath}");
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static string? FindJavaViaWhereExe(List<string> checkedPaths)
|
||
{
|
||
if (!OperatingSystem.IsWindows())
|
||
{
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
ProcessStartInfo psi = new()
|
||
{
|
||
FileName = "where.exe",
|
||
Arguments = "java",
|
||
UseShellExecute = false,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
CreateNoWindow = true
|
||
};
|
||
|
||
using Process process = new() { StartInfo = psi };
|
||
if (!process.Start())
|
||
{
|
||
NoteChecked(checkedPaths, "where.exe:start-failed");
|
||
return null;
|
||
}
|
||
|
||
string output = process.StandardOutput.ReadToEnd();
|
||
process.WaitForExit(3000);
|
||
foreach (string line in output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||
{
|
||
NoteChecked(checkedPaths, $"where:{line}");
|
||
if (File.Exists(line)
|
||
&& Path.GetFileName(line).Equals(JavaExeName, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return line;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
NoteChecked(checkedPaths, $"where.exe:error:{ex.GetType().Name}");
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static string? FindJavaInCommonInstallLocations(List<string> checkedPaths)
|
||
{
|
||
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",
|
||
"Progress",
|
||
"Aurea"
|
||
];
|
||
|
||
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;
|
||
}
|
||
|
||
string direct = Path.Combine(vendorDir, "bin", JavaExeName);
|
||
NoteChecked(checkedPaths, direct);
|
||
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);
|
||
NoteChecked(checkedPaths, candidate);
|
||
if (File.Exists(candidate))
|
||
{
|
||
return candidate;
|
||
}
|
||
|
||
string nestedJre = Path.Combine(sub, "jre", "bin", JavaExeName);
|
||
NoteChecked(checkedPaths, nestedJre);
|
||
if (File.Exists(nestedJre))
|
||
{
|
||
return nestedJre;
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore inaccessible vendor dirs
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static void NoteChecked(List<string> checkedPaths, string path)
|
||
{
|
||
if (checkedPaths.Count >= MaxCheckedPathsInError * 2)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!checkedPaths.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
checkedPaths.Add(path);
|
||
}
|
||
}
|
||
|
||
private static string JavaExeName => OperatingSystem.IsWindows() ? "java.exe" : "java";
|
||
|
||
private static string? FindOnPath(string fileName, List<string> checkedPaths)
|
||
{
|
||
string? pathEnv = Environment.GetEnvironmentVariable("PATH");
|
||
if (string.IsNullOrWhiteSpace(pathEnv))
|
||
{
|
||
NoteChecked(checkedPaths, "PATH=(leer)");
|
||
return null;
|
||
}
|
||
|
||
foreach (string dir in pathEnv.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
||
{
|
||
try
|
||
{
|
||
string candidate = Path.Combine(dir.Trim().Trim('"'), fileName);
|
||
NoteChecked(checkedPaths, $"PATH:{candidate}");
|
||
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] + "…";
|
||
}
|