From eacc1fe2ae87e828a4baf4351c256c0609a5abca Mon Sep 17 00:00:00 2001 From: "Karl E. Nelson" Date: Wed, 27 Nov 2024 10:03:44 -0800 Subject: [PATCH 01/36] Work on module support --- jpype/_core.py | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/jpype/_core.py b/jpype/_core.py index 096c7a461..6ba934a2d 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -109,13 +109,15 @@ def isJVMStarted(): return _jpype.isStarted() -def _getOldClassPath(args) -> list[str]: - for i in args: - if not isinstance(i, str): +def _getOption(args, var, sep) -> list[str]: + """ Get an option and remove it from the current jvm arguments list """ + for i,v in enumerate(args): + if not isinstance(v, str): continue - _, _, classpath = i.partition('-Djava.class.path=') - if classpath: - return classpath.split(_classpath._SEP) + _, _, value = v.partition('%s='%s) + if value: + del args[i] + return value.split(sep) return [] @@ -160,10 +162,6 @@ def _handleClassPath( return out -def _removeClassPath(args) -> tuple[str]: - return tuple(arg for arg in args if not str(arg).startswith("-Djava.class.path")) - - _JVM_started = False @@ -175,6 +173,7 @@ def startJVM( *jvmargs: str, jvmpath: typing.Optional[_PathOrStr] = None, classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, + modulepath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, ignoreUnrecognized: bool = False, convertStrings: bool = False, interrupt: bool = not interactive(), @@ -197,6 +196,7 @@ def startJVM( classpath (str, PathLike, [str, PathLike]): Set the classpath for the JVM. This will override any classpath supplied in the arguments list. A value of None will give no classpath to JVM. + modulepath (str, PathList, [str, PathList]): Set the module path for JVM. ignoreUnrecognized (bool): Option to ignore invalid JVM arguments. Default is False. convertStrings (bool): Option to force Java strings to @@ -224,7 +224,6 @@ def startJVM( has_classloader = _hasSystemClassLoader(jvmargs) - # JVM path if jvmargs: # jvm is the first argument the first argument is a path or None @@ -240,8 +239,11 @@ def startJVM( # Allow the path to be a PathLike. jvmpath = os.fspath(jvmpath) + # Handle strings and list of strings. + extra_jvm_args: list[str] = [] + # Classpath handling - old_classpath = _getOldClassPath(jvmargs) + old_classpath = _getOption(jvmargs, "-Djava.class.path", _classpath._SEP) if old_classpath: # Old style, specified in the arguments if classpath is not None: @@ -252,9 +254,19 @@ def startJVM( # Not specified at all, use the default classpath. classpath = _classpath.getClassPath() - # Handle strings and list of strings. - extra_jvm_args: list[str] = [] + # Modulepath handling + old_modulepath = _getOption(jvmargs, "--module-path", _classpath._SEP) + if old_modulepath: + # Old style, specified in the arguments + if modulepath is not None: + # Cannot apply both styles, conflict + raise TypeError('modulepath specified twice') + modulepath = old_modulepath + if modulepath is not None: + mp = _classpath._SEP.join(_handleClassPath(modulepath)) + extra_jvm_args += ['--module-path=%s'%mp ] + # Get the support library supportLib = os.path.join(os.path.dirname(os.path.dirname(__file__)), "org.jpype.jar") if not os.path.exists(supportLib): raise RuntimeError("Unable to find org.jpype.jar support library at " + supportLib) @@ -265,7 +277,6 @@ def startJVM( if cp.isascii(): # no problems extra_jvm_args += ['-Djava.class.path=%s'%cp ] - jvmargs = _removeClassPath(jvmargs) late_load = False elif has_classloader: # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 @@ -277,7 +288,6 @@ def startJVM( '-Djava.system.class.loader=org.jpype.classloader.JpypeSystemClassLoader', '-Djava.class.path=%s'%supportLib ] - jvmargs = _removeClassPath(jvmargs) else: # We are screwed no matter what we try or do. # Unfortunately the jdk maintainers don't seem to care either. @@ -295,7 +305,7 @@ def startJVM( # Keep the current locale settings, else Java will replace them. prior = [locale.getlocale(i) for i in categories] # Start the JVM - _jpype.startup(jvmpath, jvmargs + tuple(extra_jvm_args), + _jpype.startup(jvmpath, tuple(jvmargs + extra_jvm_args), ignoreUnrecognized, convertStrings, interrupt) # Collect required resources for operation initializeResources() From 3ef37ba0a83b4d097ad5107e4c8c3a4fc2b7814b Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 11:32:28 -0800 Subject: [PATCH 02/36] Remove agent logic trying to resolve non ascii --- doc/CHANGELOG.rst | 3 + jpype/_core.py | 117 +++++++++++++++++--------------- test/jpypetest/subrun.py | 15 ++-- test/jpypetest/test_coverage.py | 4 +- 4 files changed, 76 insertions(+), 63 deletions(-) diff --git a/doc/CHANGELOG.rst b/doc/CHANGELOG.rst index 9ea7594c1..0b56924f8 100644 --- a/doc/CHANGELOG.rst +++ b/doc/CHANGELOG.rst @@ -5,6 +5,9 @@ This changelog *only* contains changes from the *first* pypi release (0.5.4.3) o Latest Changes: - **1.5.2.dev0 - 2024-11-18** + + - Roll back agent change due to misbehaving JVM installs. + - **1.5.1 - 2024-11-09** - Future proofing for Python 3.14 diff --git a/jpype/_core.py b/jpype/_core.py index 6ba934a2d..23e9088fd 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -109,26 +109,21 @@ def isJVMStarted(): return _jpype.isStarted() -def _getOption(args, var, sep) -> list[str]: +def _getOption(args, var, sep=None, keep=False) -> list[str]: """ Get an option and remove it from the current jvm arguments list """ for i,v in enumerate(args): if not isinstance(v, str): continue - _, _, value = v.partition('%s='%s) + _, _, value = v.partition('%s='%var) if value: - del args[i] - return value.split(sep) + if not keep: + del args[i] + if sep is not None: + return value.split(sep) + return value return [] - -def _hasSystemClassLoader(args) -> bool: - for i in args: - if isinstance(i, str) and i.startswith('-Djava.system.class.loader'): - return True - return False - - -def _handleClassPath( +def _expandClassPath( classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None ) -> typing.Sequence[str]: """ @@ -177,6 +172,7 @@ def startJVM( ignoreUnrecognized: bool = False, convertStrings: bool = False, interrupt: bool = not interactive(), + agent: bool = False, ) -> None: """ Starts a Java Virtual Machine. Without options it will start @@ -196,7 +192,6 @@ def startJVM( classpath (str, PathLike, [str, PathLike]): Set the classpath for the JVM. This will override any classpath supplied in the arguments list. A value of None will give no classpath to JVM. - modulepath (str, PathList, [str, PathList]): Set the module path for JVM. ignoreUnrecognized (bool): Option to ignore invalid JVM arguments. Default is False. convertStrings (bool): Option to force Java strings to @@ -210,6 +205,9 @@ def startJVM( transfer control to Python rather than halting. If not specified will be False if Python is started as an interactive shell. + agent (bool): Start as agent. This allows for certain + privilaged operations, but required java.instrumentation + module. Raises: OSError: if the JVM cannot be started or is already running. @@ -222,7 +220,8 @@ def startJVM( if _JVM_started: raise OSError('JVM cannot be restarted') - has_classloader = _hasSystemClassLoader(jvmargs) + # Convert to list + jvmargs = list(jvmargs) # JVM path if jvmargs: @@ -254,49 +253,55 @@ def startJVM( # Not specified at all, use the default classpath. classpath = _classpath.getClassPath() - # Modulepath handling - old_modulepath = _getOption(jvmargs, "--module-path", _classpath._SEP) - if old_modulepath: - # Old style, specified in the arguments - if modulepath is not None: - # Cannot apply both styles, conflict - raise TypeError('modulepath specified twice') - modulepath = old_modulepath - if modulepath is not None: - mp = _classpath._SEP.join(_handleClassPath(modulepath)) - extra_jvm_args += ['--module-path=%s'%mp ] +# Code for 1.6 release when we add module support +# # Modulepath handling +# old_modulepath = _getOption(jvmargs, "--module-path", _classpath._SEP) +# if old_modulepath: +# # Old style, specified in the arguments +# if modulepath is not None: +# # Cannot apply both styles, conflict +# raise TypeError('modulepath specified twice') +# modulepath = old_modulepath +# if modulepath is not None: +# mp = _classpath._SEP.join(_expandClassPath(modulepath)) +# extra_jvm_args += ['--module-path=%s'%mp ] # Get the support library - supportLib = os.path.join(os.path.dirname(os.path.dirname(__file__)), "org.jpype.jar") - if not os.path.exists(supportLib): - raise RuntimeError("Unable to find org.jpype.jar support library at " + supportLib) - - late_load = not has_classloader - if classpath: - cp = _classpath._SEP.join(_handleClassPath(classpath)) - if cp.isascii(): - # no problems - extra_jvm_args += ['-Djava.class.path=%s'%cp ] - late_load = False - elif has_classloader: - # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 - raise ValueError("system classloader cannot be specified with non ascii characters in the classpath") - elif supportLib.isascii(): - # ok, setup the jpype system classloader and add to the path after startup - # this guarentees all classes have the same permissions as they did in the past - extra_jvm_args += [ - '-Djava.system.class.loader=org.jpype.classloader.JpypeSystemClassLoader', - '-Djava.class.path=%s'%supportLib - ] - else: - # We are screwed no matter what we try or do. - # Unfortunately the jdk maintainers don't seem to care either. - # This bug is almost 10 years old and spans 16 jdk versions and counting. - # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 - raise ValueError("jpype jar must be ascii to add to the system class path") - + support_lib = os.path.join(os.path.dirname(os.path.dirname(__file__)), "org.jpype.jar") + if not os.path.exists(support_lib): + raise RuntimeError("Unable to find org.jpype.jar support library at " + support_lib) + + system_class_loader = _getOption(jvmargs, "-Djava.system.class.loader", keep=True) + late_load = not system_class_loader + + java_class_path = _expandClassPath(classpath) + java_class_path.append(support_lib) + java_class_path = _classpath._SEP.join(java_class_path) + + # Make sure our module is always on the classpath + if java_class_path.isascii(): + # no problems + extra_jvm_args += ['-Djava.class.path=%s'%java_class_path ] + late_load = False + elif system_class_loader: + # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 + raise ValueError("system classloader cannot be specified with non ascii characters in the classpath") + elif support_lib.isascii(): + # ok, setup the jpype system classloader and add to the path after startup + # this guarentees all classes have the same permissions as they did in the past + extra_jvm_args += [ + '-Djava.system.class.loader=org.jpype.classloader.JpypeSystemClassLoader', + '-Djava.class.path=%s'%support_lib + ] + else: + # We are screwed no matter what we try or do. + # Unfortunately the jdk maintainers don't seem to care either. + # This bug is almost 10 years old and spans 16 jdk versions and counting. + # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 + raise ValueError("jpype jar must be ascii to add to the system class path") - extra_jvm_args += ['-javaagent:' + supportLib] + if agent: + extra_jvm_args += ['-javaagent:' + support_lib] try: import locale @@ -339,7 +344,7 @@ def startJVM( if late_load and classpath: # now we can add to the system classpath cl = _jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - for cp in _handleClassPath(classpath): + for cp in _expandClassPath(classpath): cl.addPath(_jpype._java_lang_String(cp)) diff --git a/test/jpypetest/subrun.py b/test/jpypetest/subrun.py index f0a24f787..41a51f181 100644 --- a/test/jpypetest/subrun.py +++ b/test/jpypetest/subrun.py @@ -23,6 +23,8 @@ import queue import unittest import common +from contextlib import redirect_stdout +import io _modules = {} # type: ignore[var-annotated] @@ -51,14 +53,17 @@ def _execute(inQueue, outQueue): ret = None (func_name, func_file, args, kwargs) = datum try: - module = _import(func_file) - func = getattr(module, func_name) - ret = func(*args, **kwargs) + f = io.StringIO() + with redirect_stdout(f): + module = _import(func_file) + func = getattr(module, func_name) + ret = func(*args, **kwargs) except Exception as ex1: traceback.print_exc() ex = ex1 + out = f.getvalue() # This may fail if we get a Java exception so timeout is used - outQueue.put([ret, ex]) + outQueue.put([ret, out, ex]) class Client(object): @@ -77,7 +82,7 @@ def execute(self, function, *args, **kwargs): self.inQueue.put([function.__name__, os.path.abspath( inspect.getfile(function)), args, kwargs]) try: - (ret, ex) = self.outQueue.get(True, self.timeout) + (ret, out, ex) = self.outQueue.get(True, self.timeout) except queue.Empty: raise AssertionError("function {func} FAILED with args: {args} and kwargs: {kwargs}" .format(func=function, args=args, kwargs=kwargs)) diff --git a/test/jpypetest/test_coverage.py b/test/jpypetest/test_coverage.py index f981caa95..6921f80ee 100644 --- a/test/jpypetest/test_coverage.py +++ b/test/jpypetest/test_coverage.py @@ -49,8 +49,8 @@ def testWin32(self): def testHandleClassPath(self): with self.assertRaises(TypeError): - jpype._core._handleClassPath([1]) - jpype._core._handleClassPath(["./*.jar"]) + jpype._core._expandClassPath([1]) + jpype._core._expandClassPath(["./*.jar"]) def testRestart(self): with self.assertRaises(OSError): From 4766f1225adc0e90673f2942444e6b3ecf24e979 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 12:37:40 -0800 Subject: [PATCH 03/36] Cleanup and work for nonascii friends --- jpype/_core.py | 32 ++-- .../jpype/classloader/DynamicClassLoader.java | 36 +++-- .../jpype/classloader/JPypeClassLoader.java | 149 ------------------ .../classloader/JpypeSystemClassLoader.java | 43 ----- test/jpypetest/test_startup.py | 6 +- 5 files changed, 48 insertions(+), 218 deletions(-) delete mode 100644 native/java/org/jpype/classloader/JPypeClassLoader.java delete mode 100644 native/java/org/jpype/classloader/JpypeSystemClassLoader.java diff --git a/jpype/_core.py b/jpype/_core.py index 23e9088fd..55cf92448 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -168,7 +168,6 @@ def startJVM( *jvmargs: str, jvmpath: typing.Optional[_PathOrStr] = None, classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, - modulepath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, ignoreUnrecognized: bool = False, convertStrings: bool = False, interrupt: bool = not interactive(), @@ -214,6 +213,10 @@ def startJVM( TypeError: if a keyword argument conflicts with the positional arguments. """ + +# Code for 1.6 +# modulepath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, + if _jpype.isStarted(): raise OSError('JVM is already started') global _JVM_started @@ -286,19 +289,19 @@ def startJVM( elif system_class_loader: # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 raise ValueError("system classloader cannot be specified with non ascii characters in the classpath") - elif support_lib.isascii(): - # ok, setup the jpype system classloader and add to the path after startup - # this guarentees all classes have the same permissions as they did in the past - extra_jvm_args += [ - '-Djava.system.class.loader=org.jpype.classloader.JpypeSystemClassLoader', - '-Djava.class.path=%s'%support_lib - ] else: - # We are screwed no matter what we try or do. - # Unfortunately the jdk maintainers don't seem to care either. - # This bug is almost 10 years old and spans 16 jdk versions and counting. - # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 - raise ValueError("jpype jar must be ascii to add to the system class path") + if not support_lib.isascii(): + import tempfile + import shutil + tmp = tempfile.gettempdir() + if not tmp.isascii(): + raise ValueError("Unable to create ascii temp directory. Clear TEMPDIR, TEMP, and TMP environment variables") + support_lib2 = os.path.join(tmp, "org.jpype.jar") + shutil.copyfile(support_lib, support_lib2) + support_lib = support_lib2 + + extra_jvm_args += ['-Djava.system.class.loader=org.jpype.classloader.DynamicClassLoader' ] + extra_jvm_args += ['-Djava.class.path=%s'%os.path.join(tmp, "org.jpype.jar") ] if agent: extra_jvm_args += ['-javaagent:' + support_lib] @@ -344,8 +347,9 @@ def startJVM( if late_load and classpath: # now we can add to the system classpath cl = _jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() + from pathlib import Path for cp in _expandClassPath(classpath): - cl.addPath(_jpype._java_lang_String(cp)) + cl.addFile(Path(cp)) def initializeResources(): diff --git a/native/java/org/jpype/classloader/DynamicClassLoader.java b/native/java/org/jpype/classloader/DynamicClassLoader.java index 2192391e9..c2e9365c5 100644 --- a/native/java/org/jpype/classloader/DynamicClassLoader.java +++ b/native/java/org/jpype/classloader/DynamicClassLoader.java @@ -14,6 +14,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; +import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; @@ -26,7 +27,7 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; -public class DynamicClassLoader extends ClassLoader +public class DynamicClassLoader extends URLClassLoader { List loaders = new LinkedList<>(); @@ -34,7 +35,20 @@ public class DynamicClassLoader extends ClassLoader public DynamicClassLoader(ClassLoader parent) { - super(parent); + super(new URL[0], parent); + } + + + public void addFile(String path) throws Throwable + { + addURL(Paths.get(path).toAbsolutePath().toUri().toURL()); + } + + // this is required to add a Java agent even if it is already in the path + @SuppressWarnings("unused") + private void appendToClassPathForInstrumentation(String path) throws Throwable + { + addURL(Paths.get(path).toAbsolutePath().toUri().toURL()); } public int getCode() @@ -77,7 +91,7 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) } }); - loaders.add(new URLClassLoader(urls.toArray(new URL[urls.size()]))); + loaders.add(new URLClassLoader(urls.toArray(new URL[0]))); } public void addFile(Path path) throws FileNotFoundException @@ -120,7 +134,7 @@ public Class findClass(String name) throws ClassNotFoundException, ClassFormatEr try { URLConnection connection = url.openConnection(); - try ( InputStream is = connection.getInputStream()) + try (InputStream is = connection.getInputStream()) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int bytes; @@ -146,6 +160,11 @@ public URL getResource(String name) URL url = this.getParent().getResource(name); if (url != null) return url; + + url = super.getResource(name); + if (url != null) + return url; + for (ClassLoader cl : this.loaders) { url = cl.getResource(name); @@ -164,12 +183,11 @@ public URL getResource(String name) public Enumeration getResources(String name) throws IOException { ArrayList out = new ArrayList<>(); - Enumeration urls = getParent().getResources(name); - out.addAll(Collections.list(urls)); + out.addAll(Collections.list(getParent().getResources(name))); + out.addAll(Collections.list(super.getResources(name))); for (URLClassLoader cl : this.loaders) { - urls = cl.findResources(name); - out.addAll(Collections.list(urls)); + out.addAll(Collections.list(cl.findResources(name))); } // Both with and without / should generate the same result if (name.endsWith("/")) @@ -201,7 +219,7 @@ public void scanJar(Path p1) return; if (Files.isDirectory(p1)) return; - try ( JarFile jf = new JarFile(p1.toFile())) + try (JarFile jf = new JarFile(p1.toFile())) { Enumeration entries = jf.entries(); URI abs = p1.toAbsolutePath().toUri(); diff --git a/native/java/org/jpype/classloader/JPypeClassLoader.java b/native/java/org/jpype/classloader/JPypeClassLoader.java deleted file mode 100644 index 21e5268bb..000000000 --- a/native/java/org/jpype/classloader/JPypeClassLoader.java +++ /dev/null @@ -1,149 +0,0 @@ -/* **************************************************************************** - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - See NOTICE file for details. -**************************************************************************** */ -package org.jpype.classloader; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.TreeMap; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; - -/** - * Specialized class loader for JPype resources. - *

- * Loader to convert the internally stored resources into java classes. This - * prevents class load order problems when there are class dependencies. - *

- */ -public class JPypeClassLoader extends ClassLoader -{ - - static private JPypeClassLoader instance; - private TreeMap map = new TreeMap<>(); - - /** - * Get the class loader. - * - * @return the singleton class loader. - */ - public static JPypeClassLoader getInstance() - { - if (instance == null) - { - JPypeClassLoader.instance = new JPypeClassLoader(getSystemClassLoader()); - } - return instance; - } - - private JPypeClassLoader(ClassLoader parent) - { - super(parent); - } - - /** - * Add a class to the class loader. - *

- * This can be called from within python to add a class to the Java JVM. - * - * @param name is the name of the class. - * @param code is the byte code. - */ - public void importClass(String name, byte[] code) - { - map.put(name, code); - } - - /** - * Import a jar from memory into the class loader. - *

- * Does not handle unknown jar entry lengths. - * - * @param bytes - */ - public void importJar(byte[] bytes) - { - try (JarInputStream is = new JarInputStream(new ByteArrayInputStream(bytes))) - { - while (true) - { - JarEntry nextEntry = is.getNextJarEntry(); - if (nextEntry == null) - break; - - // Skip directories and other non-class resources - long size = nextEntry.getSize(); - if (size == 0) - continue; - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - int q; - while ((q = is.read()) != -1) - baos.write(q); - byte[] data = baos.toByteArray(); - - // Store all classes we find - String name = nextEntry.getName(); - importClass(name, data); - } - } catch (IOException ex) - { - throw new RuntimeException(ex); - } - } - - /** - * Loads a class from the class loader. - * - * @param name is the name of the class with java class notation (using dots). - * @return the class - * @throws ClassNotFoundException was not found by the class loader. - * @throws ClassFormatError if the class byte code was invalid. - */ - @Override - public Class findClass(String name) throws ClassNotFoundException, ClassFormatError - { - String mname = name.replace('.', '/') + ".class"; - byte[] data = map.get(mname); - if (data == null) - { - // Call the default implementation, throws ClassNotFoundException - return super.findClass(name); - } - - Class cls = defineClass(name, data, 0, data.length); - if (cls == null) - throw new ClassFormatError("Class load was null"); - return cls; - } - - /** - * Overload for thunk resources. - * - * @param s - * @return - */ - @Override - public InputStream getResourceAsStream(String s) - { - if (this.map.containsKey(s)) - { - return new ByteArrayInputStream(this.map.get(s)); - } - return super.getResourceAsStream(s); - } -} diff --git a/native/java/org/jpype/classloader/JpypeSystemClassLoader.java b/native/java/org/jpype/classloader/JpypeSystemClassLoader.java deleted file mode 100644 index efa1853ff..000000000 --- a/native/java/org/jpype/classloader/JpypeSystemClassLoader.java +++ /dev/null @@ -1,43 +0,0 @@ -/* **************************************************************************** - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - See NOTICE file for details. -**************************************************************************** */ -package org.jpype.classloader; - -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Paths; - -public final class JpypeSystemClassLoader extends URLClassLoader { - - public JpypeSystemClassLoader(ClassLoader parent) throws Throwable { - super(new URL[0], parent); - } - - public void addPath(String path) throws Throwable { - addURL(Paths.get(path).toAbsolutePath().toUri().toURL()); - } - - public void addPaths(String[] paths) throws Throwable { - for (String path : paths) { - addPath(path); - } - } - - // this is required to add a Java agent even if it is already in the path - @SuppressWarnings("unused") - private void appendToClassPathForInstrumentation(String path) throws Throwable { - addPath(path); - } -} diff --git a/test/jpypetest/test_startup.py b/test/jpypetest/test_startup.py index 8dff5d4e1..0a3eadc00 100644 --- a/test/jpypetest/test_startup.py +++ b/test/jpypetest/test_startup.py @@ -152,7 +152,7 @@ def testNonASCIIPath(self): """ jpype.startJVM(jvmpath=Path(self.jvmpath), classpath="test/jar/unicode_à😎/sample_package.jar") cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.JpypeSystemClassLoader")) + self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) assert dir(jpype.JPackage('org.jpype.sample_package')) == ['A', 'B'] @@ -162,7 +162,7 @@ def testOldStyleNonASCIIPath(self): """ jpype.startJVM("-Djava.class.path=test/jar/unicode_à😎/sample_package.jar", jvmpath=Path(self.jvmpath)) cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.JpypeSystemClassLoader")) + self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) assert dir(jpype.JPackage('org.jpype.sample_package')) == ['A', 'B'] def testNonASCIIPathWithSystemClassLoader(self): @@ -207,7 +207,7 @@ def testDefaultSystemClassLoader(self): # we introduce no behavior change unless absolutely necessary jpype.startJVM(jvmpath=Path(self.jvmpath)) cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertNotEqual(type(cl), jpype.JClass("org.jpype.classloader.JpypeSystemClassLoader")) + self.assertNotEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) def testServiceWithNonASCIIPath(self): jpype.startJVM( From 6387988e4d01a15533da7da45d9482c7c4a7739d Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 12:40:46 -0800 Subject: [PATCH 04/36] Ready for merge --- jpype/_core.py | 9 +- .../jpype/classloader/DynamicClassLoader.java | 30 ++-- .../jpype/classloader/JPypeClassLoader.java | 149 ------------------ .../classloader/JpypeSystemClassLoader.java | 43 ----- test/jpypetest/test_startup.py | 6 +- 5 files changed, 30 insertions(+), 207 deletions(-) delete mode 100644 native/java/org/jpype/classloader/JPypeClassLoader.java delete mode 100644 native/java/org/jpype/classloader/JpypeSystemClassLoader.java diff --git a/jpype/_core.py b/jpype/_core.py index 23e9088fd..e9d09abad 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -168,7 +168,6 @@ def startJVM( *jvmargs: str, jvmpath: typing.Optional[_PathOrStr] = None, classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, - modulepath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, ignoreUnrecognized: bool = False, convertStrings: bool = False, interrupt: bool = not interactive(), @@ -214,6 +213,10 @@ def startJVM( TypeError: if a keyword argument conflicts with the positional arguments. """ + +# Code for 1.6 +# modulepath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, + if _jpype.isStarted(): raise OSError('JVM is already started') global _JVM_started @@ -290,7 +293,7 @@ def startJVM( # ok, setup the jpype system classloader and add to the path after startup # this guarentees all classes have the same permissions as they did in the past extra_jvm_args += [ - '-Djava.system.class.loader=org.jpype.classloader.JpypeSystemClassLoader', + '-Djava.system.class.loader=org.jpype.classloader.DynamicClassLoader', '-Djava.class.path=%s'%support_lib ] else: @@ -345,7 +348,7 @@ def startJVM( # now we can add to the system classpath cl = _jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() for cp in _expandClassPath(classpath): - cl.addPath(_jpype._java_lang_String(cp)) + cl.addFile(_jpype._java_lang_String(cp)) def initializeResources(): diff --git a/native/java/org/jpype/classloader/DynamicClassLoader.java b/native/java/org/jpype/classloader/DynamicClassLoader.java index 2192391e9..8f3623508 100644 --- a/native/java/org/jpype/classloader/DynamicClassLoader.java +++ b/native/java/org/jpype/classloader/DynamicClassLoader.java @@ -14,6 +14,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; +import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; @@ -26,7 +27,7 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; -public class DynamicClassLoader extends ClassLoader +public class DynamicClassLoader extends URLClassLoader { List loaders = new LinkedList<>(); @@ -34,7 +35,14 @@ public class DynamicClassLoader extends ClassLoader public DynamicClassLoader(ClassLoader parent) { - super(parent); + super(new URL[0], parent); + } + + // this is required to add a Java agent even if it is already in the path + @SuppressWarnings("unused") + private void appendToClassPathForInstrumentation(String path) throws Throwable + { + addURL(Paths.get(path).toAbsolutePath().toUri().toURL()); } public int getCode() @@ -77,7 +85,7 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) } }); - loaders.add(new URLClassLoader(urls.toArray(new URL[urls.size()]))); + loaders.add(new URLClassLoader(urls.toArray(new URL[0]))); } public void addFile(Path path) throws FileNotFoundException @@ -120,7 +128,7 @@ public Class findClass(String name) throws ClassNotFoundException, ClassFormatEr try { URLConnection connection = url.openConnection(); - try ( InputStream is = connection.getInputStream()) + try (InputStream is = connection.getInputStream()) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int bytes; @@ -146,6 +154,11 @@ public URL getResource(String name) URL url = this.getParent().getResource(name); if (url != null) return url; + + url = super.getResource(name); + if (url != null) + return url; + for (ClassLoader cl : this.loaders) { url = cl.getResource(name); @@ -164,12 +177,11 @@ public URL getResource(String name) public Enumeration getResources(String name) throws IOException { ArrayList out = new ArrayList<>(); - Enumeration urls = getParent().getResources(name); - out.addAll(Collections.list(urls)); + out.addAll(Collections.list(getParent().getResources(name))); + out.addAll(Collections.list(super.getResources(name))); for (URLClassLoader cl : this.loaders) { - urls = cl.findResources(name); - out.addAll(Collections.list(urls)); + out.addAll(Collections.list(cl.findResources(name))); } // Both with and without / should generate the same result if (name.endsWith("/")) @@ -201,7 +213,7 @@ public void scanJar(Path p1) return; if (Files.isDirectory(p1)) return; - try ( JarFile jf = new JarFile(p1.toFile())) + try (JarFile jf = new JarFile(p1.toFile())) { Enumeration entries = jf.entries(); URI abs = p1.toAbsolutePath().toUri(); diff --git a/native/java/org/jpype/classloader/JPypeClassLoader.java b/native/java/org/jpype/classloader/JPypeClassLoader.java deleted file mode 100644 index 21e5268bb..000000000 --- a/native/java/org/jpype/classloader/JPypeClassLoader.java +++ /dev/null @@ -1,149 +0,0 @@ -/* **************************************************************************** - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - See NOTICE file for details. -**************************************************************************** */ -package org.jpype.classloader; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.TreeMap; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; - -/** - * Specialized class loader for JPype resources. - *

- * Loader to convert the internally stored resources into java classes. This - * prevents class load order problems when there are class dependencies. - *

- */ -public class JPypeClassLoader extends ClassLoader -{ - - static private JPypeClassLoader instance; - private TreeMap map = new TreeMap<>(); - - /** - * Get the class loader. - * - * @return the singleton class loader. - */ - public static JPypeClassLoader getInstance() - { - if (instance == null) - { - JPypeClassLoader.instance = new JPypeClassLoader(getSystemClassLoader()); - } - return instance; - } - - private JPypeClassLoader(ClassLoader parent) - { - super(parent); - } - - /** - * Add a class to the class loader. - *

- * This can be called from within python to add a class to the Java JVM. - * - * @param name is the name of the class. - * @param code is the byte code. - */ - public void importClass(String name, byte[] code) - { - map.put(name, code); - } - - /** - * Import a jar from memory into the class loader. - *

- * Does not handle unknown jar entry lengths. - * - * @param bytes - */ - public void importJar(byte[] bytes) - { - try (JarInputStream is = new JarInputStream(new ByteArrayInputStream(bytes))) - { - while (true) - { - JarEntry nextEntry = is.getNextJarEntry(); - if (nextEntry == null) - break; - - // Skip directories and other non-class resources - long size = nextEntry.getSize(); - if (size == 0) - continue; - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - int q; - while ((q = is.read()) != -1) - baos.write(q); - byte[] data = baos.toByteArray(); - - // Store all classes we find - String name = nextEntry.getName(); - importClass(name, data); - } - } catch (IOException ex) - { - throw new RuntimeException(ex); - } - } - - /** - * Loads a class from the class loader. - * - * @param name is the name of the class with java class notation (using dots). - * @return the class - * @throws ClassNotFoundException was not found by the class loader. - * @throws ClassFormatError if the class byte code was invalid. - */ - @Override - public Class findClass(String name) throws ClassNotFoundException, ClassFormatError - { - String mname = name.replace('.', '/') + ".class"; - byte[] data = map.get(mname); - if (data == null) - { - // Call the default implementation, throws ClassNotFoundException - return super.findClass(name); - } - - Class cls = defineClass(name, data, 0, data.length); - if (cls == null) - throw new ClassFormatError("Class load was null"); - return cls; - } - - /** - * Overload for thunk resources. - * - * @param s - * @return - */ - @Override - public InputStream getResourceAsStream(String s) - { - if (this.map.containsKey(s)) - { - return new ByteArrayInputStream(this.map.get(s)); - } - return super.getResourceAsStream(s); - } -} diff --git a/native/java/org/jpype/classloader/JpypeSystemClassLoader.java b/native/java/org/jpype/classloader/JpypeSystemClassLoader.java deleted file mode 100644 index efa1853ff..000000000 --- a/native/java/org/jpype/classloader/JpypeSystemClassLoader.java +++ /dev/null @@ -1,43 +0,0 @@ -/* **************************************************************************** - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - See NOTICE file for details. -**************************************************************************** */ -package org.jpype.classloader; - -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Paths; - -public final class JpypeSystemClassLoader extends URLClassLoader { - - public JpypeSystemClassLoader(ClassLoader parent) throws Throwable { - super(new URL[0], parent); - } - - public void addPath(String path) throws Throwable { - addURL(Paths.get(path).toAbsolutePath().toUri().toURL()); - } - - public void addPaths(String[] paths) throws Throwable { - for (String path : paths) { - addPath(path); - } - } - - // this is required to add a Java agent even if it is already in the path - @SuppressWarnings("unused") - private void appendToClassPathForInstrumentation(String path) throws Throwable { - addPath(path); - } -} diff --git a/test/jpypetest/test_startup.py b/test/jpypetest/test_startup.py index 8dff5d4e1..0a3eadc00 100644 --- a/test/jpypetest/test_startup.py +++ b/test/jpypetest/test_startup.py @@ -152,7 +152,7 @@ def testNonASCIIPath(self): """ jpype.startJVM(jvmpath=Path(self.jvmpath), classpath="test/jar/unicode_à😎/sample_package.jar") cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.JpypeSystemClassLoader")) + self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) assert dir(jpype.JPackage('org.jpype.sample_package')) == ['A', 'B'] @@ -162,7 +162,7 @@ def testOldStyleNonASCIIPath(self): """ jpype.startJVM("-Djava.class.path=test/jar/unicode_à😎/sample_package.jar", jvmpath=Path(self.jvmpath)) cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.JpypeSystemClassLoader")) + self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) assert dir(jpype.JPackage('org.jpype.sample_package')) == ['A', 'B'] def testNonASCIIPathWithSystemClassLoader(self): @@ -207,7 +207,7 @@ def testDefaultSystemClassLoader(self): # we introduce no behavior change unless absolutely necessary jpype.startJVM(jvmpath=Path(self.jvmpath)) cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertNotEqual(type(cl), jpype.JClass("org.jpype.classloader.JpypeSystemClassLoader")) + self.assertNotEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) def testServiceWithNonASCIIPath(self): jpype.startJVM( From 6950c29fa7f6a3c8b2a34ee4a8fde35ada6ea3b4 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 15:41:04 -0800 Subject: [PATCH 05/36] Fixed bug in the order of operations --- .../jpype/classloader/DynamicClassLoader.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/native/java/org/jpype/classloader/DynamicClassLoader.java b/native/java/org/jpype/classloader/DynamicClassLoader.java index 8f3623508..711850f86 100644 --- a/native/java/org/jpype/classloader/DynamicClassLoader.java +++ b/native/java/org/jpype/classloader/DynamicClassLoader.java @@ -151,17 +151,27 @@ public Class findClass(String name) throws ClassNotFoundException, ClassFormatEr @Override public URL getResource(String name) { + // Search our parent URL url = this.getParent().getResource(name); if (url != null) return url; + + // Otherwise search locally + return findResource(name); + } - url = super.getResource(name); - if (url != null) - return url; - - for (ClassLoader cl : this.loaders) + @Override + public URL findResource(String name) + { + // Check local first + URL url = super.findResource(name); + if (url != null) + return url; + + // Use one of the subs + for (URLClassLoader cl : this.loaders) { - url = cl.getResource(name); + url = cl.findResource(name); if (url != null) return url; } From cc63e92c10fd4a08e1fb7997b2ec30bfade5a8bf Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 16:07:27 -0800 Subject: [PATCH 06/36] Fix warning --- jpype/_core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jpype/_core.py b/jpype/_core.py index 9fe3aed7e..218ad868d 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -304,7 +304,8 @@ def startJVM( # this guarentees all classes have the same permissions as they did in the past extra_jvm_args += [ '-Djava.system.class.loader=org.jpype.classloader.DynamicClassLoader', - '-Djava.class.path=%s'%support_lib + '-Djava.class.path=%s'%support_lib, + '-Xshare:off' ] if agent: From df0f73b29e1a9bf29d7637b99e548bf5ee8f2148 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 16:07:52 -0800 Subject: [PATCH 07/36] No need to make an extra class loader. --- native/common/jp_classloader.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/native/common/jp_classloader.cpp b/native/common/jp_classloader.cpp index 0dbfa750d..454d6b623 100644 --- a/native/common/jp_classloader.cpp +++ b/native/common/jp_classloader.cpp @@ -41,6 +41,13 @@ JPClassLoader::JPClassLoader(JPJavaFrame& frame) jclass dynamicLoaderClass = frame.getEnv()->FindClass("org/jpype/classloader/DynamicClassLoader"); if (dynamicLoaderClass != nullptr) { + // Use the one in place already + if (frame.IsInstanceOf(m_SystemClassLoader.get(), dynamicLoaderClass)) + { + m_BootLoader = m_SystemClassLoader; + return; + } + // Easy the Dynamic loader is already in the path, so just use it as the bootloader jmethodID newDyLoader = frame.GetMethodID(dynamicLoaderClass, "", "(Ljava/lang/ClassLoader;)V"); From f828f80c168b79768deecc2dfefba163fda30fb0 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 16:53:54 -0800 Subject: [PATCH 08/36] Push to test removal --- jpype/_core.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/jpype/_core.py b/jpype/_core.py index 9fe3aed7e..7807dfdfd 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -275,30 +275,28 @@ def startJVM( raise RuntimeError("Unable to find org.jpype.jar support library at " + support_lib) system_class_loader = _getOption(jvmargs, "-Djava.system.class.loader", keep=True) - late_load = not system_class_loader java_class_path = _expandClassPath(classpath) java_class_path.append(support_lib) java_class_path = _classpath._SEP.join(java_class_path) + remove = None # Make sure our module is always on the classpath - if java_class_path.isascii(): - # no problems - extra_jvm_args += ['-Djava.class.path=%s'%java_class_path ] - late_load = False - elif system_class_loader: - # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 - raise ValueError("system classloader cannot be specified with non ascii characters in the classpath") - else: + if not java_class_path.isascii(): + if system_class_loader: + # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 + raise ValueError("system classloader cannot be specified with non ascii characters in the classpath") + + # If we are not installed on an ascii path then we will need to copy the jar to a new location if not support_lib.isascii(): import tempfile import shutil tmp = tempfile.gettempdir() if not tmp.isascii(): raise ValueError("Unable to create ascii temp directory. Clear TEMPDIR, TEMP, and TMP environment variables") - support_lib2 = os.path.join(tmp, "org.jpype.jar") - shutil.copyfile(support_lib, support_lib2) - support_lib = support_lib2 + remove = os.path.join(tmp, "org.jpype.jar") + shutil.copyfile(support_lib, remove) + support_lib = remove # ok, setup the jpype system classloader and add to the path after startup # this guarentees all classes have the same permissions as they did in the past @@ -306,6 +304,11 @@ def startJVM( '-Djava.system.class.loader=org.jpype.classloader.DynamicClassLoader', '-Djava.class.path=%s'%support_lib ] + late_load = True + else: + # no problems + extra_jvm_args += ['-Djava.class.path=%s'%java_class_path ] + late_load = False if agent: extra_jvm_args += ['-javaagent:' + support_lib] @@ -354,6 +357,8 @@ def startJVM( from pathlib import Path for cp in _expandClassPath(classpath): cl.addFile(Path(cp)) + if remove is not None: + os.remove(remove) def initializeResources(): From 70cc3c1e6a20ebf2c763a27990f520ffad762aa3 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 16:55:28 -0800 Subject: [PATCH 09/36] Merging from main --- jpype/_core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jpype/_core.py b/jpype/_core.py index 218ad868d..abf6286f3 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -354,6 +354,7 @@ def startJVM( cl = _jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() from pathlib import Path for cp in _expandClassPath(classpath): + print("Late load", cp) cl.addFile(Path(cp)) From 94f21a1b1d01da723a9a8f9c2bb5c0b06d6156b1 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 18:21:29 -0800 Subject: [PATCH 10/36] Remove the need for late load... Everyone is equal! --- jpype/_core.py | 30 +++++++++--------------------- test/jpypetest/common.py | 5 ++--- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/jpype/_core.py b/jpype/_core.py index a480376aa..d97a82a7d 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -293,27 +293,32 @@ def startJVM( import shutil tmp = tempfile.gettempdir() if not tmp.isascii(): - raise ValueError("Unable to create ascii temp directory. Clear TEMPDIR, TEMP, and TMP environment variables") + raise ValueError("Unable to find ascii temp directory. Clear TEMPDIR, TEMP, and TMP environment variables") remove = os.path.join(tmp, "org.jpype.jar") shutil.copyfile(support_lib, remove) support_lib = remove + java_class_path = _expandClassPath(classpath) + java_class_path.append(support_lib) + java_class_path = _classpath._SEP.join(java_class_path) + # ok, setup the jpype system classloader and add to the path after startup # this guarentees all classes have the same permissions as they did in the past extra_jvm_args += [ '-Djava.system.class.loader=org.jpype.classloader.DynamicClassLoader', '-Djava.class.path=%s'%support_lib, + '-Djpype.class.path=%s'%java_class_path, '-Xshare:off' ] - late_load = True else: # no problems extra_jvm_args += ['-Djava.class.path=%s'%java_class_path ] - late_load = False if agent: extra_jvm_args += ['-javaagent:' + support_lib] + print("extra", extra_jvm_args) + try: import locale # Gather a list of locale settings that Java may override (excluding LC_ALL) @@ -341,24 +346,7 @@ def startJVM( raise RuntimeError(f"{jvmpath} is older than required Java version{version}") from ex raise - """Prior versions of JPype used the jvmargs to setup the class paths via - JNI (Java Native Interface) option strings: - i.e -Djava.class.path=... - See: https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/invocation.html - - Unfortunately, only ascii paths work because url encoding is not handled correctly - see: https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 - - To resolve this issue, we add the classpath after initialization since Java has - had the utilities to correctly encode it since 1.0 - """ - if late_load and classpath: - # now we can add to the system classpath - cl = _jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - from pathlib import Path - for cp in _expandClassPath(classpath): - print("Late load", cp) - cl.addFile(Path(cp)) + # Clean up if remove is not None: os.remove(remove) diff --git a/test/jpypetest/common.py b/test/jpypetest/common.py index 10e62e60c..7e1c6694e 100644 --- a/test/jpypetest/common.py +++ b/test/jpypetest/common.py @@ -117,9 +117,8 @@ def setUp(self): args.append( "-javaagent:lib/org.jacoco.agent-0.8.5-runtime.jar=destfile=build/coverage/jacoco.exec,includes=org.jpype.*") warnings.warn("using JaCoCo") - import pathlib - jpype.addClassPath(pathlib.Path("lib/*").absolute()) - jpype.addClassPath(pathlib.Path("test/jar/*").absolute()) + jpype.addClassPath(path.join(root, "../lib/*")) + jpype.addClassPath(path.join(root, "jar/*")) classpath_arg %= jpype.getClassPath() args.append(classpath_arg) _jpype.enableStacktraces(True) From 86c7878dd6235109c37315380f448ac8253cae71 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 18:23:59 -0800 Subject: [PATCH 11/36] Jar support for jpype.class.path --- .../jpype/classloader/DynamicClassLoader.java | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/native/java/org/jpype/classloader/DynamicClassLoader.java b/native/java/org/jpype/classloader/DynamicClassLoader.java index 711850f86..daa29edae 100644 --- a/native/java/org/jpype/classloader/DynamicClassLoader.java +++ b/native/java/org/jpype/classloader/DynamicClassLoader.java @@ -1,6 +1,7 @@ package org.jpype.classloader; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -26,6 +27,8 @@ import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.logging.Logger; public class DynamicClassLoader extends URLClassLoader { @@ -35,7 +38,51 @@ public class DynamicClassLoader extends URLClassLoader public DynamicClassLoader(ClassLoader parent) { - super(new URL[0], parent); + super(launch(), parent); + } + + private static URL[] launch() + { + String cp = System.getProperty("jpype.class.path"); + if (cp == null) + return new URL[0]; + + ArrayList path = new ArrayList<>(); + int off = 0, next; + do + { + next = cp.indexOf(File.pathSeparator, off); + String element = (next == -1) + ? cp.substring(off) + : cp.substring(off, next); + if (!element.isEmpty()) + { + try + { + URL url = Paths.get(element).toUri().toURL(); + if (url != null) + path.add(url); + } catch (MalformedURLException ex) + { + System.err.println("Malformed url "+ element); + } catch (IOException ex) + { + System.err.println("Unable to open "+ element); + } + } + off = next + 1; + } while (next != -1); + + System.out.println("jpype.class.path " + cp); + System.clearProperty("jpype.class.path"); + System.setProperty("java.class.path", cp); + return path.toArray(new URL[0]); + } + + public void deferred() + { + System.getProperty("jpype.class.path"); + } // this is required to add a Java agent even if it is already in the path @@ -155,7 +202,7 @@ public URL getResource(String name) URL url = this.getParent().getResource(name); if (url != null) return url; - + // Otherwise search locally return findResource(name); } @@ -165,9 +212,9 @@ public URL findResource(String name) { // Check local first URL url = super.findResource(name); - if (url != null) - return url; - + if (url != null) + return url; + // Use one of the subs for (URLClassLoader cl : this.loaders) { From a7197fb1e975466b75079f1378cd22b84f3a929c Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 18:30:31 -0800 Subject: [PATCH 12/36] Cleanup --- .../jpype/classloader/DynamicClassLoader.java | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/native/java/org/jpype/classloader/DynamicClassLoader.java b/native/java/org/jpype/classloader/DynamicClassLoader.java index daa29edae..b2c0fffe8 100644 --- a/native/java/org/jpype/classloader/DynamicClassLoader.java +++ b/native/java/org/jpype/classloader/DynamicClassLoader.java @@ -27,8 +27,6 @@ import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import java.util.logging.Level; -import java.util.logging.Logger; public class DynamicClassLoader extends URLClassLoader { @@ -41,6 +39,15 @@ public DynamicClassLoader(ClassLoader parent) super(launch(), parent); } + /** + * Special routine for handling non-ascii paths. + * + * If we are loaded as the system ClassLoader, then we will use + * "jpype.class.path" rather than "java.class.path" during the load process. + * We will move it into the expected place after so no one is the wiser. + * + * @return + */ private static URL[] launch() { String cp = System.getProperty("jpype.class.path"); @@ -48,13 +55,14 @@ private static URL[] launch() return new URL[0]; ArrayList path = new ArrayList<>(); - int off = 0, next; - do + int last = 0; + int next = 0; + + while (next!=-1) { - next = cp.indexOf(File.pathSeparator, off); - String element = (next == -1) - ? cp.substring(off) - : cp.substring(off, next); + // Find the parts + next = cp.indexOf(File.pathSeparator, last); + String element = (next == -1) ? cp.substring(last) : cp.substring(last, next); if (!element.isEmpty()) { try @@ -64,27 +72,18 @@ private static URL[] launch() path.add(url); } catch (MalformedURLException ex) { - System.err.println("Malformed url "+ element); - } catch (IOException ex) - { - System.err.println("Unable to open "+ element); + System.err.println("Malformed url in classpath skipped " + element); } } - off = next + 1; - } while (next != -1); + last = next + 1; + } - System.out.println("jpype.class.path " + cp); + // Replace the path System.clearProperty("jpype.class.path"); System.setProperty("java.class.path", cp); return path.toArray(new URL[0]); } - public void deferred() - { - System.getProperty("jpype.class.path"); - - } - // this is required to add a Java agent even if it is already in the path @SuppressWarnings("unused") private void appendToClassPathForInstrumentation(String path) throws Throwable From c8ed83929bfad21604b4214f2bc886d465d7370d Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 21:59:04 -0800 Subject: [PATCH 13/36] Working on debugging dbapi2 and forName --- jpype/_core.py | 20 +++++++------------- jpype/dbapi2.py | 9 +++++---- native/common/include/jp_method.h | 4 ++-- native/common/include/jp_methoddispatch.h | 4 ++-- native/common/jp_method.cpp | 4 ++-- native/common/jp_methoddispatch.cpp | 4 ++-- native/python/pyjp_method.cpp | 7 +++++-- 7 files changed, 25 insertions(+), 27 deletions(-) diff --git a/jpype/_core.py b/jpype/_core.py index d97a82a7d..c025ea4ae 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -158,7 +158,7 @@ def _expandClassPath( _JVM_started = False - +_tmp = None def interactive(): return bool(getattr(sys, 'ps1', sys.flags.interactive)) @@ -280,7 +280,6 @@ def startJVM( java_class_path.append(support_lib) java_class_path = _classpath._SEP.join(java_class_path) - remove = None # Make sure our module is always on the classpath if not java_class_path.isascii(): if system_class_loader: @@ -291,12 +290,13 @@ def startJVM( if not support_lib.isascii(): import tempfile import shutil - tmp = tempfile.gettempdir() - if not tmp.isascii(): + global _tmp + _tmp = tempfile.TemporaryDirectory(dir = tempfile.gettempdir()) + if not _tmp.name.isascii(): raise ValueError("Unable to find ascii temp directory. Clear TEMPDIR, TEMP, and TMP environment variables") - remove = os.path.join(tmp, "org.jpype.jar") - shutil.copyfile(support_lib, remove) - support_lib = remove + sl2 = os.path.join(_tmp.name, "org.jpype.jar") + shutil.copyfile(support_lib, sl2) + support_lib = sl2 java_class_path = _expandClassPath(classpath) java_class_path.append(support_lib) @@ -317,8 +317,6 @@ def startJVM( if agent: extra_jvm_args += ['-javaagent:' + support_lib] - print("extra", extra_jvm_args) - try: import locale # Gather a list of locale settings that Java may override (excluding LC_ALL) @@ -346,10 +344,6 @@ def startJVM( raise RuntimeError(f"{jvmpath} is older than required Java version{version}") from ex raise - # Clean up - if remove is not None: - os.remove(remove) - def initializeResources(): global _JVM_started diff --git a/jpype/dbapi2.py b/jpype/dbapi2.py index e391e3af8..72e4e6e66 100644 --- a/jpype/dbapi2.py +++ b/jpype/dbapi2.py @@ -401,23 +401,24 @@ def connect(dsn, *, driver=None, driver_args=None, """ Properties = _jpype.JClass("java.util.Properties") if driver: - _jpype.JClass('java.lang.Class').forName(driver).newInstance() + _jpype.JClass('java.lang.Class').forName(driver,True,_jpype.JPypeClassLoader).newInstance() DM = _jpype.JClass('java.sql.DriverManager') + #DM.setLogStream(_jpype.JClass("java.lang.System").out) # User is supplying Java properties if isinstance(driver_args, Properties): - connection = DM.getConnection(dsn, driver_args) + connection = DM.getConnection(dsn, driver_args, caller=False) # User is supplying a mapping that can be converted Properties elif isinstance(driver_args, typing.Mapping): info = Properties() for k, v in driver_args.items(): info.setProperty(k, v) - connection = DM.getConnection(dsn, info) + connection = DM.getConnection(dsn, info, caller=False) # User supplied nothing elif driver_args is None: - connection = DM.getConnection(dsn) + connection = DM.getConnection(dsn, caller=False) # Otherwise use the kwargs else: diff --git a/native/common/include/jp_method.h b/native/common/include/jp_method.h index ab6789ee0..41f593428 100644 --- a/native/common/include/jp_method.h +++ b/native/common/include/jp_method.h @@ -48,7 +48,7 @@ class JPMethod : public JPResource * */ JPMatch::Type matches(JPJavaFrame &frame, JPMethodMatch& match, bool isInstance, JPPyObjectVector& args); - JPPyObject invoke(JPJavaFrame &frame, JPMethodMatch& match, JPPyObjectVector& arg, bool instance); + JPPyObject invoke(JPJavaFrame &frame, JPMethodMatch& match, JPPyObjectVector& arg, bool instance, bool caller); JPPyObject invokeCallerSensitive(JPMethodMatch& match, JPPyObjectVector& arg, bool instance); JPValue invokeConstructor(JPJavaFrame &frame, JPMethodMatch& match, JPPyObjectVector& arg); @@ -116,4 +116,4 @@ class JPMethod : public JPResource jint m_Modifiers{}; } ; -#endif // _JPMETHOD_H_ \ No newline at end of file +#endif // _JPMETHOD_H_ diff --git a/native/common/include/jp_methoddispatch.h b/native/common/include/jp_methoddispatch.h index db524f62b..f4cea6f99 100644 --- a/native/common/include/jp_methoddispatch.h +++ b/native/common/include/jp_methoddispatch.h @@ -63,7 +63,7 @@ class JPMethodDispatch : public JPResource return JPModifier::isBeanAccessor(m_Modifiers); } - JPPyObject invoke(JPJavaFrame& frame, JPPyObjectVector& vargs, bool instance); + JPPyObject invoke(JPJavaFrame& frame, JPPyObjectVector& vargs, bool instance, bool no_caller); JPValue invokeConstructor(JPJavaFrame& frame, JPPyObjectVector& vargs); bool matches(JPJavaFrame& frame, JPPyObjectVector& args, bool instance); @@ -89,4 +89,4 @@ class JPMethodDispatch : public JPResource JPMethodCache m_LastCache{}; } ; -#endif // _JPMETHODDISPATCH_H_ \ No newline at end of file +#endif // _JPMETHODDISPATCH_H_ diff --git a/native/common/jp_method.cpp b/native/common/jp_method.cpp index debc9557d..a08920a96 100644 --- a/native/common/jp_method.cpp +++ b/native/common/jp_method.cpp @@ -201,11 +201,11 @@ void JPMethod::packArgs(JPJavaFrame &frame, JPMethodMatch &match, JP_TRACE_OUT; // GCOVR_EXCL_LINE } -JPPyObject JPMethod::invoke(JPJavaFrame& frame, JPMethodMatch& match, JPPyObjectVector& arg, bool instance) +JPPyObject JPMethod::invoke(JPJavaFrame& frame, JPMethodMatch& match, JPPyObjectVector& arg, bool instance, bool caller) { JP_TRACE_IN("JPMethod::invoke"); // Check if it is caller sensitive - if (isCallerSensitive()) + if (caller && isCallerSensitive()) return invokeCallerSensitive(match, arg, instance); size_t alen = m_ParameterTypes.size(); diff --git a/native/common/jp_methoddispatch.cpp b/native/common/jp_methoddispatch.cpp index 35627af44..7e6f8ef16 100644 --- a/native/common/jp_methoddispatch.cpp +++ b/native/common/jp_methoddispatch.cpp @@ -209,12 +209,12 @@ bool JPMethodDispatch::findOverload(JPJavaFrame& frame, JPMethodMatch &bestMatch JP_TRACE_OUT; } -JPPyObject JPMethodDispatch::invoke(JPJavaFrame& frame, JPPyObjectVector& args, bool instance) +JPPyObject JPMethodDispatch::invoke(JPJavaFrame& frame, JPPyObjectVector& args, bool instance, bool caller) { JP_TRACE_IN("JPMethodDispatch::invoke"); JPMethodMatch match(frame, args, instance); findOverload(frame, match, args, instance, true); - return match.m_Overload->invoke(frame, match, args, instance); + return match.m_Overload->invoke(frame, match, args, instance, caller); JP_TRACE_OUT; } diff --git a/native/python/pyjp_method.cpp b/native/python/pyjp_method.cpp index facf2a994..2f46302e3 100644 --- a/native/python/pyjp_method.cpp +++ b/native/python/pyjp_method.cpp @@ -96,15 +96,18 @@ static PyObject *PyJPMethod_call(PyJPMethod *self, PyObject *args, PyObject *kwa // Clear any pending interrupts if we are on the main thread if (hasInterrupt()) frame.clearInterrupt(false); + bool caller = true; + if (kwargs != nullptr && PyDict_GetItemString(kwargs, "caller") == Py_False) + caller = false; PyObject *out = nullptr; if (self->m_Instance == nullptr) { JPPyObjectVector vargs(args); - out = self->m_Method->invoke(frame, vargs, false).keep(); + out = self->m_Method->invoke(frame, vargs, false, caller).keep(); } else { JPPyObjectVector vargs(self->m_Instance, args); - out = self->m_Method->invoke(frame, vargs, true).keep(); + out = self->m_Method->invoke(frame, vargs, true, caller).keep(); } return out; JP_PY_CATCH(nullptr); // GCOVR_EXCL_LINE From 4e63379b5c046e5cd77af3001064b3391690e4cd Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 07:47:44 -0800 Subject: [PATCH 14/36] Still working on Database services --- jpype/dbapi2.py | 6 +-- native/java/org/jpype/JPypeContext.java | 49 +++++++++---------- .../jpype/classloader/DynamicClassLoader.java | 4 +- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/jpype/dbapi2.py b/jpype/dbapi2.py index 72e4e6e66..0b39ad97c 100644 --- a/jpype/dbapi2.py +++ b/jpype/dbapi2.py @@ -407,18 +407,18 @@ def connect(dsn, *, driver=None, driver_args=None, # User is supplying Java properties if isinstance(driver_args, Properties): - connection = DM.getConnection(dsn, driver_args, caller=False) + connection = DM.getConnection(dsn, driver_args) # User is supplying a mapping that can be converted Properties elif isinstance(driver_args, typing.Mapping): info = Properties() for k, v in driver_args.items(): info.setProperty(k, v) - connection = DM.getConnection(dsn, info, caller=False) + connection = DM.getConnection(dsn, info) # User supplied nothing elif driver_args is None: - connection = DM.getConnection(dsn, caller=False) + connection = DM.getConnection(dsn) # Otherwise use the kwargs else: diff --git a/native/java/org/jpype/JPypeContext.java b/native/java/org/jpype/JPypeContext.java index c997d3759..db85cf37c 100644 --- a/native/java/org/jpype/JPypeContext.java +++ b/native/java/org/jpype/JPypeContext.java @@ -19,11 +19,8 @@ import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.nio.Buffer; import java.nio.ByteOrder; -import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; @@ -244,21 +241,21 @@ private void shutdown() if (freeResources) { - // Release all Python references - try - { - JPypeReferenceQueue.getInstance().stop(); - } catch (Throwable th) - { - } - - // Release any C++ resources - try - { - this.typeManager.shutdown(); - } catch (Throwable th) - { - } + // Release all Python references + try + { + JPypeReferenceQueue.getInstance().stop(); + } catch (Throwable th) + { + } + + // Release any C++ resources + try + { + this.typeManager.shutdown(); + } catch (Throwable th) + { + } } // Execute post hooks @@ -324,9 +321,10 @@ public void _addPost(Runnable run) } /** - * Call a method using reflection.This method creates a stackframe so that - * caller sensitive methods will execute properly. + * Call a method using reflection. * + * This method creates a stackframe so that caller sensitive methods will + * execute properly. * * @param method is the method to call. * @param obj is the object to operate on, it will be null if the method is @@ -344,10 +342,11 @@ public Object callMethod(Method method, Object obj, Object[] args) return method.invoke(obj, args); } catch (InvocationTargetException ex) { +// ex.printStackTrace(); throw ex.getCause(); } } - + /** * Helper function for collect rectangular, */ @@ -637,22 +636,22 @@ private static void scanExistingJars() } } - private static long getTotalMemory() + private static long getTotalMemory() { return Runtime.getRuntime().totalMemory(); } - private static long getFreeMemory() + private static long getFreeMemory() { return Runtime.getRuntime().freeMemory(); } - private static long getMaxMemory() + private static long getMaxMemory() { return Runtime.getRuntime().maxMemory(); } - private static long getUsedMemory() + private static long getUsedMemory() { return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); } diff --git a/native/java/org/jpype/classloader/DynamicClassLoader.java b/native/java/org/jpype/classloader/DynamicClassLoader.java index b2c0fffe8..1021217c3 100644 --- a/native/java/org/jpype/classloader/DynamicClassLoader.java +++ b/native/java/org/jpype/classloader/DynamicClassLoader.java @@ -57,8 +57,8 @@ private static URL[] launch() ArrayList path = new ArrayList<>(); int last = 0; int next = 0; - - while (next!=-1) + + while (next != -1) { // Find the parts next = cp.indexOf(File.pathSeparator, last); From 4344295721a3fafda60ca7143d9c8f69f9ad2562 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 12:43:33 -0800 Subject: [PATCH 15/36] Rework to use reflector class --- jpype/_classpath.py | 6 +- jpype/_core.py | 2 +- native/common/jp_classloader.cpp | 2 +- native/java/manifest.txt | 2 +- ...ClassLoader.java => JPypeClassLoader.java} | 108 ++++++++---------- native/java/org/jpype/JPypeContext.java | 33 +++--- native/java/org/jpype/JPypeReflector.java | 27 +++++ native/java/org/jpype/html/Html.java | 4 +- native/java/org/jpype/pkg/JPypePackage.java | 6 +- native/java0/org/jpype/Reflector0.java | 38 ++++++ test/jpypetest/test_startup.py | 6 +- 11 files changed, 147 insertions(+), 87 deletions(-) rename native/java/org/jpype/{classloader/DynamicClassLoader.java => JPypeClassLoader.java} (79%) create mode 100644 native/java/org/jpype/JPypeReflector.java create mode 100644 native/java0/org/jpype/Reflector0.java diff --git a/jpype/_classpath.py b/jpype/_classpath.py index 2d8c919eb..160017005 100644 --- a/jpype/_classpath.py +++ b/jpype/_classpath.py @@ -52,7 +52,7 @@ def addClassPath(path1: typing.Union[str, _os.PathLike]) -> None: path1 = path2.joinpath(path1) # If the JVM is already started then we will have to load the paths - # immediately into the DynamicClassLoader + # immediately into the JPypeClassLoader if _jpype.isStarted(): Paths = _jpype.JClass('java.nio.file.Paths') JContext = _jpype.JClass('org.jpype.JPypeContext') @@ -62,9 +62,9 @@ def addClassPath(path1: typing.Union[str, _os.PathLike]) -> None: if len(paths) == 0: return for path in paths: - classLoader.addFile(Paths.get(str(path))) + classLoader.addPath(Paths.get(str(path))) else: - classLoader.addFile(Paths.get(str(path1))) + classLoader.addPath(Paths.get(str(path1))) _CLASSPATHS.append(path1) diff --git a/jpype/_core.py b/jpype/_core.py index c025ea4ae..f547f7376 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -305,7 +305,7 @@ def startJVM( # ok, setup the jpype system classloader and add to the path after startup # this guarentees all classes have the same permissions as they did in the past extra_jvm_args += [ - '-Djava.system.class.loader=org.jpype.classloader.DynamicClassLoader', + '-Djava.system.class.loader=org.jpype.JPypeClassLoader', '-Djava.class.path=%s'%support_lib, '-Djpype.class.path=%s'%java_class_path, '-Xshare:off' diff --git a/native/common/jp_classloader.cpp b/native/common/jp_classloader.cpp index 454d6b623..cac86282a 100644 --- a/native/common/jp_classloader.cpp +++ b/native/common/jp_classloader.cpp @@ -38,7 +38,7 @@ JPClassLoader::JPClassLoader(JPJavaFrame& frame) m_SystemClassLoader = JPObjectRef(frame, frame.CallStaticObjectMethodA(classLoaderClass, getSystemClassLoader, nullptr)); - jclass dynamicLoaderClass = frame.getEnv()->FindClass("org/jpype/classloader/DynamicClassLoader"); + jclass dynamicLoaderClass = frame.getEnv()->FindClass("org/jpype/JPypeClassLoader"); if (dynamicLoaderClass != nullptr) { // Use the one in place already diff --git a/native/java/manifest.txt b/native/java/manifest.txt index 0b556c29d..81c97d3eb 100644 --- a/native/java/manifest.txt +++ b/native/java/manifest.txt @@ -1,3 +1,3 @@ Manifest-Version: 1.0 Premain-Class: org.jpype.agent.JPypeAgent - +Multi-Release: true diff --git a/native/java/org/jpype/classloader/DynamicClassLoader.java b/native/java/org/jpype/JPypeClassLoader.java similarity index 79% rename from native/java/org/jpype/classloader/DynamicClassLoader.java rename to native/java/org/jpype/JPypeClassLoader.java index 1021217c3..003284ecd 100644 --- a/native/java/org/jpype/classloader/DynamicClassLoader.java +++ b/native/java/org/jpype/JPypeClassLoader.java @@ -1,4 +1,4 @@ -package org.jpype.classloader; +package org.jpype; import java.io.ByteArrayOutputStream; import java.io.File; @@ -7,6 +7,7 @@ import java.io.InputStream; import java.net.MalformedURLException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; @@ -22,21 +23,24 @@ import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; -public class DynamicClassLoader extends URLClassLoader +public class JPypeClassLoader extends URLClassLoader { - List loaders = new LinkedList<>(); HashMap> map = new HashMap<>(); + int code = 0; - public DynamicClassLoader(ClassLoader parent) + public JPypeClassLoader(ClassLoader parent) { - super(launch(), parent); + super(initial(), parent); + } + + public int getCode() + { + return code; } /** @@ -48,7 +52,7 @@ public DynamicClassLoader(ClassLoader parent) * * @return */ - private static URL[] launch() + private static URL[] initial() { String cp = System.getProperty("jpype.class.path"); if (cp == null) @@ -91,11 +95,6 @@ private void appendToClassPathForInstrumentation(String path) throws Throwable addURL(Paths.get(path).toAbsolutePath().toUri().toURL()); } - public int getCode() - { - return loaders.hashCode(); - } - /** * Add a set of jars to the classpath. * @@ -103,11 +102,10 @@ public int getCode() * @param glob * @throws IOException */ - public void addFiles(Path root, String glob) throws IOException + public void addPaths(Path root, String glob) throws IOException { final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher(glob); - List urls = new LinkedList<>(); Files.walkFileTree(root, new SimpleFileVisitor() { @@ -118,7 +116,7 @@ public FileVisitResult visitFile(Path path, if (pathMatcher.matches(root.relativize(path))) { URL url = path.toUri().toURL(); - urls.add(url); + addURL(url); } return FileVisitResult.CONTINUE; } @@ -131,23 +129,15 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) } }); - loaders.add(new URLClassLoader(urls.toArray(new URL[0]))); } - public void addFile(Path path) throws FileNotFoundException + public void addPath(Path path) throws FileNotFoundException { try { if (!Files.exists(path)) throw new FileNotFoundException(path.toString()); - URL[] urls = new URL[] - { - path.toUri().toURL() - }; - loaders.add(new URLClassLoader(urls)); - - // Scan the file for directory entries - this.scanJar(path); + this.addURL(path.toUri().toURL()); } catch (MalformedURLException ex) { // This should never happen @@ -186,7 +176,7 @@ public Class findClass(String name) throws ClassNotFoundException, ClassFormatEr buffer.flush(); byte[] data = buffer.toByteArray(); - return defineClass(name, data, 0, data.length); + return defineClass(null, data, 0, data.length); } } catch (IOException ex) { @@ -194,18 +184,6 @@ public Class findClass(String name) throws ClassNotFoundException, ClassFormatEr throw new ClassNotFoundException(name); } - @Override - public URL getResource(String name) - { - // Search our parent - URL url = this.getParent().getResource(name); - if (url != null) - return url; - - // Otherwise search locally - return findResource(name); - } - @Override public URL findResource(String name) { @@ -214,31 +192,23 @@ public URL findResource(String name) if (url != null) return url; - // Use one of the subs - for (URLClassLoader cl : this.loaders) - { - url = cl.findResource(name); - if (url != null) - return url; - } // Both with and without / should generate the same result if (name.endsWith("/")) name = name.substring(0, name.length() - 1); if (map.containsKey(name)) return map.get(name).get(0); + + // We have some resource which must be sourced to a particular class loader + if (name.startsWith("org/jpype/")) + return getResource("META-INF/versions/0/" + name); return null; } @Override - public Enumeration getResources(String name) throws IOException + public Enumeration findResources(String name) throws IOException { ArrayList out = new ArrayList<>(); - out.addAll(Collections.list(getParent().getResources(name))); - out.addAll(Collections.list(super.getResources(name))); - for (URLClassLoader cl : this.loaders) - { - out.addAll(Collections.list(cl.findResources(name))); - } + out.addAll(Collections.list(super.findResources(name))); // Both with and without / should generate the same result if (name.endsWith("/")) name = name.substring(0, name.length() - 1); @@ -254,6 +224,25 @@ public void addResource(String name, URL url) this.map.get(name).add(url); } + @Override + public void addURL(URL url) + { + // Mark our cache as dirty + code = code * 98745623 + url.hashCode(); + super.addURL(url); + Path path; + try + { + path = Paths.get(url.toURI()); + } catch (URISyntaxException ex) + { + return; + } + + // Scan for missing resources + scanJar(path); + } + /** * Recreate missing directory entries for Jars that lack indexing. * @@ -261,18 +250,19 @@ public void addResource(String name, URL url) * properly importing their contents. This procedure scans a jar file when * loaded to build missing directories. * - * @param p1 + * @param path */ - public void scanJar(Path p1) + void scanJar(Path path) { - if (!Files.exists(p1)) + if (!Files.exists(path)) return; - if (Files.isDirectory(p1)) + if (Files.isDirectory(path)) return; - try (JarFile jf = new JarFile(p1.toFile())) + + try (JarFile jf = new JarFile(path.toFile())) { Enumeration entries = jf.entries(); - URI abs = p1.toAbsolutePath().toUri(); + URI abs = path.toAbsolutePath().toUri(); Set urls = new java.util.HashSet(); while (entries.hasMoreElements()) { diff --git a/native/java/org/jpype/JPypeContext.java b/native/java/org/jpype/JPypeContext.java index db85cf37c..d4fcc00be 100644 --- a/native/java/org/jpype/JPypeContext.java +++ b/native/java/org/jpype/JPypeContext.java @@ -27,7 +27,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; -import org.jpype.classloader.DynamicClassLoader; +import java.util.logging.Level; +import java.util.logging.Logger; import org.jpype.manager.TypeFactory; import org.jpype.manager.TypeFactoryNative; import org.jpype.manager.TypeManager; @@ -77,11 +78,12 @@ public class JPypeContext private long context; private TypeFactory typeFactory; private TypeManager typeManager; - private DynamicClassLoader classLoader; + private JPypeClassLoader classLoader; private final AtomicInteger shutdownFlag = new AtomicInteger(); private final List shutdownHooks = new ArrayList<>(); private final List postHooks = new ArrayList<>(); public static boolean freeResources = true; + public JPypeReflector reflector = null; static public JPypeContext getInstance() { @@ -92,20 +94,32 @@ static public JPypeContext getInstance() * Start the JPype system. * * @param context is the C++ portion of the context. - * @param bootLoader is the classloader holding JPype resources. + * @param loader is the classloader holding JPype resources. * @return the created context. */ - static JPypeContext createContext(long context, ClassLoader bootLoader, String nativeLib, boolean interrupt) + static JPypeContext createContext(long context, ClassLoader loader, String nativeLib, boolean interrupt) { if (nativeLib != null) { System.load(nativeLib); } INSTANCE.context = context; - INSTANCE.classLoader = (DynamicClassLoader) bootLoader; + INSTANCE.classLoader = (JPypeClassLoader) loader; INSTANCE.typeFactory = new TypeFactoryNative(); INSTANCE.typeManager = new TypeManager(context, INSTANCE.typeFactory); INSTANCE.initialize(interrupt); + + try + { + INSTANCE.reflector = (JPypeReflector) Class.forName("org.jpype.Reflector0", true, loader) + .getConstructor() + .newInstance(); + } catch (ClassNotFoundException | NoSuchMethodException | SecurityException + | InstantiationException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException ex) + { + System.err.println("Unable to create reflector "+ ex); + } scanExistingJars(); return INSTANCE; @@ -460,15 +474,6 @@ public boolean isShutdown() return shutdownFlag.get() > 0; } -// public void incrementProxy() -// { -// proxyCount.incrementAndGet(); -// } -// -// public void decrementProxy() -// { -// proxyCount.decrementAndGet(); -// } /** * Clear the current interrupt. * diff --git a/native/java/org/jpype/JPypeReflector.java b/native/java/org/jpype/JPypeReflector.java new file mode 100644 index 000000000..f92d6de0e --- /dev/null +++ b/native/java/org/jpype/JPypeReflector.java @@ -0,0 +1,27 @@ +package org.jpype; + +import java.lang.reflect.Method; + +/** + * + * @author nelson85 + */ +public interface JPypeReflector +{ + /** + * Call a method using reflection. + * + * This method creates a stackframe so that caller sensitive methods will + * execute properly. + * + * @param method is the method to call. + * @param obj is the object to operate on, it will be null if the method is + * static. + * @param args the arguments to method. + * @return the object that results form the invocation. + * @throws java.lang.Throwable throws whatever type the called method + * produces. + */ + public Object callMethod(Method method, Object obj, Object[] args) + throws Throwable; +} diff --git a/native/java/org/jpype/html/Html.java b/native/java/org/jpype/html/Html.java index 35739ee48..e97a02af3 100644 --- a/native/java/org/jpype/html/Html.java +++ b/native/java/org/jpype/html/Html.java @@ -72,8 +72,8 @@ public static List parseAttributes(Document doc, String str) static { - try (InputStream is = JPypeContext.getInstance().getClass().getClassLoader() - .getResourceAsStream("org/jpype/html/entities.txt"); + ClassLoader cl = ClassLoader.getSystemClassLoader(); + try (InputStream is = cl.getResourceAsStream("org/jpype/html/entities.txt"); InputStreamReader isr = new InputStreamReader(is); BufferedReader rd = new BufferedReader(isr)) { diff --git a/native/java/org/jpype/pkg/JPypePackage.java b/native/java/org/jpype/pkg/JPypePackage.java index 1541ccea4..1321b40d9 100644 --- a/native/java/org/jpype/pkg/JPypePackage.java +++ b/native/java/org/jpype/pkg/JPypePackage.java @@ -25,9 +25,9 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Map; +import org.jpype.JPypeClassLoader; import org.jpype.JPypeContext; import org.jpype.JPypeKeywords; -import org.jpype.classloader.DynamicClassLoader; /** * Representation of a JPackage in Java. @@ -45,13 +45,13 @@ public class JPypePackage // A mapping from Python names into Paths into the module/jar file system. Map contents; int code; - private final DynamicClassLoader classLoader; + private final JPypeClassLoader classLoader; public JPypePackage(String pkg) { this.pkg = pkg; this.contents = JPypePackageManager.getContentMap(pkg); - this.classLoader = ((DynamicClassLoader)(JPypeContext.getInstance().getClassLoader())); + this.classLoader = ((JPypeClassLoader)(JPypeContext.getInstance().getClassLoader())); this.code = classLoader.getCode(); } diff --git a/native/java0/org/jpype/Reflector0.java b/native/java0/org/jpype/Reflector0.java new file mode 100644 index 000000000..f4ff1891f --- /dev/null +++ b/native/java0/org/jpype/Reflector0.java @@ -0,0 +1,38 @@ +package org.jpype; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class Reflector0 implements JPypeReflector +{ + + public Reflector0() + {} + + /** + * Call a method using reflection. + * + * This method creates a stackframe so that caller sensitive methods will + * execute properly. + * + * @param method is the method to call. + * @param obj is the object to operate on, it will be null if the method is + * static. + * @param args the arguments to method. + * @return the object that results form the invocation. + * @throws java.lang.Throwable throws whatever type the called method + * produces. + */ + public Object callMethod(Method method, Object obj, Object[] args) + throws Throwable + { + try + { + return method.invoke(obj, args); + } catch (InvocationTargetException ex) + { +// ex.printStackTrace(); + throw ex.getCause(); + } + } +} diff --git a/test/jpypetest/test_startup.py b/test/jpypetest/test_startup.py index 0a3eadc00..d5a4c86c4 100644 --- a/test/jpypetest/test_startup.py +++ b/test/jpypetest/test_startup.py @@ -152,7 +152,7 @@ def testNonASCIIPath(self): """ jpype.startJVM(jvmpath=Path(self.jvmpath), classpath="test/jar/unicode_à😎/sample_package.jar") cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) + self.assertEqual(type(cl), jpype.JClass("org.jpype.JPypeClassLoader")) assert dir(jpype.JPackage('org.jpype.sample_package')) == ['A', 'B'] @@ -162,7 +162,7 @@ def testOldStyleNonASCIIPath(self): """ jpype.startJVM("-Djava.class.path=test/jar/unicode_à😎/sample_package.jar", jvmpath=Path(self.jvmpath)) cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) + self.assertEqual(type(cl), jpype.JClass("org.jpype.JPypeClassLoader")) assert dir(jpype.JPackage('org.jpype.sample_package')) == ['A', 'B'] def testNonASCIIPathWithSystemClassLoader(self): @@ -207,7 +207,7 @@ def testDefaultSystemClassLoader(self): # we introduce no behavior change unless absolutely necessary jpype.startJVM(jvmpath=Path(self.jvmpath)) cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertNotEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) + self.assertNotEqual(type(cl), jpype.JClass("org.jpype.JPypeClassLoader")) def testServiceWithNonASCIIPath(self): jpype.startJVM( From 3cefadd3f26186f662a88745ae7c37db869a8f62 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 13:07:10 -0800 Subject: [PATCH 16/36] Why must we specify stuff on our private methods? --- jpype/_core.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/jpype/_core.py b/jpype/_core.py index f547f7376..57eaae47d 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -109,7 +109,7 @@ def isJVMStarted(): return _jpype.isStarted() -def _getOption(args, var, sep=None, keep=False) -> list[str]: +def _getOption(args, var, sep=None, keep=False): """ Get an option and remove it from the current jvm arguments list """ for i,v in enumerate(args): if not isinstance(v, str): @@ -224,28 +224,28 @@ def startJVM( raise OSError('JVM cannot be restarted') # Convert to list - jvmargs = list(jvmargs) + jvm_args: list[str] = list(jvmargs) # JVM path - if jvmargs: + if jvm_args: # jvm is the first argument the first argument is a path or None - if jvmargs[0] is None or (isinstance(jvmargs[0], str) and not jvmargs[0].startswith('-')): - if jvmpath: + if jvm_args[0] is None or (isinstance(jvm_args[0], str) and not jvm_args[0].startswith('-')): + if jvm_path: raise TypeError('jvmpath specified twice') - jvmpath = jvmargs[0] - jvmargs = jvmargs[1:] + jvm_path = jvm_args[0] + jvm_args = jvm_args[1:] if not jvmpath: jvmpath = getDefaultJVMPath() else: # Allow the path to be a PathLike. - jvmpath = os.fspath(jvmpath) + jvmpath = os.fspath(jvm_path) # Handle strings and list of strings. extra_jvm_args: list[str] = [] # Classpath handling - old_classpath = _getOption(jvmargs, "-Djava.class.path", _classpath._SEP) + old_classpath = _getOption(jvm_args, "-Djava.class.path", _classpath._SEP) if old_classpath: # Old style, specified in the arguments if classpath is not None: @@ -258,7 +258,7 @@ def startJVM( # Code for 1.6 release when we add module support # # Modulepath handling -# old_modulepath = _getOption(jvmargs, "--module-path", _classpath._SEP) +# old_modulepath = _getOption(jvm_args, "--module-path", _classpath._SEP) # if old_modulepath: # # Old style, specified in the arguments # if modulepath is not None: @@ -274,7 +274,7 @@ def startJVM( if not os.path.exists(support_lib): raise RuntimeError("Unable to find org.jpype.jar support library at " + support_lib) - system_class_loader = _getOption(jvmargs, "-Djava.system.class.loader", keep=True) + system_class_loader = _getOption(jvm_args, "-Djava.system.class.loader", keep=True) java_class_path = _expandClassPath(classpath) java_class_path.append(support_lib) @@ -324,7 +324,7 @@ def startJVM( # Keep the current locale settings, else Java will replace them. prior = [locale.getlocale(i) for i in categories] # Start the JVM - _jpype.startup(jvmpath, tuple(jvmargs + extra_jvm_args), + _jpype.startup(jvmpath, tuple(jvm_args + extra_jvm_args), ignoreUnrecognized, convertStrings, interrupt) # Collect required resources for operation initializeResources() From 37037607f69a691b574c0e39fa48d9e9bd24021e Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 13:14:47 -0800 Subject: [PATCH 17/36] Shut up mypy --- jpype/_core.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/jpype/_core.py b/jpype/_core.py index 57eaae47d..440111d5e 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -125,7 +125,7 @@ def _getOption(args, var, sep=None, keep=False): def _expandClassPath( classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None -) -> typing.Sequence[str]: +) -> typing.List[str]: """ Return a classpath which represents the given tuple of classpath specifications """ @@ -230,7 +230,7 @@ def startJVM( if jvm_args: # jvm is the first argument the first argument is a path or None if jvm_args[0] is None or (isinstance(jvm_args[0], str) and not jvm_args[0].startswith('-')): - if jvm_path: + if jvmpath: raise TypeError('jvmpath specified twice') jvm_path = jvm_args[0] jvm_args = jvm_args[1:] @@ -278,10 +278,10 @@ def startJVM( java_class_path = _expandClassPath(classpath) java_class_path.append(support_lib) - java_class_path = _classpath._SEP.join(java_class_path) + classpath = _classpath._SEP.join(java_class_path) # Make sure our module is always on the classpath - if not java_class_path.isascii(): + if not classpath.isascii(): if system_class_loader: # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 raise ValueError("system classloader cannot be specified with non ascii characters in the classpath") @@ -298,16 +298,12 @@ def startJVM( shutil.copyfile(support_lib, sl2) support_lib = sl2 - java_class_path = _expandClassPath(classpath) - java_class_path.append(support_lib) - java_class_path = _classpath._SEP.join(java_class_path) - # ok, setup the jpype system classloader and add to the path after startup # this guarentees all classes have the same permissions as they did in the past extra_jvm_args += [ '-Djava.system.class.loader=org.jpype.JPypeClassLoader', '-Djava.class.path=%s'%support_lib, - '-Djpype.class.path=%s'%java_class_path, + '-Djpype.class.path=%s'%classpath, '-Xshare:off' ] else: From ff12201f301b2a83d7a6e8cccb18cde69efce832 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 13:39:51 -0800 Subject: [PATCH 18/36] Update build process to include reflector --- jpype/_core.py | 4 +++- native/java/manifest.txt | 4 +++- setupext/build_ext.py | 9 ++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/jpype/_core.py b/jpype/_core.py index 440111d5e..25d958663 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -278,6 +278,7 @@ def startJVM( java_class_path = _expandClassPath(classpath) java_class_path.append(support_lib) + java_class_path = filter(len, java_class_path) classpath = _classpath._SEP.join(java_class_path) # Make sure our module is always on the classpath @@ -308,10 +309,11 @@ def startJVM( ] else: # no problems - extra_jvm_args += ['-Djava.class.path=%s'%java_class_path ] + extra_jvm_args += ['-Djava.class.path=%s'%classpath ] if agent: extra_jvm_args += ['-javaagent:' + support_lib] + print(extra_jvm_args) try: import locale diff --git a/native/java/manifest.txt b/native/java/manifest.txt index 81c97d3eb..cc3a0089c 100644 --- a/native/java/manifest.txt +++ b/native/java/manifest.txt @@ -1,3 +1,5 @@ Manifest-Version: 1.0 +Specification-Title: org.jpype +Implementation-Title: org.jpype +Implementation-Version: 1.5.1 Premain-Class: org.jpype.agent.JPypeAgent -Multi-Release: true diff --git a/setupext/build_ext.py b/setupext/build_ext.py index 00cc0a72d..a04b406c5 100644 --- a/setupext/build_ext.py +++ b/setupext/build_ext.py @@ -311,10 +311,17 @@ def build_java_ext(self, ext): cmd1 = shlex.split('%s -cp "%s" -d "%s" -g:none -source %s -target %s -encoding UTF-8' % (javac, classpath, build_dir, target_version, target_version)) cmd1.extend(ext.sources) - os.makedirs("build/classes", exist_ok=True) + os.makedirs("build/classes/META-INF", exist_ok=True) + self.announce(" %s" % " ".join(cmd1), level=distutils.log.INFO) + subprocess.check_call(cmd1) + + cmd1 = shlex.split('%s -cp "%s" -d "%s" -g:none -source %s -target %s -encoding UTF-8' % + (javac, build_dir, build_dir+"/META-INF/versions/0", target_version, target_version)) + cmd1.extend(["native/java0/org/jpype/Reflector0.java"]) self.announce(" %s" % " ".join(cmd1), level=distutils.log.INFO) subprocess.check_call(cmd1) + manifest = None try: for file in glob.iglob("native/java/**/*.*", recursive=True): From 4f1eb89277761126dc76bce25cee4179dcbc3ec1 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 13:45:07 -0800 Subject: [PATCH 19/36] Revised some tests which don't run properly in non-ascii root --- test/jpypetest/common.py | 10 ++++++++++ test/jpypetest/test_startup.py | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/test/jpypetest/common.py b/test/jpypetest/common.py index 7e1c6694e..9d3d9764d 100644 --- a/test/jpypetest/common.py +++ b/test/jpypetest/common.py @@ -67,6 +67,16 @@ def f(self): raise unittest.SkipTest("numpy required") return f +def requireAscii(func): + def f(self): + try: + root = path.dirname(path.abspath(path.dirname(__file__))) + if root.isascii(): + return func(self) + except ImportError: + pass + raise unittest.SkipTest("Ascii root directory required") + return f class UseFunc(object): def __init__(self, obj, func, attr): diff --git a/test/jpypetest/test_startup.py b/test/jpypetest/test_startup.py index 0a3eadc00..a36a4372c 100644 --- a/test/jpypetest/test_startup.py +++ b/test/jpypetest/test_startup.py @@ -20,6 +20,7 @@ import os from pathlib import Path import unittest +import common root = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) cp = os.path.join(root, 'classes').replace('\\', '/') @@ -181,6 +182,7 @@ def testOldStyleNonASCIIPathWithSystemClassLoader(self): "-Djava.class.path=test/jar/unicode_à😎/sample_package.jar" ) + @common.requireAscii def testASCIIPathWithSystemClassLoader(self): jpype.startJVM( "-Djava.system.class.loader=jpype.startup.TestSystemClassLoader", @@ -192,6 +194,7 @@ def testASCIIPathWithSystemClassLoader(self): self.assertEqual(type(classloader), test_classLoader) assert dir(jpype.JPackage('jpype.startup')) == ['TestSystemClassLoader'] + @common.requireAscii def testOldStyleASCIIPathWithSystemClassLoader(self): jpype.startJVM( self.jvmpath, @@ -203,6 +206,7 @@ def testOldStyleASCIIPathWithSystemClassLoader(self): self.assertEqual(type(classloader), test_classLoader) assert dir(jpype.JPackage('jpype.startup')) == ['TestSystemClassLoader'] + @common.requireAscii def testDefaultSystemClassLoader(self): # we introduce no behavior change unless absolutely necessary jpype.startJVM(jvmpath=Path(self.jvmpath)) From fb94b982c10d0e2c1eae86713ed4fe1eaa688b11 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 13:56:22 -0800 Subject: [PATCH 20/36] Remove agent option for now --- jpype/_core.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/jpype/_core.py b/jpype/_core.py index 25d958663..38313b10d 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -171,7 +171,6 @@ def startJVM( ignoreUnrecognized: bool = False, convertStrings: bool = False, interrupt: bool = not interactive(), - agent: bool = False, ) -> None: """ Starts a Java Virtual Machine. Without options it will start @@ -204,9 +203,6 @@ def startJVM( transfer control to Python rather than halting. If not specified will be False if Python is started as an interactive shell. - agent (bool): Start as agent. This allows for certain - privilaged operations, but required java.instrumentation - module. Raises: OSError: if the JVM cannot be started or is already running. @@ -311,10 +307,6 @@ def startJVM( # no problems extra_jvm_args += ['-Djava.class.path=%s'%classpath ] - if agent: - extra_jvm_args += ['-javaagent:' + support_lib] - print(extra_jvm_args) - try: import locale # Gather a list of locale settings that Java may override (excluding LC_ALL) From 9223000aeb41b5ece562496b552ac941baf2c890 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 22:24:29 -0800 Subject: [PATCH 21/36] Works on nonascii path --- jpype/_core.py | 4 ++-- native/common/include/jp_context.h | 1 + native/common/jp_context.cpp | 5 ++++- native/common/jp_javaframe.cpp | 2 +- test/jpypetest/test_caller_sensitive.py | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/jpype/_core.py b/jpype/_core.py index 38313b10d..c2f223c96 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -228,14 +228,14 @@ def startJVM( if jvm_args[0] is None or (isinstance(jvm_args[0], str) and not jvm_args[0].startswith('-')): if jvmpath: raise TypeError('jvmpath specified twice') - jvm_path = jvm_args[0] + jvmpath = jvm_args[0] jvm_args = jvm_args[1:] if not jvmpath: jvmpath = getDefaultJVMPath() else: # Allow the path to be a PathLike. - jvmpath = os.fspath(jvm_path) + jvmpath = os.fspath(jvmpath) # Handle strings and list of strings. extra_jvm_args: list[str] = [] diff --git a/native/common/include/jp_context.h b/native/common/include/jp_context.h index 0b357f786..bc3a2242b 100644 --- a/native/common/include/jp_context.h +++ b/native/common/include/jp_context.h @@ -223,6 +223,7 @@ class JPContext private: JPClassRef m_Array; + JPObjectRef m_Reflector; // Java Functions jmethodID m_Object_ToStringID{}; diff --git a/native/common/jp_context.cpp b/native/common/jp_context.cpp index d6e25cb2f..612f078b5 100644 --- a/native/common/jp_context.cpp +++ b/native/common/jp_context.cpp @@ -238,7 +238,10 @@ void JPContext::initializeResources(JNIEnv* env, bool interrupt) // Set up methods after everything is start so we get better error // messages - m_CallMethodID = frame.GetMethodID(contextClass, "callMethod", + jclass reflectorClass = frame.FindClass("org/jpype/JPypeReflector"); + jfieldID reflectorField = frame.GetFieldID(contextClass, "reflector", "Lorg/jpype/JPypeReflector;"); + m_Reflector = JPObjectRef(frame, frame.GetObjectField(m_JavaContext.get(), reflectorField)); + m_CallMethodID = frame.GetMethodID(reflectorClass, "callMethod", "(Ljava/lang/reflect/Method;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"); m_Context_collectRectangularID = frame.GetMethodID(contextClass, "collectRectangular", diff --git a/native/common/jp_javaframe.cpp b/native/common/jp_javaframe.cpp index 62bfd2626..136b69ccc 100644 --- a/native/common/jp_javaframe.cpp +++ b/native/common/jp_javaframe.cpp @@ -1162,7 +1162,7 @@ jobject JPJavaFrame::callMethod(jobject method, jobject obj, jobject args) v[0].l = method; v[1].l = obj; v[2].l = args; - return frame.keep(frame.CallObjectMethodA(m_Context->m_JavaContext.get(), m_Context->m_CallMethodID, v)); + return frame.keep(frame.CallObjectMethodA(m_Context->m_Reflector.get(), m_Context->m_CallMethodID, v)); JP_TRACE_OUT; } diff --git a/test/jpypetest/test_caller_sensitive.py b/test/jpypetest/test_caller_sensitive.py index 1c2c733fc..ca97a6af8 100644 --- a/test/jpypetest/test_caller_sensitive.py +++ b/test/jpypetest/test_caller_sensitive.py @@ -109,4 +109,4 @@ def testStackWalker1(self): def testStackWalker2(self): self.assertEqual(self.obj.callStackWalker2(), jpype.JClass( - jpype.java.lang.Class.forName("org.jpype.JPypeContext")).class_) + jpype.java.lang.Class.forName("org.jpype.Reflector0")).class_) From 81f568752626446da984b3144b22c98520c9e7cd Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 22:34:48 -0800 Subject: [PATCH 22/36] Start clean up --- jpype/_core.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/jpype/_core.py b/jpype/_core.py index c2f223c96..5015ba782 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -163,6 +163,33 @@ def _expandClassPath( def interactive(): return bool(getattr(sys, 'ps1', sys.flags.interactive)) +def _findTemp(): + dirlist = [] + # Mirror Python tempfile with a check for ascii + for envname in 'TMPDIR', 'TEMP', 'TMP': + dirname = os.getenv(envname) + if dirname and dirname.isascii(): + dirlist.append(dirname) + if os.name == 'nt': + for envname in [ os.path.expanduser(r'~\AppData\Local\Temp'), + os.path.expandvars(r'%SYSTEMROOT%\Temp'), + r'c:\temp', r'c:\tmp', r'\temp', r'\tmp' ]: + if dirname and dirname.isascii(): + dirlist.append(dirname) + else: + dirlist.extend([ '/tmp', '/var/tmp', '/usr/tmp' ]) + + name = str(os.getpid) + for d in dirlist: + p = Path("%s/%s"%(d,name)) + try: + p.touch() + p.unlink() + except Exception: + continue + return d + raise SystemError("Unable to find non-ansii path") + def startJVM( *jvmargs: str, @@ -288,7 +315,7 @@ def startJVM( import tempfile import shutil global _tmp - _tmp = tempfile.TemporaryDirectory(dir = tempfile.gettempdir()) + _tmp = tempfile.TemporaryDirectory(dir = _findTemp()) if not _tmp.name.isascii(): raise ValueError("Unable to find ascii temp directory. Clear TEMPDIR, TEMP, and TMP environment variables") sl2 = os.path.join(_tmp.name, "org.jpype.jar") From b657fbed98aba9846ccc1d952294c7e07cf2603c Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 22:40:17 -0800 Subject: [PATCH 23/36] Cleanup --- doc/CHANGELOG.rst | 2 ++ native/common/include/jp_method.h | 4 ++-- native/common/include/jp_methoddispatch.h | 4 ++-- native/common/jp_method.cpp | 4 ++-- native/common/jp_methoddispatch.cpp | 4 ++-- native/python/pyjp_method.cpp | 7 ++----- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/doc/CHANGELOG.rst b/doc/CHANGELOG.rst index 0b56924f8..be81528ed 100644 --- a/doc/CHANGELOG.rst +++ b/doc/CHANGELOG.rst @@ -8,6 +8,8 @@ Latest Changes: - Roll back agent change due to misbehaving JVM installs. + - Correct issues with non-ascii path for jdbc connections and forName. + - **1.5.1 - 2024-11-09** - Future proofing for Python 3.14 diff --git a/native/common/include/jp_method.h b/native/common/include/jp_method.h index 41f593428..ab6789ee0 100644 --- a/native/common/include/jp_method.h +++ b/native/common/include/jp_method.h @@ -48,7 +48,7 @@ class JPMethod : public JPResource * */ JPMatch::Type matches(JPJavaFrame &frame, JPMethodMatch& match, bool isInstance, JPPyObjectVector& args); - JPPyObject invoke(JPJavaFrame &frame, JPMethodMatch& match, JPPyObjectVector& arg, bool instance, bool caller); + JPPyObject invoke(JPJavaFrame &frame, JPMethodMatch& match, JPPyObjectVector& arg, bool instance); JPPyObject invokeCallerSensitive(JPMethodMatch& match, JPPyObjectVector& arg, bool instance); JPValue invokeConstructor(JPJavaFrame &frame, JPMethodMatch& match, JPPyObjectVector& arg); @@ -116,4 +116,4 @@ class JPMethod : public JPResource jint m_Modifiers{}; } ; -#endif // _JPMETHOD_H_ +#endif // _JPMETHOD_H_ \ No newline at end of file diff --git a/native/common/include/jp_methoddispatch.h b/native/common/include/jp_methoddispatch.h index f4cea6f99..db524f62b 100644 --- a/native/common/include/jp_methoddispatch.h +++ b/native/common/include/jp_methoddispatch.h @@ -63,7 +63,7 @@ class JPMethodDispatch : public JPResource return JPModifier::isBeanAccessor(m_Modifiers); } - JPPyObject invoke(JPJavaFrame& frame, JPPyObjectVector& vargs, bool instance, bool no_caller); + JPPyObject invoke(JPJavaFrame& frame, JPPyObjectVector& vargs, bool instance); JPValue invokeConstructor(JPJavaFrame& frame, JPPyObjectVector& vargs); bool matches(JPJavaFrame& frame, JPPyObjectVector& args, bool instance); @@ -89,4 +89,4 @@ class JPMethodDispatch : public JPResource JPMethodCache m_LastCache{}; } ; -#endif // _JPMETHODDISPATCH_H_ +#endif // _JPMETHODDISPATCH_H_ \ No newline at end of file diff --git a/native/common/jp_method.cpp b/native/common/jp_method.cpp index a08920a96..debc9557d 100644 --- a/native/common/jp_method.cpp +++ b/native/common/jp_method.cpp @@ -201,11 +201,11 @@ void JPMethod::packArgs(JPJavaFrame &frame, JPMethodMatch &match, JP_TRACE_OUT; // GCOVR_EXCL_LINE } -JPPyObject JPMethod::invoke(JPJavaFrame& frame, JPMethodMatch& match, JPPyObjectVector& arg, bool instance, bool caller) +JPPyObject JPMethod::invoke(JPJavaFrame& frame, JPMethodMatch& match, JPPyObjectVector& arg, bool instance) { JP_TRACE_IN("JPMethod::invoke"); // Check if it is caller sensitive - if (caller && isCallerSensitive()) + if (isCallerSensitive()) return invokeCallerSensitive(match, arg, instance); size_t alen = m_ParameterTypes.size(); diff --git a/native/common/jp_methoddispatch.cpp b/native/common/jp_methoddispatch.cpp index 7e6f8ef16..35627af44 100644 --- a/native/common/jp_methoddispatch.cpp +++ b/native/common/jp_methoddispatch.cpp @@ -209,12 +209,12 @@ bool JPMethodDispatch::findOverload(JPJavaFrame& frame, JPMethodMatch &bestMatch JP_TRACE_OUT; } -JPPyObject JPMethodDispatch::invoke(JPJavaFrame& frame, JPPyObjectVector& args, bool instance, bool caller) +JPPyObject JPMethodDispatch::invoke(JPJavaFrame& frame, JPPyObjectVector& args, bool instance) { JP_TRACE_IN("JPMethodDispatch::invoke"); JPMethodMatch match(frame, args, instance); findOverload(frame, match, args, instance, true); - return match.m_Overload->invoke(frame, match, args, instance, caller); + return match.m_Overload->invoke(frame, match, args, instance); JP_TRACE_OUT; } diff --git a/native/python/pyjp_method.cpp b/native/python/pyjp_method.cpp index 2f46302e3..facf2a994 100644 --- a/native/python/pyjp_method.cpp +++ b/native/python/pyjp_method.cpp @@ -96,18 +96,15 @@ static PyObject *PyJPMethod_call(PyJPMethod *self, PyObject *args, PyObject *kwa // Clear any pending interrupts if we are on the main thread if (hasInterrupt()) frame.clearInterrupt(false); - bool caller = true; - if (kwargs != nullptr && PyDict_GetItemString(kwargs, "caller") == Py_False) - caller = false; PyObject *out = nullptr; if (self->m_Instance == nullptr) { JPPyObjectVector vargs(args); - out = self->m_Method->invoke(frame, vargs, false, caller).keep(); + out = self->m_Method->invoke(frame, vargs, false).keep(); } else { JPPyObjectVector vargs(self->m_Instance, args); - out = self->m_Method->invoke(frame, vargs, true, caller).keep(); + out = self->m_Method->invoke(frame, vargs, true).keep(); } return out; JP_PY_CATCH(nullptr); // GCOVR_EXCL_LINE From 3844c8c0db1215e9202f908ec83c8de73bca637e Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 22:51:24 -0800 Subject: [PATCH 24/36] Whitespace --- native/common/jp_context.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/common/jp_context.cpp b/native/common/jp_context.cpp index 612f078b5..67c5526f9 100644 --- a/native/common/jp_context.cpp +++ b/native/common/jp_context.cpp @@ -239,7 +239,7 @@ void JPContext::initializeResources(JNIEnv* env, bool interrupt) // Set up methods after everything is start so we get better error // messages jclass reflectorClass = frame.FindClass("org/jpype/JPypeReflector"); - jfieldID reflectorField = frame.GetFieldID(contextClass, "reflector", "Lorg/jpype/JPypeReflector;"); + jfieldID reflectorField = frame.GetFieldID(contextClass, "reflector", "Lorg/jpype/JPypeReflector;"); m_Reflector = JPObjectRef(frame, frame.GetObjectField(m_JavaContext.get(), reflectorField)); m_CallMethodID = frame.GetMethodID(reflectorClass, "callMethod", "(Ljava/lang/reflect/Method;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"); From 25a2408580af92f4be6022471b3936775155ff05 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Thu, 28 Nov 2024 23:01:09 -0800 Subject: [PATCH 25/36] Jdk 1.8 build failed. Try again --- setupext/build_ext.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setupext/build_ext.py b/setupext/build_ext.py index a04b406c5..5016aac67 100644 --- a/setupext/build_ext.py +++ b/setupext/build_ext.py @@ -311,8 +311,7 @@ def build_java_ext(self, ext): cmd1 = shlex.split('%s -cp "%s" -d "%s" -g:none -source %s -target %s -encoding UTF-8' % (javac, classpath, build_dir, target_version, target_version)) cmd1.extend(ext.sources) - os.makedirs("build/classes", exist_ok=True) - os.makedirs("build/classes/META-INF", exist_ok=True) + os.makedirs("build/classes/META-INF/versions/0", exist_ok=True) self.announce(" %s" % " ".join(cmd1), level=distutils.log.INFO) subprocess.check_call(cmd1) From 39427984d13f5ac1b4668592809366998ff8565c Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Fri, 29 Nov 2024 09:07:27 -0800 Subject: [PATCH 26/36] Another fix for (broken) Windows --- jpype/_core.py | 3 ++- native/java/org/jpype/JPypeClassLoader.java | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/jpype/_core.py b/jpype/_core.py index 5015ba782..0a5be9ead 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -324,10 +324,11 @@ def startJVM( # ok, setup the jpype system classloader and add to the path after startup # this guarentees all classes have the same permissions as they did in the past + from urllib.parse import quote extra_jvm_args += [ '-Djava.system.class.loader=org.jpype.JPypeClassLoader', '-Djava.class.path=%s'%support_lib, - '-Djpype.class.path=%s'%classpath, + '-Djpype.class.path=%s'%quote(classpath), '-Xshare:off' ] else: diff --git a/native/java/org/jpype/JPypeClassLoader.java b/native/java/org/jpype/JPypeClassLoader.java index 003284ecd..6e628780e 100644 --- a/native/java/org/jpype/JPypeClassLoader.java +++ b/native/java/org/jpype/JPypeClassLoader.java @@ -5,6 +5,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -26,6 +27,9 @@ import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.net.URLDecoder; +import java.util.logging.Level; +import java.util.logging.Logger; public class JPypeClassLoader extends URLClassLoader { @@ -57,6 +61,14 @@ private static URL[] initial() String cp = System.getProperty("jpype.class.path"); if (cp == null) return new URL[0]; + + try + { + cp = URLDecoder.decode(cp, "UTF-8"); + } catch (UnsupportedEncodingException ex) + { + // ignored + } ArrayList path = new ArrayList<>(); int last = 0; From 9b15bd8ed1ff5fe4a768b3b52e174172bb2ad8a2 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Fri, 29 Nov 2024 09:51:20 -0800 Subject: [PATCH 27/36] Fix setupext for JDK 8 --- jpype/_core.py | 2 +- setupext/build_ext.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jpype/_core.py b/jpype/_core.py index 0a5be9ead..f58ddb1dd 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -301,7 +301,7 @@ def startJVM( java_class_path = _expandClassPath(classpath) java_class_path.append(support_lib) - java_class_path = filter(len, java_class_path) + java_class_path = list(filter(len, java_class_path)) classpath = _classpath._SEP.join(java_class_path) # Make sure our module is always on the classpath diff --git a/setupext/build_ext.py b/setupext/build_ext.py index 5016aac67..28b6d6b39 100644 --- a/setupext/build_ext.py +++ b/setupext/build_ext.py @@ -311,13 +311,13 @@ def build_java_ext(self, ext): cmd1 = shlex.split('%s -cp "%s" -d "%s" -g:none -source %s -target %s -encoding UTF-8' % (javac, classpath, build_dir, target_version, target_version)) cmd1.extend(ext.sources) - os.makedirs("build/classes/META-INF/versions/0", exist_ok=True) self.announce(" %s" % " ".join(cmd1), level=distutils.log.INFO) subprocess.check_call(cmd1) cmd1 = shlex.split('%s -cp "%s" -d "%s" -g:none -source %s -target %s -encoding UTF-8' % - (javac, build_dir, build_dir+"/META-INF/versions/0", target_version, target_version)) - cmd1.extend(["native/java0/org/jpype/Reflector0.java"]) + (javac, build_dir, os.path.join(build_dir,"META-INF","versions","0"), target_version, target_version)) + cmd1.extend(glob.glob(os.path.join("native","java0","**","*.java"), recursive=True)) + os.makedirs(os.path.join(build_dir,"META-INF","versions","0"), exist_ok=True) self.announce(" %s" % " ".join(cmd1), level=distutils.log.INFO) subprocess.check_call(cmd1) From 3a5d118edadb4fbc9cb5c016b410c14208e3b178 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Fri, 29 Nov 2024 10:06:23 -0800 Subject: [PATCH 28/36] Cleanup pass --- jpype/_core.py | 2 +- native/java/manifest.txt | 1 - native/java/org/jpype/JPypeContext.java | 31 +-------------------- native/java/org/jpype/agent/JPypeAgent.java | 11 -------- test/jpypetest/common.py | 3 +- 5 files changed, 3 insertions(+), 45 deletions(-) delete mode 100644 native/java/org/jpype/agent/JPypeAgent.java diff --git a/jpype/_core.py b/jpype/_core.py index f58ddb1dd..7a2ff30d2 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -171,7 +171,7 @@ def _findTemp(): if dirname and dirname.isascii(): dirlist.append(dirname) if os.name == 'nt': - for envname in [ os.path.expanduser(r'~\AppData\Local\Temp'), + for dirname in [ os.path.expanduser(r'~\AppData\Local\Temp'), os.path.expandvars(r'%SYSTEMROOT%\Temp'), r'c:\temp', r'c:\tmp', r'\temp', r'\tmp' ]: if dirname and dirname.isascii(): diff --git a/native/java/manifest.txt b/native/java/manifest.txt index cc3a0089c..fa49425d5 100644 --- a/native/java/manifest.txt +++ b/native/java/manifest.txt @@ -2,4 +2,3 @@ Manifest-Version: 1.0 Specification-Title: org.jpype Implementation-Title: org.jpype Implementation-Version: 1.5.1 -Premain-Class: org.jpype.agent.JPypeAgent diff --git a/native/java/org/jpype/JPypeContext.java b/native/java/org/jpype/JPypeContext.java index d4fcc00be..40c93fd06 100644 --- a/native/java/org/jpype/JPypeContext.java +++ b/native/java/org/jpype/JPypeContext.java @@ -27,8 +27,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; import org.jpype.manager.TypeFactory; import org.jpype.manager.TypeFactoryNative; import org.jpype.manager.TypeManager; @@ -73,7 +71,7 @@ public class JPypeContext public final String VERSION = "1.5.2.dev0"; - private static JPypeContext INSTANCE = new JPypeContext(); + private static final JPypeContext INSTANCE = new JPypeContext(); // This is the C++ portion of the context. private long context; private TypeFactory typeFactory; @@ -333,33 +331,6 @@ public void _addPost(Runnable run) { this.postHooks.add(run); } - - /** - * Call a method using reflection. - * - * This method creates a stackframe so that caller sensitive methods will - * execute properly. - * - * @param method is the method to call. - * @param obj is the object to operate on, it will be null if the method is - * static. - * @param args the arguments to method. - * @return the object that results form the invocation. - * @throws java.lang.Throwable throws whatever type the called method - * produces. - */ - public Object callMethod(Method method, Object obj, Object[] args) - throws Throwable - { - try - { - return method.invoke(obj, args); - } catch (InvocationTargetException ex) - { -// ex.printStackTrace(); - throw ex.getCause(); - } - } /** * Helper function for collect rectangular, diff --git a/native/java/org/jpype/agent/JPypeAgent.java b/native/java/org/jpype/agent/JPypeAgent.java deleted file mode 100644 index c15ac7334..000000000 --- a/native/java/org/jpype/agent/JPypeAgent.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.jpype.agent; - -import java.lang.instrument.Instrumentation; - -public class JPypeAgent -{ - public static void premain(String agentArgs, Instrumentation inst) { - // This doesn't have to do anything. - // We just need to be an agent to load elevated privileges - } -} diff --git a/test/jpypetest/common.py b/test/jpypetest/common.py index 9d3d9764d..8494ff568 100644 --- a/test/jpypetest/common.py +++ b/test/jpypetest/common.py @@ -74,8 +74,7 @@ def f(self): if root.isascii(): return func(self) except ImportError: - pass - raise unittest.SkipTest("Ascii root directory required") + raise unittest.SkipTest("Ascii root directory required") return f class UseFunc(object): From f38b427acb728a1ccbfa2fc44223d8f20e671aa9 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Fri, 29 Nov 2024 10:10:18 -0800 Subject: [PATCH 29/36] Cleanup and test --- jpype/_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jpype/_core.py b/jpype/_core.py index 5015ba782..88053eb98 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -179,13 +179,13 @@ def _findTemp(): else: dirlist.extend([ '/tmp', '/var/tmp', '/usr/tmp' ]) - name = str(os.getpid) + name = str(os.getpid()) for d in dirlist: p = Path("%s/%s"%(d,name)) try: p.touch() p.unlink() - except Exception: + except Exception as ex: continue return d raise SystemError("Unable to find non-ansii path") From 37647491e3f81bbbcb5af2bd49d3e62cc1ec88e0 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Fri, 29 Nov 2024 15:17:13 -0800 Subject: [PATCH 30/36] Try again for delete. --- jpype/_core.py | 19 +++++++------- native/common/jp_context.cpp | 47 ++++++++++++++++++++++++++++++++--- native/python/pyjp_module.cpp | 32 ++++++++++++++++++++++-- 3 files changed, 83 insertions(+), 15 deletions(-) diff --git a/jpype/_core.py b/jpype/_core.py index 33ab4f10d..20e520278 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -158,7 +158,6 @@ def _expandClassPath( _JVM_started = False -_tmp = None def interactive(): return bool(getattr(sys, 'ps1', sys.flags.interactive)) @@ -303,6 +302,7 @@ def startJVM( java_class_path.append(support_lib) java_class_path = list(filter(len, java_class_path)) classpath = _classpath._SEP.join(java_class_path) + tmp = None # Make sure our module is always on the classpath if not classpath.isascii(): @@ -314,13 +314,14 @@ def startJVM( if not support_lib.isascii(): import tempfile import shutil - global _tmp - _tmp = tempfile.TemporaryDirectory(dir = _findTemp()) - if not _tmp.name.isascii(): - raise ValueError("Unable to find ascii temp directory. Clear TEMPDIR, TEMP, and TMP environment variables") - sl2 = os.path.join(_tmp.name, "org.jpype.jar") - shutil.copyfile(support_lib, sl2) - support_lib = sl2 + fd, path = tempfile.mkstemp(dir = _findTemp()) + if not path.isascii(): + raise ValueError("Unable to find ascii temp directory.") + shutil.copyfile(support_lib, path) + support_lib = path + tmp = path + os.close(fd) + # Don't remove # ok, setup the jpype system classloader and add to the path after startup # this guarentees all classes have the same permissions as they did in the past @@ -343,7 +344,7 @@ def startJVM( prior = [locale.getlocale(i) for i in categories] # Start the JVM _jpype.startup(jvmpath, tuple(jvm_args + extra_jvm_args), - ignoreUnrecognized, convertStrings, interrupt) + ignoreUnrecognized, convertStrings, interrupt, tmp) # Collect required resources for operation initializeResources() # Restore locale diff --git a/native/common/jp_context.cpp b/native/common/jp_context.cpp index 67c5526f9..eaf6e44c6 100644 --- a/native/common/jp_context.cpp +++ b/native/common/jp_context.cpp @@ -22,6 +22,18 @@ #include "jp_platform.h" #include "jp_gc.h" +#ifdef WIN32 +#include +#else +#if defined(_HPUX) && !defined(_IA64) +#include +#else +#include +#endif // HPUX +#include +#endif + + JPResource::~JPResource() = default; @@ -159,6 +171,35 @@ void JPContext::attachJVM(JNIEnv* env) initializeResources(env, false); } +std::string getShared() +{ +#ifdef WIN32 + // Windows specific + char path[MAX_PATH]; + HMODULE hm = NULL; + if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCSTR) &getShared, &hm) != 0 && + GetModuleFileName(hm, path, sizeof(path)) != 0) + { + // This is needed when there is no-ascii characters in path + char shortPathBuffer[MAX_PATH]; + GetShortPathName(path, shortPathBuffer, MAX_PATH); + return shortPathBuffer; + } +#else + // Linux specific + Dl_info info; + if (dladdr((void*)getShared, &info)) + return info.dli_fname; +#endif + // Generic + JPPyObject import = JPPyObject::use(PyImport_AddModule("importlib.util")); + JPPyObject jpype = JPPyObject::call(PyObject_CallMethod(import.get(), "find_spec", "s", "_jpype")); + JPPyObject origin = JPPyObject::call(PyObject_GetAttrString(jpype.get(), "origin")); + return JPPyString::asStringUTF8(origin.get()); +} + void JPContext::initializeResources(JNIEnv* env, bool interrupt) { JPJavaFrame frame = JPJavaFrame::external(this, env); @@ -215,10 +256,8 @@ void JPContext::initializeResources(JNIEnv* env, bool interrupt) if (!m_Embedded) { - JPPyObject import = JPPyObject::use(PyImport_AddModule("importlib.util")); - JPPyObject jpype = JPPyObject::call(PyObject_CallMethod(import.get(), "find_spec", "s", "_jpype")); - JPPyObject origin = JPPyObject::call(PyObject_GetAttrString(jpype.get(), "origin")); - val[2].l = frame.fromStringUTF8(JPPyString::asStringUTF8(origin.get())); + std::string shared = getShared(); + val[2].l = frame.fromStringUTF8(shared); } // Required before launch diff --git a/native/python/pyjp_module.cpp b/native/python/pyjp_module.cpp index 338e92d24..2999689c1 100644 --- a/native/python/pyjp_module.cpp +++ b/native/python/pyjp_module.cpp @@ -20,6 +20,9 @@ #include "jp_gc.h" #include "jp_stringtype.h" #include "jp_classloader.h" +#ifdef WIN32 +#include +#endif void PyJPModule_installGC(PyObject* module); @@ -229,6 +232,7 @@ int PyJP_IsInstanceSingle(PyObject* obj, PyTypeObject* type) #ifndef ANDROID extern JNIEnv *Android_JNI_GetEnv(); +static string jarTmpPath; static PyObject* PyJPModule_startup(PyObject* module, PyObject* pyargs) { JP_PY_TRY("PyJPModule_startup"); @@ -238,11 +242,23 @@ static PyObject* PyJPModule_startup(PyObject* module, PyObject* pyargs) char ignoreUnrecognized = true; char convertStrings = false; char interrupt = false; + PyObject* tmp; - if (!PyArg_ParseTuple(pyargs, "OO!bbb", &vmPath, &PyTuple_Type, &vmOpt, - &ignoreUnrecognized, &convertStrings, &interrupt)) + if (!PyArg_ParseTuple(pyargs, "OO!bbbO", &vmPath, &PyTuple_Type, &vmOpt, + &ignoreUnrecognized, &convertStrings, &interrupt, &tmp)) return nullptr; + if (tmp != Py_None) + { + if (!(JPPyString::check(tmp))) + { + PyErr_SetString(PyExc_TypeError, "Java jar path must be a string"); + return nullptr; + } + jarTmpPath = JPPyString::asStringUTF8(tmp); + } + + if (!(JPPyString::check(vmPath))) { PyErr_SetString(PyExc_TypeError, "Java JVM path must be a string"); @@ -298,6 +314,18 @@ static PyObject* PyJPModule_shutdown(PyObject* obj, PyObject* pyargs, PyObject* return nullptr; JPContext_global->shutdownJVM(destroyJVM, freeJVM); + +#ifdef WIN32 + // Thus far this doesn't work on WINDOWS. The issue is a bug in the JVM + // is holding the file open and there is no apparent method to close it + // so that this can succeed + if (jarTmpPath != "") + remove(jarTmpPath.c_str()); +#else + if (jarTmpPath != "") + unlink(jarTmpPath.c_str()); +#endif + Py_RETURN_NONE; JP_PY_CATCH(nullptr); } From 67f732d436f7a3de44520af55071ddc44c996b14 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Fri, 29 Nov 2024 15:26:07 -0800 Subject: [PATCH 31/36] Fix broken test --- test/jpypetest/test_fault.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/jpypetest/test_fault.py b/test/jpypetest/test_fault.py index c84ab3e30..7c0853def 100644 --- a/test/jpypetest/test_fault.py +++ b/test/jpypetest/test_fault.py @@ -1083,13 +1083,15 @@ def testStartupBadArg(self): with self.assertRaisesRegex(TypeError, "takes exactly"): _jpype.startup() with self.assertRaisesRegex(TypeError, "must be tuple"): - _jpype.startup(object(), object(), True, True, True) + _jpype.startup(object(), object(), True, True, True, None) with self.assertRaisesRegex(TypeError, "must be strings"): - _jpype.startup("", (object(),), True, True, True) + _jpype.startup("", (object(),), True, True, True, None) with self.assertRaisesRegex(TypeError, "must be a string"): - _jpype.startup(object(), tuple(), True, True, True) + _jpype.startup(object(), tuple(), True, True, True, None) + with self.assertRaisesRegex(TypeError, "must be a string"): + _jpype.startup("", tuple(), True, True, True, object()) with self.assertRaisesRegex(OSError, "started"): - _jpype.startup("", tuple(), True, True, True) + _jpype.startup("", tuple(), True, True, True, None) def testGetClass(self): with self.assertRaisesRegex(TypeError, "not found"): From e95010f3a39041b276088c731ab4ffc48f5150d3 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Sat, 30 Nov 2024 11:19:42 -0800 Subject: [PATCH 32/36] Cleanup and doc --- native/java/org/jpype/JPypeClassLoader.java | 39 +++++++++++-- native/java/org/jpype/JPypeContext.java | 61 ++++++++++++--------- 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/native/java/org/jpype/JPypeClassLoader.java b/native/java/org/jpype/JPypeClassLoader.java index 6e628780e..fc4e3f2ad 100644 --- a/native/java/org/jpype/JPypeClassLoader.java +++ b/native/java/org/jpype/JPypeClassLoader.java @@ -28,9 +28,13 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.net.URLDecoder; -import java.util.logging.Level; -import java.util.logging.Logger; +/** + * Class loader for JPype. + * + * This is augmented to manage directory resources, allow for late loading, + * and handling of resources on non-ASCII paths. + */ public class JPypeClassLoader extends URLClassLoader { @@ -42,6 +46,11 @@ public JPypeClassLoader(ClassLoader parent) super(initial(), parent); } + /** + * Used to keep the cache up to date. + * + * @return + */ public int getCode() { return code; @@ -58,13 +67,14 @@ public int getCode() */ private static URL[] initial() { + // Check to see if we have a late loaded path String cp = System.getProperty("jpype.class.path"); if (cp == null) return new URL[0]; - + try { - cp = URLDecoder.decode(cp, "UTF-8"); + cp = URLDecoder.decode(cp, "UTF-8"); } catch (UnsupportedEncodingException ex) { // ignored @@ -100,7 +110,7 @@ private static URL[] initial() return path.toArray(new URL[0]); } - // this is required to add a Java agent even if it is already in the path + // This is required to add a Java agent even if it is already in the path @SuppressWarnings("unused") private void appendToClassPathForInstrumentation(String path) throws Throwable { @@ -143,6 +153,12 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) } + /** + * Add a path to the loader after the JVM is started. + * + * @param path + * @throws FileNotFoundException + */ public void addPath(Path path) throws FileNotFoundException { try @@ -229,6 +245,15 @@ public Enumeration findResources(String name) throws IOException return Collections.enumeration(out); } + /** + * Add a resource to the search. + * + * Many jar files lack directory support which is needed for the packaging + * import. + * + * @param name + * @param url + */ public void addResource(String name, URL url) { if (!this.map.containsKey(name)) @@ -241,7 +266,11 @@ public void addURL(URL url) { // Mark our cache as dirty code = code * 98745623 + url.hashCode(); + + // add to the search tree super.addURL(url); + + // See if it is a path Path path; try { diff --git a/native/java/org/jpype/JPypeContext.java b/native/java/org/jpype/JPypeContext.java index 40c93fd06..9561a8fe2 100644 --- a/native/java/org/jpype/JPypeContext.java +++ b/native/java/org/jpype/JPypeContext.java @@ -95,39 +95,46 @@ static public JPypeContext getInstance() * @param loader is the classloader holding JPype resources. * @return the created context. */ - static JPypeContext createContext(long context, ClassLoader loader, String nativeLib, boolean interrupt) + private static JPypeContext createContext(long context, ClassLoader loader, String nativeLib, boolean interrupt) throws Throwable { - if (nativeLib != null) - { - System.load(nativeLib); - } - INSTANCE.context = context; - INSTANCE.classLoader = (JPypeClassLoader) loader; - INSTANCE.typeFactory = new TypeFactoryNative(); - INSTANCE.typeManager = new TypeManager(context, INSTANCE.typeFactory); - INSTANCE.initialize(interrupt); - try { - INSTANCE.reflector = (JPypeReflector) Class.forName("org.jpype.Reflector0", true, loader) - .getConstructor() - .newInstance(); - } catch (ClassNotFoundException | NoSuchMethodException | SecurityException - | InstantiationException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException ex) + if (nativeLib != null) + { + System.load(nativeLib); + } + INSTANCE.context = context; + INSTANCE.classLoader = (JPypeClassLoader) loader; + INSTANCE.typeFactory = new TypeFactoryNative(); + INSTANCE.typeManager = new TypeManager(context, INSTANCE.typeFactory); + INSTANCE.initialize(interrupt); + + try + { + INSTANCE.reflector = (JPypeReflector) Class.forName("org.jpype.Reflector0", true, loader) + .getConstructor() + .newInstance(); + } catch (ClassNotFoundException | NoSuchMethodException | SecurityException + | InstantiationException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException ex) + { + throw new RuntimeException("Unable to create reflector "+ ex.getMessage(), ex); + } + + scanExistingJars(); + return INSTANCE; + } catch (Throwable ex) { - System.err.println("Unable to create reflector "+ ex); + ex.printStackTrace(System.err); + throw ex; } - - scanExistingJars(); - return INSTANCE; } private JPypeContext() { } - void initialize(boolean interrupt) + private void initialize(boolean interrupt) { // Okay everything is setup so lets give it a go. this.typeManager.init(); @@ -278,7 +285,7 @@ private void shutdown() } - static native void onShutdown(long ctxt); + private static native void onShutdown(long ctxt); public void addShutdownHook(Thread th) { @@ -331,7 +338,7 @@ public void _addPost(Runnable run) { this.postHooks.add(run); } - + /** * Helper function for collect rectangular, */ @@ -426,7 +433,7 @@ private Object unpack(int size, Object parts) return a1; } - public Object assemble(int[] dims, Object parts) + private Object assemble(int[] dims, Object parts) { int n = dims.length; if (n == 1) @@ -497,12 +504,12 @@ public long getExcValue(Throwable th) return 0; } - public Exception createException(long l0, long l1) + private Exception createException(long l0, long l1) { return new PyExceptionProxy(l0, l1); } - public boolean order(Buffer b) + private boolean order(Buffer b) { if (b instanceof java.nio.ByteBuffer) return ((java.nio.ByteBuffer) b).order() == ByteOrder.LITTLE_ENDIAN; From fad418d25798eb1771ccf8f6f9c3647a253e22c5 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Mon, 2 Dec 2024 13:44:33 -0800 Subject: [PATCH 33/36] Work on module support --- .gitignore | 1 + jpype/_core.py | 26 ++-- project/jpype_java/nb-configuration.xml | 38 ++++++ project/jpype_java/pom.xml | 126 ++++++++++++++++++ .../jpype_java/src/main/java/module-info.java | 17 +++ .../{jpype_java => jpype_java_old}/build.xml | 0 .../nbproject/license.txt | 0 .../nbproject/project.properties | 0 .../nbproject/project.xml | 0 .../test/org/jpype/manager/TestDefault.java | 0 .../jpype/manager/TestMethodResolution.java | 0 .../org/jpype/manager/TestTypeManager.java | 0 .../org/jpype/manager/TypeFactoryHarness.java | 0 .../org/jpype/pkg/JPypePackageNGTest.java | 0 .../test/org/jpype/pkg/ListPackage.java | 0 15 files changed, 197 insertions(+), 11 deletions(-) create mode 100644 project/jpype_java/nb-configuration.xml create mode 100644 project/jpype_java/pom.xml create mode 100644 project/jpype_java/src/main/java/module-info.java rename project/{jpype_java => jpype_java_old}/build.xml (100%) rename project/{jpype_java => jpype_java_old}/nbproject/license.txt (100%) rename project/{jpype_java => jpype_java_old}/nbproject/project.properties (100%) rename project/{jpype_java => jpype_java_old}/nbproject/project.xml (100%) rename project/{jpype_java => jpype_java_old}/test/org/jpype/manager/TestDefault.java (100%) rename project/{jpype_java => jpype_java_old}/test/org/jpype/manager/TestMethodResolution.java (100%) rename project/{jpype_java => jpype_java_old}/test/org/jpype/manager/TestTypeManager.java (100%) rename project/{jpype_java => jpype_java_old}/test/org/jpype/manager/TypeFactoryHarness.java (100%) rename project/{jpype_java => jpype_java_old}/test/org/jpype/pkg/JPypePackageNGTest.java (100%) rename project/{jpype_java => jpype_java_old}/test/org/jpype/pkg/ListPackage.java (100%) diff --git a/.gitignore b/.gitignore index 171de2920..59234fe4d 100755 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,4 @@ jacoco/ wheelhouse/ vc*.pdb *.class +/project/jpype_java_old/nbproject/private/ diff --git a/jpype/_core.py b/jpype/_core.py index 20e520278..c2ad8b6d0 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -194,6 +194,7 @@ def startJVM( *jvmargs: str, jvmpath: typing.Optional[_PathOrStr] = None, classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, + modulepath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, ignoreUnrecognized: bool = False, convertStrings: bool = False, interrupt: bool = not interactive(), @@ -216,6 +217,9 @@ def startJVM( classpath (str, PathLike, [str, PathLike]): Set the classpath for the JVM. This will override any classpath supplied in the arguments list. A value of None will give no classpath to JVM. + modulepath (str, PathLike, [str, PathLike]): Set the modulepath for the JVM. + This will override any modulepath supplied in the arguments + list. A value of None will give no classpath to JVM. ignoreUnrecognized (bool): Option to ignore invalid JVM arguments. Default is False. convertStrings (bool): Option to force Java strings to @@ -279,17 +283,17 @@ def startJVM( classpath = _classpath.getClassPath() # Code for 1.6 release when we add module support -# # Modulepath handling -# old_modulepath = _getOption(jvm_args, "--module-path", _classpath._SEP) -# if old_modulepath: -# # Old style, specified in the arguments -# if modulepath is not None: -# # Cannot apply both styles, conflict -# raise TypeError('modulepath specified twice') -# modulepath = old_modulepath -# if modulepath is not None: -# mp = _classpath._SEP.join(_expandClassPath(modulepath)) -# extra_jvm_args += ['--module-path=%s'%mp ] + # Modulepath handling + old_modulepath = _getOption(jvm_args, "--module-path", _classpath._SEP) + if old_modulepath: + # Old style, specified in the arguments + if modulepath is not None: + # Cannot apply both styles, conflict + raise TypeError('modulepath specified twice') + modulepath = old_modulepath + if modulepath is not None: + mp = _classpath._SEP.join(_expandClassPath(modulepath)) + extra_jvm_args += ['--module-path=%s'%mp ] # Get the support library support_lib = os.path.join(os.path.dirname(os.path.dirname(__file__)), "org.jpype.jar") diff --git a/project/jpype_java/nb-configuration.xml b/project/jpype_java/nb-configuration.xml new file mode 100644 index 000000000..5ae57930c --- /dev/null +++ b/project/jpype_java/nb-configuration.xml @@ -0,0 +1,38 @@ + + + + + + true + none + 2 + 2 + 8 + 80 + true + project + none + 2 + 2 + 8 + 80 + true + NEW_LINE + NEW_LINE + LEAVE_ALONE + LEAVE_ALONE + NEW_LINE + LEAVE_ALONE + NEW_LINE + + diff --git a/project/jpype_java/pom.xml b/project/jpype_java/pom.xml new file mode 100644 index 000000000..3dbe9b759 --- /dev/null +++ b/project/jpype_java/pom.xml @@ -0,0 +1,126 @@ + + + 4.0.0 + org.jpype + org.jpype + 1.5.1 + jar + + UTF-8 + 9 + 9 + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + add-source + generate-sources + + add-source + + + + ../../native/java + + + + + + + + maven-resources-plugin + 3.3.1 + + + copy-resources + validate + + copy-resources + + + ${basedir}/target/classes/org/jpype/html + true + + + ../../native/java/org/jpype/html + true + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + compile-java-9 + + compile + + + 9 + 9 + + ../../native/java + + + + + + compile-java-0 + compile + + compile + + + 9 + + ../../native/java0 + + ${project.build.outputDirectory}/META-INF/versions/0 + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + + + true + lib/ + + true + + true + + + + + + + + + + \ No newline at end of file diff --git a/project/jpype_java/src/main/java/module-info.java b/project/jpype_java/src/main/java/module-info.java new file mode 100644 index 000000000..9eedec00c --- /dev/null +++ b/project/jpype_java/src/main/java/module-info.java @@ -0,0 +1,17 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/module-info.java to edit this template + */ + +module org.jpype { + requires java.sql; + + exports org.jpype; + exports org.jpype.manager; + exports org.jpype.html; + exports org.jpype.javadoc; + exports org.jpype.pickle; + exports org.jpype.pkg; + exports org.jpype.proxy; + exports org.jpype.ref; +} diff --git a/project/jpype_java/build.xml b/project/jpype_java_old/build.xml similarity index 100% rename from project/jpype_java/build.xml rename to project/jpype_java_old/build.xml diff --git a/project/jpype_java/nbproject/license.txt b/project/jpype_java_old/nbproject/license.txt similarity index 100% rename from project/jpype_java/nbproject/license.txt rename to project/jpype_java_old/nbproject/license.txt diff --git a/project/jpype_java/nbproject/project.properties b/project/jpype_java_old/nbproject/project.properties similarity index 100% rename from project/jpype_java/nbproject/project.properties rename to project/jpype_java_old/nbproject/project.properties diff --git a/project/jpype_java/nbproject/project.xml b/project/jpype_java_old/nbproject/project.xml similarity index 100% rename from project/jpype_java/nbproject/project.xml rename to project/jpype_java_old/nbproject/project.xml diff --git a/project/jpype_java/test/org/jpype/manager/TestDefault.java b/project/jpype_java_old/test/org/jpype/manager/TestDefault.java similarity index 100% rename from project/jpype_java/test/org/jpype/manager/TestDefault.java rename to project/jpype_java_old/test/org/jpype/manager/TestDefault.java diff --git a/project/jpype_java/test/org/jpype/manager/TestMethodResolution.java b/project/jpype_java_old/test/org/jpype/manager/TestMethodResolution.java similarity index 100% rename from project/jpype_java/test/org/jpype/manager/TestMethodResolution.java rename to project/jpype_java_old/test/org/jpype/manager/TestMethodResolution.java diff --git a/project/jpype_java/test/org/jpype/manager/TestTypeManager.java b/project/jpype_java_old/test/org/jpype/manager/TestTypeManager.java similarity index 100% rename from project/jpype_java/test/org/jpype/manager/TestTypeManager.java rename to project/jpype_java_old/test/org/jpype/manager/TestTypeManager.java diff --git a/project/jpype_java/test/org/jpype/manager/TypeFactoryHarness.java b/project/jpype_java_old/test/org/jpype/manager/TypeFactoryHarness.java similarity index 100% rename from project/jpype_java/test/org/jpype/manager/TypeFactoryHarness.java rename to project/jpype_java_old/test/org/jpype/manager/TypeFactoryHarness.java diff --git a/project/jpype_java/test/org/jpype/pkg/JPypePackageNGTest.java b/project/jpype_java_old/test/org/jpype/pkg/JPypePackageNGTest.java similarity index 100% rename from project/jpype_java/test/org/jpype/pkg/JPypePackageNGTest.java rename to project/jpype_java_old/test/org/jpype/pkg/JPypePackageNGTest.java diff --git a/project/jpype_java/test/org/jpype/pkg/ListPackage.java b/project/jpype_java_old/test/org/jpype/pkg/ListPackage.java similarity index 100% rename from project/jpype_java/test/org/jpype/pkg/ListPackage.java rename to project/jpype_java_old/test/org/jpype/pkg/ListPackage.java From b932bad2ff0cb4ef9853b2b0952d9ca9243b40bb Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Mon, 2 Dec 2024 13:47:20 -0800 Subject: [PATCH 34/36] Removing bean access as it requires java.management which is an extra requirement --- native/common/include/jp_gc.h | 1 - native/common/jp_gc.cpp | 2 -- native/java/org/jpype/JPypeContext.java | 6 +----- native/java/org/jpype/html/Html.java | 1 - 4 files changed, 1 insertion(+), 9 deletions(-) diff --git a/native/common/include/jp_gc.h b/native/common/include/jp_gc.h index cad182122..a50c83189 100644 --- a/native/common/include/jp_gc.h +++ b/native/common/include/jp_gc.h @@ -63,7 +63,6 @@ class JPGarbageCollection jmethodID _freeMemoryID; jmethodID _maxMemoryID; jmethodID _usedMemoryID; - jmethodID _heapMemoryID; size_t last_python; size_t last_java; diff --git a/native/common/jp_gc.cpp b/native/common/jp_gc.cpp index 6d2a47ce0..d2b6e04d0 100644 --- a/native/common/jp_gc.cpp +++ b/native/common/jp_gc.cpp @@ -160,7 +160,6 @@ void JPGarbageCollection::init(JPJavaFrame& frame) _freeMemoryID = frame.GetStaticMethodID(ctxt, "getFreeMemory", "()J"); _maxMemoryID = frame.GetStaticMethodID(ctxt, "getMaxMemory", "()J"); _usedMemoryID = frame.GetStaticMethodID(ctxt, "getUsedMemory", "()J"); - _heapMemoryID = frame.GetStaticMethodID(ctxt, "getHeapMemory", "()J"); running = true; high_water = getWorkingSize(); @@ -257,7 +256,6 @@ void JPGarbageCollection::onEnd() jlong freeMemory = frame.CallStaticLongMethodA(_ContextClass, _freeMemoryID, nullptr); jlong maxMemory = frame.CallStaticLongMethodA(_ContextClass, _maxMemoryID, nullptr); jlong usedMemory = frame.CallStaticLongMethodA(_ContextClass, _usedMemoryID, nullptr); - jlong heapMemory = frame.CallStaticLongMethodA(_ContextClass, _heapMemoryID, nullptr); printf("consider gc run=%d (current=%ld, low=%ld, high=%ld, limit=%ld) %ld\n", run_gc, current, low_water, high_water, limit, limit - pred); printf(" java total=%ld free=%ld max=%ld used=%ld heap=%ld\n", totalMemory, freeMemory, maxMemory, usedMemory, heapMemory); diff --git a/native/java/org/jpype/JPypeContext.java b/native/java/org/jpype/JPypeContext.java index 9561a8fe2..878b4957d 100644 --- a/native/java/org/jpype/JPypeContext.java +++ b/native/java/org/jpype/JPypeContext.java @@ -639,9 +639,5 @@ private static long getUsedMemory() return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); } - private static long getHeapMemory() - { - java.lang.management.MemoryMXBean memoryBean = java.lang.management.ManagementFactory.getMemoryMXBean(); - return memoryBean.getHeapMemoryUsage().getUsed(); - } + } diff --git a/native/java/org/jpype/html/Html.java b/native/java/org/jpype/html/Html.java index e97a02af3..ad48fba95 100644 --- a/native/java/org/jpype/html/Html.java +++ b/native/java/org/jpype/html/Html.java @@ -25,7 +25,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import org.jpype.JPypeContext; import org.w3c.dom.Attr; import org.w3c.dom.Document; From 773ec4069a2a22e16419375bc8a72569768a24b5 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Mon, 2 Dec 2024 14:54:20 -0800 Subject: [PATCH 35/36] Working on loading into module --- jpype/_core.py | 62 +- project/jpype_java/nb-configuration.xml | 1 + .../src/main/java/org/jpype/JPypeMain.java | 18 + .../jpype_java_old/nbproject/build-impl.xml | 1781 +++++++++++++++++ .../nbproject/genfiles.properties | 5 + .../nbproject/project.properties | 7 +- 6 files changed, 1847 insertions(+), 27 deletions(-) create mode 100644 project/jpype_java/src/main/java/org/jpype/JPypeMain.java create mode 100644 project/jpype_java_old/nbproject/build-impl.xml create mode 100644 project/jpype_java_old/nbproject/genfiles.properties diff --git a/jpype/_core.py b/jpype/_core.py index c2ad8b6d0..fc727645d 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -130,6 +130,8 @@ def _expandClassPath( Return a classpath which represents the given tuple of classpath specifications """ out: list[str] = [] + if classpath is None: + return out if isinstance(classpath, (str, os.PathLike)): classpath = (classpath,) try: @@ -195,6 +197,7 @@ def startJVM( jvmpath: typing.Optional[_PathOrStr] = None, classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, modulepath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, + modules: typing.Union[typing.Sequence[str], str, None] = None, ignoreUnrecognized: bool = False, convertStrings: bool = False, interrupt: bool = not interactive(), @@ -270,6 +273,24 @@ def startJVM( # Handle strings and list of strings. extra_jvm_args: list[str] = [] + # Get the support library + support_lib = os.path.join(os.path.dirname(os.path.dirname(__file__)), "org.jpype.jar") + if not os.path.exists(support_lib): + raise RuntimeError("Unable to find org.jpype.jar support library at " + support_lib) + + # If we are not installed on an ascii path then we will need to copy the jar to a new location + if not support_lib.isascii(): + import tempfile + import shutil + fd, path = tempfile.mkstemp(dir = _findTemp()) + if not path.isascii(): + raise ValueError("Unable to find ascii temp directory.") + shutil.copyfile(support_lib, path) + support_lib = path + tmp = path + os.close(fd) + # Don't remove + # Classpath handling old_classpath = _getOption(jvm_args, "-Djava.class.path", _classpath._SEP) if old_classpath: @@ -282,7 +303,6 @@ def startJVM( # Not specified at all, use the default classpath. classpath = _classpath.getClassPath() -# Code for 1.6 release when we add module support # Modulepath handling old_modulepath = _getOption(jvm_args, "--module-path", _classpath._SEP) if old_modulepath: @@ -291,19 +311,22 @@ def startJVM( # Cannot apply both styles, conflict raise TypeError('modulepath specified twice') modulepath = old_modulepath - if modulepath is not None: - mp = _classpath._SEP.join(_expandClassPath(modulepath)) - extra_jvm_args += ['--module-path=%s'%mp ] - # Get the support library - support_lib = os.path.join(os.path.dirname(os.path.dirname(__file__)), "org.jpype.jar") - if not os.path.exists(support_lib): - raise RuntimeError("Unable to find org.jpype.jar support library at " + support_lib) + # Modules + old_modules = _getOption(jvm_args, "--add-modules", ',') + if old_modules: + # Old style, specified in the arguments + if modules is not None: + # Cannot apply both styles, conflict + raise TypeError('modules specified twice') + modules = old_modules + if modules is None: + modules = [] + modules.append("org.jpype") system_class_loader = _getOption(jvm_args, "-Djava.system.class.loader", keep=True) java_class_path = _expandClassPath(classpath) - java_class_path.append(support_lib) java_class_path = list(filter(len, java_class_path)) classpath = _classpath._SEP.join(java_class_path) tmp = None @@ -314,19 +337,6 @@ def startJVM( # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 raise ValueError("system classloader cannot be specified with non ascii characters in the classpath") - # If we are not installed on an ascii path then we will need to copy the jar to a new location - if not support_lib.isascii(): - import tempfile - import shutil - fd, path = tempfile.mkstemp(dir = _findTemp()) - if not path.isascii(): - raise ValueError("Unable to find ascii temp directory.") - shutil.copyfile(support_lib, path) - support_lib = path - tmp = path - os.close(fd) - # Don't remove - # ok, setup the jpype system classloader and add to the path after startup # this guarentees all classes have the same permissions as they did in the past from urllib.parse import quote @@ -338,7 +348,13 @@ def startJVM( ] else: # no problems - extra_jvm_args += ['-Djava.class.path=%s'%classpath ] + if classpath: + extra_jvm_args += ['-Djava.class.path=%s'%classpath ] + + mp =_expandClassPath(modulepath) + mp.append(support_lib) + extra_jvm_args += ['--module-path=%s'%_classpath._SEP.join(mp) ] + extra_jvm_args += ['--add-modules=%s'%(",".join(modules))] try: import locale diff --git a/project/jpype_java/nb-configuration.xml b/project/jpype_java/nb-configuration.xml index 5ae57930c..c4b9dd492 100644 --- a/project/jpype_java/nb-configuration.xml +++ b/project/jpype_java/nb-configuration.xml @@ -34,5 +34,6 @@ Any value defined here will override the pom.xml file value but is only applicab NEW_LINE LEAVE_ALONE NEW_LINE + true diff --git a/project/jpype_java/src/main/java/org/jpype/JPypeMain.java b/project/jpype_java/src/main/java/org/jpype/JPypeMain.java new file mode 100644 index 000000000..1bea06a1a --- /dev/null +++ b/project/jpype_java/src/main/java/org/jpype/JPypeMain.java @@ -0,0 +1,18 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package org.jpype; + +/** + * + * @author nelson85 + */ +public class JPypeMain +{ + public static void main(String[] args) + { + JPypeMain main = new JPypeMain(); + System.out.println(main.getClass().getModule()); + } +} diff --git a/project/jpype_java_old/nbproject/build-impl.xml b/project/jpype_java_old/nbproject/build-impl.xml new file mode 100644 index 000000000..f0ef9b9ba --- /dev/null +++ b/project/jpype_java_old/nbproject/build-impl.xml @@ -0,0 +1,1781 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set src.java.dir + Must set test.src.dir + Must set test.harness.dir + Must set build.dir + Must set dist.dir + Must set build.classes.dir + Must set dist.javadoc.dir + Must set build.test.classes.dir + Must set build.test.results.dir + Must set build.classes.excludes + Must set dist.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tests executed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set JVM to use for profiling in profiler.info.jvm + Must set profiler agent JVM arguments in profiler.info.jvmargs.agent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To run this application from the command line without Ant, try: + + java -jar "${dist.jar.resolved}" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + Must select one file in the IDE or set run.class + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set debug.class + + + + + Must select one file in the IDE or set debug.class + + + + + Must set fix.includes + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + Must select one file in the IDE or set profile.class + This target only works when run from inside the NetBeans IDE. + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + + + Must select some files in the IDE or set test.includes + + + + + Must select one file in the IDE or set run.class + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + Some tests failed; see details above. + + + + + + + + + Must select some files in the IDE or set test.includes + + + + Some tests failed; see details above. + + + + Must select some files in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + Some tests failed; see details above. + + + + + Must select one file in the IDE or set test.class + + + + Must select one file in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + + + + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project/jpype_java_old/nbproject/genfiles.properties b/project/jpype_java_old/nbproject/genfiles.properties new file mode 100644 index 000000000..de59bcefd --- /dev/null +++ b/project/jpype_java_old/nbproject/genfiles.properties @@ -0,0 +1,5 @@ +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=07f287a1 +nbproject/build-impl.xml.script.CRC32=dd1dbfbf +nbproject/build-impl.xml.stylesheet.CRC32=12e0a6c2@1.109.0.48 diff --git a/project/jpype_java_old/nbproject/project.properties b/project/jpype_java_old/nbproject/project.properties index 3ec6b403a..bb5767b6e 100755 --- a/project/jpype_java_old/nbproject/project.properties +++ b/project/jpype_java_old/nbproject/project.properties @@ -71,10 +71,10 @@ javac.source=1.8 javac.target=1.8 javac.test.classpath=\ ${javac.classpath}:\ - ${build.classes.dir}:\ ${libs.testng.classpath} javac.test.modulepath=\ - ${javac.modulepath} + ${javac.modulepath}:\ + ${build.classes.dir} javac.test.processorpath=\ ${javac.test.classpath} javadoc.additionalparam= @@ -105,8 +105,7 @@ run.jvmargs= run.modulepath=\ ${javac.modulepath} run.test.classpath=\ - ${javac.test.classpath}:\ - ${build.test.classes.dir} + ${javac.test.classpath} run.test.modulepath=\ ${javac.test.modulepath} source.encoding=UTF-8 From 13107ae9882508e224c0348fafe0508cf46d2921 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Mon, 2 Dec 2024 15:16:53 -0800 Subject: [PATCH 36/36] Work on clean up and build --- native/java/module-info.java | 28 +++++++++++++++++++ native/java/org/jpype/JPypeClassLoader.java | 15 ++++++++++ .../jpype_java/src/main/java/module-info.java | 17 ----------- .../src/main/java/org/jpype/JPypeMain.java | 18 ------------ setupext/build_ext.py | 2 +- 5 files changed, 44 insertions(+), 36 deletions(-) create mode 100644 native/java/module-info.java delete mode 100644 project/jpype_java/src/main/java/module-info.java delete mode 100644 project/jpype_java/src/main/java/org/jpype/JPypeMain.java diff --git a/native/java/module-info.java b/native/java/module-info.java new file mode 100644 index 000000000..a8e2535f0 --- /dev/null +++ b/native/java/module-info.java @@ -0,0 +1,28 @@ +/* **************************************************************************** + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + See NOTICE file for details. +**************************************************************************** */ + +module org.jpype { + requires java.sql; + + exports org.jpype; + exports org.jpype.manager; + exports org.jpype.html; + exports org.jpype.javadoc; + exports org.jpype.pickle; + exports org.jpype.pkg; + exports org.jpype.proxy; + exports org.jpype.ref; +} diff --git a/native/java/org/jpype/JPypeClassLoader.java b/native/java/org/jpype/JPypeClassLoader.java index fc4e3f2ad..9e0eb6645 100644 --- a/native/java/org/jpype/JPypeClassLoader.java +++ b/native/java/org/jpype/JPypeClassLoader.java @@ -1,3 +1,18 @@ +/* **************************************************************************** + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + See NOTICE file for details. +**************************************************************************** */ package org.jpype; import java.io.ByteArrayOutputStream; diff --git a/project/jpype_java/src/main/java/module-info.java b/project/jpype_java/src/main/java/module-info.java deleted file mode 100644 index 9eedec00c..000000000 --- a/project/jpype_java/src/main/java/module-info.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license - * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/module-info.java to edit this template - */ - -module org.jpype { - requires java.sql; - - exports org.jpype; - exports org.jpype.manager; - exports org.jpype.html; - exports org.jpype.javadoc; - exports org.jpype.pickle; - exports org.jpype.pkg; - exports org.jpype.proxy; - exports org.jpype.ref; -} diff --git a/project/jpype_java/src/main/java/org/jpype/JPypeMain.java b/project/jpype_java/src/main/java/org/jpype/JPypeMain.java deleted file mode 100644 index 1bea06a1a..000000000 --- a/project/jpype_java/src/main/java/org/jpype/JPypeMain.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license - * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template - */ -package org.jpype; - -/** - * - * @author nelson85 - */ -public class JPypeMain -{ - public static void main(String[] args) - { - JPypeMain main = new JPypeMain(); - System.out.println(main.getClass().getModule()); - } -} diff --git a/setupext/build_ext.py b/setupext/build_ext.py index 28b6d6b39..09e6fe51c 100644 --- a/setupext/build_ext.py +++ b/setupext/build_ext.py @@ -300,7 +300,7 @@ def build_java_ext(self, ext): distutils.log.info( "Jar cache is missing, using --enable-build-jar to recreate it.") - target_version = "1.8" + target_version = "9" # build the jar try: dirname = os.path.dirname(self.get_ext_fullpath("JAVA"))