Interact with User Interface via Python

62 views
Skip to first unread message

Donal Harrigan - NOAA Federal

unread,
Dec 19, 2019, 3:26:09 PM12/19/19
to Jep Project
Looking to do the following:

Select or de-select a button on the user interface (main thread) from a python script.

I've tried using Display.getDefault().asyncExec(), but it requires a java.lang.Runnable, so I've tried something similiar to this thread:

and run into the same issue.

Is it possible to toggle a button on and off and have the action execute on the main thread via a python script without getting the Invalid Thread Access error?

Thank you!

Ben Steffensmeier

unread,
Dec 19, 2019, 4:38:40 PM12/19/19
to Jep Project
As I mentioned in the discussion you referenced, you can only use a jproxy object on the thread where it is created. So if you need to use a jproxy Runnable to execute python code on the main thread then you must create it from an interpreter running on the main thread.

If your Jep interpreter is on a different thread then you will need to write the logic to push the button in Java and then you can call the Java code from Python and the Java will be able to execute on the main thread.

Donal Harrigan - NOAA Federal

unread,
Dec 19, 2019, 5:06:34 PM12/19/19
to Jep Project
Hi Ben,

Let me add a little more context to see if what you are suggesting is possible.

I'm working within a larger piece of software where the python script I'm writing is executed by selecting it from a dropdown in the main thread (or user interface). The Java code is compiled in plugins that I'm not really supposed to alter, so adding some java code to the stock framework to do a button press isn't really an option right now. Could I, however, write a stand-alone piece of Java code that gets called by the Python script that could interact with the UI if I pass it the UI thread from the python script?

Hope this makes sense.

Ben Steffensmeier

unread,
Dec 20, 2019, 12:28:32 AM12/20/19
to Jep Project
Jep loads Java classes using a Java ClassLoader. The ClassLoader can be provided during construction in the JepConfig or if none is provided the ClassLoader that was used to load the Interpreter class will be used. If you are adding a class to an existing application then you will need to make your class available to that ClassLoader. If it is a simple java application this may just be a matter of adding it to the classpath. Many frameworks have more complex logic in class loaders so the exact method of getting your class available to the ClassLoader will depend on the framework. If you can make your Java class available to the Jep ClassLoader then you will be able to access it from Python with an import statement. You can then call static methods on it to do whatever you need to for switching threads. If your Java class implements Runnable you would be able to construct an instance from Python and submit it to the main thread using asyncExec() from Python. The only constraint from Jep is that no Python code/objects can be accessed from another thread.

Ben

Donal Harrigan - NOAA Federal

unread,
Dec 20, 2019, 6:46:36 PM12/20/19
to Jep Project
Ben,

Thank you, this has been very helpful.

I've compiled my .java code into a .class and created a .jar file that contains the class. I've stored that .jar to a local directory and now if I understand correctly I need to either add that directory to the classpath or have the ClassLoader see that directory? Can this be done through the python script itself? I'm able to get the classloader via ClassLoader.getSystemClassLoader() not sure if there's a method I can use to force it to see my .jar file. If that's the wrong approach, can I add to the classpath in python? Is it as simple as os.environ['CLASSPATH'] += path?

Ben Steffensmeier

unread,
Dec 21, 2019, 12:02:44 AM12/21/19
to Jep Project
You just need to add your class to your application the same as any other class used, if it is a regular Java application you just need to add your jar to the classpath, but many frameworks have other mechanisms for configuring classloaders and it is entirely dependent on your framework. Jep is just concerned with exposing classes available in the JVM to Python, how you get the classes into the JVM is up to your application.

When it comes to configuring the ClassLoader from Python, you should be able to call anything from Python that you can do in Java, unfortunately as far as I know typical ClassLoaders provide no mechanism for adding jars at runtime. The problem of adding jars at runtime isn't really a Jep related problem so you likely need to expand your search, for example this stackoverflow question seems applicable. The accepted answer to that question is written in Java but you should be able to call all the same Java methods from Python to achieve the same results.

Donal Harrigan - NOAA Federal

unread,
Dec 21, 2019, 7:30:12 PM12/21/19
to Jep Project
I really appreciate your assistance on this topic. I finally have some working code, but there may still be an issue with loading the .jar at runtime, what do you make of the error I'm receiving.

Uncompiled .java code
package com.myAddons.myRunnableExecution;

import com.raytheon.viz.gfe.core.DataManager;

public class myRunnableExecution implements Runnable {

public void run() {

DataManager.getCurrentInstance().getGridManager().toggleTemporalEditor();

}
}


Python script:
import SmartScript
from java.lang import ClassLoader, Class
from java.lang.reflect import Array
from java.io import File
from java.net import URLClassLoader, URL
from org.eclipse.swt.widgets import Display

cl = ClassLoader.getSystemClassLoader()
jarFile = File("/localapps/runtime/myJava/myRunnableExecution.jar")        
uriCL = jarFile.toURI().toURL()        
urlCls = Class.forName("java.net.URL")
urlArr = Array.newInstance(urlCls,1)
Array.set(urlArr, 0, uriCL)
myCL = URLClassLoader(urlArr,cl)
from com.myAddons import myRunnableExecution

class Procedure (SmartScript.SmartScript):
   
    def __init__(self, dbss):
        SmartScript.SmartScript.__init__(self, dbss)
   
    def execute(self, editArea, timeRange, varDict):
        Display.getDefault().asyncExec(myRunnableExecution.run())

Subsequent error:
!ENTRY com.raytheon.viz.gfe 2 0 2019-12-22 00:23:45.994
!MESSAGE Error in procedure ToggleTestPostingVersionNEW
!STACK 0
jep.JepException: <type 'exceptions.ImportError'>: com.myAddons.myRunnableExecution.run cannot be found by com.raytheon.viz.gfe_1.17.0.2019111211
at /awips2/python/lib/python2.7/site-packages/jep/java_import_hook.__getattr__(java_import_hook.py:32)
at /home/donal.harrigan/caveData/etc/user/donal.harrigan/gfe/userPython/procedures/ToggleTestPostingVersionNEW.execute(ToggleTestPostingVersionNEW.py:35)
at /home/donal.harrigan/caveData/common/base/python/MasterInterface.runMethod(MasterInterface.py:141)
at /awips2/cave/etc/gfe/userPython/utilities/ProcedureInterface.runProcedure(ProcedureInterface.py:111)
at jep.Jep.eval(Native Method)
at jep.Jep.eval(Jep.java:609)
at com.raytheon.uf.common.python.PythonScript.internalExecute(PythonScript.java:285)
at com.raytheon.viz.gfe.procedures.ProcedureController.executeProcedure(ProcedureController.java:155)
at com.raytheon.viz.gfe.procedures.ProcedureJobPool$ProcedureJob.execute(ProcedureJobPool.java:381)
at com.raytheon.viz.gfe.procedures.ProcedureJobPool$ProcedureJob.processRequest(ProcedureJobPool.java:345)
at com.raytheon.viz.gfe.procedures.ProcedureJobPool$ProcedureJob.run(ProcedureJobPool.java:324)
at org.eclipse.core.internal.jobs.Worker.run(Worker.java:55)
Caused by: java.lang.ClassNotFoundException: com.myAddons.myRunnableExecution.run cannot be found by com.raytheon.viz.gfe_1.17.0.2019111211
at org.eclipse.osgi.internal.loader.BundleLoader.findClassInternal(BundleLoader.java:461)
at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:372)
at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:364)
at org.eclipse.osgi.internal.loader.ModuleClassLoader.loadClass(ModuleClassLoader.java:161)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 8 more


Ben Steffensmeier

unread,
Dec 21, 2019, 9:56:50 PM12/21/19
to Jep Project
You can only do a Python import statement on classes that are available to the Jep ClassLoader, your Class is still not available to the Jep ClassLoader, it is only available to the new ClassLoader you created. I think you need to change this line:

from com.myAddons import myRunnableExecution

to something like this:

myRunnableExecution = myCL.loadClass("com.myAddons.myRunnableExecution")

Or maybe this:

myRunnableExecution = Class.forName("com.myAddons.myRunnableExecution", True, myCL);

Once you have loaded the Class object from your new Class loader Jep should wrap it with a PyJClass so you can call Constructors and static methods just like an object imported with a Python import statement.

This line also looks wrong:

Display.getDefault().asyncExec(myRunnableExecution.run())

myRunnableExecution is a class object and you are trying to call an instance method without constructing an instance. Also asyncExec is expecting a Runnable, You are calling the run method, which is void so it will return None and pass a NoneType object into asyncExec which will just be null in Java. Instead you should construct an instance of your Runnable class like this:

Display.getDefault().asyncExec(myRunnableExecution())

Ben

Donal Harrigan - NOAA Federal

unread,
Dec 27, 2019, 4:54:50 AM12/27/19
to Jep Project
So I was able to get my python script and my java program talking to each other using your suggestions and also creating a custom MANIFEST.MF file.

There's still something funky going on where when trying to interact with the UI Thread.

For example, the print statement in the following python code returns a DataManager instance (as I would expect):

import SmartScript
from java.lang import ClassLoader, Class
from java.lang.reflect import Array
from java.io import File
from java.net import URLClassLoader, URL
from org.eclipse.swt.widgets import Display
from org.eclipse.ui import PlatformUI
from com.raytheon.viz.gfe.core import DataManager

jarFile = File("/localapps/runtime/myJava/myRunnableExecution.jar")        
uriCL = jarFile.toURI().toURL()        
cl = ClassLoader.getSystemClassLoader()
urlCls = Class.forName("java.net.URL")
urlArr = Array.newInstance(urlCls,1)
Array.set(urlArr, 0, uriCL)
myCL = URLClassLoader(urlArr,cl)
myRunnableExecution = Class.forName("com.myAddons.myRunnableExecution.myRunnableExecution", True, myCL)

class Procedure (SmartScript.SmartScript):
   
    def __init__(self, dbss):
        SmartScript.SmartScript.__init__(self, dbss)
   
    def execute(self, editArea, timeRange, varDict):
        wbws = PlatformUI.getWorkbench().getWorkbenchWindows()
        for wbWindow in wbws:
            dm = DataManager.getInstance(wbWindow)
            print dm
        
        Display.getDefault().asyncExec(myRunnableExecution())


Prints
com.raytheon.viz.gfe.core.DataManager

But my java program

package com.myAddons.myRunnableExecution;

import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.IWorkbenchWindow;
import com.raytheon.viz.gfe.core.DataManager;

public class myRunnableExecution implements Runnable {

public void run() {
System.out.print("2");
int i;
DataManager dm;
IWorkbenchWindow wbwArr[] = PlatformUI.getWorkbench().getWorkbenchWindows();
System.out.print(wbwArr);
for (i=0; i < wbwArr.length; i++) {
dm = DataManager.getInstance(wbwArr[i]);
System.out.print(dm);
}
}
}


Returns the following error:

Unhandled event loop exception
org.eclipse.swt.SWTException: Failed to execute runnable (java.lang.IllegalStateException: Workbench has not been created yet.)
at org.eclipse.swt.SWT.error(SWT.java:4533)
at org.eclipse.swt.SWT.error(SWT.java:4448)
at org.eclipse.swt.widgets.Synchronizer.runAsyncMessages(Synchronizer.java:185)
at org.eclipse.swt.widgets.Display.runAsyncMessages(Display.java:4528)
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4146)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$4.run(PartRenderingEngine.java:1121)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:336)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1022)
at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:150)
at org.eclipse.ui.internal.Workbench$5.run(Workbench.java:687)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:336)
at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:604)
at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:148)
at com.raytheon.uf.viz.personalities.cave.component.CAVEApplication.startComponent(CAVEApplication.java:173)
at com.raytheon.uf.viz.application.VizApplication.start(VizApplication.java:102)
at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:196)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:134)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:104)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:388)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:243)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:673)
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:610)
at org.eclipse.equinox.launcher.Main.run(Main.java:1519)
at org.eclipse.equinox.launcher.Main.main(Main.java:1492)
Caused by: java.lang.IllegalStateException: Workbench has not been created yet.
at org.eclipse.ui.PlatformUI.getWorkbench(PlatformUI.java:93)
at com.myAddons.myRunnableExecution.myRunnableExecution.run(myRunnableExecution.java:18)
at org.eclipse.swt.widgets.RunnableLock.run(RunnableLock.java:35)
at org.eclipse.swt.widgets.Synchronizer.runAsyncMessages(Synchronizer.java:182)
... 25 more


I'm not savvy enough to figure out why the Java program seems to not know the UI exists while the python script does. As a refresher, to initiate the python script, I select it from a drop-down menu in the UI I want to interact with using the Java program. I'll include the MANIFEST.MF that I created below in case something related to the PlatformUI needs to be added there?

MANIFEST.MF
Manifest-Version: 1.0
Class-Path: /awips2/cave/plugins/com.raytheon.viz.gfe_1.17.0.201911121
 1/com.raytheon.viz.gfe.jar /awips2/edex/lib/plugins/com.raytheon.uf.c
 ommon.time.jar /awips2/cave/plugins/com.raytheon.uf.common.time_1.16.
 0.2019111211/ /awips2/cave/plugins/com.raytheon.viz.ui_1.16.0.2019111
 211/com.raytheon.viz.ui.jar /awips2/cave/plugins/com.raytheon.uf.comm
 on.jms_1.18.0.2019111211.jar /awips2/cave/plugins/com.raytheon.uf.com
 mon.status_1.17.0.2019111211.jar /awips2/cave/plugins/com.raytheon.uf
 .viz.core_1.16.1.2019111211/com.raytheon.uf.viz.core.jar /awips2/cave
 /plugins/org.eclipse.ui_3.108.0.v20160518-1929.jar /awips2/cave/plugi
 ns/org.eclipse.ui.workbench_3.108.1.v20160819-2118.jar /awips2/cave/p
 lugins/org.eclipse.jface_3.12.0.v20160518-1929.jar /awips2/cave/plugi
 ns/org.eclipse.e4.ui.workbench3_0.13.100.v20160506-0759.jar /awips2/c
 ave/plugins/org.eclipse.core.runtime_3.12.0.v20160606-1342.jar /awips
 2/cave/plugins/org.eclipse.equinox.common_3.8.0.v20160509-1230.jar /a
 wips2/cave/plugins/org.eclipse.ui.navigator.resources_3.5.100.v201605
 18-1929.jar /awips2/cave/plugins/org.eclipse.e4.ui.workbench_1.4.0.v2
 0160517-1624.jar /awips2/cave/plugins/org.eclipse.core.commands_3.8.0
 .v20160316-1921.jar /awips2/cave/plugins/org.eclipse.e4.core.di_1.6.1
 .v20160712-0927.jar /awips2/cave/plugins/org.eclipse.e4.ui.model.work
 bench_1.2.0.v20160229-1459.jar /awips2/cave/plugins/org.eclipse.swt_3
 .105.1.v20160907-0248.jar /awips2/cave/plugins/org.eclipse.swt.gtk.li
 nux.x86_64_3.105.1.v20160907-0248.jar /awips2/cave/plugins/org.eclips
 e.osgi_3.11.1.v20160708-1632.jar /awips2/cave/plugins/org.eclipse.osg
 i.services_3.5.100.v20160504-1419.jar /awips2/cave/plugins/org.eclips
 e.core.jobs_3.8.0.v20160509-0411.jar /awips2/cave/plugins/org.eclipse
 .equinox.registry_3.6.100.v20160223-2218.jar 
Created-By: 1.8.0_144 (Oracle Corporation)
Main-Class: com.myAddons.myRunnableExecution.myRunnableExecution


Thanks again for your time!

Ben Steffensmeier

unread,
Dec 27, 2019, 11:33:09 AM12/27/19
to Jep Project
My guess is that since you made your own ClassLoader, the PlatformUI class loaded from within your class used your new ClassLoader and is a separate class from the PlatformUI that the rest of the Application is using. Since you can access the correct DataManager from Python you could try passing the DataManager into your Java class through a Constructor argument and storing it in a field so it will be available in the run method. This may not work if your Class has a separate DataManager class loaded but the quickest way to find out is to try it. If that does not work you would need to do more research into interacting with OSGi ClassLoaders which is a fairly complex topic that is not really related to Jep.

Ben

Donal Harrigan - NOAA Federal

unread,
Dec 28, 2019, 2:21:03 AM12/28/19
to Jep Project
Hi Ben,

Understand we're coming to the end of the line here as far as Jep support is concerned. I really do appreciate all your help; I've learned a lot. I have tried passing the DataManager instance from python to java, but I strayed from that path due to the fact that I was getting a type error I couldn't resolve. Based on the limited information that came back to me on web searches, it may be that the DataManager instance from python is being converted to a string when it's sent to java?

If I send the DataManager from python like this:
Display.getDefault().asyncExec(myRunnableExecution(dm))

To my Java code that looks like this:
package com.myAddons.myRunnableExecution;

import com.raytheon.viz.gfe.core.DataManager;

public class myRunnableExecution implements Runnable {
DataManager dataManager;
public myRunnableExecution(DataManager dataManager) {
System.out.print(dataManager);
this.dataManager = dataManager;
}

public void run() {
System.out.print("here");
System.out.print(this.dataManager);
}
}


I get the following error to which I'm like 'cmon man!':

!ENTRY com.raytheon.viz.gfe 2 0 2019-12-28 07:15:51.669
!MESSAGE Error in procedure ToggleTestPostingVersionNEW
!STACK 0
jep.JepException: <type 'exceptions.TypeError'>: Error converting parameter 1: Expected com.raytheon.viz.gfe.core.DataManager but received a com.raytheon.viz.gfe.core.DataManager.
at /home/donal.harrigan/caveData/etc/user/donal.harrigan/gfe/userPython/procedures/ToggleTestPostingVersionNEW.execute(ToggleTestPostingVersionNEW.py:41)

Ben Steffensmeier

unread,
Dec 28, 2019, 11:00:59 AM12/28/19
to Jep Project
This is the same type of problem as your PlatformUI/Workbench issues. A Java Class within the JVM is uniquely defined by the class name and the ClassLoader used to load it. Your problem is that there are two classes called com.raytheon.viz.gfe.core.DataManager, one which was created by your ClassLoader and one created by the normal Application ClassLoader and even though these two Classes come from the same jar they are not compatible. The error you are seeing is because Jep cannot convert the classes but this would be true of a pure Java application as well, classes with the same name cannot be used interchangeably if they come from a different ClassLoader.

I suspect your problems may be based around this line:

cl = ClassLoader.getSystemClassLoader()

Normally when a ClassLoader needs a class it will start with the parent ClassLoader and only loads the class itself if the parent cannot. So in your case when myRunnable needs com.raytheon.viz.gfe.core.DataManager it starts with parent, which is this SystemClassLoader. Since your application is using OSGi the System ClassLoader is pretty minimal and most of the classes are loaded by OSGi loaders. Since your parent couldn't find com.raytheon.viz.gfe.core.DataManager you had to modify your MANIFEST.MF, which got your class loaded but created a new version of the DataManager class leading to your problems.

If you can find a smarter parent ClassLoader then you shouldn't need the classpath to be defined in your MANIFEST.MF and you should be able to use the same DataManager class as the rest of the Application(or even the PlatformUI class if you prefer that version). This all goes back to how the OSGi ClassLoaders are wired up but some ideas to try would be something like this:

from java.lang import Thread
cl
= Thread.currentThread().getContextClassLoader()

This uses an internal field of jep that may change in future versions but might be useful for troubleshooting:

import jep
cl
= jep.__loader__

I don't have an OSGi Application to test right now so I have not tried this, but something like this should be able to get the right ClassLoader for an OSGi bundle and that ClassLaoder will be able to load any classes in that bundle.

from org.eclipse.core.runtime import Platform
from org.osgi.framework.wiring import BundleWiring
cl
= Platform.getBundle("com.raytheon.viz.gfe").adapt(BundleWiring).getClassLoader().

The simplest way in Java to get a ClassLaoder that can load the application DataManager class would be to get the ClassLoader that alrady loaded it like this: DataManager.class.getClassLoader(). Unfortunately in Jep the internals of PyJClass prevent you from calling Class methods like getClassLoader(). This is something I would like to change in the future but it is not currently a priority. Normally I would just recommend writing some Java code to do that for you but that's how we got here to begin with so that may be a dead end for you.

Ben
Reply all
Reply to author
Forward
Message has been deleted
0 new messages