use std::sync::Arc;
use epics_base_rs::server::ioc_app::IocApplication;
use epics_base_rs::server::iocsh::registry::{
ArgDesc, ArgType, ArgValue, CommandContext, CommandDef, CommandOutcome,
};
use crate::manager::PortManager;
use crate::trace::{TraceFile, TraceInfoMask, TraceIoMask, TraceMask};
pub fn register_asyn_commands(mut app: IocApplication, mgr: Arc<PortManager>) -> IocApplication {
for def in build_asyn_commands(mgr) {
app = app.register_shell_command(def);
}
app
}
fn arg_int(args: &[ArgValue], i: usize) -> Option<i64> {
match args.get(i) {
Some(ArgValue::Int(v)) => Some(*v),
Some(ArgValue::Double(v)) => Some(*v as i64),
Some(ArgValue::String(s)) => s.parse::<i64>().ok(),
_ => None,
}
}
fn arg_str(args: &[ArgValue], i: usize) -> Option<String> {
match args.get(i) {
Some(ArgValue::String(s)) => Some(s.clone()),
Some(ArgValue::Int(v)) => Some(v.to_string()),
Some(ArgValue::Double(v)) => Some(v.to_string()),
_ => None,
}
}
fn report_ports(mgr: &Arc<PortManager>, level: i32, port: Option<&str>) {
if let Some(name) = port {
match mgr.find_runtime_handle(name) {
Ok(handle) => {
let _ = handle
.port_handle()
.report_blocking(level)
.map_err(|e| eprintln!("asynReport {name}: {e}"));
}
Err(e) => eprintln!("asynReport: {e}"),
}
} else {
for name in mgr.list_port_names() {
if let Ok(handle) = mgr.find_runtime_handle(&name) {
let _ = handle
.port_handle()
.report_blocking(level)
.map_err(|e| eprintln!("asynReport {name}: {e}"));
}
}
}
}
pub fn register_asyn_commands_on_shell(
shell: &epics_base_rs::server::iocsh::IocShell,
mgr: Arc<PortManager>,
) {
for def in build_asyn_commands(mgr) {
shell.register(def);
}
}
pub fn build_asyn_commands(mgr: Arc<PortManager>) -> Vec<CommandDef> {
let mut out = Vec::new();
{
let mgr_r = mgr.clone();
out.push(CommandDef::new(
"asynReport",
vec![
ArgDesc {
name: "level",
arg_type: ArgType::Int,
optional: true,
},
ArgDesc {
name: "port",
arg_type: ArgType::String,
optional: true,
},
],
"asynReport [level] [portName] - Report registered ports",
move |args: &[ArgValue], _ctx: &CommandContext| {
let level = arg_int(args, 0).unwrap_or(0) as i32;
let port = arg_str(args, 1);
report_ports(&mgr_r, level, port.as_deref());
Ok(CommandOutcome::Continue)
},
));
}
{
let mgr_r = mgr.clone();
out.push(CommandDef::new(
"asynSetOption",
vec![
ArgDesc {
name: "portName",
arg_type: ArgType::String,
optional: false,
},
ArgDesc {
name: "addr",
arg_type: ArgType::Int,
optional: false,
},
ArgDesc {
name: "key",
arg_type: ArgType::String,
optional: false,
},
ArgDesc {
name: "value",
arg_type: ArgType::String,
optional: false,
},
],
"asynSetOption portName addr key value",
move |args: &[ArgValue], ctx: &CommandContext| {
let port = arg_str(args, 0).ok_or_else(|| "portName required".to_string())?;
let addr = arg_int(args, 1).unwrap_or(0) as i32;
let key = arg_str(args, 2).ok_or_else(|| "key required".to_string())?;
let value = arg_str(args, 3).unwrap_or_default();
match mgr_r.find_port_handle(&port) {
Ok(handle) => match handle.set_option_addr_blocking(addr, &key, &value) {
Ok(()) => Ok(CommandOutcome::Continue),
Err(e) => {
ctx.println(&format!("asynSetOption: {e}"));
Ok(CommandOutcome::Continue)
}
},
Err(e) => {
ctx.println(&format!("asynSetOption: {e}"));
Ok(CommandOutcome::Continue)
}
}
},
));
}
{
let mgr_r = mgr.clone();
out.push(CommandDef::new(
"asynSetTraceMask",
vec![
ArgDesc {
name: "portName",
arg_type: ArgType::String,
optional: true,
},
ArgDesc {
name: "addr",
arg_type: ArgType::Int,
optional: true,
},
ArgDesc {
name: "mask",
arg_type: ArgType::String,
optional: false,
},
],
"asynSetTraceMask [portName] [addr] mask",
move |args: &[ArgValue], ctx: &CommandContext| {
let port = arg_str(args, 0).filter(|s| !s.is_empty());
let addr = arg_int(args, 1).unwrap_or(-1) as i32;
let mask_str = arg_str(args, 2).ok_or_else(|| "mask required".to_string())?;
match TraceMask::from_symbolic(&mask_str) {
Ok(m) => {
let trace = mgr_r.trace_manager();
if let Some(p) = port.as_deref() {
if addr >= 0 {
trace.set_device_trace_mask(p, addr, m);
} else {
trace.set_trace_mask(Some(p), m);
}
} else {
trace.set_trace_mask(None, m);
}
Ok(CommandOutcome::Continue)
}
Err(e) => {
ctx.println(&format!("asynSetTraceMask: {e}"));
Ok(CommandOutcome::Continue)
}
}
},
));
}
{
let mgr_r = mgr.clone();
out.push(CommandDef::new(
"asynSetTraceIOMask",
vec![
ArgDesc {
name: "portName",
arg_type: ArgType::String,
optional: true,
},
ArgDesc {
name: "addr",
arg_type: ArgType::Int,
optional: true,
},
ArgDesc {
name: "mask",
arg_type: ArgType::String,
optional: false,
},
],
"asynSetTraceIOMask [portName] [addr] mask",
move |args: &[ArgValue], ctx: &CommandContext| {
let port = arg_str(args, 0).filter(|s| !s.is_empty());
let addr = arg_int(args, 1).unwrap_or(-1) as i32;
let mask_str = arg_str(args, 2).ok_or_else(|| "mask required".to_string())?;
match TraceIoMask::from_symbolic(&mask_str) {
Ok(m) => {
let trace = mgr_r.trace_manager();
if let Some(p) = port.as_deref() {
if addr >= 0 {
trace.set_device_trace_io_mask(p, addr, m);
} else {
trace.set_trace_io_mask(Some(p), m);
}
} else {
trace.set_trace_io_mask(None, m);
}
Ok(CommandOutcome::Continue)
}
Err(e) => {
ctx.println(&format!("asynSetTraceIOMask: {e}"));
Ok(CommandOutcome::Continue)
}
}
},
));
}
{
let mgr_r = mgr.clone();
out.push(CommandDef::new(
"asynSetTraceInfoMask",
vec![
ArgDesc {
name: "portName",
arg_type: ArgType::String,
optional: true,
},
ArgDesc {
name: "addr",
arg_type: ArgType::Int,
optional: true,
},
ArgDesc {
name: "mask",
arg_type: ArgType::String,
optional: false,
},
],
"asynSetTraceInfoMask [portName] [addr] mask",
move |args: &[ArgValue], ctx: &CommandContext| {
let port = arg_str(args, 0).filter(|s| !s.is_empty());
let addr = arg_int(args, 1).unwrap_or(-1) as i32;
let mask_str = arg_str(args, 2).ok_or_else(|| "mask required".to_string())?;
match TraceInfoMask::from_symbolic(&mask_str) {
Ok(m) => {
let trace = mgr_r.trace_manager();
if let Some(p) = port.as_deref() {
if addr >= 0 {
trace.set_device_trace_info_mask(p, addr, m);
} else {
trace.set_trace_info_mask(Some(p), m);
}
} else {
trace.set_trace_info_mask(None, m);
}
Ok(CommandOutcome::Continue)
}
Err(e) => {
ctx.println(&format!("asynSetTraceInfoMask: {e}"));
Ok(CommandOutcome::Continue)
}
}
},
));
}
{
let mgr_r = mgr.clone();
out.push(CommandDef::new(
"asynSetTraceFile",
vec![
ArgDesc {
name: "portName",
arg_type: ArgType::String,
optional: true,
},
ArgDesc {
name: "addr",
arg_type: ArgType::Int,
optional: true,
},
ArgDesc {
name: "filename",
arg_type: ArgType::String,
optional: true,
},
],
"asynSetTraceFile [portName] [addr] [filename]",
move |args: &[ArgValue], ctx: &CommandContext| {
let port = arg_str(args, 0).filter(|s| !s.is_empty());
let addr = arg_int(args, 1).unwrap_or(-1) as i32;
let filename = arg_str(args, 2).unwrap_or_default();
let target = match filename.as_str() {
"" | "stderr" => TraceFile::Stderr,
"stdout" => TraceFile::Stdout,
path => match std::fs::File::create(path) {
Ok(f) => TraceFile::File(Arc::new(std::sync::Mutex::new(f))),
Err(e) => {
ctx.println(&format!("asynSetTraceFile: fopen failed: {e}"));
return Ok(CommandOutcome::Continue);
}
},
};
let trace = mgr_r.trace_manager();
if let Some(p) = port.as_deref() {
if addr >= 0 {
trace.set_device_trace_file(p, addr, target);
} else {
trace.set_trace_file(Some(p), target);
}
} else {
trace.set_trace_file(None, target);
}
Ok(CommandOutcome::Continue)
},
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::AsynResult;
use crate::exception::AsynException;
use crate::param::ParamType;
use crate::port::{PortDriver, PortDriverBase, PortFlags};
use crate::user::AsynUser;
use std::sync::Mutex;
struct DummyDriver {
base: PortDriverBase,
}
impl DummyDriver {
fn new(name: &str) -> Self {
let mut base = PortDriverBase::new(name, 1, PortFlags::default());
base.create_param("VAL", ParamType::Int32).unwrap();
Self { base }
}
}
impl PortDriver for DummyDriver {
fn base(&self) -> &PortDriverBase {
&self.base
}
fn base_mut(&mut self) -> &mut PortDriverBase {
&mut self.base
}
}
fn fresh_mgr_with_port(name: &str) -> Arc<PortManager> {
let mgr = Arc::new(PortManager::new());
let _ = mgr.register_port(DummyDriver::new(name)).unwrap();
mgr
}
#[test]
fn iocsh_set_trace_mask_updates_port_mask() {
let mgr = fresh_mgr_with_port("trace_mask_port");
let observed: Arc<Mutex<Vec<AsynException>>> = Arc::new(Mutex::new(Vec::new()));
let observed_clone = observed.clone();
mgr.exception_manager().add_callback(move |ev| {
observed_clone.lock().unwrap().push(ev.exception);
});
let cmds = build_asyn_commands(mgr.clone());
let set_trace_mask = cmds
.iter()
.find(|c| c.name == "asynSetTraceMask")
.expect("asynSetTraceMask must be registered");
let trace = mgr.trace_manager().clone();
let mask = TraceMask::from_symbolic("ERROR+WARNING").unwrap();
trace.set_trace_mask(Some("trace_mask_port"), mask);
assert_eq!(cmds.len(), 6, "asynShellCommands.c registers 6 commands");
assert!(set_trace_mask.args.len() == 3);
let evs = observed.lock().unwrap();
assert!(
evs.iter().any(|e| matches!(e, AsynException::TraceMask)),
"set_trace_mask must fire asynExceptionTraceMask"
);
assert!(trace.is_enabled("trace_mask_port", TraceMask::ERROR));
assert!(trace.is_enabled("trace_mask_port", TraceMask::WARNING));
}
#[test]
fn iocsh_registers_six_c_parity_commands() {
let mgr = Arc::new(PortManager::new());
let cmds = build_asyn_commands(mgr);
let names: Vec<&str> = cmds.iter().map(|c| c.name.as_str()).collect();
for expected in [
"asynReport",
"asynSetOption",
"asynSetTraceMask",
"asynSetTraceIOMask",
"asynSetTraceInfoMask",
"asynSetTraceFile",
] {
assert!(
names.contains(&expected),
"iocsh registration must include {expected}"
);
}
}
#[test]
fn report_request_op_invokes_driver_report() -> AsynResult<()> {
let mgr = fresh_mgr_with_port("report_port");
let handle = mgr.find_port_handle("report_port")?;
handle.report_blocking(0)?;
handle.report_blocking(2)?;
Ok(())
}
fn make_ctx() -> CommandContext {
use epics_base_rs::server::database::PvDatabase;
let rt = tokio::runtime::Runtime::new().unwrap();
let db = Arc::new(PvDatabase::new());
let handle = rt.handle().clone();
let ctx = CommandContext::new(db, handle);
std::mem::forget(rt);
ctx
}
#[test]
fn iocsh_trace_setters_route_addr_to_device_announce() {
let mgr = fresh_mgr_with_port("trace_dev_port");
let ctx = make_ctx();
let observed: Arc<Mutex<Vec<(AsynException, i32)>>> = Arc::new(Mutex::new(Vec::new()));
let obs = observed.clone();
mgr.exception_manager().add_callback(move |ev| {
obs.lock().unwrap().push((ev.exception, ev.addr));
});
let cmds = build_asyn_commands(mgr.clone());
let io_cmd = cmds
.iter()
.find(|c| c.name == "asynSetTraceIOMask")
.expect("asynSetTraceIOMask must be registered");
let _ = io_cmd.handler.call(
&[
ArgValue::String("trace_dev_port".to_string()),
ArgValue::Int(5),
ArgValue::String("HEX".to_string()),
],
&ctx,
);
let info_cmd = cmds
.iter()
.find(|c| c.name == "asynSetTraceInfoMask")
.expect("asynSetTraceInfoMask must be registered");
let _ = info_cmd.handler.call(
&[
ArgValue::String("trace_dev_port".to_string()),
ArgValue::Int(7),
ArgValue::String("SOURCE".to_string()),
],
&ctx,
);
let file_cmd = cmds
.iter()
.find(|c| c.name == "asynSetTraceFile")
.expect("asynSetTraceFile must be registered");
let _ = file_cmd.handler.call(
&[
ArgValue::String("trace_dev_port".to_string()),
ArgValue::Int(2),
ArgValue::String("stderr".to_string()),
],
&ctx,
);
let evs = observed.lock().unwrap();
assert!(
evs.iter()
.any(|(e, a)| matches!(e, AsynException::TraceIoMask) && *a == 5),
"asynSetTraceIOMask addr=5 must fire a device-scoped announce; observed: {evs:?}"
);
assert!(
evs.iter()
.any(|(e, a)| matches!(e, AsynException::TraceInfoMask) && *a == 7),
"asynSetTraceInfoMask addr=7 must fire a device-scoped announce; observed: {evs:?}"
);
assert!(
evs.iter()
.any(|(e, a)| matches!(e, AsynException::TraceFile) && *a == 2),
"asynSetTraceFile addr=2 must fire a device-scoped announce; observed: {evs:?}"
);
}
}