Files

1710 lines
58 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
List<string> args =
[
"-cp", Quote(classpath!),
"SonicMfContainerTool",
command,
"--domain", _connection.DomainName,
"--url", _connection.ConnectionUrl,
"--user", _connection.Username,
"--timeout", Math.Clamp(_connection.TimeoutSeconds, 5, 600).ToString()
];
if (!string.IsNullOrWhiteSpace(container))
{
args.Add("--container");
args.Add(container);
}
ProcessStartInfo psi = new()
{
FileName = javaExe!,
Arguments = string.Join(" ", args.Select(EscapeArg)),
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
WorkingDirectory = classDir!
};
psi.Environment["ESB_SONIC_PASSWORD"] = _connection.Password ?? string.Empty;
using Process process = new() { StartInfo = psi };
if (!process.Start())
{
return (false, null, "Java-Prozess konnte nicht gestartet werden.");
}
using CancellationTokenSource timeoutCts =
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 10, 600)));
Task<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}";
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? libDir, string? libError) = ResolveLibDirectoryDetailed();
if (libDir 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 = BuildClasspath(libDir, 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 = BuildClasspath(libDir, null);
string runtimeClasspath = BuildClasspath(libDir, 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,
Arguments =
$"{releaseArgs} -encoding UTF-8 -cp {Quote(compileClasspath)} -d {Quote(workDir)} {Quote(javaTarget)}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
using Process compile = new() { StartInfo = compilePsi };
if (!compile.Start())
{
return (false, null, null, null, "javac konnte nicht gestartet werden.");
}
string cOut = await compile.StandardOutput.ReadToEndAsync(cancellationToken);
string cErr = await compile.StandardError.ReadToEndAsync(cancellationToken);
await compile.WaitForExitAsync(cancellationToken);
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-Lib: {libDir}");
}
}
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;
}
}
private (string? Dir, string? Error) ResolveLibDirectoryDetailed()
{
List<(string Path, string Label)> candidates = [];
if (!string.IsNullOrWhiteSpace(_connection.MfClientLibPath))
{
candidates.Add((_connection.MfClientLibPath.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))
{
candidates.Add((Path.Combine(mqHome.Trim().Trim('"'), "lib"), "MQ_HOME\\lib"));
}
if (!string.IsNullOrWhiteSpace(esbHome))
{
string esb = esbHome.Trim().Trim('"');
candidates.Add((Path.Combine(esb, "lib"), "ESB_HOME\\lib"));
candidates.Add((Path.Combine(esb, "MQ", "lib"), "ESB_HOME\\MQ\\lib"));
candidates.Add((Path.Combine(esb, "mq", "lib"), "ESB_HOME\\mq\\lib"));
}
if (!string.IsNullOrWhiteSpace(_connection.SonicHome))
{
string sonicHome = _connection.SonicHome.Trim();
candidates.Add((Path.Combine(sonicHome, "lib"), "SonicHome\\lib"));
candidates.Add((sonicHome, "SonicHome"));
// ESB-Home zeigt oft auf Broker/ESB; Client-JARs liegen unter MQ*/lib oder sibling MQ.
foreach (string nested in EnumerateLikelyLibRoots(sonicHome))
{
candidates.Add((nested, "SonicHome-nested\\lib"));
}
string? parent = Directory.GetParent(sonicHome)?.FullName;
if (!string.IsNullOrWhiteSpace(parent))
{
candidates.Add((Path.Combine(parent, "lib"), "SonicHome-Parent\\lib"));
foreach (string nested in EnumerateLikelyLibRoots(parent))
{
candidates.Add((nested, "SonicHome-Parent-nested\\lib"));
}
}
}
List<string> checkedPaths = [];
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
foreach ((string path, string label) in candidates)
{
if (string.IsNullOrWhiteSpace(path) || !seen.Add(path))
{
continue;
}
checkedPaths.Add($"{label}='{path}'");
if (!Directory.Exists(path))
{
continue;
}
if (DirectoryHasSonicClientJars(path))
{
return (path, null);
}
}
string sonicHomeVal = _connection.SonicHome ?? string.Empty;
bool sonicHomeExists = !string.IsNullOrWhiteSpace(sonicHomeVal) && Directory.Exists(sonicHomeVal);
string libPath = string.IsNullOrWhiteSpace(sonicHomeVal)
? "(SonicHome leer)"
: Path.Combine(sonicHomeVal, "lib");
if (sonicHomeExists && Directory.Exists(libPath)
&& !Directory.EnumerateFiles(libPath, "*.jar", SearchOption.TopDirectoryOnly).Any())
{
return (null,
$"Sonic-Client-JARs fehlen unter '{libPath}' (Ordner existiert, aber keine *.jar).\n" +
$"Verwendetes SonicHome: '{sonicHomeVal}'\n" +
"MfApi braucht u.a. mgmt_client.jar / mfcontext.jar / sonic_Client.jar.\n" +
"Falls SonicHome auf ESB zeigt: auf MQ-Home setzen oder MfClientLibPath auf den MQ\\lib-Ordner.\n" +
$"Geprüft: {string.Join("; ", checkedPaths)}");
}
if (sonicHomeExists && !Directory.Exists(libPath))
{
return (null,
$"SonicHome='{sonicHomeVal}' gefunden, aber Lib-Ordner fehlt: '{libPath}'.\n" +
"Sonic-Client-Installation prüfen oder MfClientLibPath setzen " +
"(erwartet *.jar, u.a. mgmt_client.jar).\n" +
$"Geprüft: {string.Join("; ", checkedPaths)}");
}
return (null,
"Sonic-Client-JARs nicht gefunden. SonicHome\\lib, MQ_HOME\\lib, ESB_HOME\\lib oder MfClientLibPath setzen.\n" +
$"Verwendetes SonicHome: '{(string.IsNullOrWhiteSpace(sonicHomeVal) ? "(leer)" : sonicHomeVal)}'\n" +
$"Geprüft:\n - {string.Join("\n - ", checkedPaths)}\n" +
"Benötigt u.a. mgmt_client.jar / mfcontext.jar / sonic_Client.jar.");
}
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;
}
}
}
}
private static bool DirectoryHasSonicClientJars(string path)
{
try
{
string[] jars = Directory.EnumerateFiles(path, "*.jar", SearchOption.TopDirectoryOnly).ToArray();
if (jars.Length == 0)
{
return false;
}
// Bevorzugt echte Sonic-Client-Libs; sonst jeder JAR-Ordner als Fallback.
string[] markers =
[
"mgmt_client", "mfcontext", "sonic_Client", "sonic_client", "mf_client", "Sonic"
];
if (jars.Any(j => markers.Any(m =>
Path.GetFileName(j).Contains(m, StringComparison.OrdinalIgnoreCase))))
{
return true;
}
return jars.Length > 0;
}
catch
{
return false;
}
}
private static string BuildClasspath(string libDir, string? extraEntry)
{
// Java-Classpath-Wildcard für alle JARs im Lib-Ordner
string jars = Path.Combine(libDir, "*");
return string.IsNullOrWhiteSpace(extraEntry)
? jars
: jars + Path.PathSeparator + extraEntry;
}
private static string? ResolveToolSourcePath()
{
string[] candidates =
[
Path.Combine(AppContext.BaseDirectory, "Tools", "SonicMfContainerTool.java"),
Path.Combine(Directory.GetCurrentDirectory(), "Tools", "SonicMfContainerTool.java"),
Path.Combine(AppContext.BaseDirectory, "SonicMfContainerTool.java")
];
return candidates.FirstOrDefault(File.Exists);
}
private 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] + "…";
}