Fix UnsupportedClassVersionError: ship Java 8 SonicMfContainerTool.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-24 09:33:35 +02:00
co-authored by Cursor
parent 22b8ff4e85
commit d6cf49532b
3 changed files with 251 additions and 74 deletions
@@ -210,24 +210,28 @@ public sealed class SonicMfApiExecutor
return (false, null, null, null, libError); return (false, null, null, null, libError);
} }
// Bevorzugt: vorcompilierte .jar/.class aus Tools\ (kein javac nötig). int runtimeMajor = await GetJavaClassMajorAsync(javaExe, cancellationToken);
if (TryResolvePrebuiltTool(out string? prebuiltDir, out string? prebuiltEntry, out string? prebuiltError) // Sonic/SMC nutzt typisch Java 8 (major 52). Nie mit höherer Class-Version starten.
// 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) && prebuiltDir is not null && prebuiltEntry is not null)
{ {
string classpath = BuildClasspath(libDir, prebuiltEntry); string classpath = BuildClasspath(libDir, prebuiltEntry);
return (true, javaExe, prebuiltDir, classpath, null); return (true, javaExe, prebuiltDir, classpath, null);
} }
// Fallback: zur Laufzeit aus .java kompilieren (braucht javac + Sonic-Libs). // Zur Laufzeit aus .java für Java 8 kompilieren (--release 8 / -source 1.8 -target 1.8).
string? sourcePath = ResolveToolSourcePath(); string? sourcePath = ResolveToolSourcePath();
if (sourcePath is null) if (sourcePath is null)
{ {
return (false, null, null, null, return (false, null, null, null,
(prebuiltError ?? "Kein vorcompilierter SonicMfContainerTool gefunden.") + (prebuiltError ?? "Kein SonicMfContainerTool gefunden.") +
" Tools\\SonicMfContainerTool.java/.class/.jar fehlen im Ausgabeverzeichnis."); " Tools\\SonicMfContainerTool.java/.class fehlen.\n" +
$"Runtime-Java: {javaExe} (class major max={runtimeMajor}).");
} }
string workDir = Path.Combine(Path.GetTempPath(), "esb-sonic-mf"); string workDir = Path.Combine(Path.GetTempPath(), "esb-sonic-mf-j8");
Directory.CreateDirectory(workDir); Directory.CreateDirectory(workDir);
string javaTarget = Path.Combine(workDir, "SonicMfContainerTool.java"); string javaTarget = Path.Combine(workDir, "SonicMfContainerTool.java");
@@ -237,24 +241,33 @@ public sealed class SonicMfApiExecutor
string compileClasspath = BuildClasspath(libDir, null); string compileClasspath = BuildClasspath(libDir, null);
string runtimeClasspath = BuildClasspath(libDir, workDir); string runtimeClasspath = BuildClasspath(libDir, workDir);
bool needsCompile = !File.Exists(classFile)
|| File.GetLastWriteTimeUtc(classFile) < File.GetLastWriteTimeUtc(sourcePath);
if (needsCompile) bool classOk = File.Exists(classFile)
&& ReadClassFileMajorVersion(classFile) is int existingMajor
&& existingMajor <= runtimeMajor
&& File.GetLastWriteTimeUtc(classFile) >= File.GetLastWriteTimeUtc(sourcePath);
if (!classOk)
{ {
string? javac = ResolveJavacExecutable(javaExe); string? javac = ResolveJavacExecutable(javaExe);
if (javac is null) if (javac is null)
{ {
return (false, null, null, null, return (false, null, null, null,
"Vorcompilierter SonicMfContainerTool fehlt und javac wurde nicht gefunden. " + "SonicMfContainerTool muss für Java 8 gebaut werden, aber javac fehlt.\n" +
"JDK installieren oder Tools\\SonicMfContainerTool.jar/.class mit ausliefern. " + "Tools\\SonicMfContainerTool.class (major<=52) mit ausliefern oder JDK installieren.\n" +
$"Gefundene java.exe: {javaExe}"); $"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() ProcessStartInfo compilePsi = new()
{ {
FileName = javac, FileName = javac,
Arguments = $"-encoding UTF-8 -cp {Quote(compileClasspath)} -d {Quote(workDir)} {Quote(javaTarget)}", Arguments =
$"{releaseArgs} -encoding UTF-8 -cp {Quote(compileClasspath)} -d {Quote(workDir)} {Quote(javaTarget)}",
UseShellExecute = false, UseShellExecute = false,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
@@ -273,19 +286,25 @@ public sealed class SonicMfApiExecutor
string cErr = await compile.StandardError.ReadToEndAsync(cancellationToken); string cErr = await compile.StandardError.ReadToEndAsync(cancellationToken);
await compile.WaitForExitAsync(cancellationToken); await compile.WaitForExitAsync(cancellationToken);
if (compile.ExitCode != 0 || !File.Exists(classFile)) int? builtMajor = File.Exists(classFile) ? ReadClassFileMajorVersion(classFile) : null;
if (compile.ExitCode != 0 || builtMajor is null || builtMajor > runtimeMajor)
{ {
return (false, null, null, null, return (false, null, null, null,
"Kompilieren von SonicMfContainerTool fehlgeschlagen.\n" + "Kompilieren von SonicMfContainerTool für Java 8 fehlgeschlagen.\n" +
Truncate((cErr + "\n" + cOut).Trim()) + Truncate((cErr + "\n" + cOut).Trim()) +
$"\nClasspath-Lib: {libDir}"); $"\nErzeugt major={builtMajor?.ToString() ?? "?"}, Runtime erlaubt <={runtimeMajor}.\n" +
$"Classpath-Lib: {libDir}");
} }
} }
return (true, javaExe, workDir, runtimeClasspath, null); return (true, javaExe, workDir, runtimeClasspath, null);
} }
private bool TryResolvePrebuiltTool(out string? workDir, out string? classpathEntry, out string? error) private bool TryResolvePrebuiltTool(
int runtimeMajor,
out string? workDir,
out string? classpathEntry,
out string? error)
{ {
workDir = null; workDir = null;
classpathEntry = null; classpathEntry = null;
@@ -300,27 +319,133 @@ public sealed class SonicMfApiExecutor
foreach (string dir in searchDirs.Where(Directory.Exists)) foreach (string dir in searchDirs.Where(Directory.Exists))
{ {
string jar = Path.Combine(dir, "SonicMfContainerTool.jar");
if (File.Exists(jar))
{
workDir = dir;
classpathEntry = jar;
return true;
}
string classFile = Path.Combine(dir, "SonicMfContainerTool.class"); string classFile = Path.Combine(dir, "SonicMfContainerTool.class");
if (File.Exists(classFile)) if (File.Exists(classFile))
{
int? major = ReadClassFileMajorVersion(classFile);
if (major is not null && major <= runtimeMajor)
{ {
workDir = dir; workDir = dir;
classpathEntry = dir; classpathEntry = dir;
return true; return true;
} }
error =
$"Tools\\SonicMfContainerTool.class ist zu neu (major={major}, braucht <={runtimeMajor} / Java 8).";
continue;
} }
error = "Tools\\SonicMfContainerTool.jar/.class nicht gefunden."; string jar = Path.Combine(dir, "SonicMfContainerTool.jar");
if (File.Exists(jar))
{
// JAR ohne Version-Check nur wenn Runtime >= 52; bei Unsicherheit neu kompilieren.
if (runtimeMajor >= 52)
{
workDir = dir;
classpathEntry = jar;
return true;
}
}
}
error ??= "Tools\\SonicMfContainerTool.class (Java 8) nicht gefunden.";
return false; 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 52; // Sonic-Default Java 8
}
string err = await p.StandardError.ReadToEndAsync(cancellationToken);
string output = await p.StandardOutput.ReadToEndAsync(cancellationToken);
await p.WaitForExitAsync(cancellationToken);
string text = (err + "\n" + output).ToLowerInvariant();
if (text.Contains("version \"1.8") || text.Contains("version \"8"))
{
return 52;
}
Match m = Regex.Match(text, @"version ""(\d+)");
if (m.Success && int.TryParse(m.Groups[1].Value, out int major) && major >= 9)
{
// Java 9+ class major = 44 + version => 9→53, 11→55, 17→61, 21→65
return 44 + major;
}
}
catch
{
// ignore
}
return 52;
}
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() private (string? Dir, string? Error) ResolveLibDirectoryDetailed()
{ {
List<(string Path, string Label)> candidates = []; List<(string Path, string Label)> candidates = [];
@@ -1,33 +1,26 @@
import java.lang.reflect.Method;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.Iterator; import java.util.Iterator;
import java.util.Set; import java.util.Set;
import javax.management.ObjectName; import javax.management.ObjectName;
import com.sonicsw.mf.jmx.client.JMSConnectorAddress;
import com.sonicsw.mf.jmx.client.JMSConnectorClient;
import com.sonicsw.mf.mgmtapi.runtime.IAgentProxy;
import com.sonicsw.mf.mgmtapi.runtime.MFProxyFactory;
/** /**
* Sonic MF Management Runtime API helper (Aurea CX Messenger / Progress Sonic). * Sonic Management Application API helper (same restart as SMC).
* *
* Documented path (Management Application API): * Uses reflection so this file compiles with Java 8 without Sonic jars on the
* Hashtable env with ConnectionURLs / DefaultUser / DefaultPassword * compile classpath. At runtime SonicHome\\lib must provide:
* mgmt_client / mfcontext / sonic_Client (etc.)
*
* Flow (CX Messenger Management Application API):
* Hashtable ConnectionURLs/DefaultUser/DefaultPassword
* -> JMSConnectorAddress / JMSConnectorClient.connect * -> JMSConnectorAddress / JMSConnectorClient.connect
* -> MFProxyFactory.createAgentProxy(connector, ObjectName) * -> MFProxyFactory.createAgentProxy(connector, ObjectName)
* -> IAgentProxy.restart() (same lifecycle action as SMC Restart) * -> IAgentProxy.restart()
* *
* ObjectName pattern: {domain}.{container}:ID=AGENT * ObjectName: {domain}.{container}:ID=AGENT
* *
* Usage: * Compile: javac --release 8 SonicMfContainerTool.java
* java -cp "...libs...;." SonicMfContainerTool ping|list|restart
* --domain proalpha-test
* --url tcp://host:13070
* --user Administrator
* --password *** (or env ESB_SONIC_PASSWORD)
* [--container DE-Test] (required for restart)
* [--timeout 120]
*/ */
public final class SonicMfContainerTool public final class SonicMfContainerTool
{ {
@@ -55,7 +48,7 @@ public final class SonicMfContainerTool
} }
long timeoutMs = Math.max(5_000L, a.timeoutSec * 1000L); long timeoutMs = Math.max(5_000L, a.timeoutSec * 1000L);
JMSConnectorClient connector = connect(a.url, a.user, a.password, timeoutMs); Object connector = connect(a.url, a.user, a.password, timeoutMs);
try try
{ {
if ("ping".equals(a.command)) if ("ping".equals(a.command))
@@ -77,16 +70,28 @@ public final class SonicMfContainerTool
} }
finally finally
{ {
try { connector.disconnect(); } catch (Exception ignore) { /* ignore */ } try
{
connector.getClass().getMethod("disconnect").invoke(connector);
}
catch (Exception ignore)
{
/* ignore */
}
} }
} }
catch (Throwable t) catch (Throwable t)
{ {
fail(t.getClass().getSimpleName() + ": " + t.getMessage()); Throwable root = t;
while (root.getCause() != null && root.getCause() != root)
{
root = root.getCause();
}
fail(root.getClass().getName() + ": " + root.getMessage());
} }
} }
private static JMSConnectorClient connect(String url, String user, String password, long timeoutMs) private static Object connect(String url, String user, String password, long timeoutMs)
throws Exception throws Exception
{ {
Hashtable env = new Hashtable(); Hashtable env = new Hashtable();
@@ -94,23 +99,59 @@ public final class SonicMfContainerTool
env.put("DefaultUser", user); env.put("DefaultUser", user);
env.put("DefaultPassword", password == null ? "" : password); env.put("DefaultPassword", password == null ? "" : password);
JMSConnectorAddress address = new JMSConnectorAddress(env); Class addressCl = Class.forName("com.sonicsw.mf.jmx.client.JMSConnectorAddress");
JMSConnectorClient connector = new JMSConnectorClient(); Object address = addressCl.getConstructor(new Class[] { Hashtable.class }).newInstance(new Object[] { env });
connector.connect(address, timeoutMs);
Class clientCl = Class.forName("com.sonicsw.mf.jmx.client.JMSConnectorClient");
Object connector = clientCl.getConstructor(new Class[] {}).newInstance(new Object[] {});
Method connect = null;
Method[] methods = clientCl.getMethods();
for (int i = 0; i < methods.length; i++)
{
Method m = methods[i];
if ("connect".equals(m.getName()) && m.getParameterTypes().length == 2)
{
connect = m;
break;
}
}
if (connect == null)
{
fail("JMSConnectorClient.connect(address, timeout) nicht gefunden.");
return null;
}
Class[] pts = connect.getParameterTypes();
Object timeoutArg;
if (pts[1] == Long.TYPE || pts[1] == Long.class)
{
timeoutArg = Long.valueOf(timeoutMs);
}
else if (pts[1] == Integer.TYPE || pts[1] == Integer.class)
{
timeoutArg = Integer.valueOf((int) Math.min(Integer.MAX_VALUE, timeoutMs));
}
else
{
timeoutArg = Long.valueOf(timeoutMs);
}
connect.invoke(connector, new Object[] { address, timeoutArg });
System.out.println("INFO:Connected url=" + url + " user=" + user); System.out.println("INFO:Connected url=" + url + " user=" + user);
return connector; return connector;
} }
private static void ping(JMSConnectorClient connector, String domain) throws Exception private static void ping(Object connector, String domain) throws Exception
{ {
Set names = connector.queryNames(new ObjectName("*:ID=AGENT"), null); Set names = queryAgents(connector);
int matched = 0; int matched = 0;
if (names != null) if (names != null)
{ {
for (Iterator it = names.iterator(); it.hasNext(); ) for (Iterator it = names.iterator(); it.hasNext(); )
{ {
ObjectName on = (ObjectName) it.next(); ObjectName on = (ObjectName) it.next();
if (belongsToDomain(on, domain)) if (extractContainer(on, domain) != null)
{ {
matched++; matched++;
} }
@@ -119,9 +160,9 @@ public final class SonicMfContainerTool
System.out.println("OK:MfApiPing domain=" + domain + " agents=" + matched); System.out.println("OK:MfApiPing domain=" + domain + " agents=" + matched);
} }
private static void list(JMSConnectorClient connector, String domain) throws Exception private static void list(Object connector, String domain) throws Exception
{ {
Set names = connector.queryNames(new ObjectName("*:ID=AGENT"), null); Set names = queryAgents(connector);
int count = 0; int count = 0;
if (names != null) if (names != null)
{ {
@@ -143,32 +184,49 @@ public final class SonicMfContainerTool
System.out.println("OK:List count=" + count); System.out.println("OK:List count=" + count);
} }
private static void restart(JMSConnectorClient connector, String domain, String container) throws Exception private static void restart(Object connector, String domain, String container) throws Exception
{ {
String shortName = shortContainer(container); String shortName = shortContainer(container);
ObjectName on = resolveAgentObjectName(connector, domain, shortName); ObjectName on = resolveAgentObjectName(connector, domain, shortName);
System.out.println("INFO:ObjectName=" + on); System.out.println("INFO:ObjectName=" + on);
IAgentProxy agent = MFProxyFactory.createAgentProxy(connector, on); Class factoryCl = Class.forName("com.sonicsw.mf.mgmtapi.runtime.MFProxyFactory");
Method create = factoryCl.getMethod("createAgentProxy", new Class[] {
Class.forName("com.sonicsw.mf.jmx.client.JMSConnectorClient"),
ObjectName.class
});
Object agent = create.invoke(null, new Object[] { connector, on });
String stateBefore = safeState(agent); String stateBefore = safeState(agent);
System.out.println("INFO:StateBefore=" + stateBefore); System.out.println("INFO:StateBefore=" + stateBefore);
agent.restart(); agent.getClass().getMethod("restart").invoke(agent);
System.out.println("OK:RestartInvoked container=" + shortName + " domain=" + domain System.out.println("OK:RestartInvoked container=" + shortName + " domain=" + domain
+ " stateBefore=" + stateBefore); + " stateBefore=" + stateBefore);
} }
private static ObjectName resolveAgentObjectName(JMSConnectorClient connector, String domain, String container) private static Set queryAgents(Object connector) throws Exception
{
Method query = connector.getClass().getMethod("queryNames", new Class[] {
ObjectName.class, javax.management.QueryExp.class
});
return (Set) query.invoke(connector, new Object[] { new ObjectName("*:ID=AGENT"), null });
}
private static ObjectName resolveAgentObjectName(Object connector, String domain, String container)
throws Exception throws Exception
{ {
ObjectName preferred = new ObjectName(domain + "." + container + ":ID=AGENT"); ObjectName preferred = new ObjectName(domain + "." + container + ":ID=AGENT");
Set exact = connector.queryNames(preferred, null); Method query = connector.getClass().getMethod("queryNames", new Class[] {
ObjectName.class, javax.management.QueryExp.class
});
Set exact = (Set) query.invoke(connector, new Object[] { preferred, null });
if (exact != null && !exact.isEmpty()) if (exact != null && !exact.isEmpty())
{ {
return (ObjectName) exact.iterator().next(); return (ObjectName) exact.iterator().next();
} }
Set names = connector.queryNames(new ObjectName("*:ID=AGENT"), null); Set names = queryAgents(connector);
if (names != null) if (names != null)
{ {
for (Iterator it = names.iterator(); it.hasNext(); ) for (Iterator it = names.iterator(); it.hasNext(); )
@@ -182,15 +240,9 @@ public final class SonicMfContainerTool
} }
} }
// Fallback: documented canonical name even if not yet in query result
return preferred; return preferred;
} }
private static boolean belongsToDomain(ObjectName on, String domain)
{
return extractContainer(on, domain) != null;
}
private static String extractContainer(ObjectName on, String domain) private static String extractContainer(ObjectName on, String domain)
{ {
if (on == null || isBlank(domain)) if (on == null || isBlank(domain))
@@ -224,12 +276,12 @@ public final class SonicMfContainerTool
return container; return container;
} }
private static String safeState(IAgentProxy agent) private static String safeState(Object agent)
{ {
try try
{ {
String s = agent.getStateString(); Object s = agent.getClass().getMethod("getStateString").invoke(agent);
return s == null ? "?" : s; return s == null ? "?" : String.valueOf(s);
} }
catch (Throwable t) catch (Throwable t)
{ {
@@ -282,7 +334,7 @@ public final class SonicMfContainerTool
else if ("--container".equals(key)) a.container = val; else if ("--container".equals(key)) a.container = val;
else if ("--timeout".equals(key)) else if ("--timeout".equals(key))
{ {
try { a.timeoutSec = Integer.parseInt(val); } catch (Exception ignore) { /* keep default */ } try { a.timeoutSec = Integer.parseInt(val); } catch (Exception ignore) { /* keep */ }
} }
} }