use super::registry::*;
pub(crate) fn register(registry: &mut CommandRegistry) {
registry.register(cmd_ioc_build());
registry.register(cmd_ioc_run());
registry.register(cmd_ioc_pause());
registry.register(cmd_core_release());
registry.register(cmd_system());
registry.register(cmd_dlload());
}
fn cmd_ioc_build() -> CommandDef {
CommandDef::new(
"iocBuild",
vec![],
"iocBuild — Initialize the IOC and leave it in a quiescent state.",
|_args: &[ArgValue], ctx: &CommandContext| {
use crate::server::ioc_app::ShellTransition;
match crate::server::ioc_app::build_from_shell(ctx.bridge()) {
ShellTransition::Done => Ok(CommandOutcome::Continue),
ShellTransition::Failed => Ok(CommandOutcome::Failed),
ShellTransition::Refused => {
crate::runtime::log::errlog_printf(&crate::server::ioc_app::build_refusal());
Ok(CommandOutcome::Failed)
}
ShellTransition::NotOurs => {
if crate::server::ioc_app::build_without_application(|| {
ctx.block_on(async { ctx.db().ioc_init().await });
}) {
Ok(CommandOutcome::Continue)
} else {
Ok(CommandOutcome::Failed)
}
}
}
},
)
}
fn cmd_ioc_run() -> CommandDef {
CommandDef::new(
"iocRun",
vec![],
"iocRun — Bring the IOC out of its initial quiescent state to the \
running state.",
|_args: &[ArgValue], ctx: &CommandContext| {
use crate::server::ioc_app::ShellTransition;
match crate::server::ioc_app::run_from_shell(ctx.bridge()) {
ShellTransition::Done => return Ok(CommandOutcome::Continue),
ShellTransition::Failed => return Ok(CommandOutcome::Failed),
ShellTransition::Refused | ShellTransition::NotOurs => {}
}
if crate::server::ioc_app::ioc_run() == 0 {
Ok(CommandOutcome::Continue)
} else {
Ok(CommandOutcome::Failed)
}
},
)
}
fn cmd_ioc_pause() -> CommandDef {
CommandDef::new(
"iocPause",
vec![],
"iocPause — Bring a running IOC to a quiescent state with record \
processing frozen.",
|_args: &[ArgValue], _ctx: &CommandContext| {
if crate::server::ioc_app::ioc_pause() == 0 {
Ok(CommandOutcome::Continue)
} else {
Ok(CommandOutcome::Failed)
}
},
)
}
fn cmd_core_release() -> CommandDef {
CommandDef::new(
"coreRelease",
vec![],
"coreRelease — Print release information for iocCore.",
|_args: &[ArgValue], ctx: &CommandContext| {
for line in core_release_block() {
ctx.println(&line);
}
Ok(CommandOutcome::Continue)
},
)
}
pub(crate) fn core_release_block() -> [String; 4] {
let rule = "#".repeat(76);
[
rule.clone(),
format!("## Rev. {VCS_VERSION}"),
format!("## Rev. Date {VCS_VERSION_DATE}"),
rule,
]
}
const VCS_VERSION: &str = env!("EPICS_VCS_VERSION");
const VCS_VERSION_DATE: &str = env!("EPICS_VCS_VERSION_DATE");
const _: () = assert!(!VCS_VERSION.is_empty() && !VCS_VERSION_DATE.is_empty());
fn cmd_dlload() -> CommandDef {
CommandDef::new(
"dlload",
vec![ArgDesc {
name: "path/library.so",
arg_type: ArgType::Path,
}],
"dlload <path/library.so> — Load the given shared library. \
Example: dlload myLibrary.so",
|args: &[ArgValue], ctx: &CommandContext| {
let ArgValue::String(name) = &args[0] else {
return Ok(CommandOutcome::Continue);
};
ctx.println(&format!(
"epicsLoadLibrary failed: {name}: this IOC is statically \
linked and has no dynamic loader, and a shared library has \
no registrar path into its compile-time registries"
));
Ok(CommandOutcome::Failed)
},
)
}
fn cmd_system() -> CommandDef {
CommandDef::new(
"system",
vec![ArgDesc {
name: "command string",
arg_type: ArgType::String,
}],
"system <command string> — Send command string to the system \
command interpreter for execution.",
|args: &[ArgValue], _ctx: &CommandContext| {
let cmd = match &args[0] {
ArgValue::String(s) => s.as_str(),
_ => return Ok(CommandOutcome::Failed),
};
Ok(run_system(cmd))
},
)
}
fn run_system(cmd: &str) -> CommandOutcome {
#[cfg(windows)]
let mut c = {
let mut c = std::process::Command::new("cmd");
c.arg("/C").arg(cmd);
c
};
#[cfg(not(windows))]
let mut c = {
let mut c = std::process::Command::new("/bin/sh");
c.arg("-c").arg(cmd);
c
};
match c.status() {
Ok(st) if st.success() => CommandOutcome::Continue,
_ => CommandOutcome::Failed,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::database::PvDatabase;
use std::sync::Arc;
fn make_ctx() -> CommandContext {
let rt = tokio::runtime::Runtime::new().unwrap();
let db = Arc::new(PvDatabase::new());
let bridge = {
let _guard = rt.enter();
crate::runtime::task::BlockingBridge::capture()
};
let ctx = CommandContext::new(db, bridge);
std::mem::forget(rt);
ctx
}
fn run(ctx: &CommandContext, name: &str, tokens: &[&str]) -> (String, bool) {
let mut reg = CommandRegistry::new();
register(&mut reg);
let cmd = reg.get(name).unwrap();
let tokens: Vec<String> = tokens.iter().map(|t| t.to_string()).collect();
let args = parse_args(&tokens, &cmd.args).unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
let outcome = ctx.with_output(std::fs::File::create(&path).unwrap(), || {
cmd.handler.call(&args, ctx)
});
let failed = matches!(outcome, Ok(CommandOutcome::Failed));
(std::fs::read_to_string(&path).unwrap(), failed)
}
#[test]
fn dlload_takes_cs_path_argument_and_reports_cs_failure_line() {
let ctx = make_ctx();
{
let mut reg = CommandRegistry::new();
register(&mut reg);
let def = reg.get("dlload").unwrap();
assert_eq!(def.args.len(), 1);
assert_eq!(def.args[0].name, "path/library.so");
assert!(matches!(def.args[0].arg_type, ArgType::Path));
}
let (out, failed) = run(&ctx, "dlload", &["myLibrary.so"]);
assert!(failed, "C returns -1, so the shell line fails");
assert!(
out.starts_with("epicsLoadLibrary failed: myLibrary.so"),
"C's own prefix and the name it was given, got: {out:?}"
);
assert!(
out.contains("statically"),
"the reason travels in the message, got: {out:?}"
);
}
#[test]
fn ioc_run_and_ioc_pause_carry_the_transition_status() {
use crate::server::ioc_app::{IocState, get_ioc_state, note_scan_owner_started};
let ctx = make_ctx();
{
let mut reg = CommandRegistry::new();
register(&mut reg);
assert!(reg.get("iocRun").unwrap().args.is_empty());
assert!(reg.get("iocPause").unwrap().args.is_empty());
}
assert!(run(&ctx, "iocRun", &[]).1, "iocRun from iocVoid fails");
assert!(run(&ctx, "iocPause", &[]).1, "iocPause from iocVoid fails");
note_scan_owner_started();
assert!(!run(&ctx, "iocPause", &[]).1, "a running IOC pauses");
assert_eq!(get_ioc_state(), IocState::Paused);
assert!(!run(&ctx, "iocRun", &[]).1, "a paused IOC runs again");
assert_eq!(get_ioc_state(), IocState::Running);
}
#[test]
fn core_release_prints_four_lines_without_the_base_version() {
let ctx = make_ctx();
let (out, failed) = run(&ctx, "coreRelease", &[]);
assert!(!failed, "coreRelease never fails in C");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 4, "the base-version line is dropped: {out:?}");
assert_eq!(lines[0], "#".repeat(76));
assert_eq!(lines[3], "#".repeat(76));
assert_eq!(lines[1], format!("## Rev. {VCS_VERSION}"));
assert_eq!(lines[2], format!("## Rev. Date {VCS_VERSION_DATE}"));
assert!(
!out.contains("epics-rs") && !out.contains("R7.0.10"),
"the base-version identity line is gone: {out:?}"
);
assert!(lines[1].len() > "## Rev. ".len(), "empty revision: {out:?}");
assert!(
lines[2].len() > "## Rev. Date ".len(),
"empty date: {out:?}"
);
}
#[test]
#[cfg(unix)]
fn system_runs_a_shell_pipeline_and_reports_the_exit_status() {
let ctx = make_ctx();
{
let mut reg = CommandRegistry::new();
register(&mut reg);
assert_eq!(
reg.get("system").unwrap().args.len(),
1,
"C declares one argument"
);
}
let (out, failed) = run(&ctx, "system", &["true | true"]);
assert!(!failed, "a 0 exit is a clean line");
assert!(out.is_empty(), "C prints nothing on success: {out:?}");
let (out, failed) = run(&ctx, "system", &["exit 3"]);
assert!(failed, "a non-zero exit must fail the line");
assert!(out.is_empty(), "C prints no diagnostic of its own: {out:?}");
}
}