use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use spvirit_codec::spvd_decode::{DecodedValue, FieldDesc, FieldType, StructureDesc, TypeCode};
use spvirit_server::PvaServer;
use spvirit_server::pvstore::{PvInfo, Source};
use spvirit_types::{NtPayload, NtScalar, ScalarValue};
use tokio::sync::{RwLock, mpsc};
struct AccessControlSource {
inner: Arc<dyn Source>,
put_allow_list: Vec<String>,
access_counts: Arc<RwLock<HashMap<String, AtomicU64>>>,
}
impl AccessControlSource {
fn new(inner: Arc<dyn Source>, put_allow_list: Vec<String>) -> Self {
Self {
inner,
put_allow_list,
access_counts: Arc::new(RwLock::new(HashMap::new())),
}
}
async fn bump(&self, name: &str) {
let counts = self.access_counts.read().await;
if let Some(counter) = counts.get(name) {
counter.fetch_add(1, Ordering::Relaxed);
} else {
drop(counts);
let mut counts = self.access_counts.write().await;
counts
.entry(name.to_string())
.or_insert_with(|| AtomicU64::new(0))
.fetch_add(1, Ordering::Relaxed);
}
}
async fn total_accesses(&self) -> u64 {
let counts = self.access_counts.read().await;
counts.values().map(|c| c.load(Ordering::Relaxed)).sum()
}
}
impl Source for AccessControlSource {
fn claim(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<PvInfo>> + Send + '_>> {
let name = name.to_string();
Box::pin(async move {
if name == "CTRL:ACCESS_COUNT" {
return Some(PvInfo {
descriptor: StructureDesc {
struct_id: Some("epics:nt/NTScalar:1.0".to_string()),
fields: vec![FieldDesc {
name: "value".to_string(),
field_type: FieldType::Scalar(TypeCode::Float64),
}],
},
writable: false,
});
}
let mut info = self.inner.claim(&name).await?;
if info.writable && !self.put_allow_list.contains(&name) {
info.writable = false; }
Some(info)
})
}
fn get(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<NtPayload>> + Send + '_>> {
let name = name.to_string();
Box::pin(async move {
self.bump(&name).await;
if name == "CTRL:ACCESS_COUNT" {
let total = self.total_accesses().await;
return Some(NtPayload::Scalar(NtScalar::from_value(ScalarValue::F64(
total as f64,
))));
}
println!("[acl] GET '{}'", name);
self.inner.get(&name).await
})
}
fn put(
&self,
name: &str,
value: &DecodedValue,
) -> Pin<Box<dyn Future<Output = Result<Vec<(String, NtPayload)>, String>> + Send + '_>> {
let name = name.to_string();
let value = value.clone();
Box::pin(async move {
self.bump(&name).await;
if !self.put_allow_list.contains(&name) {
println!("[acl] PUT '{}' -> DENIED", name);
return Err(format!(
"access denied: '{}' is not on the PUT allow-list",
name
));
}
println!("[acl] PUT '{}' -> ALLOWED", name);
self.inner.put(&name, &value).await
})
}
fn subscribe(
&self,
name: &str,
) -> Pin<Box<dyn Future<Output = Option<mpsc::Receiver<NtPayload>>> + Send + '_>> {
let name = name.to_string();
Box::pin(async move {
self.bump(&name).await;
println!("[acl] SUBSCRIBE '{}'", name);
self.inner.subscribe(&name).await
})
}
fn names(&self) -> Pin<Box<dyn Future<Output = Vec<String>> + Send + '_>> {
Box::pin(async move {
let mut names = self.inner.names().await;
names.push("CTRL:ACCESS_COUNT".to_string());
names.sort();
names
})
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut server = PvaServer::builder()
.ao("CTRL:SETPOINT", 0.0)
.ai("CTRL:READBACK", 0.0)
.ai("CTRL:STATUS", 0.0)
.build();
let store = server.store().clone();
let acl = Arc::new(AccessControlSource::new(
store.clone(),
vec!["CTRL:SETPOINT".to_string()],
));
server.add_source("acl", -1, acl);
tokio::spawn(async move {
loop {
if let Some(sp) = store.get_value("CTRL:SETPOINT").await {
if let ScalarValue::F64(target) = sp {
if let Some(ScalarValue::F64(current)) = store.get_value("CTRL:READBACK").await
{
let next = current + 0.1 * (target - current);
store
.set_value("CTRL:READBACK", ScalarValue::F64(next))
.await;
}
}
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
});
println!("Passthrough / access-control source server running on port 5075");
println!(" Writable (allowed): CTRL:SETPOINT");
println!(" Read-only (denied): CTRL:READBACK, CTRL:STATUS");
println!(" Virtual PV: CTRL:ACCESS_COUNT");
println!();
println!("Try: spput CTRL:SETPOINT 42.0 # succeeds");
println!(" spput CTRL:READBACK 99.0 # access denied");
server.run().await
}