GatewayHook getRPCHandler Implementation

I'm working on a module that will contain a variety of scripting functions. Because I don't want every function defined in one class, I've created 3 (for now), and am having issues "combining" them.

In the sdk-examples scripting-function example, there is only one class of functions, which can be easily returned in the getRPCHandler method.

In my case, I'm having 3 separate module classes.


image

My issue is that ultimately, getRPCHandler is only returning one object. What is the best way to "combine" these 3 module classes together, into one object to be returned? Or is an entirely different approach required at this point?

Are you actually using RPC from Vision clients or the Designer?

While the example conflates and RPC handler with a class for scripting functions, that is a very simplistic combination, and only meaningful if you do not ever need to push messages back to specific Vision/Designer clients.

What RPC functionality do you really need? Do your scripting classes really need to be instantiated? (Can you not use static functions and just pass the class itself to register?)

This was asked/answered here as well.

Basically, you get one RPC handler, but that doesn't have to be the same object as your script modules, so multiplex it out however you want.

Also for your consideration: for years now we (IA) no longer add new scripting functions to Designer / Vision Client scope and relay them over RPC. We only add new scripting functions to Gateway scope.

I do not need to target specific clients. The designer, I'm just sticking as closely to the scripting function example as possible. Any coding choice I make is basically a result of that. I'm open to any suggestions.

Do this. Three (or however many) classes of static functions. Private constructor (not used). Register those directly.

Omit getRPCHandler if you don't need it.

1 Like

Meh. ):

I've had this in place already, is this what you mean?

By omitting getRPCHandler, I assume you mean simply deleting the implementation of it. Which then I get this error, when attempting to run one of the module functions in the Designer script console:

Let's step back. What are your script modules actually doing? Do they actually need any RPC, at all?
The public SDK example leads you down that path, but you don't need to use it if you can run your logic locally/self contained.

They are just running SQL queries against our gateway DB's. I also plan on having the functions check the module's LicenseState to determine how they execute. I don't know if they need RPC, could you tell me a bit more about that? What kind of situations require an RPC?

Well, one alternative would be to have your designer/client scoped classes rely on the existing facilities in the platform for sending SQL queries from the client. Probably not worth it, though; this is a pretty classic case for RPC.

Basically - the designer and client don't have all of the same facilities as the gateway - one of the obvious things is that the designer is not actually connecting to your database(s). Instead, the designer just has to talk to the gateway, and the gateway talks to the database. This is more efficient on your database, eliminates the security risk of having every designer/client get DB credentials, etc. So the RPC mechanism built into the platform for use from clients/designers is all about "expose a limited subset of the functionality a module on the gateway has to clients/designers".

1 Like

Okay, so it seems like in this case, I do actually need the RPC then. So back to the original problem, what's the best way to "compile" all script modules together to return, in the getRPCHandler method?

Hello, I'm facing the same issue and found this discussion in the forum. Have you found a solution to this problem?

I would recommend supplying more information about your specific problem, or possibly making a new post about it with some screenshots of your code possibly and an explanation of what it is you are trying to accomplish.

1 Like

I'm working on a module that contains multiple scripting functions. I am new to ignition module development and I am following IA's scripting function module training example available in its GitHub repository to do my development. I was able to develop a module with a single function, but am having issues combining more than one function in a single module. As an example, here is the overall structure of my module with two functions F1 and F2.

scripting-library
|
|
|
+---client
| | build.gradle.kts
| |
| |
| ---src
| +---main
| | ---java
| | ---com
| | ---inductiveautomation
| | ---ignition
| | ---scriptinglibrary
| | ---client
| | ScriptingLibraryClientHook.java
| | ClientScriptModuleF1.java
| | ClientScriptModuleF2.java
| |
+---common
| | build.gradle.kts
| |
| |
| ---src
| +---main
| | +---java
| | | ---com
| | | ---inductiveautomation
| | | ---ignition
| | | ---scriptinglibrary
| | | ---common
| | | AbstractScriptModuleF1.java
| | | AbstractScriptModuleF2.java
| | | InterfceClassF1.java
| | | InterfceClassF2.java
| | |
| | ---resources
| | ---com
| | ---inductiveautomation
| | ---ignition
| | ---scriptinglibrary
| | ---common
| | AbstractScriptModuleF1.properties
| | AbstractScriptModuleF2.properties
| |
+---designer
| | build.gradle.kts
| |
| |
| ---src
| +---main
| | ---java
| | ---com
| | ---inductiveautomation
| | ---ignition
| | ---scriptinglibrary
| | ---designer
| | ScriptingLibraryDesignerHook.java
| |
+---gateway
| | build.gradle.kts
| |
| |
| ---src
| +---main
| | ---java
| | ---com
| | ---inductiveautomation
| | ---ignition
| | ---scriptinglibrary
| | ---gateway
| | ScriptingLibraryGatewayHook.java
| | GatewayScriptModuleF1.java
| | GatewayScriptModuleF2.java
| |

This is an screen capture of "ScriptingLibraryGatewayHook.java".

My question is how I should set up this part of the code when I have more than one scripting function in my module, especially how to use both functions in getRPCHandler (it only returns one parameter).

Just for your knowledge it should return an object that implements an interface of your RPC

    private ApexRPCInterface rpc;

    @Override
    public void startup(ClientContext context, LicenseState activationState) throws Exception {
        logger.info(context.getDefaultDatasourceName());
        this.context = context;
        logger.info("Setup Ran");
        this.rpc = ModuleRPCFactory.create(
                "org.apex.rpm",
                ApexRPCInterface.class);
    }

This RPC is not going to be the script module you are trying to use. It should be something that you need to run things on the gateway.

So for example:

package org.apex.util;

import java.sql.SQLException;

import org.python.core.PyObject;

import com.inductiveautomation.ignition.common.Dataset;
import com.inductiveautomation.ignition.gateway.datasource.SRConnection;
import com.inductiveautomation.ignition.gateway.model.GatewayContext;

public class ApexRPC implements ApexRPCInterface {
    private GatewayContext gatewayContext;

    public ApexRPC(GatewayContext context) {
        this.gatewayContext = context;
    }

    @Override
    public Dataset runQuery(String query, String connectionName) throws SQLException {
        Dataset result;
        try (SRConnection con = gatewayContext.getDatasourceManager().getConnection(connectionName)) {
            result = con.runQuery(query);
        }
        return result;
    }
}

You can add other things to your Interface to implement on the actual RPC. This then gives the capability for you to access the gateway functionality you don't have in the client.

This is as I understand it based around some RPC models. The key is you do not/should not expose the entire gateway context to your client or designer. Instead limit it to your functional requirements.

And for those experts out there if I am wrong in this I would like to know to improve what I have going on.

1 Like

I keep repeating this, but build an RPC handler object that multiplexes/delegates the requests. Something like this:

class MultiplexingRPCHandler 
    implements ScriptingFunctionInterfaceA, ScriptingFunctionInterfaceB {

    private final ScriptingFunctionImplA a;
    private final ScriptingFunctionImplB b;

    MultiplexingRPCHandler(ScriptingFunctionImplA a, ScriptingFunctionImplA b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public void functionFromA() {
        a.functionFromA();
    }

    @Override
    public void functionFromB() {
        b.functionFromB();
    }

}
1 Like

The connection between the RPC interface and the scripting functions is an artificial constraint implied by the example, but not actually true.

What you actually need is:

  • An interface in Common that stubs the functions for which you need RPC. Only those functions, not anything else your scripts might do.

  • An implementation of that interface in Client (and indirectly Designer) that delegates to the RPC invocation methods for each such function.

  • An implementation of that interface in Gateway that actually does the work, possibly with per-client session management. This class can be just wrapper functions that delegate anywhere else in the gateway you wish.

The actual classes or class instances in which you register scripting functions do not have to be the above. They just have to call the above where needed.

You can register many different scripting function classes, organized appropriately, of which some call RPC methods.

I recommend you not register your RPC classes as scripting modules.

1 Like

Following the pattern Phil describes above will also make your migration to 8.3's new RPC interface as smooth as possible. You'll still have to recompile against the new classes/APIs, but it'll be a pretty straightforward migration.

3 Likes

Thanks to all of you for your helpful comments.

As shown in the example Scripting Library module structure tree I provided in my previous message,

  1. I have defined separate interfaces (and abstract base classes) for each scripting function F1 and F2 in Common.

The purpose of this step is to follow @pturmel's comment "An interface in Common that stubs the functions for which you need RPC. Only those functions, not anything else your scripts might do."

  1. To follow @pturmel 's comment "An implementation of that interface in Client (and indirectly Designer) that delegates to the RPC invocation methods for each such function.",

The following code snippet has been developed in ClientScriptModuleF1 that makes this class delegate calls to the rpc object. The same logic has been implemented in ClientScriptModuleF2.

package com.inductiveautomation.ignition.scriptinglibrary.client;

import com.inductiveautomation.ignition.client.gateway_interface.ModuleRPCFactory;

import com.inductiveautomation.ignition.scriptinglibrary.common.InterfceClassF1 ;

import com.inductiveautomation.ignition.scriptinglibrary.common.AbstractScriptModuleF1;

public class ClientScriptModuleF1 extends AbstractScriptModuleF1 {

    private final InterfceClassF1 rpc;

    public ClientScriptModuleF1() {

        rpc = ModuleRPCFactory.create(

                "com.inductiveautomation.ignition.scriptinglib.ScriptingLibrary",

                InterfceClassF1.class

        );

    }

    @Override

    protected String FunctionFromF1Impl(String param1, String param2) {

        return rpc.FunctionFromF1(param1, param2);

    }

}

Also, the following code snippet has been included in ScriptingLibraryClientHook and ScriptingLibraryDesignerHook

@Override

    public void initializeScriptManager(ScriptManager manager) {

        super.initializeScriptManager(manager);

        manager.addScriptModule(

                "system.newlib",

                new ClientScriptModuleF1(),

                new PropertiesFileDocProvider());

        manager.addScriptModule(

                "system.newlib",

                new ClientScriptModuleF2(),

                new PropertiesFileDocProvider());

    }

}
  1. To follow @pturmel 's third comment, I have developed GatewayScriptModuleF1 and GatewayScriptModuleF2 classes as implementation of the interfaces in Gateway that actually does the work.
    And
    To manage multiple objects in RPC handler I added the following class to Gateway scope as suggested by @Kevin.Herron:
package com.inductiveautomation.ignition.scriptinglibrary.gateway;

import com.inductiveautomation.ignition.scriptinglibrary.common.InterfceClassF1;

import com.inductiveautomation.ignition.scriptinglibrary.common.InterfceClassF2;

public class MultiplexingRPCHandler implements InterfceClassF1, InterfceClassF2 {

    private final GatewayScriptModuleF1 scriptModuleF1;

    private final GatewayScriptModuleF2 scriptModuleF2;

    public MultiplexingRPCHandler(GatewayScriptModuleF1 scriptModuleF1, GatewayScriptModuleF2 scriptModuleF2) {

        this.scriptModuleF1 = scriptModuleF1;

        this.scriptModuleF2 = scriptModuleF2;

    }



    @Override

    public String FunctionFromF1(String param1, String param2) {

        return scriptModuleF1.FunctionFromF1Impl(param1, param2);

    }



    @Override

    public String FunctionFromF2(String param1, String param2) {

        return scriptModuleF2.FunctionFromF2Impl(param1, param2);

    }

}

Then, revised ScriptingLibraryGatewayHook as follows:

public class ScriptingLibraryGatewayHook extends AbstractGatewayModuleHook {

    private final Logger logger = LoggerFactory.getLogger(getClass());



    private final GatewayScriptModuleF1 scriptModuleF1 = new GatewayScriptModuleF1();

    private final GatewayScriptModuleF2 scriptModuleF2 = new GatewayScriptModuleF2();



    private final MultiplexingRPCHandler rpcHandler = new MultiplexingRPCHandler(scriptModuleF1, scriptModuleF2);



    @Override

    public void setup(GatewayContext context) {

    }



    @Override

    public void startup(LicenseState activationState) {

    }



    @Override

    public void shutdown() {

    }



    @Override

    public void initializeScriptManager(ScriptManager manager) {

        super.initializeScriptManager(manager);

        manager.addScriptModule(

                "system.newlib",

                scriptModuleF1,

                new PropertiesFileDocProvider());

        manager.addScriptModule(

                "system.newlib",

                scriptModuleF2,

                new PropertiesFileDocProvider());

    }

    @Override

    public Object getRPCHandler(ClientReqSession session, String projectName) {return rpcHandler;}

    }

When building and testing my developed module, it appears to work correctly. Would you mind confirming whether I am on the right track?