[Bug] system.util.sendMessage fails with remoteServers when payload is not a dict

In Ignition 8.1.10, when calling system.util.sendMessage, if using scope=G and a value is specified for remoteServers, the message is never received on the remote server if payload is an Ignition type, such as com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper$ObjectWrapper. I need to first convert the payload to a Python dict before sending the message.

This does not appear to be the case using scope=S if remoteServers is not specified. In that case, the message is sent successfully, even when payload is not a true dict.

To summarize, this works:

system.util.sendMessage(
		project='my-project',
		messageHandler='my-message',
		payload=self.custom.myCustomParam,
		scope='S')

But this does not:

system.util.sendMessage(
		project='my-project',
		messageHandler='my-message',
		payload=self.custom.myCustomParam,
		scope='G',
		remoteServers=myRemoteServerAddress)

Not terribly surprising, since Perspective runs in its own classloader (like all modules). Any scope other than ‘S’ won’t be able to deserialize a Perspective class instance (deserializing being required for objects passed through a network).

Not a bug, but a side effect of Ignition’s module isolation.

Makes perfect sense. Still, this behavior was unexpected, and it was difficult to figure out what was going wrong.

I think there’s an open ticket on our end about the Perspective wrapper objects “leaking” into scripting.

IGN-1660 if anyone else from IA is playing along. I’m going to add this thread to the ticket.

What is the solution for this in general terms when trying to use Perspective properties as pyDicts?
I’m trying to send custom parameters as pyDicts but not sure how to convert “” type into a proper python dictionary type.

EDIT: Seems like just casting it with dict(objectRefHere) did the trick for what i was trying to do.

2 Likes

Has there been any update on this behavior? Casting the object as a dictionary seems to be a decent workaround, but has to be done at every level of a nested list/dictionary object. I'm having to do a bit of type manipulation in perspective before calling message handlers quite often.

We have the code below in a script module that converts Ignition-specific objects to native Python lists and dicts. We pass the Ignition object to the propertyToPython function, and get back Python lists and/or dicts.

def _isDictLikeObject(value):
	return 'iteritems' in dir(value)


def _isListLikeObject(value):
	return 'append' in dir(value)
	    	
	
def dictLikeObjectToDict(dictLikeObject):
	pyDict = {}
	for key, item in dict(dictLikeObject).iteritems():
		item = propertyToPython(item)				
		pyDict[key] = item
	return pyDict


def listLikeObjectToList(listLikeObject):
	pyList = []
	for item in list(listLikeObject):
		item = propertyToPython(item)				
		pyList.append(item)
	return pyList


def propertyToPython(property):
	"""
	Accepts a value, object, or array property that might be
	encoded as an Ignition ObjectWrapper or ArrayWrapper and
	returns the property as Python dicts and lists.
	"""
	if _isDictLikeObject(property):
		property = dictLikeObjectToDict(property)
	elif _isListLikeObject(property):
		property = listLikeObjectToList(property)
	return property
2 Likes

Thanks for sharing!

I landed on something similar

def unwrap(object): 
	if str(object).startswith('<ArrayWrapper>'):
		object = list(object)
		object = [unwrap(item) for item in object]
	elif str(object).startswith('<ObjectWrapper>'):
		object = dict(object)
		object = {key: unwrap(value) for key, value in object.items()}
	return object

Would prefer to explicitly check for ArrayWrapper and ObjectWrapper types, but not having any luck.
<type 'com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper$ArrayWrapper'>
<type 'com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper$ObjectWrapper'>

Check out the system.util.deepCopy() function in the Ignition Extensions module.

Thanks for the recommendation. The Ignition Extensions module sounds like a great idea!
Unfortunately, I'm not able to load that module into my environment at the moment.
I have tried python's copy.deepcopy() but it throws an error.

Yeah, it pukes if it hits an object that it isn’t prepared for.

Here is a pure Jython wrapper that should help.

I like this. It looks to be basically the same as our solution, but more concise.

A word of caution, though: An earlier implementation of our solution also checked for specific strings in the type names, similar to what you're doing with <ArrayWrapper> and <ObjectWrapper>, but that broke in an Ignition update at some point when Ignition changed the classes used in certain situations. That's why we settled on checking for the presence of the iteritems and append methods to determine if the object is a list or a dict.

1 Like

great point, I'll use the in dir() logic that you provided. Thanks!

Consider using the python builtin hasattr() instead of x in dir(). Should be much faster.

The script in the link I provided shows how to explicitly check for the types.

For a more concise and resilient deep copy before serializing, try this.

(Just stumbled on this thread while researching remoteServers and realized I knew a good answer to the question being asked even though it was a bit old... Figured I'd help anyone else who lands here later...)