eva_sdk/
ll.rs

1use busrt::rpc::{RpcError, RpcEvent, RpcResult};
2use eva_common::{
3    acl::Acl,
4    payload::{pack, unpack},
5    prelude::*,
6    services,
7};
8pub use logicline::global::{ingress, processor, set_recording};
9pub use logicline::{Processor, Snapshot, action};
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12use std::sync::OnceLock;
13
14use crate::hmi::XParamsOwned;
15
16static SNAPSHOT_ACL_MAPPING: OnceLock<SnapshotAclMapping> = OnceLock::new();
17
18pub fn set_snapshot_acl_mapping(mapping: SnapshotAclMapping) -> EResult<()> {
19    SNAPSHOT_ACL_MAPPING
20        .set(mapping)
21        .map_err(|_| Error::core("Snapshot ACL mapping already set"))
22}
23
24pub(crate) fn api_ll_info() -> EResult<Vec<u8>> {
25    #[derive(Serialize)]
26    struct Response {
27        recording: bool,
28    }
29    let current_recording_state = logicline::global::is_recording();
30    pack(&Response {
31        recording: current_recording_state,
32    })
33}
34
35pub(crate) fn api_ll_snapshot(params: Option<Value>, acl: Option<&Acl>) -> EResult<Vec<u8>> {
36    #[derive(Deserialize)]
37    #[serde(deny_unknown_fields)]
38    struct Params {
39        #[serde(default)]
40        line_filter: String,
41    }
42    let mut line_filter = String::new();
43    if let Some(p) = params
44        && !p.is_unit()
45    {
46        line_filter = Params::deserialize(p)?.line_filter;
47    }
48    if line_filter == "#" || line_filter == "*" {
49        line_filter.clear();
50    }
51    let mut snapshot = logicline::global::snapshot_filtered(|l| l.name().starts_with(&line_filter));
52    if let Some(acl) = acl
53        && let Some(mapping) = SNAPSHOT_ACL_MAPPING.get()
54    {
55        snapshot = mapping.format_snapshot(snapshot, acl);
56    }
57    pack(&snapshot)
58}
59
60pub(crate) fn api_ll_set_recording(params: Value) -> EResult<Vec<u8>> {
61    #[derive(Deserialize)]
62    #[serde(deny_unknown_fields)]
63    struct Params {
64        recording: bool,
65    }
66    let params: Params = Params::deserialize(params)?;
67    logicline::global::set_recording(params.recording);
68    Ok(vec![])
69}
70
71#[derive(Clone, Serialize, Deserialize, Default)]
72pub struct SnapshotAclMapping {
73    inner: BTreeMap<String, OID>,
74}
75
76impl SnapshotAclMapping {
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Add OID mapping for ACL checks in snapshots. source: `line/step_name`
82    pub fn add(&mut self, source: impl AsRef<str>, oid: OID) {
83        self.inner.insert(source.as_ref().to_owned(), oid);
84    }
85
86    pub fn format_snapshot(
87        &self,
88        mut snapshot: logicline::Snapshot,
89        acl: &Acl,
90    ) -> logicline::Snapshot {
91        for line in snapshot.lines_mut().values_mut() {
92            let line_name = line.name().to_owned();
93            for step in line.steps_mut() {
94                for i in step.info_mut() {
95                    let source = format!("{}/{}", line_name, i.name());
96                    let Some(oid) = self.inner.get(&source) else {
97                        continue;
98                    };
99                    if acl.check_item_read(oid) {
100                        continue;
101                    }
102                    *i = i.to_modified(
103                        None,
104                        Some(serde_json::Value::String("<hidden>".to_owned())),
105                        None,
106                        None,
107                    );
108                }
109            }
110        }
111        snapshot
112    }
113}
114
115pub fn handle_default_rpc(event: RpcEvent, info: &services::ServiceInfo) -> RpcResult {
116    let method = event.parse_method()?;
117    let payload = event.payload();
118    match method {
119        "ll.info" => Ok(Some(api_ll_info()?)),
120        "ll.snapshot" => Ok(Some(api_ll_snapshot(
121            if payload.is_empty() {
122                None
123            } else {
124                unpack(payload)?
125            },
126            None,
127        )?)),
128        "ll.set_recording" => {
129            if payload.is_empty() {
130                return Err(RpcError::params(None));
131            }
132            Ok(Some(api_ll_set_recording(unpack(payload)?)?))
133        }
134        "x" => {
135            if payload.is_empty() {
136                Err(RpcError::params(None))
137            } else {
138                let xp: XParamsOwned = unpack(payload)?;
139                match xp.method() {
140                    "ll.info" => Ok(Some(api_ll_info()?)),
141                    "ll.snapshot" => Ok(Some(api_ll_snapshot(Some(xp.params), Some(&xp.acl))?)),
142                    "ll.set_recording" => {
143                        xp.acl.require_admin()?;
144                        Ok(Some(api_ll_set_recording(xp.params)?))
145                    }
146                    _ => Err(RpcError::method(None)),
147                }
148            }
149        }
150        _ => crate::service::svc_handle_default_rpc(method, info),
151    }
152}