Where do/should Gateway Scripts live with respect to an inherited project structure?

I have the inheritence structure base_script inherited by base_sfc inherited by project0_base which is then inherited by both project0_dev and project0_staging. It is a mess. But the reason I have done this is because I keep running into project ownership issues, while still wanting to have a project that contains all of my scripts for use downstream.

My current problem is related to Gateway Scripts, and who owns them. I need to use message handler Gateway Scripts to interact with my server filesystem, and they have to run through handlers. I have implemented a handful of functions akin to ls and cat. Anyway, I want these scripts to be owned by my base_script project, as it contains all the shared scripts, but this is what happens when I try to call a message handler pointed at the Gateway Scripts:

This is when I have the project marked for inheritance and marked disabled:
com.inductiveautomation.ignition.client.gateway_interface.GatewayException: com.inductiveautomation.ignition.client.gateway_interface.GatewayException: Project 'base_script' is disabled.

But if I enable the project it says this: com.inductiveautomation.ignition.client.gateway_interface.GatewayException: com.inductiveautomation.ignition.client.gateway_interface.GatewayException: Project 'base_script' is inheritable and cannot execute message handler scripts.

If I copy paste them into project0_dev's Gateway Scripts, they work! It is both enabled and not inheritable.

My guess is when the project is disabled, the message handlers are offline, fine. But if I activate the project, it rejects the message handlers because it is inheritable. But that's the whole point! I don't want to have to manually duplicate these across various active leaf projects, especially when they are system-wide scripts. So I am not sure what to do.

One thing I have considered is making a sibling project to base_sfc that is base_gatewayscripts because I can leave it as inheriting base_scripts but I can also leave it enabled and uninheritable. It's an unappealing workaround, and I don't even know that it would work. A leaf project thats sole purpose is to run gateway filesystem requests :person_shrugging:

Any advice would be appreciated. This is one of the final steps required for my current project.

Resources don't execute in an inherited project. So things like gateway event scripts, messages etc... only execute in a non- inherited project.

What you need to do, is write the actual logic in a library script in your base script project, and then call it with a one line as a message in your lead project where you want it to reside.

Put all of the logic in project library scripts, in the "universe" project at the root of your hierarchy, if you must. Then, in leaf projects, use one-line calls to the appropriate logic from event scripts and message handlers. (I don't agree with your desire, but this is what you must do to achieve it.)

I recommend you not put all your scripts (eggs) in one inheritable project (basket). I recommend a hierarchy something like this:

universe
├bg_common
│├gsp
│├background1
│├background2
│└webdev
└ui_common
 ├perspective_common
 │├ui1
 │└ui2
 └vision_common
  ├ui3
  └ui4

In this arrangement, universe and *_common contain only well-tested resources, and are only edited/modified in appropriate maintenance windows in production. (Edits to these will cause scripting restarts in the gateway in all projects under them.)

When developing something that will end up in one of these inheritable projects, use a testing leaf project below it for your development work. When well tested, send the resources to the appropriate inheritable project and delete the testing project.

Note that perspective_common is the appropriate place for re-usable Views, and vision_common in the right place for reusable windows and templates. All other inheritable projects only contain project library scripts.

{ gsp is the gateway scripting project. }

5 Likes

I recommend you not put all your scripts (eggs) in one inheritable project (basket). I recommend a hierarchy something like this:

Okay, I see this structure as an upgrade of my own.

What is really frustrating is what I want is something that can allow me to invoke gateway scripts from anywhere without having to go through the rigamarole of setting up hardcoded gateway scripts that simply reflect back to project scripts but run in the gateway scope. It makes it very annoying to do filesystem IO.

Do you know of any way to do the equivalent of

def messageHandler(payload):
    try:
        return getattr(<what_goes_here?>, payload['method'])(*payload['args'], **payload['kwargs'])
    except Exception as e:
        return {"error": str(e)}

There's no apparent link in globals() or locals() and while the context menus pop up, I can't seem to call them programmatically.

Vision clients and the gateway are truly separate processes, even if running in the same computer. Some form of messaging or RPC is required. Ignition system calls that need gateway action do the RPC for you. If you have a custom requirement, you have to do the RPC yourself (system.util.sendMessage() or system.util.sendRequest()). Without RPC, all scripting is confined to the JVM in which it runs. Full stop.

Yes, I understand. I just wish they could be handled more fluidly than the static Gateway Scripts. I tried calling a project script from within the gateway script (on a project that definitely has the script available) and it didn't really work.

I just want to be able to do something like

result = system.util.gatewayInvocation(callback, *args, **kwargs) that handles the dispatch of the passed callback via a system.util.sendRequest call

It is very annoying to have to generate a gateway script on a leaf project in order to run things in gateway scope, especially so for filesystem commands like reading or writing files.

I made a suggestion on the ideas page: system.util.gatewayInvocation -- new method for invoking Gateway-scoped scripts from the script console / project scripts | Voters | Inductive Automation

What you've described is the holy grail of malware known as an RCE, Remote Code Execution. Not gonna happen.

Ignition's setup works the way it does so that all scripting that runs in the gateway needs to pre-exist in a project. No dynamic code, short of python eval, which itself would have to pre-exist in a project.

On top of that, the callback is itself code in the local JVM, not in the gateway JVM, so how do expect it would work?

2 Likes

Ignition's setup works the way it does so that all scripting that runs in the gateway needs to pre-exist in a project. No dynamic code, short of python eval, which itself would have to pre-exist in a project.
On top of that, the callback is itself code in the local JVM, not in the gateway JVM, so how do expect it would work?

What if callback is a path to a script in the project library specified? It doesn't need to execute arbitrary code any more than a project script might, just invoke it without having a hardcoded in-between function. It would be conceptually the same as

# Pseudocodeish
def messageHandler(payload):
    return system.util.runScript(
        payload['projectName'], payload['callback_path'], 
        *payload['args'], **payload['kwargs'])

I wasn't really suggesting executing truly arbirary code, I realize thats a massive security issue (although if it's all internal to Ignition... :eyes:). Just a way to run establish project script code that won't actually function if within the project scope.

Have a message handler that does the following:

  • Split the supplied function name on the dots.
  • Look up the first part in python's normal globals() dictionary.
  • Use repeated getattr() calls to drill down to the specific function
  • Unpack nested positional and keyword arguments
  • Call the specific function with those arguments, returning its return value to the message originator.

It should be all internal to Ignition, but it is theoretically possible to spoof/alter the behavior of a Vision Client. That's the whole point of things like cutting off Legacy DB permissions. (Yeah, it would have to be a determined attacker, but Ignition is used in infrastructure applications that would be potential targets of hostile nation-states.)

1 Like

Have a message handler that does the following:

I have looked down this road before and I didn't manage to make it work -- I ran into an issue that perhaps you know how to solve: what do I feed to getattr()? What is the object that I can hand it that will give me access to the scripts? I just tried the following and it crashed my gateway (thats why you get a screenshot and not formatted code). The payload I handed it was {"projectName": "myProject", "callback": "MyScripts.ls", "args": [], "kwargs": {}}. Printing out globals() showed "MyScripts" in the output. Normally I wouldnt try to hand back the result but I am trying to crawl my way down and check out what's going on.

Anyway -- if I can do all this this way (which I am not 100% sure yet that I can), it could be a builtin command!

You're on the right track. But the project name part is moot, as the message handler is inside a project, and it is that project's scripts that will be exposed in globals(). If you want to access all of your projects like this, put such a message handle in each of them. Then the caller can target any project they like.

That probably explains why it didn't work quite right when I was bouncing between projects, until I had pinned down project inheritence a bit better.

The projectName field is half vestigal half this-is-how-my-version-of-a-builtin-would-work so that they can access other projects, although I guess I understand if thats not ok from a security perspective.

In general, I feel that if I have to copy-paste a function into a bunch of different places, it means that there is something wrong with the ability of the system to communicate internally. Hence a unified system command! With all kinds of error handling and documentation.

Did I mention I would really love for this to be a system command?

Okay -- its kind of working actually. I can call methods that would actually not work when called from the script console:

#MyScripts in 'base_script' inherited by 'dev'
import os
def local_ls(path):
	return os.listdir(path)

def invokeGateway(callback, *args, **kwargs):
	return system.util.sendRequest("dev", "runScript", 
		{"callback": callback, "args": args, "kwargs": kwargs})
# script console in project base_script (should be agnostic to project?)
payload = {"callback": "MyScripts.local_ls", "args": ["/"]}
system.util.sendRequest("dev", "runScript", payload)
# Gateway message script 'runScript' in project "dev" 
# which is a grandchild of base_script
def handleMessage(payload):
	try:
		callback = payload['callback']
		args = payload.get('args', [])
		kwargs = payload.get('kwargs', {})
		cb_parts = callback.split('.')
		tgt = globals()
		for i in range(len(cb_parts)):
			if i == 0:
				#the handling for globals() doesn't support hasattr apparently
				tgt = tgt[cb_parts[i]] 
			elif not hasattr(tgt, cb_parts[i]):
				e = {"error": "missing {} from {}".format(cb_parts[i], str(tgt))}
				return e
			tgt = getattr(tgt, cb_parts[i])
		result = tgt(*args, **kwargs)
		return result
	except Exception as e:
		return {"error": str(e)}

It actually works finally!

1 Like

That just means there should be function in your "universe" project that does this work, and your message handlers in your leaf nodes would be just one-liners. :man_shrugging:

Write a module. Modules can declare any desired RPC they like.

That just means there should be function in your "universe" project that does this work, and your message handlers in your leaf nodes would be just one-liners. :man_shrugging:

Thats sort of how it is now, base_script is the equivalent of universe at the moment, I will refactor the projects some time in the coming weeks. Still spiritually opposed to copy-pasting boiler code regardless of the number of lines.

Write a module. Modules can declare any desired RPC they like.

Unfortunately I do not know how to do this. I don't write in Java, so while I am sure I could do this with enough effort, it is not worth the investment for me at the moment. To even consider it I'd basically need stumble upon an "almost what I need" full tutorial from install-the-JDK all the way to publish-module-to-marketplace.

Then it is probably best to stick to methods that might be a bit "brute force", but that you understand and can maintain. When/if you get a breather, you can expand your tool set.

1 Like

Time permitting, start here, then move over to the Module sub-forum, and ask your "I'm stuck on this aspect" questions there.

4 Likes

Thank you!

1 Like

I have been thinking about this diagram a lot, how to improve some of the shortcomings to the large projects we have... We have projects with hundreds of views so any project design implementation choices we made early on are coming home to roost. In the diagram

universe
├bg_common
│├gsp
│├background1
│├background2
│└webdev
└ui_common
 ├perspective_common
 │├ui1
 │└ui2
 └vision_common
  ├ui3
  └ui4

where would you put the actual projects that put all the inherited resources together (i.e. those that don't contain reusable views, but the projects in which the reusable, global views actually get used)? I'm assuming that's what ui1- ui4 are. If that's the case, it seems to me that ui1 then wouldn't be able to use scripts in the gsp project because bg_common is a sibling to ui_common, not a parent. The overall idea here is still valuable and much better than what we have now - again, this is for a large project with probably close to a 1000 views and the accompanying scripts, styles and so on (by project, I almost mean product, not Ignition project - we do have 2 level inheritance set up and many different Ignition projects to split up the functionality; it just got sloppy after a few years and many developers). I will probably create a new thread to aggregate all the information I can, but wanted to get your input on this in the meantime.