using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Win32;
using ZA.CoreService.ESBCertificateManager.Models;
namespace ZA.CoreService.ESBCertificateManager.Services;
///
/// 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).
///
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;
}
///
/// Liefert den konfigurierten SonicHome-Pfad und die gefundene java.exe (oder null).
/// Für UI-Status und Diagnose.
///
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 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 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 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 stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
Task 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 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 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;
}
///
/// 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.
///
private async Task<(string? JavaExe, int Major, List Probed)> FindJavaWithMinimumMajorAsync(
int minMajor,
CancellationToken cancellationToken)
{
List probed = [];
List 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 checkedPaths = [];
HashSet 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 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 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 CheckedPaths { get; } = [];
}
private string? ResolveJavaExecutable() => DiscoverJava().JavaExe;
private JavaDiscoveryResult DiscoverJava()
{
JavaDiscoveryResult result = new();
List 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 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 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 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 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 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 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 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 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 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 checkedPaths)
{
string text;
try
{
text = File.ReadAllText(scriptPath);
}
catch
{
return null;
}
// JAVA_HOME / JRE_HOME / JDK_HOME Assignments
Regex homeAssign = new(
@"(?im)(?:set\s+)?(?JAVA_HOME|JRE_HOME|JDK_HOME)\s*=\s*(?[^\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)(?(?:[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 checkedPaths, int depth)
{
if (!Directory.Exists(root) || depth < 1)
{
return null;
}
List preferred = [];
List 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 EnumerateSiblingMqEsbRoots(string parent, string currentSonicHome)
{
if (!Directory.Exists(parent))
{
yield break;
}
IEnumerable 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 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 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 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 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 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 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 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] + "…";
}