What is the best way to await the joining of a system.util.invokeAsyncronous() call?

I want to execute a ~60s long script from a GUI element without blocking my UI thread. I also want to do something once it is done (the button to trigger the event will do something like for x in list: x.componentEnabled = False, and I want to undo this when it is complete).

What is the best practice for monitoring that thread and triggering an event when it is complete? Putting a thead.join() in the button probably violates the entire point of calling it asynchronously.

At the end of the async code call system.util.invokeLater to interact with UI components ( e.g. re-enable all the buttons).

1 Like

Thank you! I had temporarily just been using a messageBox loop to get in the way but this is really what I wanted.

In other words, don't wait for it. Simply start a foreground task at the end of the background task.

(Go back and forth with more functions as needed.)

1 Like

Ok actually coming back to this -- is there a way to do the following:

def do_work_safely(event, cb, *args, **kwargs):
  myScripts.lock_gui(event)
  thread = system.util.invokeAsynchronous(cb, args, kwargs)
  system.util.invokeLater(thread, myScripts.unlock_gui)

I want to dispatch an async worker but have it do something when it is done. I don't want to have to define what it will do when it is done from within the definition of the work, in case it is context-dependent.

Sure - compose a new function that calls your async thread task and calls invokeLater for you, then pass that composed task to system.util.invokeAsynchronous. As in, something like:

def do_work_safely(event, cb, *args, **kwargs):
	myScripts.lock_gui(event)
	
	def asyncTask(*args, **kwargs):	
		cb(*args, **kwargs)
		system.util.invokeLater(myScripts.unlock_gui)
	
	system.util.invokeAsynchronous(asyncTask, args, kwargs)
2 Likes

Ah very clever. Thank you!

If you want it to be really brain-breaking, look into decorators, which you could use to "wrap" an async call in custom logic. Not necessarily recommended for ease of maintenance, but quite fun to use to program in the short term :laughing:

2 Likes

Already tried decorators, they suffer the problem that I am not currently using a state-based lock system and they will all try to lock and unlock on sub-calls to other functions.

Would work if I had a state-based lock system but thats the next upgrade :wink:

Ah -- invokeLater doesn't allow for arguments for the callback?

I can bypass this with a memory tag containing the most recent event -- what object type is most appropriate for storing a gui event?

No you can't. Tags aren't suitable for storing arbitrary data types.
If you had to store something in memory in the local JVM, system.util.getGlobals() is the way to do so. However, you probably don't.

No, it doesn't, but you can use functools.partial to compose a callable that requires no arguments.
https://docs.python.org/2.7/library/functools.html#functools.partial

Okay, so how could I do this where there is an event object that I am passing around such that the script knows upon which GUI it is working?

I wanted to do something like this

def locker(event, cb, *args, **kwargs):
    result = None
    try:
    	lock(event)
    	
    	def asyncTask(event, *args, **kwargs):
    		result = cb(event, *args, **kwargs)	
    		system.util.invokeLater(unlock, [event])
    		return result    		
        
        result = system.util.invokeAsynchronous(asyncTask, [event] + list(args), kwargs)
    except Exception as e:
    	ee = "EXCEPTION locker(<event>, {}, {}, {})\n{}".format(cb.__name__, str(args), str(kwargs), str(e))
    	system.gui.messageBox(ee)
    finally:
        unlock(event)
    return result

def _set_lock(event, state):
	components = [<list_of_component_names]
	for component in components:
		event.source.parent.getComponent(component).componentEnabled = state

def lock(event):
	_set_lock(event, False)

def unlock(event):
	_set_lock(event, True)

and then in a given button, for instance, I would call myScripts.locker(event, myScripts.burn_it_all, "with_impunity", temperature=9000) and now I would not worry that someone would touch things while it was busy because the gui would be locked but the gui thread would not be blocked.

If you look at my later.py, you will find that my wrappers for the various invoke operations return CompleteableFuture instances, with which you can do .whenComplete(doSomethingElse).

At the moment, you have to subclass BiConsumer to use that, but when IA picks up jython 2.7.4, it will include my fix for the auto wrapping of callables for that purpose. (:

Hint here:

1 Like

https://www.automation-pros.com/ignition/later.py -- ERR_TIMED_OUT

Overly aggressive Anti-Virus? Loads for me from home. (That server is in an Atlanta datacenter.)

Current content, fwiw:

# Convenience functions for Ignition development, typically loaded
# as "shared.later" in v7.x.
#
# Copyright 2008-2022 Automation Professionals, LLC <sales@automation-pros.com>
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#   1. Redistributions of source code must retain the above copyright notice,
#      this list of conditions and the following disclaimer.
#   2. Redistributions in binary form must reproduce the above copyright notice,
#      this list of conditions and the following disclaimer in the documentation
#      and/or other materials provided with the distribution.
#   3. The name of the author may not be used to endorse or promote products
#      derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.

# Java 8 or later is required, due to the use of CompletableFuture and BiConsumer.

from java.lang import Runnable, StackTraceElement, Thread, Throwable
from java.util.concurrent import CompletableFuture
from java.util.function import BiConsumer

import java.lang.Exception
import sys

logger = system.util.getLogger("shared."+__name__)

#------------
# Handle gateway scope where invokeLater doesn't exist.
#
# Generic user code should use shared.later.invokeLater() instead of
# system.util.invokeLater() if gateway scope compatibility is needed.
#
try:
	g = system.util.persistent('shared.later')
except:
	g = system.util.getGlobals()

try:
	invokeLater = system.util.invokeLater
except AttributeError:
	try:
		from com.inductiveautomation.ignition.gateway import IgnitionGateway as GwContext
	except:
		from com.inductiveautomation.ignition.gateway import SRContext as GwContext
	_old = g.get('FakeUIThreadExec', None)
	if _old:
		_old.shutdown()
	del _old
	_executor = GwContext.get().createExecutionManager('FakeUIThread', 1)
	g['FakeUIThreadExec'] = _executor
	class _runnable(Runnable):
		def __init__(self, func):
			self.func = func
		def run(self):
			self.func()

	def invokeLater(func, delay=0):
		delay = int(delay)
		if delay>0:
			_executor.executeOnce(_runnable(func), delay)
		else:
			_executor.executeOnce(_runnable(func))

#------------
# Helper for determining module hierarchy of a class
def fullClassName(cls):
	if cls.__bases__:
		return fullClassName(cls.__bases__[0])+"."+cls.__name__
	return cls.__name__

#------------
# Java wrapper for Jython exceptions to preserve the python
# stack trace.
#
class PythonAsJavaException(java.lang.Exception):

	def __init__(self, pyexc, tb=None):
		super(PythonAsJavaException, self).__init__(repr(pyexc), None, True, True)
		traceElements = []
		if tb is None:
			tb = sys.exc_info()[2]
		while tb:
			code = tb.tb_frame.f_code
			vnames = code.co_varnames
			if vnames and vnames[0] in ('cls', 'self'):
				ref = tb.tb_frame.f_locals[vnames[0]]
				if vnames[0] == 'self':
					className = fullClassName(ref.__class__)
				else:
					className = fullClassName(ref)
			else:
				className = '<global>'
			traceElements.append(StackTraceElement(className, code.co_name, code.co_filename, tb.tb_lineno))
			tb = tb.tb_next
		traceElements.reverse()
		self.setStackTrace(traceElements)

#------------
# Given a future, wait for it to complete, and log any exception produced
# at the warning level.  Return the given value if present, otherwise
# return the future's result.  This function must not be called on the GUI
# thread -- that will freeze the UI until the future completes.
#
def futureExLog(f, retv=None):
	try:
		if retv is None:
			retv = f.get()
		else:
			f.get()
	except Throwable, t:
		logger.warn("Future Exception:", t)
	return retv

#------------
# If a future needs only to have its exceptions logged, attach an instance
# of FutureLogger to it with .whenComplete().
#
# Like so:
#    shared.later.callAsync(.....).whenComplete(shared.later.FutureLogger())
#
class FutureLogger(BiConsumer):
	def __init__(self, otherLogger=None):
		self.logger = otherLogger if otherLogger else logger
	def accept(self, retv, exc):
		if exc:
			self.logger.warn("Future Exception:", exc)

commonLogger = FutureLogger()

#------------
# Asynchronous method execution
#
# This routine accepts a target function, and a variable argument list, and
# schedules the function call for asynchronous execution.  The function can
# be a bare (no parens) object method, but must not call any gui or
# gui component methods.
#
# The return value of the function completes a future, which is itself returned
# to caller.  If the caller discards the future, the target function's return
# value or exception will be discarded.
#
# Do *NOT* wait for the future in a GUI thread!
#
def callAsync(func, *args, **kwargs):
	future = CompletableFuture()
	def callAsyncInvoked():
		try:
			future.complete(func(*args, **kwargs))
		except Throwable, t:
			future.completeExceptionally(t)
		except Exception, e:
			future.completeExceptionally(PythonAsJavaException(e))
	system.util.invokeAsynchronous(callAsyncInvoked)
	return future

#------------
# Java Runnable wrapper for Python Callable with args
class JavaCallable(Runnable):
	__slots__ = ['_f', '_func', '_args', '_kwa']
	def __init__(self, future, func, *args, **kwargs):
		self._f = future
		self._func = func
		self._args = args
		self._kwa = kwargs
	def run(self):
		try:
			self._f.complete(self._func(*self._args, **self._kwa))
		except Throwable, t:
			self._f.completeExceptionally(t)
		except Exception, e:
			self._f.completeExceptionally(PythonAsJavaException(e))

#------------
# Asynchronous method execution
#
# This routine accepts a target function, a thread name, and a variable
# argument list, and schedules the function call for asynchronous execution
# in a java Thread of that name.  The function can
# be a bare (no parens) object method, but must not call any gui or
# gui component methods.
#
# The return value of the function completes a future, which is itself returned
# to the caller (along with the thread itself).  If the caller discards the future,
# the target function's return value or exception will be discarded.
#
# Do *NOT* wait for the future in a GUI thread!
#
def callThread(func, threadName, *args, **kwargs):
	future = CompletableFuture()
	runnable = JavaCallable(future, func, *args, **kwargs)
	thread = Thread(runnable, threadName)
	thread.start()
	return (future, thread)

#------------
# Deferred property assignment
#
# Procedures executing in the 'invokeAsynchronous' environment are not allowed
# to assign to properties of gui objects.  This routine accepts a target
# object, a property name, and a new value, and schedules the assignment
# in the gui thread.
#
# Successful assignment completes a future with a None value.  The future is
# returned to the caller.  If the caller discards the future, the target
# assignment's success or failure will be discarded.
#
# Do *NOT* wait for the future in a GUI thread!
#
# An emulation of invokeLater is used in gateway scopes.
#
def assignLater(comp, prop, val, ms = 0):
	future = CompletableFuture()
	def assignLaterInvoked():
		try:
			try:
				setattr(comp, prop, val)
			except AttributeError:
				comp.setPropertyValue(prop, val)
			future.complete(None)
		except Throwable, t:
			future.completeExceptionally(t)
		except Exception, e:
			future.completeExceptionally(PythonAsJavaException(e))
	invokeLater(assignLaterInvoked, ms)
	return future

#------------
# Deferred property assignment from a background function
#
# Functions executing in the 'invokeAsynchronous' environment are not allowed
# to assign to properties of gui objects.  This routine accepts a target
# object, a property name, a callable function, and its arguments, runs the
# function in the background, then schedules the assignment of the result
# in the gui thread.
#
# Successful assignment completes a future with a None value.  The future is
# returned to the caller.  If the caller discards the future, the target
# assignment's success or failure will be discarded.
#
# Do *NOT* wait for the future in a GUI thread!
#
# An emulation of invokeLater is used in gateway scopes, though somewhat
# pointless, as there are no gui objects in the gateway.  But it works on
# any arbitrary object's properties.
#
def assignAsyncLater(comp, prop, func, *args, **kwargs):
	future = CompletableFuture()
	def assignAsyncLaterInvoked():
		try:
			val = func(*args, **kwargs)
			def assignLaterInvoked():
				try:
					try:
						setattr(comp, prop, val)
					except AttributeError:
						comp.setPropertyValue(prop, val)
				except Throwable, t:
					future.completeExceptionally(t)
				except Exception, e:
					future.completeExceptionally(PythonAsJavaException(e))
				future.complete(val)
			invokeLater(assignLaterInvoked)
		except Throwable, t:
			future.completeExceptionally(t)
		except Exception, e:
			future.completeExceptionally(PythonAsJavaException(e))
	system.util.invokeAsynchronous(assignAsyncLaterInvoked)
	return future

#------------
# Deferred method execution
#
# Procedures executing in the 'invokeAsynchronous' environment are not allowed
# to execute methods of gui objects.  This routine accepts a target
# function, and a variable argument list, and schedules the function call
# in the gui thread.  The function can be a bare (no parens) object method.
#
# If kwargs includes 'ms', that is removed and used in the outer invokeLater
#
# The return value of the function completes a future, which is itself returned
# to caller.  If the caller discards the future, the target function's return
# value or exception will be discarded.
#
# Do *NOT* wait for the future in a GUI thread!
#
# An emulation of invokeLater is used in gateway scopes.
#
def callLater(func, *args, **kwargs):
	future = CompletableFuture()
	ms = kwargs.pop('ms', 0)
	def callLaterInvoked():
		try:
			future.complete(func(*args, **kwargs))
		except Throwable, t:
			future.completeExceptionally(t)
		except Exception, e:
			future.completeExceptionally(PythonAsJavaException(e))
	invokeLater(callLaterInvoked, ms)
	return future

#------------
# Deferred Asynchronous method execution
#
# This routine accepts a target function, and a variable argument list, and
# schedules the function call for asynchronous execution, but only after also
# waiting for gui events to complete.  The function can be a bare (no parens)
# object method, but must not call any gui or gui component methods.
#
# The return value of the function completes a future, which is itself returned
# to caller.  If the caller discards the future, the target function's return
# value or exception will be discarded.
#
# Do *NOT* wait for the future in a GUI thread!
#
# An emulation of invokeLater is used in gateway scopes.
#
def callAsyncLater(func, *args, **kwargs):
	future = CompletableFuture()
	def callLaterInvoked():
		def callAsyncInvoked():
			try:
				future.complete(func(*args, **kwargs))
			except Throwable, t:
				future.completeExceptionally(t)
			except Exception, e:
				future.completeExceptionally(PythonAsJavaException(e))
		system.util.invokeAsynchronous(callAsyncInvoked)
	invokeLater(callLaterInvoked)
	return future

#------------
# Deferred Asynchronous method execution
#
# This routine accepts a target function, a time, and a variable argument
# list, and schedules the function call for asynchronous execution, but
# only after also waiting the specified milliseconds after all gui events
# complete.  The function can be a bare (no parens) object method, but must
# not call any gui or gui component methods.
#
# The return value of the function completes a future, which is itself returned
# to caller.  If the caller discards the future, the target function's return
# value or exception will be discarded.
#
# Do *NOT* wait for the future in a GUI thread!
#
# An emulation of invokeLater is used in gateway scopes.
#
def callAsyncDelayed(func, delay=1000, *args, **kwargs):
	future = CompletableFuture()
	def callLaterInvoked():
		def callAsyncInvoked():
			try:
				future.complete(func(*args, **kwargs))
			except Throwable, t:
				future.completeExceptionally(t)
			except Exception, e:
				future.completeExceptionally(PythonAsJavaException(e))
		system.util.invokeAsynchronous(callAsyncInvoked)
	invokeLater(callLaterInvoked, delay)
	return future

#------------
# Deferred tag assignment
#
# This routine accepts a target tag path and a new value, and
# schedules the assignment in the gui thread.
def writeTagLater(tag, val, ms = 0):
	def writeTagLaterInvoked():
		system.tag.write(tag, val, True)
	invokeLater(writeTagLaterInvoked, ms)
	return val

#------------
# Deferred tag assignment
#
# This routine accepts a list of target tag paths and corresponding list of
# new values, and schedules the assignment in the gui thread.
def writeTagsLater(taglist, vallist, ms = 0):
	future = CompletableFuture()
	def writeTagsLaterInvoked():
		system.tag.writeAll(taglist, vallist)
	invokeLater(writeTagsLaterInvoked, ms)

#------------
# Deferred tag assignment
#
# This routine accepts a list of target tag paths and corresponding list of
# new values, and schedules the assignment synchronously in a background
# thread.  Then a property assignment is scheduled back on the gui thread.
def writeAsyncAssign(taglist, vallist, comp, prop, val):
	def writeAsyncAssignInvoked():
		for t, v in map(None, taglist, vallist):
			system.tag.writeSynchronous(t, v, 250)
		app.util.assignLater(comp, prop, val)
	system.util.invokeAsynchronous(writeAsyncAssignInvoked)

#
# kate: tab-width 4; indent-width 4; tab-indents on; dynamic-word-wrap off; indent-mode python; line-numbers on;
#
1 Like

note that you are only blocking one client. if there is another computer with the same view open, your code will not block its buttons and the other user could click something

3 Likes