1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//! **A name registered once MUST be callable from every shell in the process.**
//!
//! C has one command table — `iocshCommandHead`, the single list
//! `iocshRegister` writes (`iocsh.cpp:684-700`) and every `iocshBody` walks
//! (`:1290-1302`) — so a registrar's command is callable from `st.cmd` and from
//! the `epics>` prompt with nothing for the registrar to decide.
//!
//! This port builds up to four shells per `IocApplication::run` — the startup
//! script's, the `afterIocRunning` queue's, the interactive tail's, and the
//! protocol runner's — and each used to construct a `CommandRegistry` of its
//! own. A command therefore existed in whichever shells its owner remembered to
//! hand it to: `register_shell_command` reached every shell EXCEPT the script's,
//! and a command returned by a link-set installer could reach the script's by no
//! route at all, because that shell is constructed before `iocInit` runs the
//! installer. `casr`, `pvxsr`, `dbsr` and the qsrv runtime commands were four
//! samples of that one split.
//!
//! The two cases below are the two directions of the split, and neither needs a
//! second registration to pass.
use std::sync::{Arc, Mutex};
use epics_base_rs::server::database::PvDatabase;
use epics_base_rs::server::ioc_app::IocApplication;
use epics_base_rs::server::iocsh::IocShell;
use epics_base_rs::server::iocsh::registry::{ArgValue, CommandDef, CommandOutcome};
/// A command that does nothing but record that it was reached, so "callable"
/// means the handler ran and not merely that a lookup succeeded.
fn recorder(name: &'static str, into: Arc<Mutex<Vec<String>>>) -> CommandDef {
CommandDef::new(
name,
vec![],
"record that this command was reached",
move |_args: &[ArgValue], _ctx: &_| {
into.lock().unwrap().push(name.to_string());
Ok(CommandOutcome::Continue)
},
)
}
/// A real `st.cmd` on disk: `on error break` is a script directive, and an
/// unknown command has to reach the shell as a script line for it to act on.
fn st_cmd(name: &str, lines: &[&str]) -> String {
let path = std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join(name);
std::fs::write(&path, format!("{}\n", lines.join("\n"))).expect("write st.cmd");
path.to_string_lossy().into_owned()
}
/// One boot covering both directions and both call sites.
///
/// `parityShellCmd` is registered through `register_shell_command`, the name
/// that used to mean "every shell but the script's"; `parityInstallerCmd` is
/// returned by a link-set installer, which C reaches through a registrar at
/// `initHookAfterCaLinkInit` and which runs here on the script's own `iocInit`
/// line — after the startup shell was built. Under `on error break` an unknown
/// command ends the script, so `run` returning `Ok` is itself part of the
/// proof; the recorded calls say which lines actually reached a handler.
///
/// The prompt half is the interactive tail's own construction —
/// `IocShell::new` on a thread of its own, as `run_uninitialized_tail` does —
/// against a database this run never saw, so the names can only have come from
/// the process's table.
#[epics_macros_rs::epics_test]
async fn one_registration_reaches_the_script_and_the_prompt() {
let calls = Arc::new(Mutex::new(Vec::new()));
let script = st_cmd(
"one_command_table.cmd",
&[
"on error break",
"parityShellCmd",
"iocInit",
"parityInstallerCmd",
],
);
let installer_calls = calls.clone();
IocApplication::new()
.port(0)
.register_shell_command(recorder("parityShellCmd", calls.clone()))
.register_link_set_installer(move |_db| async move {
vec![recorder("parityInstallerCmd", installer_calls)]
})
.startup_script(&script)
.run(|_config| async move { Ok(()) })
.await
.expect("no line of the script is an unknown command");
assert_eq!(
calls.lock().unwrap().as_slice(),
["parityShellCmd", "parityInstallerCmd"],
"both script lines must reach their handler: a shell command before \
`iocInit`, an installer's command after it"
);
// The prompt. `BlockingBridge::capture()` has to happen on the runtime;
// the shell itself runs off it, as the interactive tail's does.
let bridge = epics_base_rs::runtime::task::BlockingBridge::capture();
let prompt_calls = calls.clone();
std::thread::spawn(move || {
let shell = IocShell::new(Arc::new(PvDatabase::new()), bridge);
for name in ["parityShellCmd", "parityInstallerCmd"] {
shell
.execute_line(name)
.unwrap_or_else(|e| panic!("`{name}` at the prompt: {e}"));
}
drop(prompt_calls);
})
.join()
.expect("the prompt shell's thread");
assert_eq!(
calls.lock().unwrap().as_slice(),
[
"parityShellCmd",
"parityInstallerCmd",
"parityShellCmd",
"parityInstallerCmd"
],
"the same two names, registered once each, must also be callable from \
a shell built the way the interactive tail builds its own"
);
}