System.mongodb calls fail despite having the connector installed and status Valid

Using the snippet from system.mongodb.listConnectorInfo - Ignition User Manual 8.1 - Ignition Documentation yields an error (see below). The MongoDB connector is installed in the gateway and its status is "valid", and the scripting hint scope is set to "Gateway" and does properly hint mongo methods. Has anyone seen something like this before / know what to do? Freshly updated the gateway and designer to 8.1.31.

Valid connection:

Context hint:

Screencap of execution:

Script code:

#Scripting / Project Library / Mongo
# Retrieve and print the returned list of connectors found.
def listconnectorinfo():
	
	connectors = system.mongodb.listConnectorInfo()
	 
	print connectors[0]['name']
	print connectors[0]['status']
	print connectors[0]['description']
	print connectors[0]['error']

Error text:

Mongodb.listconnectorinfo()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<module:Mongo>", line 4, in listconnectorinfo
AttributeError: 'com.inductiveautomation.ignition.designer.gui.tool' object has no attribute 'mongodb'

The script console is executing on your local machine. The mongodb functions are only available when executing in the gateway. The 'script hint scope' drop-down in the script library affects the hints you get, but doesn't change how or where your function is actually executing.

2 Likes

In case you are scratching you head over what to do next, I recommend placing the code you wish to test that requires gateway scope into a gateway message handler. You can use system.util.sendMessage() or system.util.sendRequest() in the script console, targeting that handler, to run your test code.

4 Likes

How did you know!

That's very helpful thank you. Is it possible to abstract the method so I can call a script by name? system.util.execute I think is not appropriate, and if I try to pass the method itself I get an error like

com.inductiveautomation.ignition.client.gateway_interface.GatewayException: com.inductiveautomation.ignition.client.gateway_interface.GatewayException: Error serializing parameters.

Is it possible to use some system utility for calling a script by name in this context?

So if I want to use a Mongo database (example: for storing recipes), I need to connect a script to some kind of intermediate tag like a boolean "write_mongo" which when flipped True would trigger a gateway tag change script? Or I would need to add a message script and send a system message that I want to write/read from the Mongo database?

I see that there are only Mongo bindings for Perspective, which sucks since I am using Vision, but if I can only execute these system.mongo functions in the Gateway that means they are off the table for directly invocation in a button-press, for instance?

system.util.sendRequest, as Phil mentioned, is the way to go. You write one or more message handlers (which will be invoked on the gateway), and call them from the Vision client.

You can use getattr() on the system.mongo object to retrieve a callable function reference, but you probably don't need to do that.

1 Like

I was thinking more like getattr on the Project Library object, if I am just trying to build a shortcut to calling project methods in the gateway, but this suffers the serialization issues for sending parameters.

I have a Library named "Mongo" which simply contains this listconnectorinfo() method -- what do I getattr(????, "listconnectorinfo") on? I can't send Mongo because of serialization, and I can't use str "Mongo".

I think I am asking: Is there a way to do something that looks like getattr(system.scripting.MyScripts, "myScript")

Something that is almost (because this doesn't work):

# Payload looks like 
# {"library": "MyScripts", "script": "test_mongo_stuff", "args": [], "kwargs": {}}
def handleMessage(payload):
	return getattr(payload['library'], payload['script'])(
		*payload.get('args', []), 
		**payload.get('kwargs',{})
	)
1 Like

You can use locals() to retrieve a callable reference to your project library by string name. Again, though, you probably don't need this. A sufficiently general system is basically just eval() with extra steps. Why not just have a handler for listConnnectionInfo, one for query, etc?

2 Likes

I'll give it a try with locals(), thanks for the pointer.

I tried eval() and it got upset, although I can't remember the exact error at the moment. eval() could be a possible solution, that would be fine.

I don't want to have to duplicate every script that might need to run through the gateway, if I can build a single bridging function that will effectively just run a project script in the gateway. Am I perhaps overlooking a builtin feature that would allow me to do this? I still do not have a very intuitive handle what runs on client vs Gateway nor Project vs Gateway.

Just write your code to test in a project library:

def someTest(payload):
	#do something
	return someResult

Then have a gateway message handler to this:

	return someLibrary.someTest(payload)

Then in the script console, you can do this:

result = system.util.sendRequest('myProject', 'myTestHandler', {'someKey': 'someArg'})

Edit your script library test function to contain the code you wish to test. Then switch to the script console to run the short code part that calls it.

Presumably, the finished code will be run from gateway scope, because it must, and that gateway event can just call the same script library function.

4 Likes

@pturmel Yes this is what I have done, I just was hoping to be able to abstract it for two primary reasons: 1) I don't want to have to do this repeatedly if I can avoid it, and 2) the exercise helps me to understand better both the capabilities and general layout of the engine that I am working with.

@PGriffith locals() is apparently empty in the context of handleMessage. globals() seems to be stuck waiting for the gateway forever, which I suppose isn't a surprise.

Maybe vars() then. One of the builtins should expose project library scripts in a dictionary with simple string keys.

Don't. It is a huge security hole. You don't want to allow passing arbitrary code or function names. Use a test project library script and test message handler. Delete what you no longer need before deploying to production.

Same response from vars() and locals(), which is only the things immediately available to the handleMessage(payload) method.

# what_local
def handleMessage(payload):
	return [("vars", vars()), ("locals", locals())]
system.util.sendRequest("test_project", "what_local", payload={"library": "MyScripts"})
>>>
[('vars', {'payload': {'library': 'MyScripts'}}), ('locals', {'payload': {'library': 'MyScripts'}})]
>>>

Don't access vars() or locals() or globals() in the message handler. There are legacy jython scope issues there. Access those in the function in the library script.

Gateway event handlers, in general, should be one-liners that simply pass all relevant event information to a project library script function that does the real work. The arrangement of my example was not accidental.

(I recommend this for all event handlers everywhere in Ignition, but most do not have the legacy scope problems. Still a best practice for code maintainance. This will remain true as long as events are monolithic project resources instead of per-event definition resources.)

I am aware -- I am also comfortably far away from that security need being dominant over developmental hurdles. Everything is on a very closed system and I am the only developer at the moment. Also, the way that I am building our system has Ignition only as a piece of the puzzle -- it needs to interface with multiple systems for which it has no drivers.

Don't access vars() or locals() or globals() in the message handler. There are legacy jython scope issues there. Access those in the function in the library script.

If am not accessing them there, there is no point.

Gateway event handlers, in general, should be one-liners that simply pass all relevant event information to a project library script function that does the real work. The arrangement of my example was not accidental.

Unfortunate, but I am seeing that this is the only way forward. I guess I just need the extra decorator for every method like this.

This makes no sense. Why is there no point?

Why do you need a decorator?

This makes no sense. Why is there no point?

If I can't access the method object that I need in the place where I need to invoke it, I have no further use for it here.

Why do you need a decorator?

Sorry, I shouldn't use this term here, it has a specific meaning and that is confusing. I mean I need to make a one-liner for each of the project methods I want to invoke in the Gateway scope, as you suggested. Effectively decorating them, although not literally.

In your event handler, you could do something like this:

return getattr(someLibraryScript, payload.pop('functionNameKey'))(payload)

That will call any function in someLibraryScript whose name you supply in the payload, and pass that function the rest of the payload.

It will not call anything outside of someLibraryScript, so it is relatively safe if you dedicate someLibraryScript to functions expecting remote calls.