ScriptContext through client scope does not return project, but does in Perspective

I am using com.inductiveautomation.ignition.common.script.ScriptContext#defaultProject() to get the project from the client calling a script, so that I can make sure I read the resource out of the right project.

What I am running into is that if I run this in the Gateway scope (A Perspective Button) this seems to work fine. However when I try this out of the client scope (Script console) I am getting a null in response?

I am following the recommendation from this forum post, however potentially I am missing something here.

Here is a link to the script within the repo with the project, the specific use of the function is on line 38

I appreciate any help!

I think all you're missing is this:

In most circumstances, we’ll automatically set various values on the...

It seems like in this case we only set it during gateway scoped operations. I'm not sure if there's a reason for that or not, though.

edit: kind of just looks like an oversight to me. I'll open an issue and we'll figure it out.

Gotcha, is there another way to get a specific project resource in a called script like this? Or would it just be to pass the project name into the function when it's being called?

There’s no way around needing the project name.

But that said, I don’t know if it even matters. There’s zero chance the code you linked to will run in any client scope. It’s importing and using things that only exist in the gateway. Is this something called by an RPC from the client or am I missing something?

Yes it's called via RPC from com.bwdesigngroup.ignition.configmanager.client.scripting.ClientScriptModule, I was about to call that out when you said it was never client scope. There is an RPC to the com.bwdesigngroup.ignition.configmanager.gateway.scripting.GatewayScriptModule that is reading the requested resource, will deserialize the json into a PyObject and return it back to the client (When its done). The resources are project based, not gateway based

For now you’ll have to pass the project name as a parameter. If we end up setting it on the ScriptContext you’ll still have to pass it as a parameter in the RPC call, but you could omit it in the client script function and retrieve it from the ScriptContext so the caller doesn’t have to specify it.

1 Like

The gateway hook’s RPC method is given client session and project information. If you need it in your implementation, you should be creating appropriate RPC instances carrying this information (from a constructor, presumably). Probably should cache them, if they carry any other state.

Maybe I misunderstood this, but I took this as "Get the ScriptContext before the RPC, and it should have the project info client side"

However I tried that, and it yielded the same result, I think I understand adding the project info to the RPCs constructor, but I don't know if I am understanding where I get the project info in the first place?

Code in my Client module:

 @Override
    protected String getConfigImpl(String configPath) throws ProjectInvalidException {
        String projectName = ScriptContext.defaultProject();
        logger.info("Client Scope project is {}", projectName);

        return rpc.getConfig(configPath);
    }

this still returns a null projectName

I think Phil is suggesting that your GatewayScriptModule should take the session and projectName provided by GatewayHook::getRPCHandler as constructor parameters.

(and these multiple GatewayScriptModule objects could be potentially cached, if needed).

1 Like

Yes, getRPCHandler should be creating handler instances that “know” what session and project they are serving. So methods on the handler can access the handler’s own field with that information. No use of ScriptContext at all.

Note that if you cache these, you should hold a weak reference to the session.

1 Like

Yes this makes a lot of sense!

A question in terms of implementation though:

In my GatewayHook I have the following to add my script module,

@Override
public void initializeScriptManager(ScriptManager manager) {
    super.initializeScriptManager(manager);

    manager.addScriptModule(
                "system.config",
                scriptModule,
                new PropertiesFileDocProvider());
}

I am passing it the constant instance of the ScriptModule. I interpret this as making a singular instance of the module accessible under system.config and how would that work if I created a different instance based off different projects?

If I was creating a new instance of the scriptModule based off the project being called, would I just assume that the one added in initializeScriptManager is just being used for the designer documentation, and the one I have instantiated is doing the actual execution of the script?

What I was thinking about doing

@Override
    public Object getRPCHandler(ClientReqSession session, String projectName) {
        return new ScriptModule(session, projectName);
    }

initializeScriptManager is called separately for every leaf project in the gateway, and for the gateway scripting project, to support scripts running in the gateway. Each call should get a customized instance if there is different behavior based on the project. Every script environment gets its own tree under system.*.

And no, your script module cannot be your RPC handler. That way lies madness.

Keep in mind that your gateway scripts are not called through the RPC handler–the RPC handler is only getting referenced if your Vision client or the designer invoke it.

1 Like

Also, initializeScriptManager can be called to set up a script environment for other modules, too, like devices or custom tag providers. So you might encounter a ScriptManager without a project name.

1 Like

Good to know!

Sooo, can you explain this a little bit? This is how the sdk example does this?

EDIT:

Also how do I get the project from the ScriptManager?

Wow! That's lame.

Every public function of your script module gets installed in the python module tree. This doesn't permit you to have differing code between pure-gateway calls and RPC calls, which I would expect any non-trivial library to need.

Hmm. I haven't had to do this, but you can use the script manager you are given to run system.util.getProjectName(). If you really need to, you can reflect ProjectScriptManager to access the private defaultProject. (Probably shouldn't...)

1 Like

Hmmm I will try to figure out how to do this and report back. My guess is it lies somewhere within ScriptManager::runFunction, but now I guess I get to dig into executing Python code through Python a little better.

Create a localsMap. Lookup system. Look up util in system. Look up getProjectName in util. Pass that to runFunction. Use py2java on the return to get your string.

hmmm not having a lot of luck digging around in the python objects.

This is what I currently have for trying to dig around in it, do you know of a good source of documentation for accessing python content through Jython? Everything I am seeing on google keeps taking me to the Jython documentation from a python standpoint, not necessarily the Java standpoint.


    @Override
    public void initializeScriptManager(ScriptManager manager) {
        super.initializeScriptManager(manager);
        PythonInterpreter interpreter = new PythonInterpreter();
        PyObject systemModule = interpreter.getLocals().__finditem__("system");
        PyObject utilModule = systemModule.__finditem__("util");
        PyObject getProjectNameFunction = utilModule.__finditem__("getProjectName");
    
        String projectName = null;
        try {
            projectName = (String) manager.runFunction(getProjectNameFunction).__tojava__(String.class);
        } catch (JythonExecException e) {
            logger.info("Error running getProjectName function: {}", e.getMessage());
        }

        interpreter.cleanup();
        interpreter.close();
        logger.info("Initializing script manager for project {}", projectName);
    }

Update, I did manage to get this working for anyone looking at this for help!

I setup the WeakRef map for the modules by project name on my GatewayHook

public static WeakHashMap<GatewayScriptModule,String> scriptModules = new WeakHashMap<GatewayScriptModule,String>();

My initializeScriptManager override

@Override
    public void initializeScriptManager(ScriptManager manager) {
        super.initializeScriptManager(manager);

        PyStringMap localsMap = manager.createLocalsMap();
        PyObject getProjectName = localsMap.get((new PyString("system"))).__getattr__("util").__getattr__("getProjectName");
        
        String projectName = null;
        try {
            projectName = manager.runFunction(getProjectName).toString();
        } catch (JythonExecException e) {
            logger.error("Error getting project name", e);
        }

        if (projectName != null) {            
            GatewayScriptModule scriptModule = new GatewayScriptModule(projectName);
            scriptModules.put(scriptModule, projectName.toString());

            manager.addScriptModule(
                "system.config",
                scriptModule,
                new PropertiesFileDocProvider());
        }
    }

I then added a convenient getKey method for getting keys from a value in a map

public static <K, V> K getKey(Map<K, V> map, V value)
    {
        for (Map.Entry<K, V> entry: map.entrySet())
        {
            if (value.equals(entry.getValue())) {
                return entry.getKey();
            }
        }
        return null;
    }

Then in my RPC Handler I get the correct ScriptModule by project name

 @Override
    public Object getRPCHandler(ClientReqSession session, String projectName) {
        return Utilities.getKey(scriptModules, projectName);
    }

I would not have used WeakHashMap<...>. I would have used HashMap<String,WeakRef<GatewayScriptModule>>. Direct lookup.

1 Like