System Bridge

Prev Next

System level scripting with System Bridge

The Dise Player offers rich, secure, high-performance play-out running in a Chromium browser engine, with an app-based architecture, where HTML5 apps (templates) are managed through the CMS. These apps have access to all web engine features and run in the browser’s sandbox environment - with some extensions such as COM-port access.

Using web technologies and highly advanced solutions, applications can be built and maintained in a secure environment.

However, certain applications and integrations require running scripts with operating system level access and capabilities on the player device. These scripts need to run outside of the Dise Player and can communicate with apps/templates running inside the Player through the built-in websocket.

While this can be managed completely outside of Dise, using the Dise CMS to manage the complete solution - including external scripts - gives many benefits.

What is the System Bridge?

  • The System Bridge is a separate installation package, currently available for Windows & Linux.

  • Installed on the player device, runs in the background as a Windows service or under systemd on Linux

  • Hosts and runs JavaScript (Node.js) and Python scripts in the context of “NetworkService” on Windows or the local account on Linux.

  • The scripts themselves (and any other files required) are distributed as part of Dise One app packages (template zip files).

  • API for starting, stopping, and monitoring Python and JS scripts from within a Dise App.

  • API for exchange of data between the Python/JS script and the Dise Player via WebSocket; script can get and set player attributes in the Dise Player as well as exchange messages with the hosting App.

  • The System Bridge itself as well as any required scripting engines can be updated as a software add-on package from the Dise CMS.

Prerequisites

Installation on Windows

SystemBridge

  • Run the SystemBridge MSI.

Python:

  • Winpython64-3.13.1.0dot.exe in C:\Python\WPy64-31310.

  • Edit C:\Program Files (x86)\Dise\SystemBridge\appsettings.json.

  • Change script launcher for ".py" from "python" to "C:\\Python\\WPy64-31310\\scripts\\python.bat":

  • Any extension can be mapped to a script-launcher by adding to this set (e.g. LUA, WSH, PowerShell…) but we only provide API-wrappers for Python and Node.js.

{
    "webSocketURL": "ws://localhost:6556/SystemBridge",
    "diseWebRoot": "%PUBLIC%\\Dise",
    "scriptLaunchers": {
        ".js": "node",
        ".mjs": "node",
        ".py": "C:\\Python\\WPy64-31310\\scripts\\python.bat"
    }
}
  • From command line:

    • C:\Python\WPy64-31310\scripts\env.bat

    • pip install websockets


Script development

SystemBridgeSupport
63.29 KB

This contains:

  • Sample scripts and SystemBridge integration modules for Node.js and Python.

  • The node-project “echoingdevsocket“ which can be used when developing with PlayerSim and the SystemBridge Service, but without the need of a running Dise Player; this hosts an echoing WebSocket the same way the Player does. To run:
     npm install
     npm run start

  • A webpage, “websockettraffic.html“, which is useful for observing the communication between system-scripts, Template Apps and the Service.


Security considerations

The Player hosts a WebSocket (“ws:“) on localhost port 6556. This is not intended to be accessible from the Internet (WAN). There is no authentication (log-in) required for connection. The local firewall is by default opened for all incoming port-traffic of the Player (mqCef) and Player-Launcher.

If the Player is located on an untrusted LAN or is addressable from the WAN-side, some precautions are needed.

  • If WebSocket-access is not needed from the rest of the LAN, adjust the firewall-settings and make it non-accessible. Add a blocking rule for incoming traffic on port 6556. Do not remove any existing “allow“ rules for the Dise software, as they will be re-added when the Player is updated.

  • The WebSocket-Url is configurable at all levels. See the “echoingdevsocket“ project from the SystemBridge support zip for an example if you want to use a custom replacement.  In short, the WebSocket broadcasts incoming messages to all subscribers on the same path, except the message-sender.

  • A slight obfuscation-method is to keep using the built-in WS, but replace the path with something random.


Launching system-scripts from a Dise One template

The TemplateFramework implementation of SystemBridge is provided as an add-in, meaning that it will not be included when importing “index.js“. The template will need explicit references.

In tsconfig.json of a classic (namespaced) template:

  "files": [
    "../../TemplateFramework/framework/index.ts",
    "../../TemplateFramework/framework/AddIns/SystemBridge.ts",
    "../../TemplateFramework/framework/AddIns/SystemBridgeWorkerViewBase.ts"
  ],

In a template using ESM:

import * as Dise from "../../../TemplateFramework/esm/index.js";
import * as Bridge from "../../../TemplateFramework/esm/AddIns/SystemBridgeWorkerViewBase.js";

(Note that all SystemBridge specific Dise.-prefixes in the code below will be Bridge. when using ESM)

Although the SystemBridge add-in class can be used in any template, the easier way is to use the base-class SystemBridgeWorkerViewBase. This is based on the Worker template pattern, with the same functionality and behavior. In addition, the base-class provides more features out-of-the-box:

  1. The SystemBridge is created and maintained by the base, you will get a reference as a parameter to main(). This instance does not support overriding onScriptEnded as this is instead intended to be implemented as a class-member (see the example).

  2. The base provides a helper-method to start a script (called as this.runScript() ) which in addition to the remote-script handle, also returns a Promise which resolves when the script finishes. The alternative, using (SystemBridge).startScript(), relies on the (SystemBridge).onScriptEnded callback to be implemented instead.

class WorkerView extends Dise.SystemBridgeWorkerViewBase {

    // Set your name for identification here:
    uniqueTemplateName = 'BridgeTestTemplate';
    // Optional overrides:
    // waitForConnectTimeoutMs = 6000;
    // durationOverride: number = 30;

    onMessage(msg: Message<any>): void {
        console.log(msg);
        if (msg.canReply)
            msg.reply(`Template responding to "${msg.message}" from ${msg.sender}`);
    }

    onScriptEnded(localPath: string, details: ScriptEndedDetailsResponse): void {
        console.log('Ended: ' + localPath, details);
    }

    async main(bridge: Dise.RestrictedSystemBridge, content: any): Promise<void | boolean> {

        // Launch a long-living script:
        const [remoteHandle, scriptEnded] = await this.runScript('Python/sampleScript.py', content?.params);

        // Launch a "Hello world" script which console-outputs a response
        // and wait for the response immediately:
        const plainDetails = await (await this.runScript('NodeJS/Plain/HelloTemplate.js', content?.params))[1];
        console.log('Plain script details: ', plainDetails);

        // Just wait for a bit to allow the script to initialize:
        await this.controller.delay(5000);

        // setAttribute (from any template) is broadcasted through system bridge
        this.controller.setAttribute({
            name: 'test.FooBar',
            active: false,
            data: 'Good golly Miss Molly!',
        });

        // Send a message and do not expect any response:
        bridge.sendMessage(remoteHandle, "Hello!");

        // Send a message and wait for a response (an onMessage
        // event will also be fired when the reply is received):
        const responseMsg = await bridge.sendMessage(remoteHandle, "Hello! Please answer...", true);

        // A general "ping", waiting for responses from all
        // running scripts as well as the SystemBridge Service.
        const whoIsOutThere = await bridge.ping();

        // A direct "ping" to our script with timeout overridden:
        const isRemoteAlive = await bridge.ping(remoteHandle, 500);

        await this.controller.delay(5000);

        this.controller.setAttribute({
            name: 'test.FooBar',
            active: true,
            data: "Don't be lazy Miss Daisy!",
        });

        // A running script can be stopped (force-killed):
        // await this.bridge.stopScript(remoteHandle);

        const details = await scriptEnded;
        console.log('Script details: ', details);
    }
}

new Dise.TemplateController(new WorkerView());

Notes on behavior

Since the communication between all parties in a SystemBridge setup is loosely coupled through a WebSocket, the script-lifetime is maintained through the use of “keep-alive“ messages. If a template-app is ended, for example removed from the Playlist in the CMS, its running scripts will be killed within 10 seconds. A script is kept alive as long as the template-app which launched it is. If the script is re-launched from a template with the same (localhost-relative) path as the previous script-instance within this timeout, it will be kept alive and the returned handle will be the same as before.

This means that if an un-caught Error is thrown in main() before awaiting the ended-Promise, the template will cause a Client-Alarm and be restarted from fresh, possibly getting the same (live) script-instance(s) as before the failure when starting it.

If this is unwanted, the solution in this case is to catch the Error, stop the running script(s) (without awaiting) and re-throw the Error if applicable. A try-finally clause may be useful instead as stopScript() will not throw, unless the WebSocket is not open.

In some cases, where the script locks system resources, e.g. a COM-port, and needs to be stopped before a new script-version can be started. The simplest solution is to delay the starting of the script by 10 seconds, allowing the Service to kill it. If this is not acceptable, implementing another member-function to start the script is an alternative. This requires that the (possibly) running script handles an “EXIT“ message to cleanly end. See the samples for examples.

    async runSingletonScript(localPath: string, extraParameters?: string, timeoutMs?: number): Promise<[remoteName: string, endedPromise: Promise<ScriptEndedDetailsResponse>]> {
        const key = 'LastScript_' + this.uniqueTemplateName;
        const lastHandle = await this.controller.getPersistentChannelItem<string>(key);

        if (lastHandle) {
            // Inform the script (if still running) to end itself
            this.bridge.sendMessage(lastHandle, 'EXIT');
            // Give it some time to get out of the way
            await this.controller.delay(500);
        }

        let res = await super.runScript(localPath, extraParameters, timeoutMs);
        let [handle, endedPromise] = res;

        if (handle === lastHandle) {
            await this.bridge.stopScript(handle);
            await endedPromise;
            // Try again
            res = await super.runScript(localPath, extraParameters, timeoutMs);
        }

        [handle, endedPromise] = res;

        this.controller.setPersistentChannelItem(key, handle);

        return [handle, endedPromise
            .then((details) => (this.controller.setPersistentChannelItem(key, null), details))];
    }

Common functionality

The Node.js, Python and TemplateFramework implementations are similar from a use-case perspective, but there are differences.

Creating an object for communication

In the Template App, you will get the SystemBridge object automatically when subclassing SystemBridgeWorkerViewBase. The “manual” way to connect is:

const bridge = new SystemBridge('MyUniqueName');
const connected: boolean = await bridge.waitForScriptServiceConnection(4000); // Timeout in ms

// Note that the SystemBridge class is a singleton. Make sure to only create it once.

When the Service starts scripts, the two first command-line parameters are:

  1. A unique handle for this script instance

  2. The handle of the Template App which started it

Additional parameters can be included when calling startScript/runScript.

Node.js:

import { getSystemBridgeClient } from "./DiseAPI/index.js";

const args = process.argv.slice(2);
const SCRIPT_SENDER_NAME = args[0] || "SIM_SCRIPT_SENDER"
const TEMPLATE_SENDER_NAME = args[1] || "BridgeTestTemplate"

console.log("Connecting to Dise System Bridge...");
const bridge = await getSystemBridgeClient(SCRIPT_SENDER_NAME, TEMPLATE_SENDER_NAME);

// bridge is nullish if connection failed
from SystemBridge.client import DiseAPIClient

args = sys.argv[1:]
SCRIPT_SENDER_NAME = args[0] if len(args) > 0 else "SIM_SCRIPT_SENDER"
TEMPLATE_SENDER_NAME = args[1] if len(args) > 1 else "BridgeTestTemplate"

async def main():
    DiseAPI = DiseAPIClient(SCRIPT_SENDER_NAME, TEMPLATE_SENDER_NAME)
    await DiseAPI.connect()
    connected = await DiseAPI.waitForConnection()

    # connected is true if a connection could be established in time

Using Attributes

Attributes in a Template App have no specific SystemBridge calls or events. As with any template, an attribute is set with this.controller.setAttribute() and attribute changes cause the onAttribute() event-handler.

In Node.js and Python, the APIs are the same:

  • setAttribute(): Sets a Player Attribute

  • getAttribute(): Gets the current Attribute-state from the Player.
    Getting a non-existent Attribute, will return undefined/null/None.

  • The onAttribute event/callback can be used to get informed of all Attribute-changes on a Player.

Note that getAttribute() returns the Attribute-state which was last broadcast from the Player. Due to the extreme asynchronous nature of both the PlayerAPI and the SystemBridge API, doing a get- immediately after a set- (of the same Attribute), can, in theory, return either the last or the altered state. We recommend using an internal set of attributes and populate it on startup by calling getAttribute() of the relevant set, then relying on onAttribute to track changes. For example (in Node.js - Python syntax is different):

const ourAttributes = {
  'dyn.ValueOne': null,
  'dyn.ValueTwo': null,
  'trigger.CommercialBreak': null,
  'static.Summer': null,
  'static.Rainy': null,
};

bridge.onAttribute = (attribute) => {
  if (attribute.name in ourAttributes)
    ourAttributes[attribute.name] = attribute;
}

await Promise.all(
  Object.keys(ourAttributes)
    .map((name) => bridge.getAttribute(name)
      .then((attr) => ourAttributes[name] = attr));

Populates all the attributes simultaneously instead of in sequence.

Messages

All three implementations have similar support for sending any JSON-serializable object to each other, typically a string. There are size-limitations of the payload, due to constraints in the built-in Player-WebSocket. As a rule of thumb, please keep the payload under approx 500 characters. This is true for all SystemBridge communication, but a particularly easy trap to fall into with messages. If file-transfer is needed, put the file on a webserver-shared location and send the relative path as a message instead. For example, on Windows, add the file to “%PUBLIC%\Dise\myScriptSharedFiles\myfile.jpg“ and send the path “myScriptSharedFiles\myfile.jpg“ included in a message. The template will need to resolve this to the url “/myScriptSharedFiles/myfile.jpg“.

sendMessage(): Sends a message, optionally waiting for a reply.

  • Node.js/Python only allows communication with the hosting Template App. Specify true as a parameter to (a)wait for a reply.

  • In the Template App, the first parameter is the recipient’s handle/name or the template-relative path to the (started) script. Waiting for a reply is the same as with the other two.

A Template App can interchange messages with any other party, provided the handle is known. This functionality can be useful when, for example, asking a previously started script to exit or for synchronization between multiple Script-hosing Apps in different layers.

Use the onMessage event/callback for listening to messages from the host/script. For convenience, the supplied object allows replying to the message (if it is not already a response).

See the sample-scripts for examples.

Ping

Checks for the presence of other API-implementers on the WebSocket.

If no recipient-handle is provided, scripts default to checking the status of the Template App (host).

In a Template App, doing a bridge.ping() without parameters will send a “broadcast Ping“, returning an array of the responders and the response-time in milliseconds.


App API specifics

startScript/runScript

As previously documented, these are different ways of starting a script. runScript() is only available when using SystemBridgeWorkerViewBase and accessible on the this-object, rather than the bridge.

stopScript

Stops an active script by killing the process. Only scripts which were started by the running instance of the template can be stopped. This means that a “fresh“ template restart or a Client-Alarm will prevent stopping scripts started by a previous instance, even when the handle/name is known.

isScriptServiceRunning

Checks if the SystemBridge Service is running (by “ping’ing“ it).

onScriptEnded

This event signifies that one of the scripts started by this app has ended. Either because it ended on its own, was explicitly stopped or was timed out due to losing connection to the app.

Implement this as a member-function on the “View” class. If the bridge was created without relying on the ViewBase-functionality, the callback needs to be explicitly set on the bridge-object.

The event is called with the following signature:

onScriptEnded(localPath: string, details: {
    remoteName: string;
    reason: 'ENDED' | 'TIMEOUT';
    cout: string;
    exitCode?: number;
}): void

“cout” is the console-output (stdout) from the script, limited to the first 500 characters. An explicitly stopped script will not return any cout, nor an exitCode.


Additional DiseAPI Python functions

get_logger

def get_logger()

Description:

Get logger instance. Will log messages to DbgView if running on Windows.

Returns:

Logger instance.

Example:

from SystemBridge.client import get_logger
logger = get_logger()
logger.info('This is a test log message.')

ConnectTimeoutMS

4 second connection timeout.

ReceiveTimeoutMS

4 second receive timeout.