External BUS/RT services

Sometimes a program can not be built as EVA ICS service. However such programs can still connect to EVA ICS bus and have all benefits as regular services except having their processes managed by 3rd party software.

A clear example of such approach can be rPLC instances or external Windows services which e.g. provide technologies available on Microsoft Windows platform only.

External programs/services usually do not use EVA ICS SDK but use eva_common and busrt directly.

Let us review a quick example of an external service launched on a Microsoft Windows machine.

The task

The article provides a very basic external service example. External BUS/RT connections are not visible in “eva svc list” however other bus members can still call their RPC methods and receive bus events.

Let us create a service which handles two RPC methods.

Preparation

To allow external connections to EVA ICS node, open BUS/RT port by editing eva/config/bus:

eva edit config/bus

Add “- 0.0.0.0:7777” line to sockets array (as there is no strong authentication in BUS/RT, in production it is highly recommended to use an external IP of a private dedicated network only). Do not forget to restart EVA ICS node after the bus configuration is modified.

Source code

Cargo.toml

[package]
name = "es"
version = "0.1.0"
edition = "2021"

[dependencies]
busrt = { version = "0.4.5", features = ["rpc", "ipc"] }
eva-common = { version = "0.3.1", features = ["bus-rpc"] }
eventlog = "0.2.2"
log = "0.4.19"
tokio = { version = "1.28.2", features = ["full"] }
windows-service = "0.6.0"

main.rs

use busrt::rpc::{Rpc, RpcClient, RpcError, RpcEvent, RpcHandlers, RpcResult};
use busrt::{async_trait, Frame};
use eva_common::payload::pack;
use eva_common::prelude::*;
use log::{error, info};
use std::ffi::OsString;
use std::sync::atomic;
use std::time::Duration;
use windows_service::service::{
    ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType,
};
use windows_service::service_control_handler::{self, ServiceControlHandlerResult};
use windows_service::{define_windows_service, service_dispatcher};

const SLEEP_STEP: Duration = Duration::from_secs(1);

static ACTIVE: atomic::AtomicBool = atomic::AtomicBool::new(true);

const SVC_NAME: &str = "my.external.svc1";
const BUS_PATH: &str = "172.16.54.1:7777";

struct MyHandlers {}

// RPC implementation
#[async_trait]
impl RpcHandlers for MyHandlers {
    async fn handle_call(&self, event: RpcEvent) -> RpcResult {
        match event.parse_method()? {
            "test" => Ok(None),
            "hello" => Ok(Some(pack("hi there")?)),
            _ => Err(RpcError::method(None)),
        }
    }
    async fn handle_notification(&self, _event: RpcEvent) {}
    async fn handle_frame(&self, _frame: Frame) {}
}

// connects to the bus and blocks while the service is active
async fn xsvc() -> EResult<()> {
    let config = busrt::ipc::Config::new(BUS_PATH, SVC_NAME);
    let client = busrt::ipc::Client::connect(&config).await?;
    let handlers = MyHandlers {};
    let rpc = RpcClient::new(client, handlers);
    info!("{} connected", SVC_NAME);
    while rpc.is_connected() && ACTIVE.load(atomic::Ordering::Relaxed) {
        tokio::time::sleep(SLEEP_STEP).await;
    }
    Ok(())
}

define_windows_service!(ffi_service_main, service_main);

// windows service, see https://crates.io/crates/windows-service
fn service_main(_arguments: Vec<OsString>) -> Result<(), windows_service::Error> {
    let event_handler = move |control_event| -> ServiceControlHandlerResult {
        match control_event {
            ServiceControl::Stop => {
                ACTIVE.store(false, atomic::Ordering::Relaxed);
                ServiceControlHandlerResult::NoError
            }
            ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
            _ => ServiceControlHandlerResult::NotImplemented,
        }
    };
    let status_handle =
        service_control_handler::register(format!("EVA.{}", SVC_NAME), event_handler)?;
    let mut next_status = ServiceStatus {
        service_type: ServiceType::OWN_PROCESS,
        current_state: ServiceState::Running,
        controls_accepted: ServiceControlAccept::STOP,
        exit_code: ServiceExitCode::Win32(0),
        checkpoint: 0,
        wait_hint: Duration::default(),
        process_id: None,
    };
    status_handle.set_service_status(next_status.clone())?;
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap();
    rt.block_on(async move {
        while ACTIVE.load(atomic::Ordering::Relaxed) {
            if let Err(e) = xsvc().await {
                error!("{}", e);
            }
            if ACTIVE.load(atomic::Ordering::Relaxed) {
                tokio::time::sleep(SLEEP_STEP).await;
            }
        }
    });
    next_status.current_state = ServiceState::Stopped;
    status_handle.set_service_status(next_status)?;
    Ok(())
}

fn main() -> Result<(), windows_service::Error> {
    let log_name = format!("EVA.{}", SVC_NAME);
    eventlog::register(&log_name).unwrap();
    eventlog::init(&log_name, log::Level::Info).unwrap();
    service_dispatcher::start(format!("EVA.{}", SVC_NAME), ffi_service_main)?;
    Ok(())
}

Registering the service in Windows

SC.exe create EVA.my.external.svc1 binPath=path\to\file.exe

Start/stop/manage the service using CLI or Windows service manager.

Calling RPC methods

The service handles the following RPC methods:

  • test returns an empty successful response

  • hello returns “hi there” string

# external svc methods can be still called with "svc call"
eva svc call my.external.svc1 hello

External BUS/RT programs are not listed in EVA ICS services (“eva svc list”), however they can be listed in BUS/RT broker:

eva broker client.list