use std::sync::Arc;
use super::registry::*;
use crate::runtime::task::{StackSizeClass, ThreadPriority};
use crate::server::database::breakpoint::{self, BkptError, BreakpointTable};
pub(crate) fn register(registry: &mut CommandRegistry) {
registry.register(cmd_dbb());
registry.register(cmd_dbd());
registry.register(cmd_dbc());
registry.register(cmd_dbs());
registry.register(cmd_dbstat());
registry.register(cmd_dbp());
registry.register(cmd_dbap());
}
fn sval(args: &[ArgValue], index: usize) -> Option<&str> {
match args.get(index) {
Some(ArgValue::String(s)) if !s.is_empty() => Some(s.as_str()),
_ => None,
}
}
fn ival(args: &[ArgValue], index: usize) -> i32 {
match args.get(index) {
Some(ArgValue::Int(n)) => *n as i32,
_ => 0,
}
}
fn usage(ctx: &CommandContext, command: &str) -> CommandResult {
ctx.println(&format!("Usage: {command} \"record_name\""));
Ok(CommandOutcome::Failed)
}
fn refuse(ctx: &CommandContext, err: &BkptError) -> CommandResult {
ctx.println(&err.message());
Ok(CommandOutcome::Failed)
}
fn table_with_printer(ctx: &CommandContext) -> Arc<BreakpointTable> {
let table = ctx.db().breakpoints_or_install();
let db = ctx.db().clone();
let bridge = ctx.bridge().clone();
table.set_printer(Arc::new(move |name: &str| {
let ctx = CommandContext::new(db.clone(), bridge.clone());
ctx.println("");
super::commands::dbpr_report(&ctx, name, 2);
ctx.print_fmt(format_args!("-> "));
}));
table
}
fn cmd_dbb() -> CommandDef {
CommandDef::new(
"dbb",
vec![ArgDesc {
name: "record name",
arg_type: ArgType::Record,
}],
"dbb <record name> — Set Breakpoint on a record\n\
This command spawns one breakpoint continuation task per lockset, \
in which further record execution is run",
|args: &[ArgValue], ctx: &CommandContext| {
let Some(name) = sval(args, 0) else {
return usage(ctx, "dbb");
};
let table = table_with_printer(ctx);
let db = ctx.db().clone();
let spawn = |id: u64, ex: Arc<breakpoint::BinarySemaphoreHandle>| {
let db = (*db).clone();
let spawned = crate::runtime::task::spawn_dedicated_thread(
"bkptCont".to_string(),
ThreadPriority::Custom(ThreadPriority::ScanLow.value() - 1),
StackSizeClass::Big,
move || {
let closing = breakpoint::continuation_loop(db, id, ex);
print!("{closing}");
use std::io::Write;
let _ = std::io::stdout().flush();
},
);
if spawned.is_err() {
println!(" BKPT> Cannot spawn task to process record");
}
};
match table.set(&db, name, spawn) {
Ok(()) => Ok(CommandOutcome::Continue),
Err(e) => refuse(ctx, &e),
}
},
)
}
fn cmd_dbd() -> CommandDef {
CommandDef::new(
"dbd",
vec![ArgDesc {
name: "record name",
arg_type: ArgType::Record,
}],
"dbd <record name> — Remove breakpoint from a record.",
|args: &[ArgValue], ctx: &CommandContext| {
let Some(name) = sval(args, 0) else {
return usage(ctx, "dbd");
};
let Some(table) = ctx.db().breakpoints() else {
return refuse(ctx, &BkptError::NotSet);
};
match table.clear(ctx.db(), name) {
Ok(()) => {
ctx.db().retire_breakpoints_if_idle();
Ok(CommandOutcome::Continue)
}
Err(e) => refuse(ctx, &e),
}
},
)
}
fn cmd_dbc() -> CommandDef {
CommandDef::new(
"dbc",
vec![ArgDesc {
name: "record name",
arg_type: ArgType::Record,
}],
"dbc <record name> — Continue processing in a lockset until next breakpoint is found.",
|args: &[ArgValue], ctx: &CommandContext| resume(ctx, sval(args, 0), false),
)
}
fn cmd_dbs() -> CommandDef {
CommandDef::new(
"dbs",
vec![ArgDesc {
name: "record name",
arg_type: ArgType::Record,
}],
"dbs <record name> — Step through record processing within a lockset.\n\
If called without an argument, automatically steps with the last breakpoint.",
|args: &[ArgValue], ctx: &CommandContext| resume(ctx, sval(args, 0), true),
)
}
fn resume(ctx: &CommandContext, name: Option<&str>, stepping: bool) -> CommandResult {
let Some(table) = ctx.db().breakpoints() else {
return refuse(ctx, &BkptError::NoneStopped);
};
let outcome = if stepping {
table.step(ctx.db(), name)
} else {
table.cont(ctx.db(), name)
};
match outcome {
Ok(announcement) => {
if let Some(line) = announcement {
ctx.println(&line);
}
Ok(CommandOutcome::Continue)
}
Err(e) => refuse(ctx, &e),
}
}
fn cmd_dbstat() -> CommandDef {
CommandDef::new(
"dbstat",
vec![],
"dbstat — Print list of suspended records, and breakpoints set in locksets.",
|_args: &[ArgValue], ctx: &CommandContext| {
if let Some(table) = ctx.db().breakpoints() {
for line in table.status(ctx.db()) {
ctx.println(&line.render());
}
}
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_dbp() -> CommandDef {
CommandDef::new(
"dbp",
vec![
ArgDesc {
name: "record name",
arg_type: ArgType::Record,
},
ArgDesc {
name: "interest level",
arg_type: ArgType::Int,
},
],
"dbp <record name> <interest level> — Print Fields of a currently suspended \
record by a breakpoint.\n\
interest level 0 - Fields of interest to an Application developer and\n\
\x20 that can be changed as a result of record processing.\n\
\x20 1 - Fields of interest to an Application developer and\n\
\x20 that do not change during record processing.\n\
\x20 2 - Fields of major interest to a System developer.\n\
\x20 3 - Fields of minor interest to a System developer.\n\
\x20 4 - Internal record fields.",
|args: &[ArgValue], ctx: &CommandContext| {
let Some(table) = ctx.db().breakpoints() else {
return refuse(ctx, &BkptError::NoneStopped);
};
let target = match table.print_target(ctx.db(), sval(args, 0)) {
Ok(name) => name,
Err(e) => return refuse(ctx, &e),
};
let level = ival(args, 1);
let level = if level == 0 { 2 } else { level };
super::commands::dbpr_report(ctx, &target, level);
Ok(CommandOutcome::Continue)
},
)
}
fn cmd_dbap() -> CommandDef {
CommandDef::new(
"dbap",
vec![ArgDesc {
name: "record name",
arg_type: ArgType::Record,
}],
"dbap <record name> — Auto Print.\n\
Toggle automatic printing after processing a record that has a breakpoint.",
|args: &[ArgValue], ctx: &CommandContext| {
let Some(name) = sval(args, 0) else {
return usage(ctx, "dbap");
};
match breakpoint::toggle_autoprint(ctx.db(), name) {
Ok(line) => {
ctx.println(&line);
Ok(CommandOutcome::Continue)
}
Err(e) => refuse(ctx, &e),
}
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::database::PvDatabase;
use crate::server::records::ai::AiRecord;
#[test]
fn dbb_puts_its_continuation_thread_on_the_thread_list() {
let rt = tokio::runtime::Runtime::new().unwrap();
let db = Arc::new(PvDatabase::new());
rt.block_on(async {
db.add_record("BKPT:ONE", Box::new(AiRecord::new(0.0)))
.await
.expect("add_record");
});
db.build_lock_sets();
let bridge = {
let _guard = rt.enter();
crate::runtime::task::BlockingBridge::capture()
};
let ctx = CommandContext::new(db.clone(), bridge);
let mut reg = CommandRegistry::new();
register(&mut reg);
assert!(
crate::runtime::task::thread_by_name("bkptCont").is_none(),
"no continuation thread before dbb"
);
let cmd = reg.get("dbb").unwrap();
let args = parse_args(&["BKPT:ONE".to_string()], &cmd.args).unwrap();
assert!(
matches!(cmd.handler.call(&args, &ctx), Ok(CommandOutcome::Continue)),
"dbb on a record with a lock set must succeed"
);
let listed = (0..200)
.find_map(|_| {
crate::runtime::task::thread_by_name("bkptCont").or_else(|| {
std::thread::sleep(std::time::Duration::from_millis(10));
None
})
})
.expect("bkptCont must reach the thread list epicsThreadShowAll reads");
assert_eq!(
listed.epics_priority(),
ThreadPriority::ScanLow.value() - 1,
"C spawns bkptCont at epicsThreadPriorityScanLow-1 (dbBkpt.c:373)"
);
assert!(
crate::runtime::task::thread_by_id(listed.id()).is_some(),
"the row must also resolve by the id epicsThreadResume parses"
);
let stat = breakpoint::BreakpointTable::status(
&db.breakpoints().expect("dbb installs the table"),
&db,
);
assert!(
stat[0]
.render()
.ends_with(&format!("T: {:#x}", listed.id())),
"dbstat's T: column must be the thread's EPICS ID, got {:?}",
stat[0].render()
);
std::mem::forget(rt);
}
}