Are Gateway Event message handler scripts known to not throw errors when imports are wrong / unimported methods are used?

I just spent the last hour debugging why my script for generating screenshots wasn't working. Ultimately it was because I was importing java.io.ByteArrayOutputStream when I wanted java.io.ByteArrayInputStream (for parsing a base64 decoded string), but I did not find anywhere that indicated an error for importing the wrong method or much more disturbingly for invoking a method I had not imported. The logs just show nothing, the script died I guess and left no trace.

Known issue? Feature? Bug? Fixed in 8.3? I am asking here before filing to support in case it is already known.

Repeated errors in some areas of Ignition are turned down to a lower log level to avoid drowning out the logs; that might be what's going on here, or it could be something else.

I can't readily reproduce what you're seeing; if I try to call a non-existent method, even on an imported Java class, I get an error immediately in the logs:

E [MessageHandlerRunnable        ] [15:14:29.849] [gateway-messagehandler-test-sharedThread-1]: Exception while attempting to run Python function 
com.inductiveautomation.ignition.common.script.JythonExecException: Traceback (most recent call last):
  File "<MessageHandlerScript:test/handler >", line 20, in handleMessage
AttributeError: 'java.io.ByteArrayInputStream' object has no attribute 'doSomething'

	at org.python.core.Py.AttributeError(Py.java:176)
	at org.python.core.PyObject.noAttributeError(PyObject.java:965)
	at org.python.core.PyObject.__getattr__(PyObject.java:959)
	at org.python.pycode._pyx5.handleMessage$1(<MessageHandlerScript:test/handler >:23)
	at org.python.pycode._pyx5.call_function(<MessageHandlerScript:test/handler >)
	at org.python.core.PyTableCode.call(PyTableCode.java:173)
	at org.python.core.PyBaseCode.call(PyBaseCode.java:306)
	at org.python.core.PyFunction.function___call__(PyFunction.java:474)
	at org.python.core.PyFunction.__call__(PyFunction.java:469)
	at org.python.core.PyFunction.__call__(PyFunction.java:464)
	at com.inductiveautomation.ignition.common.script.ScriptManager.runFunction(ScriptManager.java:848)
	at com.inductiveautomation.ignition.common.script.ScriptManager.runFunction(ScriptManager.java:830)
	at com.inductiveautomation.ignition.gateway.project.ProjectScriptLifecycle$TrackingProjectScriptManager.runFunction(ProjectScriptLifecycle.java:887)
	at com.inductiveautomation.ignition.common.script.message.MessageHandlerRunnable.run(MessageHandlerRunnable.java:122)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
	at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:264)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java)
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: org.python.core.PyException: AttributeError: 'java.io.ByteArrayInputStream' object has no attribute 'doSomething'
	... 21 common frames omitted

So I tried with bad imports and fake methods and those do throw errors. But trying to call a method that I did not import does not throw any error, nor leave a trace in the log. For instance, accidentally importing ByteArrayOutputStream when I actually want/call ByteArrayInputStream

#write_image handler
def handleMessage(payload):
    import base64
	from java.io import File, ByteArrayInputStream # If this is ByteArrayOutputStream it just dies at the position marked below
	from javax.imageio import ImageIO
	try:
		logger = system.util.getLogger("test")
		path = payload['path']
		image = payload['image']
		ext = payload['ext']
		
		logger.info("Writing image to {}: {}".format(path, len(str(image))))
                # Decode the base64 image back into bytes
		image_data = base64.b64decode(image)
		logger.info("Image data <{}> {}".format(type(image_data), len(str(image_data))))
		byte_stream = ByteArrayInputStream(image_data)
## DIES HERE If importing ByteArrayOutputStream instead, we never see anything below this
		logger.info("byte_stream? <{}>: {}".format(type(byte_stream ), len(str(byte_stream))))
		# Read the image from the byte stream
		image = ImageIO.read(byte_stream)
		logger.info("image? <{}>: {}".format(type(image), len(str(image))))
		logger.info("Writing byte stream to {}".format(path))
		
		# Save image to the specified path
		ImageIO.write(image, ext, File(path))
		return path
	except Exception as e:
		return {'error': str(e)}
#gateway.py
def write_image(path, image, ext="png", project="base_leaf"):
	return system.util.sendRequest(project, "write_image", {"path": path, "image": image, "ext": ext})

Bunch of trash as I try to make screenshots work right (grabbing windows is inconsistent, locating the rectangle correctly doesn't work right, there's a bunch of annoying all piled in here)

#gui.py
def get_screenshot(run_name, shot_number=None, window=None, project="tb1", ext='png'):
	# Import necessary libraries
	from java.awt import Robot, Rectangle
	from java.awt import Toolkit
	from javax.imageio import ImageIO
	from java.io import File, ByteArrayOutputStream  # Add this import
	import system
	import os
	import base64
	
	# Define the path to save the screenshot
	file_dir = svc.common.work_dir(project=project, name=run_name, fs='ignition')
	file_name = "{}_shot{}_allctrls".format(run_name, shot_number)
	file_path = file_dir + "/" + file_name + "." + ext # assume linux since this goes to gateway
	
	print(file_dir)
	print(gateway.ensure_dir(file_dir))
	print(file_path)
	
#	parent_window = system.gui.getParentWindow()
	parent_window = system.gui.getWindow("Main Windows/all_controls")
	vision_client = system.gui.getParentWindow(system.gui.getWindows()[0].getRootContainer())
	if window:
		print("Using handled window")
		window_bounds = window.getBounds()
		rectangle = Rectangle(window_bounds.x, window_bounds.y, window_bounds.width, window_bounds.height)
	elif system.gui.getWindows():
	    # Take the bounds from the first window in the list, assuming it's the main Vision client window
	    vision_client_window = system.gui.getWindows()[0]
	    window_bounds = vision_client_window.getBounds()
	    rectangle = Rectangle(window_bounds.x, window_bounds.y, window_bounds.width, window_bounds.height)
	elif parent_window:
	    # Get the bounds of the Vision window (x, y, width, height)
	    print("Got parent window Main Windows/all_controls")
	    window_bounds = parent_window.getBounds()
	    rectangle = Rectangle(window_bounds.x, window_bounds.y, window_bounds.width, window_bounds.height)
	else:
	    # Fall back to the whole screen if the Vision window can't be found
	    print("Failed to find specified window, taking full screenshot")
	    screen_size = Toolkit.getDefaultToolkit().getScreenSize()
	    rectangle = Rectangle(screen_size)
	
	# Capture screenshot
	robot = Robot()
	screenshot = robot.createScreenCapture(rectangle)
	
	byte_stream = ByteArrayOutputStream()
	ImageIO.write(screenshot, ext, byte_stream)
	image_bytes = byte_stream.toByteArray()
	encoded_image = base64.b64encode(image_bytes).decode('utf-8')
	
	gateway.write_image(file_path, encoded_image, ext=ext)
	
	#	# Save screenshot
	#	ImageIO.write(screenshot, ext, File(file_path))
	
	# Notify the user
#	system.gui.messageBox("Screenshot saved at: " + file_path, "Screenshot Taken")
	return file_path

If you are importing anything in an event, you are screwing up. Move your code to a project library script function, and place all of the imports outside your functions.

Then re-analyze your situation. Include top-level logger objects in your library scripts so that it is relatively simple to include double-clause try: - except: structures to ensure your errors are logged precisely how you need. Do not rely on built-in loggers--they mute repeated errors in many cases, hiding the clues you need.

I have not had good experiences trying to call project scripts from gateway message handlers

I can't imagine why. Please share.

Whenever I do, it doesn't work correctly?

I am trying to interact with a filesystem through an instance of the gateway that is running in a kubernetes environment. Setting that up was painful enough, and it doesn't play nice with the other scripts. The only thing I managed to get to work were handwritten scripts inside of message handlers. I have spent a fair amount of time trying to abstract this layer away so that I only have to make a new helper script (project) and new message handler (gateway event) inside of a leaf project that is attached directly after my base scripts project.

Since I have a large amount of off-ignition processing to do with my data, and a lot of it can't travel through an RDB, and since there is a bug in the java udp client that limits packets to 1024 (edit: jython socket Datagram and 2048), I have to use a file system for moving data around. Calling it a thorn in my side is very polite.

That sounds a bit more than can be handled on this forum. :frowning_face:

Oh I agree! That's why I kept it isolated to my problem of the week :upside_down_face:

Java or Jython?

1 Like

Both?

Not likely.

Well either is enough to prevent me from being able to use it

My EtherNet/IP module has no trouble using Java sockets for UDP up to the network's MTU size. Jython sockets are crap. If you are relying on the latter, that is likely your answer. (The jython stdlib is way out of date--you should not use it for anything that has a java equivalent.)

When I've got a spare day I'll try to build a module at the Java level. Unfortunately I don't really speak Java so it's more of a stretch goal than anything else at this point. I bought the MQTT engine module and despite its frustrating json handling, it seems to cover my needs for now.

We're not talking about building a module using Java, just about using the Java Socket and related class from the Java standard library, like you did with these, rather than using the networking stuff available in the Jython stdlib.

I suppose I shouldn't use "module" because I didn't mean "Module". I meant package/library/script.

Like I said I don't really speak Java. I do everything humanly possible in Python, with C/C++ when absolutely forced

1 Like

So after digging for my source for the "why can't I use jython sockets > 2048" claim (erroneously said 1024), I recovered this link: https://groups.google.com/g/netty/c/CuqtCArwRwc/m/ydV4wx_U5e4J?pli=1

I guess the issue is that in the process of the jython socket Datagram processing the internal Netty implementation for Ignition will truncate the packet to 2048 before it can get all the way out. I understand the workaround is to use java directly and avoid using jython, this just sucks for me since I already know how to do this in python but not in java.

The basics of sockets are the same in all languages, because the required behavior is defined by the protocols, not the language stdlib developer. Java sockets aren't any different, fundamentally, from jython sockets. You do the same things. Just the method names change, and not that much.

Get it over with, already.

Ah yes, the protocol behavior being all that matters. This must explain why I have such an easy time building a generalized environment for other robust protocols like Modbus.

I appreciate your usual assistance, but this was not a particularly helpful statement, Phil. I have lots to do and peeling Ignition apart to use the java layers when I selected it and started using it specifically because it has python bindings is not an optimal use of my time. I provide as much feedback as I can so that the product can improve in the ways it is advertised to work.