use std::collections::BTreeMap;
use std::sync::{LazyLock, Mutex};
use super::registry::*;
pub enum VarAccess {
Int { get: fn() -> i64, set: fn(i64) },
Double { get: fn() -> f64, set: fn(f64) },
}
pub struct VarDef {
pub name: &'static str,
pub access: VarAccess,
}
static VARIABLES: LazyLock<Mutex<BTreeMap<&'static str, VarDef>>> = LazyLock::new(|| {
Mutex::new(
seeded_knobs()
.into_iter()
.map(|def| (def.name, def))
.collect(),
)
});
fn seeded_knobs() -> Vec<VarDef> {
use crate::server::records::{bo, calcout, histogram, seq};
vec![
VarDef {
name: "callbackParallelThreadsDefault",
access: VarAccess::Int {
get: || {
crate::runtime::background::callback_executor::parallel_threads_default() as i64
},
set: |value| {
crate::runtime::background::callback_executor::set_parallel_threads_default(
value as i32,
)
},
},
},
VarDef {
name: "dbTemplateMaxVars",
access: VarAccess::Int {
get: || crate::server::db_loader::db_template_max_vars() as i64,
set: |value| crate::server::db_loader::set_db_template_max_vars(value as i32),
},
},
VarDef {
name: "boHIGHlimit",
access: VarAccess::Double {
get: bo::bo_high_limit,
set: bo::set_bo_high_limit,
},
},
VarDef {
name: "boHIGHprecision",
access: VarAccess::Int {
get: || bo::bo_high_precision() as i64,
set: |value| bo::set_bo_high_precision(value as i32),
},
},
VarDef {
name: "calcoutODLYlimit",
access: VarAccess::Double {
get: calcout::calcout_odly_limit,
set: calcout::set_calcout_odly_limit,
},
},
VarDef {
name: "calcoutODLYprecision",
access: VarAccess::Int {
get: || calcout::calcout_odly_precision() as i64,
set: |value| calcout::set_calcout_odly_precision(value as i32),
},
},
VarDef {
name: "histogramSDELprecision",
access: VarAccess::Int {
get: || histogram::histogram_sdel_precision() as i64,
set: |value| histogram::set_histogram_sdel_precision(value as i32),
},
},
VarDef {
name: "seqDLYlimit",
access: VarAccess::Double {
get: seq::seq_dly_limit,
set: seq::set_seq_dly_limit,
},
},
VarDef {
name: "seqDLYprecision",
access: VarAccess::Int {
get: || seq::seq_dly_precision() as i64,
set: |value| seq::set_seq_dly_precision(value as i32),
},
},
]
}
pub fn register_variable(def: VarDef) {
VARIABLES
.lock()
.unwrap_or_else(|e| e.into_inner())
.entry(def.name)
.or_insert(def);
}
pub fn variable_names() -> Vec<&'static str> {
VARIABLES
.lock()
.unwrap_or_else(|e| e.into_inner())
.keys()
.copied()
.collect()
}
pub(crate) fn register(registry: &mut CommandRegistry) {
if VARIABLES
.lock()
.unwrap_or_else(|e| e.into_inner())
.is_empty()
{
return;
}
registry.register(cmd_var());
}
fn strtol_base0(s: &str) -> Option<i64> {
let s = s.trim_start();
let (neg, digits) = match s.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, s.strip_prefix('+').unwrap_or(s)),
};
let magnitude = if let Some(hex) = digits
.strip_prefix("0x")
.or_else(|| digits.strip_prefix("0X"))
{
i64::from_str_radix(hex, 16).ok()?
} else if digits.len() > 1 && digits.starts_with('0') {
i64::from_str_radix(&digits[1..], 8).ok()?
} else {
digits.parse::<i64>().ok()?
};
Some(if neg { -magnitude } else { magnitude })
}
fn show(ctx: &CommandContext, def: &VarDef) {
match &def.access {
VarAccess::Int { get, .. } => ctx.println(&format!("int {} = {}", def.name, get())),
VarAccess::Double { get, .. } => ctx.println(&format!(
"double {} = {}",
def.name,
crate::server::records::printf::format_g(get(), 6)
)),
}
}
fn cmd_var() -> CommandDef {
CommandDef::new(
"var",
vec![
ArgDesc {
name: "[variable",
arg_type: ArgType::String,
},
ArgDesc {
name: "[value]]",
arg_type: ArgType::String,
},
],
concat!(
"Print all, print single variable or set value to single variable\n",
" (default) - print all variables and their values defined in database definitions files\n",
" variable - if only parameter print value for this variable\n",
" value - set the value to variable",
),
|args: &[ArgValue], ctx: &CommandContext| {
let name = match args.first() {
Some(ArgValue::String(s)) => Some(s.as_str()),
_ => None,
};
let value = match args.get(1) {
Some(ArgValue::String(s)) => Some(s.as_str()),
_ => None,
};
let table = VARIABLES.lock().unwrap_or_else(|e| e.into_inner());
let Some(value) = value else {
let mut found = false;
for def in table.values() {
if name.is_none_or(|pattern| {
super::commands::epics_strn_glob_match(
def.name.as_bytes(),
def.name.len(),
pattern.as_bytes(),
)
}) {
show(ctx, def);
found = true;
}
}
if !found && let Some(name) = name {
ctx.eprintln(&format!("No known vars match '{name}'."));
return Ok(CommandOutcome::Failed);
}
return Ok(CommandOutcome::Continue);
};
let Some(def) = name.and_then(|name| table.get(name)) else {
ctx.eprintln(&format!("No known var '{}'.", name.unwrap_or_default()));
return Ok(CommandOutcome::Failed);
};
match &def.access {
VarAccess::Int { set, .. } => match strtol_base0(value) {
Some(parsed) => set(parsed),
None => {
ctx.eprintln(&format!("Invalid integer, var '{}' not changed.", def.name))
}
},
VarAccess::Double { set, .. } => match super::registry::epics_strtod_whole(value) {
Some(parsed) => set(parsed),
None => {
ctx.eprintln(&format!("Invalid double, var '{}' not changed.", def.name))
}
},
}
Ok(CommandOutcome::Continue)
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::access_security::{as_check_client_ip, set_as_check_client_ip};
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_var(ctx: &CommandContext, tokens: &[&str]) -> Result<String, String> {
let mut reg = CommandRegistry::new();
super::super::commands::register_builtins(&mut reg);
let cmd = reg.get("var").expect("`var` must be registered").clone();
let tokens: Vec<String> = tokens.iter().map(|t| (*t).to_string()).collect();
let args = parse_args(&tokens, &cmd.args).unwrap();
let out_tmp = tempfile::NamedTempFile::new().unwrap();
let err_tmp = tempfile::NamedTempFile::new().unwrap();
let (out_path, err_path) = (out_tmp.path().to_path_buf(), err_tmp.path().to_path_buf());
let mut failed = false;
ctx.with_error(std::fs::File::create(&err_path).unwrap(), || {
ctx.with_output(std::fs::File::create(&out_path).unwrap(), || {
failed = matches!(
cmd.handler.call(&args, ctx),
Ok(CommandOutcome::Failed) | Err(_)
);
});
});
if failed {
Err(std::fs::read_to_string(&err_path).unwrap())
} else {
Ok(std::fs::read_to_string(&out_path).unwrap())
}
}
#[test]
fn var_reads_and_writes_db_template_max_vars() {
use crate::server::db_loader::{db_template_max_vars, set_db_template_max_vars};
let ctx = make_ctx();
let restore = db_template_max_vars();
assert_eq!(restore, 100, "C `int dbTemplateMaxVars = 100`");
assert_eq!(
run_var(&ctx, &["dbTemplateMaxVars"]).unwrap(),
"int dbTemplateMaxVars = 100\n"
);
assert_eq!(run_var(&ctx, &["dbTemplateMaxVars", "200"]).unwrap(), "");
assert_eq!(
db_template_max_vars(),
200,
"`var` must reach the global the parser reads, not a copy"
);
assert_eq!(
run_var(&ctx, &["dbTemplateMaxVars"]).unwrap(),
"int dbTemplateMaxVars = 200\n"
);
set_db_template_max_vars(restore);
}
#[test]
fn var_reads_and_writes_as_check_client_ip() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
super::super::commands::register_builtins(&mut reg);
assert!(
reg.get("var").is_some(),
"C registers `var` (iocsh.cpp:708)"
);
assert!(
reg.get("asCheckClientIP").is_none(),
"C registers asCheckClientIP as a variable, never as a command"
);
let restore = as_check_client_ip();
assert_eq!(
run_var(&ctx, &["asCheckClientIP", "1"]).unwrap(),
"",
"C `var` prints nothing when it sets"
);
assert!(as_check_client_ip(), "`var` must reach the real knob");
assert_eq!(
run_var(&ctx, &["asCheckClientIP"]).unwrap(),
"int asCheckClientIP = 1\n"
);
assert_eq!(
run_var(&ctx, &["asCheck*"]).unwrap(),
"int asCheckClientIP = 1\n"
);
assert!(
run_var(&ctx, &[])
.unwrap()
.contains("int asCheckClientIP = 1")
);
assert_eq!(
run_var(&ctx, &["noSuchVar"]).unwrap_err(),
"No known vars match 'noSuchVar'.\n"
);
assert_eq!(
run_var(&ctx, &["noSuchVar", "1"]).unwrap_err(),
"No known var 'noSuchVar'.\n"
);
assert_eq!(run_var(&ctx, &["asCheckClientIP", "0x0"]).unwrap(), "");
assert!(!as_check_client_ip());
assert_eq!(run_var(&ctx, &["asCheckClientIP", "nope"]).unwrap(), "");
assert!(!as_check_client_ip());
set_as_check_client_ip(restore);
}
#[test]
fn every_registered_variable_round_trips_a_write() {
let ctx = make_ctx();
let mut reg = CommandRegistry::new();
super::super::commands::register_builtins(&mut reg);
let names = variable_names();
assert!(!names.is_empty(), "an empty table makes this vacuous");
for name in names {
let before = run_var(&ctx, &[name]).unwrap();
run_var(&ctx, &[name, "0"]).unwrap();
let zero = run_var(&ctx, &[name]).unwrap();
run_var(&ctx, &[name, "1"]).unwrap();
let one = run_var(&ctx, &[name]).unwrap();
assert_ne!(zero, one, "`var {name}` does not reach a real global");
let original = before
.rsplit_once(" = ")
.expect("C prints `<type> <name> = <value>`")
.1
.trim();
run_var(&ctx, &[name, original]).unwrap();
assert_eq!(run_var(&ctx, &[name]).unwrap(), before, "{name} restored");
}
}
#[test]
fn the_vendored_dbd_declares_cs_record_knobs() {
let got: Vec<(&str, &str)> = crate::server::record::dbd_generated::VARIABLES.to_vec();
assert_eq!(
got,
vec![
("boHIGHlimit", "double"),
("boHIGHprecision", "int"),
("calcoutODLYlimit", "double"),
("calcoutODLYprecision", "int"),
("histogramSDELprecision", "int"),
("seqDLYlimit", "double"),
("seqDLYprecision", "int"),
]
);
let table = VARIABLES.lock().unwrap_or_else(|e| e.into_inner());
for (name, dtype) in &got {
let def = table
.get(name)
.unwrap_or_else(|| panic!("{name} is declared by the .dbd but not registered"));
let arm = match def.access {
VarAccess::Int { .. } => "int",
VarAccess::Double { .. } => "double",
};
assert_eq!(&arm, dtype, "{name}");
}
}
#[test]
fn the_callback_default_is_the_cpu_count_and_is_settable() {
use crate::runtime::background::callback_executor as cb;
let ctx = make_ctx();
let cpus = cb::cpu_count();
assert!(cpus >= 1);
assert_eq!(
run_var(&ctx, &["callbackParallelThreadsDefault"]).unwrap(),
format!("int callbackParallelThreadsDefault = {cpus}\n")
);
assert_eq!(
run_var(&ctx, &["callbackParallelThreadsDefault", "7"]).unwrap(),
""
);
assert_eq!(cb::parallel_threads_default(), 7);
assert_eq!(
cb::cpu_count(),
cpus,
"the knob and the processor count are two globals, not one"
);
assert_eq!(
run_var(&ctx, &["callbackParallelThreadsDefault", "-2"]).unwrap(),
""
);
assert_eq!(cb::parallel_threads_default(), -2);
cb::set_parallel_threads_default(cpus);
}
#[test]
fn a_var_write_moves_what_an_existing_record_serves() {
use crate::server::record::Record;
let ctx = make_ctx();
let rec = crate::server::records::bo::BoRecord::new(0);
let before = rec.field_metadata_override("HIGH").expect("bo serves HIGH");
assert_eq!(before.precision, Some(2));
assert_eq!(before.ctrl_limits, Some((100000.0, 0.0)));
assert_eq!(run_var(&ctx, &["boHIGHprecision", "4"]).unwrap(), "");
assert_eq!(run_var(&ctx, &["boHIGHlimit", "5000.5"]).unwrap(), "");
let after = rec.field_metadata_override("HIGH").expect("bo serves HIGH");
assert_eq!(after.precision, Some(4));
assert_eq!(after.ctrl_limits, Some((5000.5, 0.0)));
assert_eq!(
run_var(&ctx, &["boHIGH*"]).unwrap(),
"double boHIGHlimit = 5000.5\nint boHIGHprecision = 4\n"
);
crate::server::records::bo::set_bo_high_precision(2);
crate::server::records::bo::set_bo_high_limit(100000.0);
}
#[test]
fn an_unparsable_double_leaves_the_knob_alone() {
let ctx = make_ctx();
for token in ["nope", "1.0 ", ""] {
assert_eq!(run_var(&ctx, &["seqDLYlimit", token]).unwrap(), "");
assert_eq!(
run_var(&ctx, &["seqDLYlimit"]).unwrap(),
"double seqDLYlimit = 100000\n",
"{token:?}"
);
}
for (token, want) in [
("1e3", "1000"),
(" 2.5", "2.5"),
("-0.5", "-0.5"),
("0x1p3", "8"),
("0x10", "16"),
] {
assert_eq!(run_var(&ctx, &["seqDLYlimit", token]).unwrap(), "");
assert_eq!(
run_var(&ctx, &["seqDLYlimit"]).unwrap(),
format!("double seqDLYlimit = {want}\n"),
"{token:?}"
);
}
crate::server::records::seq::set_seq_dly_limit(100000.0);
}
#[test]
fn strtol_base0_takes_hex_octal_and_sign() {
assert_eq!(strtol_base0("0x10"), Some(16));
assert_eq!(strtol_base0("010"), Some(8));
assert_eq!(strtol_base0("-3"), Some(-3));
assert_eq!(strtol_base0("12"), Some(12));
assert_eq!(strtol_base0(""), None);
assert_eq!(strtol_base0("1 "), None);
assert_eq!(strtol_base0("nope"), None);
}
}