use std::time::Duration;
use std::sync::Arc;
use tracing::{debug, warn};
use crate::error::{reason, Error, ErrorKind, Result};
use crate::value::{CommandDataKind, Scalar, Spectrum};
use crate::pool;
use super::polling::{self, Poller, PolledKind, MIN_PERIOD};
use super::def::{ClassDef, CommandDef};
use super::server::Server;
use super::dev::{AnyDev, Dev};
pub const CLASS_NAME: &str = "DServer";
pub struct Admin {
}
impl Admin {
pub fn def() -> ClassDef<Self> {
ClassDef::new_async(CLASS_NAME, Self::init)
.doc("The administration device of that server, one per server, through which \
its devices are restarted, polled, logged and subscribed to.")
.command(CommandDef::new("DevRestart")
.in_doc("Device name")
.exec_async(Self::dev_restart))
.command(CommandDef::new("RestartServer")
.exec_async(Self::restart_server))
.command(CommandDef::new("QueryClass")
.out_doc("Device server class(es) list")
.exec(Self::query_class))
.command(CommandDef::new("QueryDevice")
.out_doc("Device server device(s) list")
.exec(Self::query_device))
.command(command("QuerySubDevice", CommandDataKind::Void, CommandDataKind::StringArray)
.out_doc("Device server sub device(s) list"))
.command(CommandDef::new("Kill")
.exec_async(Self::kill))
.command(CommandDef::new("PolledDevice")
.out_doc("Polled device name list")
.exec(Self::polled_device))
.command(CommandDef::new("DevPollStatus")
.in_doc("Device name")
.out_doc("Device polling status")
.exec(Self::dev_poll_status))
.command(CommandDef::new("AddObjPolling")
.in_doc("Long[0]=Upd period. Str[0]=Device name. Str[1]=Object type. \
Str[2]=Object name")
.exec_async(Self::add_obj_polling))
.command(CommandDef::new("UpdObjPollingPeriod")
.in_doc("Long[0]=Upd period. Str[0]=Device name. Str[1]=Object type. \
Str[2]=Object name")
.exec_async(Self::upd_obj_polling_period))
.command(CommandDef::new("RemObjPolling")
.in_doc("Str[0]=Device name. Str[1]=Object type. Str[2]=Object name")
.exec_async(Self::rem_obj_polling))
.command(CommandDef::new("StartPolling")
.exec(Self::start_polling))
.command(CommandDef::new("StopPolling")
.exec(Self::stop_polling))
.command(command("AddLoggingTarget", CommandDataKind::StringArray, CommandDataKind::Void)
.in_doc("Str[i]=Device-name. Str[i+1]=Target-type::Target-name"))
.command(command("RemoveLoggingTarget", CommandDataKind::StringArray, CommandDataKind::Void)
.in_doc("Str[i]=Device-name. Str[i+1]=Target-type::Target-name"))
.command(command("GetLoggingTarget", CommandDataKind::String, CommandDataKind::StringArray)
.in_doc("Device name")
.out_doc("Logging target list"))
.command(command("SetLoggingLevel", CommandDataKind::LongStringArray, CommandDataKind::Void)
.in_doc("Long[i]=Logging Level. Str[i]=Device name."))
.command(command("GetLoggingLevel", CommandDataKind::StringArray, CommandDataKind::LongStringArray)
.in_doc("Device list")
.out_doc("Long[i]=Logging Level. Str[i]=Device name."))
.command(command("StartLogging", CommandDataKind::Void, CommandDataKind::Void))
.command(command("StopLogging", CommandDataKind::Void, CommandDataKind::Void))
.command(command("EventSubscriptionChange", CommandDataKind::StringArray, CommandDataKind::Long)
.in_doc("Event consumer wants to subscribe to")
.out_doc("Tango lib release"))
.command(CommandDef::new("ZmqEventSubscriptionChange")
.exec_async(Self::zmq_event_subscription_change)
.in_doc("Event consumer wants to subscribe to.\n\
device name, attribute/pipe name, action (\"subscribe\"), event name, \
<Tango client IDL version>\n\
event name can take the following values:\n\
\x20 \"change\",\n\
\x20 \"alarm\",\n\
\x20 \"periodic\",\n\
\x20 \"archive\",\n\
\x20 \"user_event\",\n\
\x20 \"attr_conf\",\n\
\x20 \"data_ready\",\n\
\x20 \"intr_change\",\n\
\x20 \"pipe\"\n\
\"info\" can also be used as single parameter to retrieve information \
about the heartbeat and event pub endpoints.")
.out_doc("Str[0] = Heartbeat pub endpoint - Str[1] = Event pub endpoint\n\
...\n\
Str[n] = Alternate Heartbeat pub endpoint - Str[n+1] = Alternate Event \
pub endpoint\n\
Str[n+1] = event name used by this server as zmq topic to send events\n\
Str[n+2] = channel name used by this server to send heartbeat events\n\
Long[0] = Tango lib release - Long[1] = Device IDL release\n\
Long[2] = Subscriber HWM - Long[3] = Multicast rate\n\
Long[4] = Multicast IVL - Long[5] = ZMQ release"))
.command(CommandDef::new("EventConfirmSubscription")
.exec_async(Self::event_confirm_subscription)
.in_doc("Str[0] = dev1 name, Str[1] = att1 name, Str[2] = event name, \
Str[3] = dev2 name, Str[4] = att2 name, Str[5] = event name,..."))
.command(command("QueryEventChannelIOR", CommandDataKind::Void, CommandDataKind::String)
.out_doc("Device server event channel IOR"))
.command(command("LockDevice", CommandDataKind::LongStringArray, CommandDataKind::Void)
.in_doc("Str[0] = Device name. Long[0] = Lock validity"))
.command(command("UnLockDevice", CommandDataKind::LongStringArray, CommandDataKind::Long)
.in_doc("Str[x] = Device name(s). Long[0] = Force flag")
.out_doc("Device global lock counter"))
.command(command("ReLockDevices", CommandDataKind::StringArray, CommandDataKind::Void)
.in_doc("Device(s) name"))
.command(command("DevLockStatus", CommandDataKind::String, CommandDataKind::LongStringArray)
.in_doc("Device name")
.out_doc("Device locking status"))
.command(command("QueryWizardClassProperty", CommandDataKind::String, CommandDataKind::StringArray)
.in_doc("Class name")
.out_doc("Class property list (name - description and default value)"))
.command(command("QueryWizardDevProperty", CommandDataKind::String, CommandDataKind::StringArray)
.in_doc("Class name")
.out_doc("Device property list (name - description and default value)"))
}
async fn init(dev: &Dev<Self>) -> Result<Self> {
add_listed_devs(dev.server()).await?;
Ok(Self {})
}
async fn kill(&self, dev: &Dev<Self>, _: ()) -> Result<()> {
let dev = dev.arc();
drop(pool::spawn(move || {
let _ = dev.server().stop();
Ok(())
}));
Ok(())
}
async fn dev_restart(&self, dev: &Dev<Self>, name: Scalar<str>) -> Result<()> {
let server = dev.server();
let name = local_dev_name(name.value());
let target = server.any_dev(name)
.filter(|target| !target.class().name().eq_ignore_ascii_case(CLASS_NAME))
.ok_or_else(|| Error::new(ErrorKind::Reason(reason::API_DeviceNotFound))
.with_description(format!("Device {name:?} is not served by this server")))?;
let (name, class) = (Arc::clone(target.name()), Arc::clone(target.class().name()));
drop(target);
server.remove_dev(&name);
server.add_dev(&class, &name)?;
server.sync_async().await
}
async fn restart_server(&self, dev: &Dev<Self>, _: ()) -> Result<()> {
let server = dev.server();
for dev in server.any_devs() {
if !dev.class().name().eq_ignore_ascii_case(CLASS_NAME) {
server.remove_dev(dev.name());
}
}
add_listed_devs(server).await?;
server.sync_async().await
}
async fn zmq_event_subscription_change(&self, dev: &Dev<Self>, args: Spectrum<str>) -> Result<(Spectrum<i32>, Spectrum<str>)> {
dev.server().backend_checked()?.zmq_event_subscription_change(args).await
}
async fn event_confirm_subscription(&self, dev: &Dev<Self>, args: Spectrum<str>) -> Result<()> {
dev.server().backend_checked()?.event_confirm_subscription(args).await
}
fn polled_device(&self, dev: &Dev<Self>, _: ()) -> Result<Vec<String>> {
Ok(poller(dev.server())?.polled_devs().iter()
.map(|name| name.to_string())
.collect())
}
fn dev_poll_status(&self, dev: &Dev<Self>, name: Scalar<str>) -> Result<Vec<String>> {
let target = find_dev(dev.server(), name.value())?;
Ok(poller(dev.server())?.status(target.name()).iter()
.map(|status| polling::format_status(status, target.polled_history(status.kind, &status.name).as_ref()))
.collect())
}
async fn add_obj_polling(&self, dev: &Dev<Self>, (longs, strs): (Spectrum<i32>, Spectrum<str>)) -> Result<()> {
let server = dev.server();
let poller = poller(server)?;
let period = period_arg(&longs)?;
let (target, kind, name) = polled_target(server, &strs)?;
if poller.period(target.name(), kind, &name).is_some() {
return Err(Error::new(ErrorKind::Reason(reason::API_AlreadyPolled))
.with_description(format!("The {} {name:?} of device {:?} is already polled",
kind.as_str(), target.name())));
}
store_polled(server, target.name(), kind, &name, Some(period)).await?;
poller.add(&target, kind, name, period)
}
async fn upd_obj_polling_period(&self, dev: &Dev<Self>, (longs, strs): (Spectrum<i32>, Spectrum<str>)) -> Result<()> {
let server = dev.server();
let poller = poller(server)?;
let period = period_arg(&longs)?;
let (target, kind, name) = polled_target(server, &strs)?;
if poller.period(target.name(), kind, &name).is_none() {
return Err(Error::new(ErrorKind::Reason(reason::API_PollObjNotFound))
.with_description(format!("The {} {name:?} of device {:?} is not polled",
kind.as_str(), target.name())));
}
store_polled(server, target.name(), kind, &name, Some(period)).await?;
poller.set_period(target.name(), kind, &name, period)
}
async fn rem_obj_polling(&self, dev: &Dev<Self>, strs: Spectrum<str>) -> Result<()> {
let server = dev.server();
let poller = poller(server)?;
let (target, kind, name) = polled_target(server, &strs)?;
if poller.period(target.name(), kind, &name).is_none() {
return Err(Error::new(ErrorKind::Reason(reason::API_PollObjNotFound))
.with_description(format!("The {} {name:?} of device {:?} is not polled",
kind.as_str(), target.name())));
}
store_polled(server, target.name(), kind, &name, None).await?;
poller.remove(target.name(), kind, &name)
}
fn start_polling(&self, dev: &Dev<Self>, _: ()) -> Result<()> {
poller(dev.server())?.resume();
Ok(())
}
fn stop_polling(&self, dev: &Dev<Self>, _: ()) -> Result<()> {
poller(dev.server())?.pause();
Ok(())
}
fn query_class(&self, dev: &Dev<Self>, _: ()) -> Result<Vec<String>> {
Ok(dev.server().any_classes().iter()
.filter(|class| !class.name().eq_ignore_ascii_case(CLASS_NAME))
.map(|class| class.name().to_string())
.collect())
}
fn query_device(&self, dev: &Dev<Self>, _: ()) -> Result<Vec<String>> {
Ok(dev.server().any_devs().iter()
.filter(|dev| !dev.class().name().eq_ignore_ascii_case(CLASS_NAME))
.map(|dev| format!("{}::{}", dev.class().name(), dev.name()))
.collect())
}
}
fn local_dev_name(name: &str) -> &str {
let name = name.split_once('#').map_or(name, |(name, _)| name);
match name.split_once("://") {
Some((_, rest)) => rest.split_once('/').map_or(rest, |(_, name)| name),
None => name,
}
}
fn find_dev(server: &Server, name: &str) -> Result<Arc<dyn AnyDev>> {
let name = local_dev_name(name);
server.any_dev(name)
.ok_or_else(|| Error::new(ErrorKind::Reason(reason::API_DeviceNotFound))
.with_description(format!("Device {name:?} is not served by this server")))
}
fn poller(server: &Server) -> Result<Poller> {
server.poller()
.ok_or_else(|| Error::new(ErrorKind::Reason(reason::API_ServerNotRunning))
.with_description(format!("Server {:?} is not served anymore", server.name())))
}
fn polled_target(server: &Server, strs: &Spectrum<str>) -> Result<(Arc<dyn AnyDev>, PolledKind, Arc<str>)> {
let mut strs = strs.iter();
let (Some(dev), Some(kind), Some(name)) = (strs.next(), strs.next(), strs.next()) else {
return Err(Error::new(ErrorKind::Reason(reason::API_WrongNumberOfArgs))
.with_description("Expected the device name, the object type and the object name"));
};
let target = find_dev(server, dev)?;
let kind = PolledKind::parse(kind)?;
let name = target.check_pollable(kind, name)?;
Ok((target, kind, name))
}
fn period_arg(longs: &Spectrum<i32>) -> Result<Duration> {
let Some(&period) = longs.as_slice().first() else {
return Err(Error::new(ErrorKind::Reason(reason::API_WrongNumberOfArgs))
.with_description("Expected the polling period"));
};
let period = u64::try_from(period).ok()
.filter(|&period| period > 0)
.map(Duration::from_millis)
.ok_or_else(|| Error::new(ErrorKind::Reason(reason::API_NotSupported))
.with_description(format!("Polling period {period} ms is not supported, a polling \
triggered by the device in particular")))?;
if period < MIN_PERIOD {
return Err(Error::new(ErrorKind::Reason(reason::API_NotSupported))
.with_description(format!("Polling period {period:?} is below the minimum of {MIN_PERIOD:?}")));
}
Ok(period)
}
async fn store_polled(server: &Server, dev: &str, kind: PolledKind, name: &str, period: Option<Duration>) -> Result<()> {
let prop = match kind {
PolledKind::Attr => "polled_attr",
PolledKind::Command => "polled_cmd",
};
let database = server.database();
let listed = database.get_dev_prop_async::<Spectrum<str>, _>(dev, prop).await?;
let mut values = Vec::new();
let mut listed = listed.iter();
while let (Some(other), Some(other_period)) = (listed.next(), listed.next()) {
if !other.eq_ignore_ascii_case(name) {
values.push(other.to_string());
values.push(other_period.to_string());
}
}
if let Some(period) = period {
values.push(name.to_string());
values.push(period.as_millis().to_string());
}
if values.is_empty() {
database.delete_dev_prop_async(dev, prop).await
} else {
database.set_dev_prop_async(dev, (prop, values)).await
}
}
async fn add_listed_devs(server: &Server) -> Result<()> {
let listed = server.database().list_server_dev_async(server.name()).await?;
for dev in &listed {
let (name, class) = (dev.name.value(), dev.value.value());
if class.eq_ignore_ascii_case(CLASS_NAME) {
continue;
}
if let Err(err) = server.add_dev(class, name) {
warn!(dev = %name, %class, %err, "Failed to add the device listed by the database");
continue;
}
debug!(dev = %name, %class, "Added the device listed by the database");
}
Ok(())
}
fn command(name: &'static str, in_kind: CommandDataKind, out_kind: CommandDataKind) -> CommandDef<Admin> {
CommandDef::new(name)
.in_kind(in_kind)
.out_kind(out_kind)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_names() {
assert_eq!(local_dev_name("a/b/c"), "a/b/c");
assert_eq!(local_dev_name("a/b/c#dbase=no"), "a/b/c");
assert_eq!(local_dev_name("tango://host:10000/a/b/c#dbase=no"), "a/b/c");
}
}