epics_base_rs/server/database/field_io.rs
1use std::collections::HashSet;
2
3use crate::error::{CaError, CaResult};
4use crate::server::snapshot::Snapshot;
5use crate::types::EpicsValue;
6
7use super::PvDatabase;
8
9/// pvxs's `record._options.process` term (`ioc/iocsource.cpp:426-448`)
10/// as the database sees it: how much processing an EXTERNAL client put
11/// drives. The one enum behind both PVA sources and the QSRV group put,
12/// so `True`/`False`/`Unset` are spelled once.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum ProcessMode {
15 /// pvxs `Unset` — process only when the record's own rules say so
16 /// (C `dbPutField`'s `pp(TRUE)` + `SCAN=Passive` test).
17 #[default]
18 Passive,
19 /// pvxs `True` — force a processing cycle after the write.
20 Force,
21 /// pvxs `False` — write the field and stop.
22 Inhibit,
23}
24
25/// C `dbPutField`'s put-disable gate (`dbAccess.c:1255-1257`):
26/// `precord->disp && paddr->pfield != &precord->disp` → `S_db_putDisabled`.
27///
28/// This is the FIRST gate an *external* put crosses. It precedes `dbPut` —
29/// so it precedes the `SPC_NOMOD` rejection of PACT/LCNT/PUTF — and it
30/// precedes the PROC-driven `dbProcess` (`dbAccess.c:1265-1277`), so
31/// `caput REC.PROC 1` on a `DISP=1` record is refused, not force-processed.
32///
33/// Single owner for both external put boundaries: the CA / `dbpf` route
34/// ([`PvDatabase::put_record_field_from_ca`]) and the QSRV precondition
35/// check ([`PvDatabase::check_external_put_preconditions`]). Internal puts
36/// (`put_pv`, link and processing writes — the `dbPut` analogue) deliberately
37/// do not cross it.
38fn check_put_disabled(
39 instance: &crate::server::record::RecordInstance,
40 field_upper: &str,
41) -> CaResult<()> {
42 if instance.common.disp != 0 && field_upper != "DISP" {
43 return Err(CaError::PutDisabled(field_upper.to_string()));
44 }
45 Ok(())
46}
47
48/// C's `dbPut` no-modify gate — the put-side consumer of the SPC_NOMOD
49/// declaration.
50///
51/// Two C rejections, both inside `dbPut` and therefore BELOW every put entry
52/// point (`dbPutField` for CA/`dbpf`, `dbPutLink` for a record's OUT link,
53/// `dbPutSpecial` for an internal one):
54///
55/// ```c
56/// /* dbAccess.c:1330-1332 */
57/// if (special == SPC_ATTRIBUTE) return S_db_noMod;
58/// /* dbAccess.c:123-126, via dbPut -> dbPutSpecial(paddr, 0) */
59/// if ((special == SPC_NOMOD) && (pass == 0)) return S_db_noMod;
60/// ```
61///
62/// INVARIANT: a field declared `special(SPC_NOMOD)` (or `SPC_ATTRIBUTE`) MUST
63/// NOT be modified by ANY runtime write, whatever route it arrives on — CA put,
64/// `dbpf`, QSRV, an internal `put_pv`, or a record's OUT link. A record's own
65/// `put_field` is NOT a gate: the hand-written array records write
66/// NELM/FTVL/NORD there because the *load* path (`dbLoadRecords` →
67/// `Record::put_field`) must set them — C likewise writes them through
68/// `dbStaticLib`'s `dbPutString`, which never crosses `dbPut`.
69///
70/// The declaration itself lives in [`RecordInstance::is_no_mod`](crate::server::record::RecordInstance::is_no_mod), which C
71/// exposes as `dbChannelSpecial(...) == SPC_NOMOD` and reads from TWO places:
72/// this gate (`dbPut`, dbAccess.c:123-126) and `rsrvCheckPut`
73/// (camessage.c:2608-2619), which feeds the CA ACCESS_RIGHTS write bit. This
74/// function is the first consumer; `epics-ca-rs`'s `compute_access` is the
75/// second.
76///
77/// The one thing that legitimately changes ACKS/ACKT is C's alarm
78/// acknowledgement, and it does NOT come through here: `dbPut` dispatches on the
79/// DBR *request type* (`DBR_PUT_ACKT`/`DBR_PUT_ACKS`, `dbAccess.c:1331-1335`)
80/// ABOVE this gate, into [`RecordInstance::put_ackt`](crate::server::record::RecordInstance::put_ackt) /
81/// [`RecordInstance::put_acks`](crate::server::record::RecordInstance::put_acks). The wire route for that is
82/// [`PvDatabase::put_alarm_ack_from_ca`].
83///
84/// `field` must already be upper-cased.
85fn check_no_mod(instance: &crate::server::record::RecordInstance, field: &str) -> CaResult<()> {
86 if instance.is_no_mod(field) {
87 return Err(CaError::ReadOnlyField(field.to_string()));
88 }
89 Ok(())
90}
91
92/// C `dbPut`'s link-field refusal (`field_type > DBF_DEVICE` →
93/// `S_db_badDbrtype`, `dbAccess.c:1340-1347`): only `dbPutField` may change a
94/// DBF_INLINK/OUTLINK/FWDLINK field — it routes them through `dbPutFieldLink`
95/// (`dbAccess.c:1261-1262`) — so every `dbPut`-analogue body refuses them
96/// before converting anything. This is what stops a record's DB OUT link
97/// (`dbPutLink` → `dbDbPutValue` → `dbPut`) from silently rewiring another
98/// record's link field on every process. The port's `dbPutField` analogue is
99/// the `put_record_field_from_ca` family, whose ordinary write path re-parses
100/// the link (`RecordInstance::put_common_field`'s INP/OUT/FLNK arms);
101/// `put_pv_no_process` is the autosave-restore entry whose C analogue is
102/// likewise `dbPutField` (`reboot_restore`), so it stays link-writable.
103///
104/// `field` must already be upper-cased.
105fn check_not_link_field(
106 instance: &crate::server::record::RecordInstance,
107 field: &str,
108) -> CaResult<()> {
109 if crate::types::dbf_link_class(instance.record.record_type(), field).is_some() {
110 return Err(CaError::BadDbrType(format!(
111 "dbPut: {field} is a link field; only dbPutField changes link fields"
112 )));
113 }
114 Ok(())
115}
116
117/// Does an *external* put to `field` drive a processing cycle on this record?
118///
119/// C `dbPutField` (`dbAccess.c:1263-1268`) and pvxs `IOCSource::
120/// doPostProcessing` (`iocsource.cpp:397-403`) ask the same question with the
121/// same terms as C `processNotifyCommon` (dbNotify.c:243-246): the `PROC` field
122/// always, else a `pp(TRUE)` field on a Passive record. (`dbrType <
123/// DBR_PUT_ACKT` is subsumed: the alarm-ack fields are not `pp(TRUE)`.) A caller
124/// that FORCES processing (`record._options.process=true`) does not consult this
125/// at all — force is the caller's own term, not the record's.
126///
127/// `PROC` and `UDF` are the ONLY two `dbCommon` `pp(TRUE)` fields
128/// (`dbCommon.dbd.pod`: PROC line 243, UDF line 552); every other `pp(TRUE)`
129/// field is declared per record TYPE and reached through
130/// [`Record::processes_after_put`](crate::server::record::Record::processes_after_put). Because the two `dbCommon` fields are NOT in
131/// any type's `process_passive_fields()` table, they are named here directly:
132/// `PROC` unconditionally (force-process on any SCAN), `UDF` on the Passive
133/// branch (an ordinary `pp` field, so it processes only when `SCAN == 0`, unlike
134/// PROC). Both `dbCommon` `pp(TRUE)` fields are thus handled at this one owner
135/// gate, uniformly for every record type. A put to UDF is accepted+stored by
136/// `put_common_field`; this gate only adds the process cycle, after which the
137/// record recomputes alarms → NO_ALARM (C ends STAT/SEVR=NO_ALARM likewise).
138///
139/// Single owner of the rule: the single-record put route
140/// ([`PvDatabase::put_record_field_from_ca`]) tests it while it already holds
141/// the instance, and the QSRV group PUT — whose C twin is `doPostProcessing` —
142/// reaches it through [`PvDatabase::put_drives_processing`]. Neither can drift
143/// from C or from the other.
144///
145/// `field` must already be upper-cased.
146pub(crate) fn put_drives_processing_of(
147 instance: &crate::server::record::RecordInstance,
148 field: &str,
149) -> bool {
150 field == "PROC"
151 || (instance.common.scan == crate::server::record::ScanType::Passive
152 && (field == "UDF" || instance.record.processes_after_put(field)))
153}
154
155/// Drain the record's per-cycle post marks ([`Record::take_cycle_posted_fields`](crate::server::record::Record::take_cycle_posted_fields))
156/// into monitor posts — the put-path counterpart of the `db_post_events` calls a
157/// C `special()` makes by hand.
158///
159/// One owner, so the two put-path drains (the normal tail, and the failing
160/// `special()` above) cannot disagree about the mask mapping.
161fn emit_cycle_posts(instance: &mut crate::server::record::RecordInstance) {
162 use crate::server::record::{CyclePostMask, EventMask};
163 for (sf, cycle_mask) in instance.record.take_cycle_posted_fields() {
164 let mask = match cycle_mask {
165 CyclePostMask::Value => EventMask::VALUE,
166 // No `monitor_mask` exists on a put path (no alarm transition is
167 // being resolved), so both LOG-carrying variants reduce to C's
168 // literal `DBE_VALUE|DBE_LOG`.
169 CyclePostMask::ValueLog | CyclePostMask::MonitorValueLog => {
170 EventMask::VALUE | EventMask::LOG
171 }
172 };
173 instance.notify_field(sf, mask);
174 }
175}
176
177/// The registry half of a SNAM `special()` — C `subRecord.c::special`
178/// (`:170-195`) and `aSubRecord.c::special` (`:552-578`), which resolve
179/// `prec->snam` through `registryFunctionFind` and assign `prec->sadr`.
180///
181/// C runs it as `dbPutSpecial(paddr, 1)`, AFTER the field is stored
182/// (`dbAccess.c:1355-1404`), so the name resolved is the STORED one:
183/// `putStringString` truncates a DBF_STRING put to `field_size - 1` — 39 for
184/// sub's `size(40)`, 40 for aSub's `size(41)` — and C looks up whatever
185/// survived that. Resolving the value the caller handed in would bind a
186/// routine whose name the record does not hold.
187///
188/// `sadr` takes the lookup result UNCONDITIONALLY, NULL included, and only then
189/// does the status decide: a non-empty unregistered name is `S_db_BadSub`,
190/// which `dbPut` adopts as the put's status (`if (status2) status = status2;`)
191/// and whose `goto done` skips the UDF clear, the field's monitor post and — in
192/// `dbPutField` — the process. The name stays stored either way.
193///
194/// INVARIANT: `RecordInstance::subroutine` is the registry resolution of the
195/// record's current SNAM, C's `prec->sadr`. Three owners perform that
196/// transition and nothing else under `src/` writes the field:
197///
198/// - `IocApp` (`ioc_app.rs`) and `IocBuilder` (`ioc_builder.rs`) at iocInit —
199/// C's `init_record`, a different function running the same lookup.
200/// - `apply_asub_dynamic_sub` (`processing.rs`) for aSub `LFLG=READ`, whose C
201/// rule deliberately differs: `fetch_values` (`aSubRecord.c:262-266`) returns
202/// `S_db_BadSub` BEFORE assigning `sadr`, so READ mode KEEPS the old routine
203/// on an unregistered name. Routing it through this owner would clear it.
204/// - this function, for every `dbPut` route, because [`special_after_put`] is
205/// the one caller and every route goes through that.
206///
207/// `RecordInstance::new` initialises the field to `None`, which is the initial
208/// state and not a transition. `put_pv_no_process` (the autosave-restore entry)
209/// writes SNAM while running NEITHER `special()` pass, so it leaves the binding
210/// stale — a whole missing `dbPutSpecial`, not this rule's half. The remaining
211/// writers are `#[test]` fixtures binding a routine without a registry.
212fn snam_special_after_put(
213 db: &PvDatabase,
214 instance: &mut crate::server::record::RecordInstance,
215 field: &str,
216) -> CaResult<()> {
217 if !instance.record.is_subroutine_name_field(field) {
218 return Ok(());
219 }
220 let Some(EpicsValue::String(stored)) = instance.record.get_field(field) else {
221 return Ok(());
222 };
223 let name = stored.as_str_lossy();
224 if name.is_empty() {
225 // aSub: `pfunc = 0` with no error, so `caput X.SNAM ""` unbinds and
226 // succeeds (`aSubRecord.c:560-561`, stored at `:575`). sub's C leaves
227 // `sadr` alone and parks PACT instead (`subRecord.c:182-186`); this
228 // port clears, because
229 // the put-side park is not implemented here and a retained routine would
230 // keep RUNNING every scan where C's parked record does nothing at all.
231 instance.subroutine = None;
232 return Ok(());
233 }
234 let resolved = db.find_subroutine_named(name.as_ref());
235 let bad_sub = resolved.is_none();
236 instance.subroutine = resolved;
237 if bad_sub {
238 return Err(CaError::BadField("SNAM: Subroutine not found".into()));
239 }
240 Ok(())
241}
242
243/// C `dbPutSpecial(paddr, 1)` — the after-put `special()`, paired with the drain
244/// of the link writes it queued ([`Record::take_special_actions`](crate::server::record::Record::take_special_actions)).
245///
246/// The pairing is the point: `special()` and its drain are ONE step, so a queued
247/// action cannot survive the put that queued it — not even when `special()`
248/// returns an error (C's `dbPut` `goto done`), because the drain happens before
249/// the status is propagated. The caller executes `out` once the record lock is
250/// released, ahead of the put-driven process cycle, which is where C runs it
251/// (inside `dbPut`, before `dbPutField`'s `dbProcess`).
252///
253/// Every `dbPut` path in this module goes through here; nothing else may call
254/// `Record::special(field, true)`.
255///
256/// Returns the scan-index delta the after-put pass produced — non-`NoChange`
257/// only for the SIMM↔SSCN swap below, which the caller applies through
258/// `update_scan_index` once the record lock is down.
259fn special_after_put(
260 db: &PvDatabase,
261 instance: &mut crate::server::record::RecordInstance,
262 field: &str,
263 out: &mut Vec<crate::server::record::ProcessAction>,
264) -> CaResult<crate::server::record::CommonFieldPutResult> {
265 let mut status = instance.record.special(field, true);
266 // The record's half above and the registry half here are ONE C function,
267 // `special(paddr, 1)`, so they share the action drain, the error-path post
268 // emission and the status. The record cannot do the lookup itself — the
269 // function registry belongs to the database, not to the record.
270 if status.is_ok() {
271 status = snam_special_after_put(db, instance, field);
272 }
273 out.extend(instance.record.take_special_actions());
274 if status.is_err() {
275 // The POSTS `special()` made are drained on the failing path for the same
276 // reason its ACTIONS are: C's `special()` calls `db_post_events` BEFORE it
277 // returns nonzero — aCalcout's NUSE arm posts the clamped value with
278 // `DBE_VALUE` and only then `return (-1)` (`aCalcoutRecord.c:495-499`) —
279 // and `dbPut`'s `goto done` skips only dbPut's OWN post. A refused put
280 // that repaired a field must still tell the subscribers what it repaired.
281 emit_cycle_posts(instance);
282 }
283 status?;
284
285 // C `special()`'s CONSTANT-link re-seed (`calcoutRecord.c:367-378`,
286 // `sCalcoutRecord.c:512-517`, `aCalcoutRecord.c:534-540`,
287 // `transformRecord.c:714-719` — the four records whose C `special()` calls
288 // `recGblInitConstantLink`): a put that leaves an input link constant
289 // re-runs the load into that input's value field and posts it. Without it a
290 // constant link is load-once dead state — the link layer delivers nothing
291 // for a constant at process time, so `caput CO.INPB 7` would store the text
292 // and leave `B` at its `.db` value forever.
293 //
294 // The record only DECLARES the pairs (`special_reseed_input_links`); the
295 // load itself is the shared `rec_gbl_init_constant_link` owner, the same one
296 // the init seed uses. Records whose C `special()` does not re-seed (calc,
297 // sub, sel, aSub, swait, …) declare nothing and are untouched.
298 if let Some(value_field) =
299 crate::server::record::reseed_constant_input_link(&mut *instance.record, field)
300 {
301 // The mask is the C call site's, and they disagree — calcout posts a
302 // literal DBE_VALUE, transform DBE_VALUE|DBE_LOG. The record carries it.
303 let mask = instance.record.special_reseed_post_mask();
304 instance.notify_field(value_field, mask);
305 }
306
307 // C `special(SPC_MOD)` pass 1 on SIMM (`longinRecord.c:171-177` and the
308 // identical arm in all 21 SSCN-bearing records):
309 // `recGblCheckSimm((dbCommon *)prec, &prec->sscn, prec->oldsimm, prec->simm);`
310 // Paired with `special_before_put`'s pass 0 (`recGblSaveSimm`), and gated
311 // per record type by `Record::uses_recgbl_simm_helpers`.
312 // C `subRecord.c::special` pass 1 (`:189-193`): the value is stored, so ask
313 // again — an SNAM that is still empty re-parks PACT, a real one leaves the
314 // record released by `special_before_put`. Only `enter_pact` here; C's pass 1
315 // never clears PACT, and neither may this.
316 if pact_park_field(&*instance.record, field) && instance.record.parks_pact() {
317 instance.enter_pact();
318 }
319
320 Ok(if field == "SIMM" {
321 instance.rec_gbl_check_simm()
322 } else {
323 crate::server::record::CommonFieldPutResult::NoChange
324 })
325}
326
327/// C `dbPutSpecial(paddr, 0)` — the before-put pass, run while the record lock
328/// is held and BEFORE the field's new value is stored.
329///
330/// The only `SPC_MOD` field in the record framework whose pass-0 does work is
331/// SIMM: `recGblSaveSimm` latches the outgoing simulation mode into OLDSIMM so
332/// the after-put pass can see the transition. Paired with
333/// [`special_after_put`]; every `dbPut` path in this module calls both.
334///
335/// The other pass-0 body is `subRecord.c::special`'s park release:
336/// `if (prec->snam[0] == 0 && prec->pact) { prec->pact = FALSE; prec->rpro =
337/// FALSE; }` (`subRecord.c:175-178`) — the record is parked exactly while it
338/// cannot process, so the store about to happen gets a clean slate and
339/// [`special_after_put`] re-takes the park if the NEW value still leaves the
340/// record unable to run. The record cannot reach PACT, so it answers
341/// [`Record::parks_pact`](crate::server::record::Record::parks_pact) and this performs the transition; the returned
342/// [`PactExit`](crate::server::record::PactExit) carries any put-notify the release freed.
343fn special_before_put(
344 instance: &mut crate::server::record::RecordInstance,
345 field: &str,
346) -> Option<crate::server::record::PactExit> {
347 if field == "SIMM" {
348 instance.rec_gbl_save_simm();
349 }
350 if pact_park_field(&*instance.record, field)
351 && instance.record.parks_pact()
352 && instance.is_processing()
353 {
354 instance.common.rpro = 0;
355 return Some(instance.leave_pact());
356 }
357 None
358}
359
360/// Who arms the queued put-notify restart that a PACT release owes.
361///
362/// C reaches `restartCheck` only from `dbNotifyCompletion` ← `recGblFwdLink`
363/// (`recGbl.c:295`) — the tail of a process CYCLE, never the `pact = FALSE`
364/// store itself. A put body that arms it directly is therefore only correct
365/// when no cycle follows the put; when one does, the replay races it.
366#[derive(Clone, Copy, PartialEq, Eq)]
367enum RestartOwner {
368 /// The put is the whole transaction. Nothing else will reach the record,
369 /// so the put body is the only site that can arm the restart.
370 ThisPut,
371 /// The caller drives a process cycle on the SAME record the moment this
372 /// put returns — an OUT link's `processTarget`, QSRV's `Force` mode. That
373 /// cycle's tail is C's owner; arming here lets the replay take the
374 /// record's gate first and the record then processes the replayed put
375 /// BEFORE the put that released the park (measured: `bump` twice, VAL 5.0
376 /// where C gives 4.0).
377 TheCycleThisPutOwes,
378}
379
380/// The finalizer for the PACT release [`special_before_put`] performs.
381///
382/// Declared BEFORE the `rec.write()` guard at every put body, so Rust's
383/// reverse-declaration drop order puts the record's DATA lock down first and
384/// this second. `PvDatabase::apply_pact_exit` re-enters the record it is handed;
385/// `parking_lot::RwLock` is not reentrant, so arming the restart from inside the
386/// put's own write guard is a deadlock, not an error.
387///
388/// A guard and not a call because the release sits ABOVE the fallible tail of
389/// the put — `special_after_put`, `put_common_field`, the rejected-conversion
390/// `Err` — and C `dbNotifyCompletion` is reached from `recGblFwdLink` on EVERY
391/// path that ends the cycle. Nothing reports a token that never reaches the
392/// consumer: [`PactExit`](crate::server::record::PactExit) has no `Drop`, and
393/// its `#[must_use]` fires only on an unused *expression*, never on a
394/// `let`-bound token left behind by a `?`. The guard is the whole enforcement.
395///
396/// The cycle body has the same debt at a different scope; its owner is
397/// `processing::CycleEndGuard`, which pays the full `end_process_cycle` tail
398/// rather than the restart drain alone.
399struct PactExitGuard<'a> {
400 db: &'a PvDatabase,
401 name: &'a str,
402 rec: &'a std::sync::Arc<parking_lot::RwLock<crate::server::record::RecordInstance>>,
403 exit: Option<crate::server::record::PactExit>,
404 owner: RestartOwner,
405}
406
407impl<'a> PactExitGuard<'a> {
408 fn new(
409 db: &'a PvDatabase,
410 name: &'a str,
411 rec: &'a std::sync::Arc<parking_lot::RwLock<crate::server::record::RecordInstance>>,
412 owner: RestartOwner,
413 ) -> Self {
414 PactExitGuard {
415 db,
416 name,
417 rec,
418 exit: None,
419 owner,
420 }
421 }
422
423 /// Take the token [`special_before_put`] produced, if it released a park.
424 fn arm(&mut self, exit: Option<crate::server::record::PactExit>) {
425 self.exit = exit;
426 }
427}
428
429impl Drop for PactExitGuard<'_> {
430 fn drop(&mut self) {
431 let Some(exit) = self.exit.take() else {
432 return;
433 };
434 // `TheCycleThisPutOwes` drops the token on purpose and loses nothing:
435 // the tail re-derives it from the record
436 // (`RecordInstance::pact_exit_without_release` reads
437 // `notify_restart_pending()` at cycle end), so the restart is armed
438 // once, by C's owner, in C's order.
439 if self.owner == RestartOwner::ThisPut {
440 self.db.apply_pact_exit(self.name, self.rec, exit);
441 }
442 }
443}
444
445/// Is `field` one C marks `special(SPC_MOD)` for this record's PACT park?
446fn pact_park_field(record: &dyn crate::server::record::Record, field: &str) -> bool {
447 record
448 .pact_park_fields()
449 .iter()
450 .any(|f| f.eq_ignore_ascii_case(field))
451}
452
453/// The `recGblResetAlarms` half of a C `monitor()` that a `special()` invokes
454/// (compress SPC_RESET, [`crate::server::record::Record::special_commits_alarms`]).
455///
456/// C's `monitor()` opens with `recGblResetAlarms(prec)` (compressRecord.c:103),
457/// committing `nsta`/`nsev` into `stat`/`sevr` — this is what clears the
458/// born-UDF alarm of a never-processed record the moment a reset field is put.
459/// Commits the alarm, posts any STAT/SEVR/AMSG/ACKS transition through the one
460/// owner ([`crate::server::database::processing::alarm_field_posts`]), and
461/// returns the `DBE_ALARM` mask C ORs into the value posts (`val_mask`,
462/// recGbl.c:213 — set iff any alarm-class field moved this cycle).
463///
464/// Shared by `dbPut`'s success tail and its rejected-conversion path: C runs
465/// `dbPutSpecial(paddr, 1)` on BOTH (dbAccess.c:83-88, "Always do special
466/// processing if needed", before the `goto done` that bails on a failed put).
467fn commit_special_reset_alarm(
468 instance: &mut crate::server::record::RecordInstance,
469) -> crate::server::recgbl::EventMask {
470 use crate::server::recgbl::EventMask;
471 let alarm_result = crate::server::recgbl::rec_gbl_reset_alarms(&mut instance.common);
472 for (af, mask) in
473 crate::server::database::processing::alarm_field_posts(&instance.common, &alarm_result)
474 {
475 instance.notify_field(af, mask);
476 }
477 if alarm_result.alarm_changed || alarm_result.amsg_changed {
478 EventMask::ALARM
479 } else {
480 EventMask::NONE
481 }
482}
483
484/// Coerce a write `value` to a record field's stored `target` type — C
485/// `dbConvert.c`'s `dbFastPutConvertRoutine[dbrType][field_type]` table.
486///
487/// The client-`dbPut` half of the shared converter
488/// [`crate::server::record::coerce_put_value`]; the internal-delivery half is
489/// `put_field_internal_default`. A `DBR_STRING` write to a `DBF_MENU` or
490/// `DBF_ENUM` field has a converter of its own in C (`putStringMenu`,
491/// `putStringEnum`) and must not fall through to `EpicsValue::convert_to`,
492/// which is field-blind and turns any unrecognised string into index 0 —
493/// C stores nothing and fails the put with `S_db_badChoice`.
494fn coerce_write_value(
495 record: &dyn crate::server::record::Record,
496 field: &str,
497 target: crate::types::DbFieldType,
498 value: EpicsValue,
499) -> CaResult<EpicsValue> {
500 crate::server::record::coerce_put_value(record, field, target, value)
501}
502
503/// What a `dbPut` of a given value means for a given field — the single owner
504/// of C `dbPut`'s value branch (`dbAccess.c:1345-1372`).
505enum PutRequest {
506 /// Write this value; already coerced to the field's native type.
507 Write(EpicsValue),
508 /// A zero-element request (`nRequest < 1`) into a **scalar** destination.
509 ///
510 /// C writes nothing, raises `LINK_ALARM`/`INVALID_ALARM` on the record, and
511 /// returns **success** — `status` stays 0, so the put is accepted and the
512 /// record's next `recGblResetAlarms` publishes the new alarm
513 /// (`dbAccess.c:1370-1372`, commit `12cfd418d`, whose subject is "fix dbPut
514 /// to *set* the target to INVALID/LINK alarm when writing empty arrays into
515 /// scalars" — not to reject the put).
516 EmptyIntoScalar,
517}
518
519/// Resolve a put into its C `dbPut` branch.
520///
521/// C picks the branch from the **destination's** element count
522/// (`dbAccess.c:1345` `no_elements > 1`): an array field clamps `nRequest` and
523/// converts — a zero-length request copies nothing and succeeds silently — while
524/// a scalar field with `nRequest < 1` takes the alarm branch. The test is on the
525/// request count and the destination, never on whether a type conversion happens
526/// to be needed.
527///
528/// [`FieldDesc`](crate::server::record::FieldDesc) carries no element count, so
529/// the destination's current value is the probe: an array-valued field reads
530/// back as an array variant. A field the record does not own (a `dbCommon`
531/// field, reached via `put_common_field`) reads back as `None` and is scalar,
532/// which is also what its `DBF_*` descriptor says in C.
533fn dbput_request(
534 record: &dyn crate::server::record::Record,
535 field: &str,
536 value: EpicsValue,
537) -> CaResult<PutRequest> {
538 let dest_is_array = record.get_field(field).is_some_and(|v| v.is_array());
539 if value.is_empty_array() && !dest_is_array {
540 return Ok(PutRequest::EmptyIntoScalar);
541 }
542 // The coercion target is the type the record STORES, not the type it
543 // SERVES — `put_field`'s arms match on what is stored. A `menu()` field is
544 // declared `DBF_MENU` and served as `DBR_ENUM` with its choices, but held as
545 // a bare `Short` choice index; coercing an incoming `Short` up to `Enum`
546 // because the `.dbd` says `DBF_MENU` would make its `Short` arm unreachable.
547 // Same rule as `put_field_internal_default` and `db_loader::apply_fields`.
548 // The `.dbd` type is the fallback for a field with no current value.
549 let target = record
550 .get_field(field)
551 .map(|v| v.db_field_type())
552 .or_else(|| crate::server::record::record_instance::declared_field_type_of(record, field));
553
554 // C `dbPut` clamps the request to the destination's element count —
555 // `if (no_elements < nRequest) nRequest = no_elements;` (dbAccess.c:1359),
556 // then converts `nRequest` elements. A multi-element request into a
557 // one-element destination therefore writes element 0 and SUCCEEDS; the
558 // surplus elements are dropped, not an error. Reduce the array to its
559 // first element here, so the record's typed `put_field` arm — and every
560 // `put_common_field` arm — sees the scalar C would have written instead of
561 // rejecting the array with a `TypeMismatch`.
562 //
563 // One array shape is exempt: a `CharArray` into a `DBF_STRING` field is how
564 // this port carries the dbChannel `$` char-array view of a string field
565 // (`dbChannel.c:486-505` re-types it to `DBF_CHAR[field_size]`, i.e. an
566 // ARRAY destination in C — its element count is 40, not 1). The `$` flag
567 // lives on the CA channel and never reaches this layer, so the char view is
568 // recognised by its shape and left to `convert_to`, which decodes the bytes
569 // back into the string field.
570 let is_char_string_view = matches!(value, EpicsValue::CharArray(_))
571 && target == Some(crate::types::DbFieldType::String);
572 let value = if !dest_is_array && value.is_array() && !is_char_string_view {
573 value.first_element().unwrap_or(value)
574 } else {
575 value
576 };
577
578 match target {
579 // A String target always runs the converter even on a type match: C's
580 // `putStringString` is not a no-op, it truncates to `field_size - 1`
581 // (see `coerce_put_value`).
582 Some(target)
583 if value.db_field_type() != target || target == crate::types::DbFieldType::String =>
584 {
585 Ok(PutRequest::Write(coerce_write_value(
586 record, field, target, value,
587 )?))
588 }
589 _ => Ok(PutRequest::Write(value)),
590 }
591}
592
593/// Apply C's [`PutRequest::EmptyIntoScalar`] effect: the field is left
594/// untouched and the record is driven to `LINK_ALARM`/`INVALID_ALARM`
595/// (`dbAccess.c:1371` `recGblSetSevr(precord, LINK_ALARM, INVALID_ALARM)`).
596fn set_empty_request_alarm(instance: &mut crate::server::record::RecordInstance) {
597 crate::server::recgbl::rec_gbl_set_sevr(
598 &mut instance.common,
599 crate::server::recgbl::alarm_status::LINK_ALARM,
600 crate::server::record::AlarmSeverity::Invalid,
601 );
602}
603
604/// What the put entry point owes the caller in the way of completion — the
605/// `dbPutField` / `dbPutNotify` split, plus the restart C's `dbNotify` state
606/// machine performs on a put-notify that had to wait for a PACT record.
607///
608/// The completion *sender* travels with the request: a restarted put must
609/// signal the ORIGINAL caller's receiver, which was handed out when the put
610/// first arrived and was deferred, so the restart cannot mint a fresh channel.
611enum NotifyRequest {
612 /// C `dbPutField` — process the record, build no `putNotify`.
613 None,
614 /// C `dbPutNotify` arriving fresh from a client: mint the wait-set channel.
615 New,
616 /// C `dbNotify.c:207-231` restart: the client's receiver already exists,
617 /// this replay carries its sender.
618 Deferred(crate::runtime::sync::oneshot::Sender<()>),
619}
620
621/// Which of C `processNotifyCommon`'s two entries a put-notify arrives on
622/// (its `first` argument, dbNotify.c:207).
623///
624/// The two differ only in what defers them, and that difference is why the
625/// entry test cannot live inside the install owner: a fresh arrival must not
626/// jump a queue, a replay IS the queue head.
627#[derive(Clone, Copy)]
628enum NotifyArrival {
629 /// `dbProcessNotify` -> `processNotifyCommon(ppn, precord, 1)`: reaching
630 /// the record for the first time.
631 Fresh,
632 /// `notifyCallback` -> `processNotifyCommon(ppn, precord, 0)`: already
633 /// popped off the restart list by `take_next_notify_restart`, C
634 /// `restartCheck` (dbNotify.c:158-168).
635 Replay,
636}
637
638impl NotifyArrival {
639 /// Must this arrival queue rather than take the slot?
640 ///
641 /// A FRESH notify defers on the full test (owned, PACT, or someone already
642 /// queued) -- C `processNotifyCommon:213` plus the PACT arm at `:225`, and
643 /// the restart-list term so a notify arriving between a completion and the
644 /// restart check cannot jump the queue. A REPLAY defers only on an
645 /// occupied slot: it must take the record with its successors still queued
646 /// behind it, exactly as `restartCheck` assigns `precord->ppn = pfirst`
647 /// while leaving the rest of `restartList` in place.
648 fn defers(self, record: &crate::server::record::RecordInstance) -> bool {
649 match self {
650 NotifyArrival::Fresh => record.notify_put_is_owned(),
651 NotifyArrival::Replay => record.notify.is_some(),
652 }
653 }
654}
655
656impl NotifyRequest {
657 fn wants_notify(&self) -> bool {
658 !matches!(self, NotifyRequest::None)
659 }
660
661 /// C `pnotifyPvt->state == notifyRestartCallbackRequested` (dbNotify.c:213)
662 /// — this put already owns the record, so the ownership test that stops a
663 /// fresh arrival does not apply to it.
664 fn is_restart(&self) -> bool {
665 matches!(self, NotifyRequest::Deferred(_))
666 }
667
668 /// The completion sender, plus the receiver to hand back — `Some` only for
669 /// a fresh request; a restart's receiver went to the client at deferral.
670 #[allow(clippy::type_complexity)]
671 fn into_completion(
672 self,
673 ) -> Option<(
674 crate::runtime::sync::oneshot::Sender<()>,
675 Option<crate::runtime::sync::oneshot::Receiver<()>>,
676 )> {
677 match self {
678 NotifyRequest::None => None,
679 NotifyRequest::New => {
680 let (tx, rx) = crate::runtime::sync::oneshot::channel();
681 Some((tx, Some(rx)))
682 }
683 NotifyRequest::Deferred(tx) => Some((tx, None)),
684 }
685 }
686}
687
688/// Snapshot NORD before a `dbPut` writes the value field — C `put_array_info`
689/// opens with `epicsUInt32 nord = prec->nord;` (`waveformRecord.c:202-216`).
690///
691/// `None` for a put to any field but VAL, and for a record type that has no
692/// NORD (the comparison in [`post_array_info`] then reduces to "unchanged").
693fn array_nord_before_put(
694 instance: &crate::server::record::RecordInstance,
695 field: &str,
696) -> Option<EpicsValue> {
697 if field == "VAL" {
698 instance.record.get_field("NORD")
699 } else {
700 None
701 }
702}
703
704/// The tail of C `put_array_info`, and the SINGLE owner of the NORD post:
705///
706/// ```c
707/// if (nord != prec->nord)
708/// db_post_events(prec, &prec->nord, DBE_VALUE | DBE_LOG);
709/// ```
710///
711/// `put_array_info` is called from `dbPut`, so it is reached by EVERY put
712/// route — CA, `dbPutLink`, internal — and by none of them conditionally. The
713/// port's array records (waveform/aai/aao/subArray) re-derive NORD inside
714/// `put_field("VAL")`; this is the post half, and every `dbPut` body in this
715/// module calls it after the value write, passing the snapshot taken by
716/// [`array_nord_before_put`].
717///
718/// Note this post is NOT the process cycle's monitor post: the compiled softIoc
719/// on a 10-second-SCAN waveform posts `NORD = 3` the instant `caput -a WP 3
720/// 1 2 3` lands, and posts no VAL at all (waveform VAL is `pp(TRUE)`, so C
721/// suppresses the value-field post in `dbPut` and the scan is 10 seconds away).
722fn post_array_info(
723 instance: &mut crate::server::record::RecordInstance,
724 old_nord: &Option<EpicsValue>,
725 origin: u64,
726) {
727 let Some(old) = old_nord else { return };
728 let moved = instance
729 .record
730 .get_field("NORD")
731 .is_some_and(|new| new != *old);
732 if moved {
733 instance.notify_field_with_origin(
734 "NORD",
735 crate::server::recgbl::EventMask::VALUE | crate::server::recgbl::EventMask::LOG,
736 origin,
737 );
738 }
739}
740
741/// C `dbPut`'s field-monitor tail (`dbAccess.c:1408-1418`) — **the one post
742/// rule every `dbPut` body shares**, reached by every put route (CA,
743/// `dbPutLink`, internal `put_pv`):
744///
745/// ```c
746/// if (precord->mlis.count &&
747/// !(isValueField && pfldDes->process_passive))
748/// db_post_events(precord, pfieldsave, DBE_VALUE | DBE_LOG);
749/// ```
750///
751/// The immediate post is suppressed for the value field ONLY when that field
752/// is `pp(TRUE)`: for the routes that then process (`dbPutField`'s pp gate,
753/// a ` PP` OUT link's `processTarget`), the cycle re-posts it via the
754/// deadband snapshot with a fresh timestamp; for the routes that do not (an
755/// NPP OUT link, a bare `dbPut` from driver code), C is silent until the
756/// next scan — measured against softIoc in `array_put_posts_nord.rs`'s
757/// header. For a value field that is NOT `pp` (calc/calcout/aSub VAL), this
758/// post is the only one there is.
759///
760/// The suppression is a static property of the field, never of the caller's
761/// intent — C's `pfldDes->process_passive` is DBD data — which is what makes
762/// it shareable: `put_pv` (which never processes) and the CA route (which
763/// may) apply the identical rule, exactly as C's one `dbPut` serves both
764/// `dbPutLink` and `dbPutField`. `process_passive_fields()` is total and
765/// fail-safe: any field of an unmodeled type (`&[]`) posts.
766fn dbput_post_put_field(instance: &mut crate::server::record::RecordInstance, field: &str) {
767 let suppress = field == instance.record.primary_field()
768 && instance
769 .record
770 .process_passive_fields()
771 .iter()
772 .any(|f| f.eq_ignore_ascii_case(field));
773 if !suppress {
774 instance.cleanup_subscribers();
775 instance.notify_field(
776 field,
777 crate::server::recgbl::EventMask::VALUE | crate::server::recgbl::EventMask::LOG,
778 );
779 }
780}
781
782impl PvDatabase {
783 /// Get a PV value synchronously, from a thread that cannot `await`.
784 ///
785 /// Works from a plain thread with no runtime entered (an iocsh thread, a
786 /// driver's own thread) and from a multi-threaded runtime worker; see
787 /// [`crate::runtime::task::block_on_sync`] for which mechanism is used
788 /// where.
789 ///
790 /// Kept as a distinct entry point for source compatibility with the
791 /// callers that predate [`Self::get_pv`] becoming a `fn`; there is no
792 /// blocking left to do, so there is no current-thread-runtime failure mode
793 /// either. C `dbGetField` is likewise a plain call from any thread.
794 pub fn get_pv_blocking(&self, name: &str) -> CaResult<EpicsValue> {
795 self.get_pv(name)
796 }
797
798 /// Get the current value of a PV or record field.
799 /// Uses resolve_field for records (3-level priority).
800 pub fn get_pv(&self, name: &str) -> CaResult<EpicsValue> {
801 let (base, field) = super::parse_pv_name(name);
802 let field = field.to_ascii_uppercase();
803
804 // Check simple PVs first (exact match)
805 let simple = self.inner.simple_pvs.lock().get(name).cloned();
806 if let Some(pv) = simple {
807 return Ok(pv.get());
808 }
809
810 // Records — alias-aware via `get_record` (epics-base PR #336).
811 if let Some(rec) = self.get_record(base) {
812 let instance = rec.read();
813 return instance
814 .resolve_field(&field)
815 .ok_or_else(|| CaError::ChannelNotFound(name.to_string()));
816 }
817
818 Err(CaError::ChannelNotFound(name.to_string()))
819 }
820
821 /// Set a PV value or record field — the C `dbPut` analogue
822 /// (`dbAccess.c:1316-1419`), whole: value write + `special`/`on_put`, the
823 /// value-field UDF clear, and the field's `DBE_VALUE|DBE_LOG` monitor
824 /// post (`dbput_post_put_field`, suppressed only for a `pp(TRUE)` value
825 /// field exactly as C's tail suppresses it). Tries record `put_field`
826 /// first, then `put_common_field` as fallback.
827 ///
828 /// Does NOT process the record — `dbPutField`'s pp gate is
829 /// [`Self::put_record_field_from_ca`] — so, as with a bare C `dbPut`, a
830 /// `pp(TRUE)` value field (ai/ao/waveform VAL …) posts nothing here and a
831 /// caller that needs a monitor on such a field must either drive a
832 /// process or use [`Self::put_pv_and_post`].
833 ///
834 /// Acquires the record's advisory write gate.
835 pub async fn put_pv(&self, name: &str, value: EpicsValue) -> CaResult<()> {
836 let _record_gate = self.acquire_put_gate(name);
837 self.put_pv_already_locked(name, value)
838 }
839
840 /// `put_pv` variant for a caller already holding the
841 /// record's advisory write gate (QSRV atomic group PUT). See
842 /// [`Self::put_record_field_from_ca_already_locked`].
843 ///
844 /// This is the whole `dbPut` body — the gate-held region, and it is a
845 /// `fn`. See `acquire_put_gate`.
846 pub fn put_pv_already_locked(&self, name: &str, value: EpicsValue) -> CaResult<()> {
847 self.put_pv_body(name, value, RestartOwner::ThisPut)
848 }
849
850 /// [`Self::put_pv_already_locked`] for a caller that drives a process
851 /// cycle on the SAME record the moment this returns — an OUT link's
852 /// `processTarget`, QSRV's `Force` mode.
853 ///
854 /// The only difference is who arms a queued put-notify restart when this
855 /// put releases a PACT park: that cycle's tail, as in C, rather than this
856 /// put body. See `RestartOwner`.
857 pub fn put_pv_already_locked_before_process(
858 &self,
859 name: &str,
860 value: EpicsValue,
861 ) -> CaResult<()> {
862 self.put_pv_body(name, value, RestartOwner::TheCycleThisPutOwes)
863 }
864
865 /// Take the L1 advisory write gate a put to `name` needs, if any.
866 ///
867 /// The gate boundary of the whole put family: EVERY `_already_locked`
868 /// entry is the body below this line and contains no `.await`, so an
869 /// caller that already owns the gate calls the body directly and a caller
870 /// that does not calls this first. C's shape exactly — `dbPutField` does
871 /// `dbScanLock(precord)` … `dbScanUnlock(precord)` around a `dbPut` that
872 /// itself never blocks (`dbAccess.c:1246-1300`).
873 ///
874 /// `None` when `name` names no record: a simple PV has no `dbCommon` and
875 /// therefore no `dbScanLock` in C either. The record lookup is repeated by
876 /// the body — a map read, and records are never removed once loaded.
877 fn acquire_put_gate(&self, name: &str) -> Option<super::record_lock::RecordWriteGuard> {
878 let (base, _) = super::parse_pv_name(name);
879 self.get_record(base)?;
880 let canonical: String = self.resolve_alias(base).unwrap_or_else(|| base.to_string());
881 Some(self.lock_record(&canonical))
882 }
883
884 /// C `IOCSource::doPreProcessing` gate (pvxs `iocsource.cpp:363-375`).
885 ///
886 /// Reject an *external* put (a PVA/CA client put routed through QSRV)
887 /// that C refuses before any write: a put to a `DISP=1` record's
888 /// non-DISP field (`S_db_putDisabled`) or to a read-only / `SPC_NOMOD`
889 /// field (`S_db_noMod`). No value is written — this is a precondition
890 /// check only. It mirrors the two gates inside
891 /// [`Self::put_record_field_from_ca`] (the Passive route) so the QSRV
892 /// `Force`/`Inhibit` routes — which go through [`Self::put_pv`] — enforce
893 /// the same preconditions. `put_pv` itself is the internal `dbPut`
894 /// analogue and deliberately does not gate DISP (internal
895 /// link/processing puts must bypass it), so the gate lives at the
896 /// external put boundary, exactly as C places `doPreProcessing` in the
897 /// source layer rather than in `dbPut`.
898 pub async fn check_external_put_preconditions(
899 &self,
900 record_name: &str,
901 field: &str,
902 ) -> CaResult<()> {
903 let field_upper = field.to_ascii_uppercase();
904 // A missing record is not a DISP/read-only precondition violation:
905 // stay silent and let the downstream put report the not-found (for
906 // QSRV, inside its own `asTrapWrite` bracket). C's `doPreProcessing`
907 // only runs against an established channel — the record is
908 // guaranteed present there — and a `BridgeChannel` likewise always
909 // binds a real record in production.
910 let Some(rec) = self.get_record(record_name) else {
911 return Ok(());
912 };
913 let instance = rec.read();
914 // Read-only / SPC_NOMOD field, through the one gate owner
915 // ([`check_no_mod`]). C tests SPC_ATTRIBUTE *before* `disp`
916 // (iocsource.cpp:365-369), so a read-only field on a DISP=1 record
917 // reports S_db_noMod, not S_db_putDisabled; the two errors carry
918 // different wire text.
919 check_no_mod(&instance, &field_upper)?;
920 // DISP=1 blocks a put to any field except DISP itself — the shared
921 // gate owner, identical to the one the CA route crosses.
922 check_put_disabled(&instance, &field_upper)?;
923 Ok(())
924 }
925
926 /// pvxs `IOCSource::doPostProcessing`'s record-side terms
927 /// (`iocsource.cpp:397-403`): does a put to `record_name.field` drive a
928 /// processing cycle on its own?
929 ///
930 /// The QSRV group PUT asks this for a member whose write bypassed
931 /// [`Self::put_record_field_from_ca`] (a `+type:"proc"` trigger, or a
932 /// member that is `changing` but has no writable leaf), so that route
933 /// applies the SAME gate as a plain field put instead of processing
934 /// unconditionally. `false` for an unknown record — there is nothing to
935 /// process. Force (`record._options.process=true`) is the caller's term
936 /// and is not asked about here; see `put_drives_processing_of`.
937 pub fn put_drives_processing(&self, record_name: &str, field: &str) -> bool {
938 let field_upper = field.to_ascii_uppercase();
939 let Some(rec) = self.get_record(record_name) else {
940 return false;
941 };
942 let instance = rec.read();
943 put_drives_processing_of(&instance, &field_upper)
944 }
945
946 /// Is `record_name.field` a DBF link field (INLINK/OUTLINK/FWDLINK)?
947 ///
948 /// The one owner of the classification lookup
949 /// ([`crate::types::dbf_link_class`] keyed by the record's type). C
950 /// callers split on it before choosing a put entry: `dbPutField` sends
951 /// link fields to `dbPutFieldLink` (`dbAccess.c:1261`), `dbProcessNotify`
952 /// short-circuits them past the notify machinery (`dbNotify.c:337-353`),
953 /// and pvxs QSRV picks `dbChannelPutField` over `dbChannelPut` for them
954 /// (`iocsource.cpp:451-458`). Port callers with a dbPutField-shaped
955 /// entry make the same split against the `dbPut`-analogue bodies' refusal
956 /// (`check_not_link_field`). `false` for an unknown record or a non-link
957 /// field.
958 pub fn is_dbf_link_field(&self, record_name: &str, field: &str) -> bool {
959 let field_upper = field.to_ascii_uppercase();
960 let Some(rec) = self.get_record(record_name) else {
961 return false;
962 };
963 let guard = rec.read();
964 crate::types::dbf_link_class(guard.record.record_type(), &field_upper).is_some()
965 }
966
967 fn put_pv_body(&self, name: &str, value: EpicsValue, owner: RestartOwner) -> CaResult<()> {
968 let (base, field) = super::parse_pv_name(name);
969 let field = field.to_ascii_uppercase();
970
971 // Check simple PVs first. The lookup is its own statement so the
972 // `!Send` directory guard is down before `pv.set(…).await` — see the
973 // `simple_pvs` field doc in `database/mod.rs`.
974 let simple = self.inner.simple_pvs.lock().get(name).cloned();
975 if let Some(pv) = simple {
976 pv.set(value);
977 return Ok(());
978 }
979
980 // Records — alias-aware (epics-base PR #336).
981 if let Some(rec) = self.get_record(base) {
982 // `base` may be an alias; resolve to the canonical record
983 // name so scan-index updates target the right entry.
984 let canonical_base: String =
985 self.resolve_alias(base).unwrap_or_else(|| base.to_string());
986 // The caller holds the advisory write gate (`dbScanLock`
987 // analogue) for `canonical_base` — see `acquire_put_gate`.
988 // It is held to the `return Ok(())` below, and that is the point:
989 // C's `dbScanLock` covers `dbPut` *including*
990 // `dbPutSpecial(paddr, 1)` and the scan-list move, so the tails
991 // below are inside the exclusion window in C and must be inside it
992 // here. Shrinking the window to end at the value write would
993 // re-open exactly the interleaving `lock_records` exists to close
994 // (`record_lock.rs`, "Rust port"). See
995 // `doc/rtems-priority-locks-design.md` §2, "the semantic question".
996 // Scoped guard: everything the put commits happens under this
997 // write guard, which ends (releasing the `!Send` parking_lot
998 // guard) before the tails below, which re-enter the database.
999 // Yields the owned outputs those tails consume. Note this is the
1000 // record's DATA lock coming down, not the advisory gate above.
1001 use crate::server::record::CommonFieldPutResult;
1002 let mut pact_exit = PactExitGuard::new(self, base, &rec, owner);
1003 let (common_result, special_actions) = {
1004 let mut instance = rec.write();
1005
1006 // C `dbPut` refuses an SPC_NOMOD / SPC_ATTRIBUTE field before it
1007 // converts anything (`dbAccess.c:1330-1332`). `put_pv` IS the
1008 // `dbPut` analogue — it sits below `dbPutLink`, so this is what
1009 // stops a record's OUT link from truncating a waveform's NELM.
1010 // The refusal is returned to the caller; `write_out_link_value`
1011 // (C `dbPutLink`) turns it into the writer's LINK/INVALID alarm.
1012 check_no_mod(&instance, &field)?;
1013
1014 // C `dbPut` refuses a link-field target the same way, before
1015 // conversion (`dbAccess.c:1340`) — see `check_not_link_field`.
1016 check_not_link_field(&instance, &field)?;
1017
1018 let request = dbput_request(&*instance.record, &field, value)?;
1019
1020 // Pre-write special hook (C EPICS dbPutSpecial pass=0).
1021 // C `dbPut` runs it on EVERY entry path — dbPutField and
1022 // dbPutLink alike (dbAccess.c) — so the OUT-link route
1023 // through this body must call it too (motor's drive-field
1024 // DMOV blink, motorRecord.cc:2582-2608, fires on put-links
1025 // in C). A non-zero status aborts the put like C.
1026 instance.record.special(&field, false)?;
1027
1028 // Capture the pre-put value so the metadata-cache
1029 // invalidation (and the downstream `DBE_PROPERTY`
1030 // emission) can be skipped when the put is a no-op —
1031 // epics-base faac1df1.
1032 let prev_value = instance.record.get_field(&field);
1033 let old_nord = array_nord_before_put(&instance, &field);
1034
1035 // Link writes the record's `special()` makes itself (C runs them
1036 // inside `dbPut`); executed below, once the record lock is released.
1037 let mut special_actions = Vec::new();
1038
1039 // put_pv is C EPICS dbPut: write value + special/on_put, clear
1040 // UDF on a value-field put, and post the field's DBE_VALUE|
1041 // DBE_LOG monitor per `dbPut`'s tail (dbAccess.c:1408-1418).
1042 // Does NOT trigger processing (that is `dbPutField`'s pp gate,
1043 // this port's `put_record_field_from_ca`), so — exactly like a
1044 // bare C `dbPut` — a pp(TRUE) value field's post stays
1045 // suppressed and its stale UDF *alarm* stands until a process
1046 // cycle recomputes stat/sevr.
1047 let common_result = match request {
1048 // C `dbAccess.c:1370-1372` — accept, write nothing, alarm.
1049 PutRequest::EmptyIntoScalar => {
1050 set_empty_request_alarm(&mut instance);
1051 CommonFieldPutResult::NoChange
1052 }
1053 PutRequest::Write(value) => {
1054 pact_exit.arm(special_before_put(&mut instance, &field));
1055 match instance.record.put_field(&field, value.clone()) {
1056 Ok(()) => {
1057 instance.record.on_put(&field);
1058 // C `dbPut` (dbAccess.c:1399-1405) keeps the value
1059 // it already stored but RETURNS the after-put
1060 // `dbPutSpecial(paddr, 1)` status, skipping the
1061 // field's monitor post and (in `dbPutField`) the
1062 // `pp(TRUE)` process. `calcRecord::special` uses
1063 // that to refuse an uncompilable CALC with
1064 // S_db_badField, so the status must not be dropped.
1065 let result = special_after_put(
1066 self,
1067 &mut instance,
1068 &field,
1069 &mut special_actions,
1070 )?;
1071 // C `dbAccess.c::dbPut:1410-1411` clears ONLY
1072 // `precord->udf = FALSE` on a value-field put —
1073 // the same clear (and the same
1074 // `is_udf_defining_put` predicate) as the CA
1075 // route and `put_pv_and_post`. stat/sevr are NOT
1076 // touched: see the comment block above.
1077 if instance.record.is_udf_defining_put(&field) {
1078 instance.common.udf = 0;
1079 }
1080 result
1081 }
1082 Err(CaError::FieldNotFound(_)) => {
1083 instance.put_common_field(&field, value)?
1084 }
1085 Err(e) => return Err(e),
1086 }
1087 }
1088 };
1089
1090 // C `dbPut` runs `dbPutSpecial(paddr, 1)` regardless of caller entry
1091 // path, so a put via this internal route commits/writes the same
1092 // `special()`-driven alarm the CA route does. State only — unlike
1093 // the CA path these commit the alarm without the STAT/SEVR posts
1094 // (C's bare `dbPut` runs no `monitor()`, so it posts no alarm
1095 // transition either).
1096 //
1097 // compress SPC_RESET: `monitor()`'s `recGblResetAlarms` commits the
1098 // born-UDF alarm (compressRecord.c:103).
1099 if instance.record.special_commits_alarms(&field) {
1100 let _ = crate::server::recgbl::rec_gbl_reset_alarms(&mut instance.common);
1101 }
1102 // histogram SGNL SPC_MOD: `add_count` raises the inverted-limits
1103 // alarm (histogramRecord.c:329-334). The port routes it through
1104 // `nsta`/`nsev` (CBUG-F12 refused), so this monitor-less special
1105 // path must commit it — check then reset — for the SOFT/INVALID to
1106 // be observable, matching the process path.
1107 if instance.record.special_checks_alarms(&field) {
1108 let inst = &mut *instance;
1109 inst.record.check_alarms(&mut inst.common);
1110 let _ = crate::server::recgbl::rec_gbl_reset_alarms(&mut inst.common);
1111 }
1112
1113 // Invalidate metadata cache only if the metadata-class
1114 // field's value actually changed (faac1df1).
1115 instance.notify_field_written_if_changed(&field, prev_value.as_ref());
1116
1117 // C `dbPut:1408-1414`'s field-monitor post, through the one owner
1118 // (`dbput_post_put_field`, shared with the CA route). `put_pv`
1119 // is the `dbPutLink` route's `dbPut` and the internal driver-put
1120 // entry, and C posts DBE_VALUE|DBE_LOG from *every* `dbPut` —
1121 // an NPP OUT link writing a calc's A, an autosave restore, a
1122 // status pusher, all post immediately. Pre-fix this body posted
1123 // nothing at all, so camonitor on anything written via `put_pv`
1124 // went silent forever (doc/calink-rtems-design.md §11.7 item 2).
1125 dbput_post_put_field(&mut instance, &field);
1126
1127 // C's `put_array_info` is likewise reached from every `dbPut` —
1128 // an OUT link that shortens a waveform posts NORD in C even when
1129 // the link is NPP and the target never processes, and the
1130 // value-field post above is suppressed for a waveform (VAL is
1131 // `pp(TRUE)`); NORD has no such second path.
1132 post_array_info(&mut instance, &old_nord, 0);
1133
1134 (common_result, special_actions)
1135 };
1136 // Lock down: arm any put-notify the SNAM park release freed.
1137 drop(pact_exit);
1138 // The record DATA lock is now down (scope ended above) before the
1139 // scan-index update and the `special()` link writes below, which
1140 // re-enter the database (they can process their target). The
1141 // advisory gate is still held — see its comment above.
1142
1143 // Update scan index if SCAN or PHAS changed. Synchronous as of
1144 // step 4, so this half of the tail adds no suspension point to the
1145 // gate-held window; C reaches `scanDelete`/`scanAdd` from inside
1146 // `dbScanLock` the same way.
1147 match common_result {
1148 CommonFieldPutResult::ScanChanged {
1149 old_scan,
1150 new_scan,
1151 phas,
1152 } => {
1153 self.update_scan_index(&canonical_base, old_scan, new_scan, phas, phas);
1154 }
1155 CommonFieldPutResult::PhasChanged {
1156 scan: s,
1157 old_phas,
1158 new_phas,
1159 } => {
1160 self.update_scan_index(&canonical_base, s, s, old_phas, new_phas);
1161 }
1162 CommonFieldPutResult::NoChange => {}
1163 }
1164
1165 // C `dbPut` runs `dbPutSpecial(paddr, 1)` to completion — the
1166 // `dbPutLink` calls a `special()` makes included — before it returns
1167 // to `dbPutField`. This is the last statement of the `dbPut`
1168 // analogue, so it is that point.
1169 self.run_special_actions(&canonical_base, &rec, special_actions);
1170
1171 // mirror the CA-write path's ASG-field notifier so
1172 // restore scripts / autosave / admin tools that go via
1173 // `put_pv` (not `put_record_field_from_ca`) also trigger
1174 // per-client `reeval_access_rights`. C `dbAccess.c::
1175 // dbPutSpecial` invokes the SPC_AS callback from dbPut
1176 // regardless of caller entry path.
1177 if field == "ASG" {
1178 crate::server::access_security::notify_asg_field_changed();
1179 }
1180
1181 return Ok(());
1182 }
1183
1184 Err(CaError::ChannelNotFound(name.to_string()))
1185 }
1186
1187 /// Write a value and post monitor events if changed.
1188 /// Equivalent to C EPICS `dbPut` + `db_post_events(DBE_VALUE|DBE_LOG)`.
1189 ///
1190 /// Use for readback/status mirror PVs that are written by sequencer-style
1191 /// code and need to be visible to CA monitors without triggering record
1192 /// processing. Clears UDF/UDF_ALARM on primary field write.
1193 ///
1194 /// `origin`: writer ID for self-write filtering. Subscribers with the
1195 /// same `ignore_origin` will skip this event. Pass 0 to disable.
1196 pub async fn put_pv_and_post(&self, name: &str, value: EpicsValue) -> CaResult<()> {
1197 self.put_pv_and_post_with_origin(name, value, 0).await
1198 }
1199
1200 /// Push a monitor event holding the simple PV's *current* value
1201 /// but with explicit alarm severity/status. Used by the gateway
1202 /// to surface upstream-disconnect to downstream monitor
1203 /// subscribers without dropping the shadow PV (which would force
1204 /// downstream clients into ECA_DISCONN reconnect storms on every
1205 /// transient hiccup). Returns `ChannelNotFound` for record-backed
1206 /// PVs — those carry their own `common.sevr/stat` in record
1207 /// processing.
1208 pub fn post_alarm(&self, name: &str, severity: u16, status: u16) -> CaResult<()> {
1209 let simple = self.inner.simple_pvs.lock().get(name).cloned();
1210 if let Some(pv) = simple {
1211 pv.post_alarm(severity, status);
1212 return Ok(());
1213 }
1214 Err(crate::error::CaError::ChannelNotFound(name.to_string()))
1215 }
1216
1217 /// Propagate a full upstream snapshot (value + alarm status/severity +
1218 /// IOC timestamp) to a simple shadow PV and fan out to downstream
1219 /// monitor subscribers. Used by the CA gateway forwarding task to avoid
1220 /// discarding the upstream alarm and timestamp decoded from the incoming
1221 /// `DBR_TIME_*` frame. Returns `ChannelNotFound` for record-backed PVs
1222 /// (those carry their own alarm engine and are not shadow PVs).
1223 pub async fn put_pv_and_post_snapshot(&self, name: &str, snapshot: Snapshot) -> CaResult<()> {
1224 let simple = self.inner.simple_pvs.lock().get(name).cloned();
1225 if let Some(pv) = simple {
1226 pv.set_snapshot(snapshot);
1227 return Ok(());
1228 }
1229 Err(CaError::ChannelNotFound(name.to_string()))
1230 }
1231
1232 /// Install upstream `DBR_CTRL_*` metadata (display / control limits,
1233 /// enum labels) on a shadow simple PV WITHOUT posting an event.
1234 ///
1235 /// The CA gateway calls this once on upstream connect, after its initial
1236 /// `DBR_CTRL_*` get, so a later downstream `DBR_CTRL_*` / `DBR_GR_*` read
1237 /// returns the real limits instead of zeroed ones. No `DBE_PROPERTY`
1238 /// monitor event fires — nothing has *changed* yet, this only seeds the
1239 /// attribute cache. Mirrors C `gatePvData::getCB` → `runDataCB` →
1240 /// `vc->setPvData(dd)` (`gatePv.cc:1693-1695`), which seeds the property
1241 /// cache from the initial control get in both cache modes before any
1242 /// monitor is enabled.
1243 ///
1244 /// Returns `ChannelNotFound` for record-backed PVs — those own their own
1245 /// metadata via record processing and are not gateway shadow PVs.
1246 pub async fn set_pv_metadata(&self, name: &str, snapshot: &Snapshot) -> CaResult<()> {
1247 let simple = self.inner.simple_pvs.lock().get(name).cloned();
1248 if let Some(pv) = simple {
1249 pv.set_metadata(metadata_from_snapshot(snapshot));
1250 return Ok(());
1251 }
1252 Err(CaError::ChannelNotFound(name.to_string()))
1253 }
1254
1255 /// Refresh a shadow simple PV's upstream metadata AND post a
1256 /// `DBE_PROPERTY` monitor event carrying `snapshot` to downstream
1257 /// property subscribers.
1258 ///
1259 /// `snapshot` is the decoded upstream `DBR_CTRL_*` property event: it
1260 /// carries the control value and the upstream `status` / `severity`,
1261 /// and (because control DBR structs carry no timestamp) an undefined
1262 /// timestamp the caller must NOT replace with a fresh wall-clock. The
1263 /// gateway's property monitor calls this on every upstream
1264 /// `DBE_PROPERTY` event, mirroring C `gatePvData::propEventCB` →
1265 /// `runDataCB` + `setPvData` + `runValueDataCB` +
1266 /// `vcPostEvent(propertyEventMask())` (`gatePv.cc:1571-1607`): the
1267 /// attribute cache is refreshed and a property event is posted with the
1268 /// upstream alarm state preserved (`setStatSevr`) and the undefined
1269 /// control-DBR timestamp left as-is (`gatePv.cc:1594-1595`).
1270 ///
1271 /// Returns `ChannelNotFound` for record-backed PVs.
1272 pub async fn post_pv_property(&self, name: &str, snapshot: Snapshot) -> CaResult<()> {
1273 let simple = self.inner.simple_pvs.lock().get(name).cloned();
1274 if let Some(pv) = simple {
1275 pv.set_metadata(metadata_from_snapshot(&snapshot));
1276 pv.post_property(snapshot).await;
1277 return Ok(());
1278 }
1279 Err(CaError::ChannelNotFound(name.to_string()))
1280 }
1281
1282 /// Like `put_pv_and_post` but with explicit origin tag.
1283 pub async fn put_pv_and_post_with_origin(
1284 &self,
1285 name: &str,
1286 value: EpicsValue,
1287 origin: u64,
1288 ) -> CaResult<()> {
1289 let (base, field) = super::parse_pv_name(name);
1290 let field = field.to_ascii_uppercase();
1291
1292 // Simple-PV path: PVs registered via `add_pv` (e.g. CA gateway
1293 // shadow PVs, IOCsh stats PVs) are stored in `simple_pvs`,
1294 // not `records`. Without this branch the function would
1295 // silently return `ChannelNotFound` for every gateway-mirrored
1296 // PV — `ProcessVariable::set_with_origin` already does the
1297 // notify-subscribers fan-out internally (tagging the event with
1298 // `origin`, same self-write contract as the record branch) so
1299 // all we need here is to delegate.
1300 let simple = self.inner.simple_pvs.lock().get(name).cloned();
1301 if let Some(pv) = simple {
1302 pv.set_with_origin(value, origin);
1303 return Ok(());
1304 }
1305
1306 if let Some(rec) = self.get_record(base) {
1307 // `put_pv_and_post` is a public record-write API —
1308 // it must take the same advisory write gate
1309 // (`dbScanLock` analogue) as `put_pv` /
1310 // `put_record_field_from_ca`, or a gateway/sequencer
1311 // write through this helper can still land between the
1312 // member writes of a QSRV atomic group or a pvalink
1313 // atomic scan epoch holding `lock_records`. `base` is
1314 // alias-resolved to the canonical record name so an alias
1315 // and its target share one gate. Held until return — same
1316 // reasoning as `put_pv_inner`'s gate: C's `dbScanLock` covers
1317 // `dbPut` including `dbPutSpecial(paddr, 1)` and the scan-list
1318 // move, so both tails below stay inside the window
1319 // (`doc/rtems-priority-locks-design.md` §2).
1320 let canonical_base: String =
1321 self.resolve_alias(base).unwrap_or_else(|| base.to_string());
1322 let _record_gate = self.lock_record(&canonical_base);
1323
1324 // Guarded: the value write + monitor post. The record's DATA guard
1325 // is released at the block close before the tails below, which
1326 // re-enter the database (`parking_lot` guards are `!Send`); the
1327 // advisory `_record_gate` still holds the processing-exclusion
1328 // window across the whole helper.
1329 use crate::server::record::CommonFieldPutResult;
1330 let mut pact_exit = PactExitGuard::new(self, base, &rec, RestartOwner::ThisPut);
1331 let (common_result, special_actions) = {
1332 let mut instance = rec.write();
1333
1334 // Same `dbPut` gate as `put_pv` — this is the third `dbPut` body
1335 // (value + monitor post), and C has ONE.
1336 check_no_mod(&instance, &field)?;
1337 check_not_link_field(&instance, &field)?;
1338
1339 let request = dbput_request(&*instance.record, &field, value)?;
1340
1341 // Pre-write special hook (C EPICS dbPutSpecial pass=0) —
1342 // C `dbPut` runs it on every entry path; this is the third
1343 // `dbPut` body and must match the other two.
1344 instance.record.special(&field, false)?;
1345
1346 let old_value = instance.record.get_field(&field);
1347 let old_stat = instance.common.stat;
1348 let old_sevr = instance.common.sevr;
1349 let old_nord = array_nord_before_put(&instance, &field);
1350
1351 // Link writes the record's `special()` makes itself (C runs them
1352 // inside `dbPut`); executed below, once the record lock is released.
1353 let mut special_actions = Vec::new();
1354
1355 // Write value + special/on_put
1356 let common_result = match request {
1357 // C `dbAccess.c:1370-1372` — accept, write nothing, alarm. UDF
1358 // is NOT cleared: C clears it at `:1409` only when the value
1359 // field was actually written, and this branch wrote nothing.
1360 PutRequest::EmptyIntoScalar => {
1361 set_empty_request_alarm(&mut instance);
1362 CommonFieldPutResult::NoChange
1363 }
1364 PutRequest::Write(value) => {
1365 pact_exit.arm(special_before_put(&mut instance, &field));
1366 match instance.record.put_field(&field, value.clone()) {
1367 Ok(()) => {
1368 instance.record.on_put(&field);
1369 // C returns the after-put special() status from
1370 // `dbPut` (dbAccess.c:1399-1405) — before the UDF
1371 // clear and the monitor post below, both of which
1372 // `goto done` skips on a non-zero status.
1373 let result = special_after_put(
1374 self,
1375 &mut instance,
1376 &field,
1377 &mut special_actions,
1378 )?;
1379 // C `dbAccess.c::dbPut:1411` clears ONLY `precord->udf
1380 // = FALSE` on a value-field put, and nothing else. It
1381 // does NOT touch stat/sevr: the UDF_ALARM stays until
1382 // the record's own process cycle recomputes it
1383 // (`rec_gbl_check_udf` no longer raises it now udf is
1384 // clear, `rec_gbl_reset_alarms` commits the new state).
1385 // A value put that does not drive a process therefore
1386 // leaves the stale UDF alarm exactly as C does — the
1387 // earlier synchronous stat/sevr clear here diverged
1388 // from C and reported NO_ALARM where C keeps UDF/INVALID.
1389 if instance.record.is_udf_defining_put(&field) {
1390 instance.common.udf = 0;
1391 }
1392 result
1393 }
1394 Err(CaError::FieldNotFound(_)) => {
1395 instance.put_common_field(&field, value)?
1396 }
1397 Err(e) => return Err(e),
1398 }
1399 }
1400 };
1401
1402 // Invalidate metadata cache only if a metadata-class
1403 // field actually changed value (faac1df1 — DBE_PROPERTY
1404 // fires on real changes, not no-op writes).
1405 instance.notify_field_written_if_changed(&field, old_value.as_ref());
1406
1407 // Post monitor events if value or alarm changed
1408 let new_value = instance.record.get_field(&field);
1409 let value_changed = old_value != new_value;
1410 let alarm_changed =
1411 old_stat != instance.common.stat || old_sevr != instance.common.sevr;
1412 let nord_changed =
1413 old_nord.is_some() && instance.record.get_field("NORD") != old_nord;
1414 if value_changed || alarm_changed || nord_changed {
1415 // Update timestamp so the snapshot carries current time
1416 instance.common.time = crate::runtime::general_time::get_current();
1417 instance.cleanup_subscribers();
1418 if value_changed || alarm_changed {
1419 instance.notify_field_with_origin(
1420 &field,
1421 crate::server::recgbl::EventMask::VALUE
1422 | crate::server::recgbl::EventMask::LOG
1423 | crate::server::recgbl::EventMask::ALARM,
1424 origin,
1425 );
1426 }
1427 // The NORD post, through the one owner. Without it a CA
1428 // gateway forwarding upstream waveform monitors via
1429 // put_pv_and_post would update VAL on the shadow PV but
1430 // leave downstream NORD subscribers stuck at their last
1431 // seen length — a frozen-element-count bug that surfaces
1432 // in PyDM image views and similar consumers that compute
1433 // height = element_count / width.
1434 post_array_info(&mut instance, &old_nord, origin);
1435 }
1436
1437 // The `special()` link writes re-enter the database, so the record
1438 // lock goes down first (the block close releases it). C makes them
1439 // inside `dbPut`, before it returns to its caller.
1440 (common_result, special_actions)
1441 };
1442 // Lock down: arm any put-notify the SNAM park release freed.
1443 drop(pact_exit);
1444
1445 // Same scan-index owner every other `dbPut` path routes through:
1446 // a SCAN put and the SIMM↔SSCN swap (`recGblCheckSimm`) both move
1447 // the record between scan lists and must reach `update_scan_index`.
1448 match common_result {
1449 CommonFieldPutResult::ScanChanged {
1450 old_scan,
1451 new_scan,
1452 phas,
1453 } => {
1454 self.update_scan_index(&canonical_base, old_scan, new_scan, phas, phas);
1455 }
1456 CommonFieldPutResult::PhasChanged {
1457 scan: s,
1458 old_phas,
1459 new_phas,
1460 } => {
1461 self.update_scan_index(&canonical_base, s, s, old_phas, new_phas);
1462 }
1463 CommonFieldPutResult::NoChange => {}
1464 }
1465
1466 self.run_special_actions(&canonical_base, &rec, special_actions);
1467
1468 // same SPC_AS parity as `put_pv` / `put_pv_no_process`
1469 // / the CA-write path — a gateway mirroring `.ASG` via
1470 // `put_pv_and_post` must still trigger per-client
1471 // re-eval.
1472 if field == "ASG" {
1473 crate::server::access_security::notify_asg_field_changed();
1474 }
1475
1476 return Ok(());
1477 }
1478
1479 Err(CaError::ChannelNotFound(name.to_string()))
1480 }
1481
1482 /// Execute the link writes a record's `special()` queued
1483 /// ([`Record::take_special_actions`](crate::server::record::Record::take_special_actions)).
1484 ///
1485 /// The single consumer: every `dbPut` path in this module calls it once, at
1486 /// the end of the put and before any `pp(TRUE)` process cycle, which is
1487 /// where C runs them (`dbPut` → `dbPutSpecial(paddr, 1)` → `dbPutLink`,
1488 /// with `dbProcess` still ahead in `dbPutField`). The put is the root of the
1489 /// chain these writes start, so they get a fresh visited set, exactly like a
1490 /// client put entering `process_record_with_links`.
1491 ///
1492 /// Must be called with no record lock held: a `WriteDbLink` can process its
1493 /// target, which re-enters the database.
1494 ///
1495 /// # Synchronous, and it has to stay that way
1496 ///
1497 /// This whole tail runs inside the caller's L1 gate window, and L1 is a
1498 /// blocking priority-inheritance mutex whose guard is `!Send`
1499 /// (`server::database::record_lock`). An `.await` anywhere reachable from
1500 /// here is therefore a compile error at the spawn sites, not a review
1501 /// finding — see `doc/rtems-priority-locks-design.md` §5 steps 5–6.
1502 ///
1503 /// The one call that used to make it a genuine suspension is gone:
1504 /// `write_out_link_value` → `write_external_pv` does not call
1505 /// `LinkSet::put_value` from this thread. It stages the write on the
1506 /// database's link-put queue and returns, exactly as C `dbCaPutLink`
1507 /// stages into `pca->pputNative`, calls `addAction` and returns
1508 /// (`dbCa.c:544-631`); the `ca://` / `pva://` round trip runs on the
1509 /// queue's owner task, C's `dbCaTask` (`dbCa.c:1226-1248`). See
1510 /// [`super::link_put_queue`]. What is left inside the window is the
1511 /// re-entrant database work C also does under `dbScanLock`:
1512 /// `execute_process_actions` → `write_out_link_value` →
1513 /// `write_db_link_value` re-entering `put_pv_already_locked` and
1514 /// `process_target`, plus the cached-state `LinkSet::put_admission` probe
1515 /// both production lsets answer from a map lookup and an atomic — C's
1516 /// `if (!pca->isConnected …)` (`dbCa.c:558-561`).
1517 fn run_special_actions(
1518 &self,
1519 record_name: &str,
1520 rec: &std::sync::Arc<parking_lot::RwLock<crate::server::record::RecordInstance>>,
1521 actions: Vec<crate::server::record::ProcessAction>,
1522 ) {
1523 if actions.is_empty() {
1524 return;
1525 }
1526 let mut visited = HashSet::new();
1527 self.execute_process_actions(record_name, rec, actions, &mut visited, 0);
1528 }
1529
1530 /// CA client's unified entry point for record field put.
1531 /// Handles DISP/PROC/PACT/LCNT checks, field put, device write, and Passive process.
1532 ///
1533 /// Acquires the record's advisory write gate
1534 /// (`dbScanLock` analogue) for the duration of the write.
1535 pub async fn put_record_field_from_ca(
1536 &self,
1537 record_name: &str,
1538 field: &str,
1539 value: EpicsValue,
1540 ) -> CaResult<crate::server::record::ProcessCompletion> {
1541 let _record_gate = self.acquire_put_gate(record_name);
1542 self.put_record_field_from_ca_body(record_name, field, value, NotifyRequest::New)
1543 }
1544
1545 /// Variant for a caller that already owns the target
1546 /// record's advisory write gate — the QSRV atomic group PUT,
1547 /// which acquired every member-record gate up-front via
1548 /// [`Self::lock_records`]. The per-record gate is NOT reentrant, so the
1549 /// atomic group path MUST use this `_already_locked` entry to avoid
1550 /// dead-locking on its own `ManyRecordWriteGuard`.
1551 pub fn put_record_field_from_ca_already_locked(
1552 &self,
1553 record_name: &str,
1554 field: &str,
1555 value: EpicsValue,
1556 ) -> CaResult<crate::server::record::ProcessCompletion> {
1557 self.put_record_field_from_ca_body(record_name, field, value, NotifyRequest::New)
1558 }
1559
1560 /// Fire-and-forget variant — C `dbPutField` semantics: the put
1561 /// processes the record but creates NO put-notify wait-set (C
1562 /// builds a `putNotify` only in `dbPutNotify`, i.e. for
1563 /// WRITE_NOTIFY). A caller that does not await the returned
1564 /// receiver MUST use this entry: parking a wait-set whose receiver
1565 /// is dropped occupies `RecordInstance::notify` until the record's
1566 /// async work ends (a motor's whole motion), failing every
1567 /// legitimate WRITE_NOTIFY on the record with ECA_PUTCBINPROG in
1568 /// the meantime.
1569 pub async fn put_record_field_from_ca_no_notify(
1570 &self,
1571 record_name: &str,
1572 field: &str,
1573 value: EpicsValue,
1574 ) -> CaResult<()> {
1575 self.put_record_field_from_ca_no_notify_with_origin(record_name, field, value, 0)
1576 .await
1577 }
1578
1579 /// [`Self::put_record_field_from_ca_no_notify`] for an in-process writer
1580 /// with a self-write-filtering origin (a ported SNL state machine's
1581 /// `DbChannel`): every event the put's synchronous process cascade posts
1582 /// is tagged with `origin`, so the writer's own filtered subscriptions
1583 /// skip them. The ambient scope is sound here because the body below is
1584 /// fully synchronous — there is no await between entering the scope and
1585 /// the cascade's last post.
1586 pub async fn put_record_field_from_ca_no_notify_with_origin(
1587 &self,
1588 record_name: &str,
1589 field: &str,
1590 value: EpicsValue,
1591 origin: u64,
1592 ) -> CaResult<()> {
1593 let _record_gate = self.acquire_put_gate(record_name);
1594 let _origin_scope = crate::server::record::ambient_write_origin_scope(origin);
1595 self.put_record_field_from_ca_body(record_name, field, value, NotifyRequest::None)
1596 .map(|_| ())
1597 }
1598
1599 /// The one router for an EXTERNAL client PUT that carries pvxs's
1600 /// `record._options.process` / `.block` terms — QSRV's whole
1601 /// `onPut` decision tree (`ioc/singlesource.cpp:346-384`,
1602 /// `ioc/iocsource.cpp:397-419`) in one place, so the QSRV bridge
1603 /// channel and the native PVA source cannot disagree about what
1604 /// `process=false` or `block=true` means.
1605 ///
1606 /// A DBF link field ignores the requested mode: pvxs sends it down
1607 /// `dbPutField` whatever the client asked (`iocsource.cpp:451-458`,
1608 /// `dbNotify.c:337-353`), and the `dbPut`-analogue bodies refuse link
1609 /// fields outright, so the Passive route is the only one that can
1610 /// carry it.
1611 ///
1612 /// `doPreProcessing`'s two gates (`SPC_ATTRIBUTE` → `S_db_noMod`,
1613 /// `DISP` → `S_db_putDisabled`) run here for every mode. The Passive
1614 /// route re-checks them inside `put_record_field_from_ca`, but the
1615 /// Force / Inhibit routes go through `put_pv` — the internal `dbPut`
1616 /// analogue, which by design does not gate `DISP` — so the gate has
1617 /// to be at this boundary for the invariant to hold by construction.
1618 /// A caller that needs the rejection to precede its own ACF check
1619 /// (pvxs runs `doPreProcessing` before `doFieldPreProcessing`) still
1620 /// calls [`Self::check_external_put_preconditions`] itself; the check
1621 /// is idempotent.
1622 pub async fn put_field_from_client(
1623 &self,
1624 record_name: &str,
1625 field: &str,
1626 value: EpicsValue,
1627 process: ProcessMode,
1628 block: bool,
1629 ) -> CaResult<()> {
1630 let process = if self.is_dbf_link_field(record_name, field) {
1631 ProcessMode::Passive
1632 } else {
1633 process
1634 };
1635 self.check_external_put_preconditions(record_name, field)
1636 .await?;
1637 match process {
1638 ProcessMode::Inhibit => self.put_pv(&format!("{record_name}.{field}"), value).await,
1639 ProcessMode::Passive => {
1640 if block {
1641 let completion = self
1642 .put_record_field_from_ca(record_name, field, value)
1643 .await?;
1644 Self::await_completion(completion).await;
1645 Ok(())
1646 } else {
1647 self.put_record_field_from_ca_no_notify(record_name, field, value)
1648 .await
1649 }
1650 }
1651 ProcessMode::Force => {
1652 self.put_pv(&format!("{record_name}.{field}"), value)
1653 .await?;
1654 if block {
1655 // A blocking forced put is C `dbProcessNotify`
1656 // (`singlesource.cpp:360-369`): the reply waits for the
1657 // whole chain, async device completion included. The
1658 // bare `process_record_with_links` returns as soon as
1659 // the record goes PACT.
1660 let completion = self.process_record_with_notify(record_name).await?;
1661 Self::await_completion(completion).await;
1662 Ok(())
1663 } else {
1664 // `doPostProcessing(forceProcessing == True)`
1665 // (`iocsource.cpp:404-419`) splits on PACT: an
1666 // async-active record takes `rpro = TRUE` and does not
1667 // process, an idle one takes `putf = TRUE` and does.
1668 // `put_driven_process` is that transition's owner.
1669 self.put_driven_process(record_name).await
1670 }
1671 }
1672 }
1673 }
1674
1675 async fn await_completion(completion: crate::server::record::ProcessCompletion) {
1676 if let crate::server::record::ProcessCompletion::Async(rx) = completion {
1677 let _ = rx.await;
1678 }
1679 }
1680
1681 /// C `dbPut`'s alarm-acknowledge interception (`dbAccess.c:1331-1335`) —
1682 /// the ONLY route that may change ACKS/ACKT at runtime.
1683 ///
1684 /// ```c
1685 /// if (dbrType == DBR_PUT_ACKT && field_type <= DBF_DEVICE)
1686 /// return putAckt(paddr, pbuffer, 1, 1, 0);
1687 /// else if (dbrType == DBR_PUT_ACKS && field_type <= DBF_DEVICE)
1688 /// return putAcks(paddr, pbuffer, 1, 1, 0);
1689 /// ```
1690 ///
1691 /// The dispatch is on the DBR *request type*, not on the field: a CA client
1692 /// acknowledges by sending `DBR_PUT_ACKS` down its ordinary `REC` (VAL)
1693 /// channel. It sits ABOVE the `SPC_NOMOD` gate, which is why
1694 /// `caput REC.ACKS 2` is refused by C ("Write access denied", verified on
1695 /// softIoc 7.0.10) while `ca_put(DBR_PUT_ACKS, REC)` clears the alarm.
1696 ///
1697 /// The put-disable gate is still crossed: C tests `precord->disp` in
1698 /// `dbPutField`, above `dbPut` (`dbAccess.c:1255-1257`), so an ack to a
1699 /// `DISP=1` record is refused. `field` is the channel's field — only the
1700 /// DISP gate looks at it, exactly as in C.
1701 ///
1702 /// No process cycle: `dbPut` returns straight from `putAckt`/`putAcks`, and
1703 /// `dbPutField`'s reprocess condition requires `dbrType < DBR_PUT_ACKT`.
1704 pub async fn put_alarm_ack_from_ca(
1705 &self,
1706 record_name: &str,
1707 field: &str,
1708 ack: crate::server::record::AlarmAck,
1709 value: u16,
1710 ) -> CaResult<()> {
1711 let field_upper = field.to_ascii_uppercase();
1712 let rec = self
1713 .get_record(record_name)
1714 .ok_or_else(|| CaError::ChannelNotFound(record_name.to_string()))?;
1715 let canonical: String = self
1716 .resolve_alias(record_name)
1717 .unwrap_or_else(|| record_name.to_string());
1718 let _record_gate = self.lock_record(&canonical);
1719
1720 let mut instance = rec.write();
1721 check_put_disabled(&instance, &field_upper)?;
1722 match ack {
1723 crate::server::record::AlarmAck::Transient => instance.put_ackt(value),
1724 crate::server::record::AlarmAck::Severity => instance.put_acks(value),
1725 }
1726 Ok(())
1727 }
1728
1729 /// Fire-and-forget + caller-held gate: see
1730 /// [`Self::put_record_field_from_ca_no_notify`] and
1731 /// [`Self::put_record_field_from_ca_already_locked`].
1732 pub fn put_record_field_from_ca_no_notify_already_locked(
1733 &self,
1734 record_name: &str,
1735 field: &str,
1736 value: EpicsValue,
1737 ) -> CaResult<()> {
1738 self.put_record_field_from_ca_body(record_name, field, value, NotifyRequest::None)
1739 .map(|_| ())
1740 }
1741
1742 /// Process a record UNCONDITIONALLY with a put-notify wait-set, returning
1743 /// the completion receiver — the QSRV `record[process=true,block=true]`
1744 /// (Force + block) barrier.
1745 ///
1746 /// C `dbProcessNotify`: pvxs routes a blocking forced put through
1747 /// `dbProcessNotify` (`singlesource.cpp:360-369`), whose completion fires
1748 /// only after the record's whole processing chain — including async device
1749 /// work (a motor move, an asyn-backed AO) — settles. The value is written
1750 /// by the caller's preceding [`Self::put_pv`] (the `dbPut` analogue, no
1751 /// process); this entry then mints the wait-set, registers it into the
1752 /// record's `notify` slot so PACT records join it, and runs the full
1753 /// unconditional [`Self::process_record_with_links`] cycle (C `dbProcess`,
1754 /// the Force analogue). A fully synchronous chain returns `Ok(None)` (the
1755 /// wait-set already drained); an async record returns `Ok(Some(rx))` for
1756 /// the caller to await. A notify already in flight on the record does not
1757 /// refuse this one: it joins the record's restart queue and replays when
1758 /// the record frees, which is C's only outcome here.
1759 pub async fn process_record_with_notify(
1760 &self,
1761 record_name: &str,
1762 ) -> CaResult<crate::server::record::ProcessCompletion> {
1763 let (completion_tx, completion_rx) = crate::runtime::sync::oneshot::channel();
1764 // The gate wraps the install AND the cycle it arms, as C's does:
1765 // `dbProcessNotify` takes `dbScanLock(precord)` (dbNotify.c:355) and
1766 // `processNotifyCommon` assigns `precord->ppn` and calls `dbProcess`
1767 // before the matching `dbScanUnlock` (`:257-262`). Installing ahead of
1768 // the gate let a gate-holding put's cycle reach `complete_put_notify`
1769 // on a slot it did not fill, `take` this client's wait-set and `leave`
1770 // it -- firing a `block=true` completion for a cycle the client never
1771 // requested, and leaving this one to run unarmed.
1772 let installed = {
1773 let _record_gate = self.acquire_put_gate(record_name);
1774 self.install_notify_and_process_already_locked(
1775 record_name,
1776 completion_tx,
1777 NotifyArrival::Fresh,
1778 )
1779 }?;
1780 // The wait-set fires the oneshot only after the whole FLNK/OUT chain
1781 // (sync + async) settles. Already-completed ==> fully synchronous ==>
1782 // report immediate success. Queued (`None`) or still pending ==> hand
1783 // the receiver back to await the deferred completion.
1784 match installed {
1785 Some(notify) if notify.completed() => {
1786 Ok(crate::server::record::ProcessCompletion::Sync)
1787 }
1788 _ => Ok(crate::server::record::ProcessCompletion::Async(
1789 completion_rx,
1790 )),
1791 }
1792 }
1793
1794 /// C `dbPutField`'s put-driven process decision (`dbAccess.c:1264-1277`).
1795 ///
1796 /// Reached once the put has selected the record for processing — the `PROC`
1797 /// field, or a `pp(TRUE)` field on a Passive record. C then splits on PACT:
1798 ///
1799 /// * **async-active** — C sets `rpro = TRUE` and does NOT call `dbProcess`.
1800 /// `recGblFwdLink` (`recGbl.c:296-300`) consumes RPRO when the device
1801 /// round trip completes and queues `scanOnce`, so the value this put just
1802 /// wrote still reaches the device, one cycle later. Calling `dbProcess`
1803 /// here instead lands in dbProcess's own PACT guard, which bumps LCNT and
1804 /// after MAX_LOCK raises SCAN_ALARM — an alarm C never raises for a client
1805 /// put — while dropping the deferred reprocess entirely: on two rapid
1806 /// puts to a Passive async output, C writes both values to the device and
1807 /// the port wrote only the first.
1808 /// * **idle** — C sets `putf = TRUE` (the put-driven marker, cleared at the
1809 /// tail of the process cycle / in `complete_async_record_inner`, both the
1810 /// `recGblFwdLink:302` analogue) and calls `dbProcess`.
1811 ///
1812 /// Single owner of that decision for every external put: the `PROC`
1813 /// intercept and the `pp`-field route in
1814 /// [`Self::put_record_field_from_ca`] both go through it, so neither can
1815 /// drift from C's rule or from each other. The DB-link propagation path
1816 /// applies the same PACT→RPRO rule at its own targets (`processing.rs:3225`,
1817 /// `:4220`, `links.rs:829`).
1818 ///
1819 /// Two entries, one rule. `put_driven_process` acquires `record_name`'s
1820 /// advisory write gate (the `dbScanLock` analogue) itself;
1821 /// [`Self::put_driven_process_already_locked`] is for a caller that
1822 /// already owns it — `_record_gate` on the CA put path, or the QSRV
1823 /// atomic group's `lock_records` epoch. The gate is not
1824 /// reentrant, so a caller holding it MUST take the `_already_locked`
1825 /// entry.
1826 ///
1827 /// QSRV's group PUT is the second external caller (pvxs
1828 /// `IOCSource::doPostProcessing`, `iocsource.cpp:397-420`, whose PACT
1829 /// branch is this same RPRO deferral): it reaches the decision by its own
1830 /// route — `record._options.process`, a `+type:"proc"` member, a `pp` field
1831 /// — but once the answer is "process", the transition is this owner's, in
1832 /// both gate modes.
1833 /// The PACT (RPRO) branch is success, as it is in C: `dbPutField` returns
1834 /// the `dbProcess` status only on the branch that ran it.
1835 pub async fn put_driven_process(&self, record_name: &str) -> CaResult<()> {
1836 let _record_gate = self.acquire_put_gate(record_name);
1837 self.put_driven_process_already_locked(record_name)
1838 }
1839
1840 /// [`Self::put_driven_process`] for a caller that already owns the
1841 /// record's advisory write gate. The gate-held body — no `.await`.
1842 pub fn put_driven_process_already_locked(&self, record_name: &str) -> CaResult<()> {
1843 {
1844 let Some(rec) = self.get_record(record_name) else {
1845 return Ok(());
1846 };
1847 let mut instance = rec.write();
1848 if instance.is_processing() {
1849 instance.common.rpro = 1;
1850 return Ok(());
1851 }
1852 instance.common.putf = true;
1853 }
1854 let mut visited = HashSet::new();
1855 self.process_record_with_links_already_locked(record_name, &mut visited, 0)
1856 }
1857
1858 /// C `restartCheck` (dbNotify.c:149-170) plus the restarted
1859 /// `processNotifyCommon` it queues: pop the oldest put-notify waiting on
1860 /// `record_name` and replay it whole — value, process, callback.
1861 ///
1862 /// The pop happens **after** the record's advisory write gate is taken and
1863 /// **before** the replay releases it, so the promoted put owns the record
1864 /// across the whole promotion, as C's `precord->ppn = pfirst` does. Without
1865 /// that, a client put arriving in the gap would take the record and the
1866 /// longer-waiting notify would end up behind it.
1867 ///
1868 /// The replay goes back through the ordinary put entry, so if the record has
1869 /// ALREADY gone active again (a scan fired between the completion and this
1870 /// replay), the same test queues it once more rather than writing into a
1871 /// busy record — the deferral is closed under its own restart. Called only
1872 /// from `PvDatabase::apply_pact_exit`, the single drain owner.
1873 pub(crate) async fn restart_next_notify_put(&self, record_name: &str) {
1874 // The clients already hold their receivers; a failure here (record gone,
1875 // field refused) must still release them, which dropping the senders
1876 // does — the same completion a `dbNotifyCancel` gives the C client.
1877 let _record_gate = self.acquire_put_gate(record_name);
1878 let Some(rec) = self.get_record(record_name) else {
1879 return;
1880 };
1881 let Some(queued) = rec.write().take_next_notify_restart() else {
1882 return;
1883 };
1884 match queued {
1885 crate::server::record::DeferredNotify::Put(
1886 crate::server::record::DeferredNotifyPut {
1887 field,
1888 value,
1889 completion,
1890 },
1891 ) => {
1892 let _ = self.put_record_field_from_ca_body(
1893 record_name,
1894 &field,
1895 value,
1896 NotifyRequest::Deferred(completion),
1897 );
1898 }
1899 // C `processGetRequest` on restart: `processNotifyCommon` re-enters
1900 // with no `putCallback`, so the replay is the process alone. The
1901 // gate is already held, so this takes the already-locked entry.
1902 crate::server::record::DeferredNotify::Process { completion } => {
1903 let _ = self.install_notify_and_process_already_locked(
1904 record_name,
1905 completion,
1906 NotifyArrival::Replay,
1907 );
1908 }
1909 }
1910 }
1911
1912 /// Arm `record_name`'s wait-set around `completion` and drive one process
1913 /// cycle -- the gate-held body behind every put-notify entry in this file,
1914 /// and the only one that reaches the record's `notify` slot.
1915 ///
1916 /// **The caller MUST hold `record_name`'s advisory write gate.** That is
1917 /// the whole of C's gated region: both entries take the record lock before
1918 /// `processNotifyCommon` runs -- `dbProcessNotify` at dbNotify.c:355 for a
1919 /// fresh arrival, `notifyCallback` at `:282` for a replay -- and hold it
1920 /// across the `precord->ppn` assignment and the `dbProcess` it arms
1921 /// (`:257-262`). Being an `_already_locked` body this contains no `.await`,
1922 /// so the gate cannot be held across a suspension.
1923 ///
1924 /// Returns the wait-set so the caller can ask whether the chain settled
1925 /// synchronously. `Ok(None)` means this call drove nothing -- the notify
1926 /// queued behind the record's current owner, and the replay is what
1927 /// processes, so processing here too would run one client request twice.
1928 fn install_notify_and_process_already_locked(
1929 &self,
1930 record_name: &str,
1931 completion: crate::runtime::sync::oneshot::Sender<()>,
1932 arrival: NotifyArrival,
1933 ) -> CaResult<Option<std::sync::Arc<crate::server::record::NotifyWaitSet>>> {
1934 // Collect-then-act: clone the handle under a brief map read, drop the
1935 // map lock before taking the per-record write lock.
1936 let rec_arc = {
1937 let recs = self.inner.records.read();
1938 recs.get(record_name).cloned()
1939 }
1940 .ok_or_else(|| CaError::ChannelNotFound(record_name.to_string()))?;
1941 let notify = {
1942 let mut guard = rec_arc.write();
1943 if arrival.defers(&guard) {
1944 guard.queue_notify_put(crate::server::record::DeferredNotify::Process {
1945 completion,
1946 });
1947 return Ok(None);
1948 }
1949 // Through the one install owner. Assigning the slot here instead
1950 // would drop the prior client's Sender, and its receiver then wakes
1951 // with the RecvError the CA dispatcher reads as success.
1952 match guard.install_or_queue_notify(completion) {
1953 Some(notify) => notify,
1954 None => return Ok(None),
1955 }
1956 };
1957 let mut visited = HashSet::new();
1958 self.process_record_with_links_already_locked(record_name, &mut visited, 0)?;
1959 Ok(Some(notify))
1960 }
1961
1962 /// The gate-held body of the whole CA field-put family — a `fn`, so
1963 /// nothing in it can suspend while the L1 gate is held. The gate is the
1964 /// caller's; see `acquire_put_gate`.
1965 fn put_record_field_from_ca_body(
1966 &self,
1967 record_name: &str,
1968 field: &str,
1969 value: EpicsValue,
1970 notify_request: NotifyRequest,
1971 ) -> CaResult<crate::server::record::ProcessCompletion> {
1972 let field = field.to_ascii_uppercase();
1973 let want_notify = notify_request.wants_notify();
1974
1975 // Get record Arc — alias-aware (epics-base PR #336) so a CA
1976 // client that connects via an alias name can put fields on
1977 // the canonical record.
1978 let rec = self
1979 .get_record(record_name)
1980 .ok_or_else(|| CaError::ChannelNotFound(record_name.to_string()))?;
1981 // Normalise to the canonical name for the rest of this
1982 // function — every subsequent call (PACT/LCNT lookup,
1983 // `process_record_with_links`, `update_scan_index`) uses the
1984 // raw records map and would miss when `record_name` is an
1985 // alias. Resolve once up front.
1986 let canonical_owned;
1987 let record_name: &str = if let Some(target) = self.resolve_alias(record_name) {
1988 canonical_owned = target;
1989 &canonical_owned
1990 } else {
1991 record_name
1992 };
1993
1994 // The caller holds the record's advisory write gate — the
1995 // `dbScanLock(precord)` analogue, taken by `acquire_put_gate`
1996 // or (QSRV atomic group PUT) by `lock_records` over the whole member
1997 // set. While it is held a plain write to the same record blocks, so a
1998 // direct backing-record write cannot land between member writes of an
1999 // atomic group transaction. It is held until the function returns.
2000
2001 // Special field intercepts (read lock, then drop)
2002 {
2003 let instance = rec.read();
2004
2005 // C `dbPutField` gate order (`dbAccess.c:1252-1277`): the DISP
2006 // put-disable gate runs BEFORE `dbPut` — hence before the
2007 // SPC_NOMOD rejection of PACT/LCNT/PUTF (`dbAccess.c:123`) — and
2008 // BEFORE the PROC-driven `dbProcess`. So on a `DISP=1` record
2009 // EVERY non-DISP field, PROC included, is refused with
2010 // `S_db_putDisabled` and the record does not process.
2011 check_put_disabled(&instance, &field)?;
2012
2013 // SPC_NOMOD / read-only fields: rejected inside C's `dbPut`, i.e.
2014 // after the DISP gate above and before the PROC-driven process
2015 // below. One gate owner for every route ([`check_no_mod`]).
2016 check_no_mod(&instance, &field)?;
2017 }
2018
2019 // C `processNotifyCommon` (dbNotify.c:225-231) tests PACT ABOVE the
2020 // put — `if (precord->pact) { ... pnotify->state =
2021 // notifyRestartCallbackRequested; ... return; }` — so a put-notify that
2022 // lands on a busy record writes NOTHING: no value, no RPRO, no join of
2023 // the in-flight cycle's wait-set. The whole put is replayed by the
2024 // `PactExit` the record's PACT release hands to its `recGblFwdLink`
2025 // tail (C `dbNotifyCompletion`). Joining the running cycle instead
2026 // completed the callback one cycle early, on work that never saw this
2027 // value.
2028 //
2029 // The ownership test and the enqueue are ONE critical section (C holds
2030 // `dbScanLock` across both): a put queued onto a record that went idle
2031 // in between would wait for a restart check that already ran. A record
2032 // that goes idle in the window falls through and takes the put the
2033 // ordinary way.
2034 //
2035 // A second put-notify onto a record that already owns one is C's
2036 // "another processNotify owns the record" (dbNotify.c:213-217): it joins
2037 // the SAME queue, at the back (`ellSafeAdd`). Both tests are one arm
2038 // here because both have the same answer — the put waits, unwritten.
2039 // Refusing it instead (`S_db_Blocked` / `ECA_PUTCBINPROG`) drops the
2040 // client's value, and C never sends that status from this path.
2041 //
2042 // A fire-and-forget `dbPutField` is NOT deferred: it writes and raises
2043 // RPRO (dbAccess.c:1263-1277). Only the notify route waits.
2044 //
2045 // C `dbProcessNotify` (dbNotify.c:337-353) handles a put-notify to a
2046 // DBF link field (INLINK/OUTLINK/FWDLINK) as a dedicated early case,
2047 // ABOVE the PACT logic and the whole `processNotifyCommon` machinery:
2048 // "Only dbPutField will change link fields. Also the record is not
2049 // processed as a result." It writes the value via `dbPutField`
2050 // (`putFieldType`) and fires the done callback IMMEDIATELY — it never
2051 // reaches the PACT test, never processes, never defers. So a link
2052 // field always takes the value even on a busy or permanently-parked
2053 // record: a bare `sub` (empty `SNAM`) parks PACT=TRUE forever
2054 // (subRecord.c:119-122), and parking its link-field put on a `PactExit`
2055 // that never comes drops the value — `caput <sub>.INPA '0'` then reads
2056 // back "" instead of C's "0". The ordinary write path below already
2057 // reproduces C's link semantics for these fields (writes the value;
2058 // `put_drives_processing_of` is false — no link field is `pp` or PROC —
2059 // so it processes nothing and returns immediate completion), so the
2060 // only correction the special case needs is to keep a link field OUT
2061 // of the notify PACT-defer park.
2062 let is_dbf_link_field = self.is_dbf_link_field(record_name, &field);
2063 if want_notify {
2064 let is_restart = notify_request.is_restart();
2065 let mut guard = rec.write();
2066 // A restart is already the record's owner, so only PACT can stop it
2067 // (C skips dbNotify.c:213 for it and falls straight to :225).
2068 let must_wait = if is_restart {
2069 guard.is_processing()
2070 } else if is_dbf_link_field {
2071 // Ownership only — see `notify_put_has_owner`. Keeping link
2072 // fields out of the whole decision (not just the PACT arm) left
2073 // an owned record's link put falling through to the wait-set
2074 // install below, whose only answer to an occupied slot was
2075 // `PutCallbackInProgress`.
2076 guard.notify_put_has_owner()
2077 } else {
2078 guard.notify_put_is_owned()
2079 };
2080 if must_wait {
2081 let Some((completion, completion_rx)) = notify_request.into_completion() else {
2082 // Unreachable: `want_notify` is exactly "this request
2083
2084 // carries a completion".
2085 return Ok(crate::server::record::ProcessCompletion::Sync);
2086 };
2087 let put = crate::server::record::DeferredNotifyPut {
2088 field,
2089 value,
2090 completion,
2091 };
2092 let put = crate::server::record::DeferredNotify::Put(put);
2093 if is_restart {
2094 guard.requeue_notify_put(put);
2095 } else {
2096 guard.queue_notify_put(put);
2097 }
2098 // A queued put-notify IS async: it replays and completes on the
2099 // restart check that frees it (C `notifyRestartInProgress` /
2100 // `notifyWaitForRestart`, dbNotify.c:213-231). `Deferred` replays
2101 // carry only the sender, so `completion_rx` is `None` there and
2102 // this maps to `Sync` — but that path is the internal restart,
2103 // not a fresh client put.
2104 return Ok(crate::server::record::ProcessCompletion::from_signal(
2105 completion_rx,
2106 ));
2107 }
2108 }
2109
2110 // PROC intercept: trigger processing on any SCAN.
2111 // Falls through to the put_notify_tx registration below
2112 // so async records (motor, asyn-backed AO) signal real
2113 // completion; otherwise WRITE_NOTIFY would return ECA_NORMAL
2114 // before the device move actually finished.
2115 //
2116 // C `dbPutField` (dbAccess.c:1265) matches the proc field by pointer
2117 // with NO value check: any write to PROC — including 0 — processes the
2118 // record (when !pact). The standard `caput REC.PROC 0` / `dbpf REC.PROC
2119 // 0` force-process idiom must therefore not be skipped for a zero value.
2120 if field == "PROC" {
2121 // C `dbCommon.dbd` declares `field(PROC,DBF_UCHAR){ pp(TRUE) }`, so
2122 // a put to PROC does BOTH: `dbPut` stores the raw byte in
2123 // `prec->proc` (retained — C never resets it), AND `pp(TRUE)` drives
2124 // the reprocess below. The prior port kept only the reprocess and
2125 // dropped the byte, so `caput REC.PROC v; caget REC.PROC` always
2126 // read 0. Store the byte through the SAME `DBF_UCHAR` common-field
2127 // path DISP/RPRO use (coercion + signed readback: `caput PROC 255` →
2128 // `caget` = -1) in its own brief write lock so both the notify and
2129 // fire-and-forget paths take it, then fall through to force-process.
2130 // C `dbPut:1408` posts DBE_VALUE|DBE_LOG for the put field (PROC is
2131 // not the record's value field, so the pp-suppression never applies).
2132 // Store the raw PROC byte (C `dbChannelPut`). A bad conversion
2133 // (`caput REC.PROC 256` / non-numeric) refuses the store AND the
2134 // client's put — but, exactly as C's `putCallback` returns
2135 // `didPut = 1` while setting `notifyError` (`dbNotify.c:528-530`),
2136 // the PROC `pp(TRUE)`-driven `dbProcess` (`dbNotify.c:243-261`) still
2137 // runs on the NOTIFY path. This is the SAME rule the general put path
2138 // applies for a rejected pp-field conversion (`field_io.rs:1748-1806`,
2139 // "Cause B"); mirror it here so PROC does not diverge from UDF: carry
2140 // the refusal, force-process when `want_notify`, then hand the Err
2141 // back so the client still sees `ECA_PUTFAIL`.
2142 let proc_store: CaResult<()> = {
2143 let rec_arc = {
2144 let recs = self.inner.records.read();
2145 recs.get(record_name).cloned()
2146 };
2147 if let Some(rec_arc) = rec_arc {
2148 let mut guard = rec_arc.write();
2149 match guard.put_common_field("PROC", value) {
2150 Ok(_) => {
2151 guard.notify_field(
2152 "PROC",
2153 crate::server::recgbl::EventMask::VALUE
2154 | crate::server::recgbl::EventMask::LOG,
2155 );
2156 Ok(())
2157 }
2158 Err(e) => Err(e),
2159 }
2160 } else {
2161 Ok(())
2162 }
2163 };
2164 if let Err(e) = proc_store {
2165 // `want_notify` ⇒ C `ca_put_callback`: the PROC process runs
2166 // despite the rejected conversion (`didPut == 1`). Fire-and-forget
2167 // ⇒ C plain `dbPutField`, which returns before `dbProcess` on a
2168 // non-zero `dbPut` status (`dbAccess.c:1263-1264`), so it must NOT
2169 // process. Either way the client is answered `ECA_PUTFAIL`.
2170 if want_notify {
2171 let _ = self.put_driven_process_already_locked(record_name);
2172 }
2173 return Err(e);
2174 }
2175 // A fire-and-forget caller parks nothing — C `dbPutField` on PROC
2176 // processes the record with no putNotify.
2177 let parked = if let Some((completion_tx, completion_rx)) =
2178 notify_request.into_completion()
2179 {
2180 // Collect-then-act: clone the handle under a brief map
2181 // read, drop the map lock before the per-record write.
2182 let rec_arc = {
2183 let recs = self.inner.records.read();
2184 recs.get(record_name).cloned()
2185 };
2186 match rec_arc {
2187 Some(rec_arc) => {
2188 match rec_arc.write().install_or_queue_notify(completion_tx) {
2189 Some(notify) => Some((notify, completion_rx)),
2190 // Queued: the entry gate let this put through, then
2191 // another notify took the slot before the install
2192 // (`process_record_with_notify` does not hold the
2193 // put gate). C queues here rather than refusing, and
2194 // the replay is what processes — so return without
2195 // driving the record.
2196 None => {
2197 return Ok(crate::server::record::ProcessCompletion::from_signal(
2198 completion_rx,
2199 ));
2200 }
2201 }
2202 }
2203 // Record gone between the put and the park: nothing to
2204 // install on. Dropping the wait-set releases the client,
2205 // which is the completion C gives a `dbNotifyCancel`.
2206 None => Some((
2207 crate::server::record::NotifyWaitSet::new(completion_tx),
2208 completion_rx,
2209 )),
2210 }
2211 } else {
2212 None
2213 };
2214 // C `dbPutField:1265-1277`: PROC is one of the two fields that
2215 // selects the record for the put-driven process — with the same
2216 // PACT→RPRO deferral as a `pp` field. Both go through the single
2217 // owner (R19-43).
2218 //
2219 // The ALREADY-LOCKED entry, unconditionally — NOT `acquire_gate`
2220 // passed through. By the time control reaches here the record's
2221 // advisory gate is held on both paths: this function took it above
2222 // when `acquire_gate`, and the caller (an atomic group PUT) holds it
2223 // when not. The gate is not reentrant, so acquiring it again
2224 // here deadlocks every PROC put.
2225 let _ = self.put_driven_process_already_locked(record_name);
2226 // The wait-set fires the oneshot only after the whole
2227 // FLNK/OUT chain (sync + async) settles. If it has
2228 // already completed the chain was fully synchronous —
2229 // report immediate success; otherwise hand the receiver
2230 // to the CA layer to await the deferred completion.
2231 return match parked {
2232 Some((notify, completion_rx)) => {
2233 if notify.completed() {
2234 Ok(crate::server::record::ProcessCompletion::Sync)
2235 } else {
2236 Ok(crate::server::record::ProcessCompletion::from_signal(
2237 completion_rx,
2238 ))
2239 }
2240 }
2241 None => Ok(crate::server::record::ProcessCompletion::Sync),
2242 };
2243 }
2244
2245 // Normal field put (write lock) — C `dbPut`, which does NOT touch
2246 // `putf`: the marker is raised only where C raises it, at the
2247 // put-driven process decision (`put_driven_process`).
2248 //
2249 // Link writes the record's `special()` makes itself. C runs them inside
2250 // `dbPut`, so they land BEFORE the `pp(TRUE)` process below — a record
2251 // wired to scaler `.COUTP` is processed with the scaler not yet armed
2252 // (scalerRecord.c:623-624, before the `:637` REQSTART).
2253 let mut special_actions = Vec::new();
2254 // Scoped guard: the put body (closure + failure handling) runs under
2255 // this write guard, whose scope ends (releasing the !Send parking_lot
2256 // guard) before the notify-process / scan-index awaits below. Yields
2257 // either the success `CommonFieldPutResult`, or `(error, should_process)`
2258 // so the notify-driven process on a rejected put runs guard-free.
2259 let mut pact_exit = PactExitGuard::new(self, record_name, &rec, RestartOwner::ThisPut);
2260 let outcome: Result<crate::server::record::CommonFieldPutResult, (CaError, bool)> = {
2261 let mut instance = rec.write();
2262
2263 // C `db_put_process` (db_access.c:1025-1043) returns 1 (didPut) even
2264 // when the internal `dbChannelPut` FAILS — a rejected conversion, an
2265 // SPC_NOMOD refusal, or an after-put `special()` error all set
2266 // `ppn->status = notifyError` yet still `return 1` — so
2267 // `processNotifyCommon` (dbNotify.c:243-246) still runs `dbProcess`
2268 // when the gate passes. The whole put write is therefore wrapped so
2269 // that ANY failure inside it — `dbput_request`, `special()` pass 0,
2270 // `put_field`, `special_after_put`, `put_common_field` — is caught at
2271 // ONE place below: on the notify path we evaluate the SAME process gate
2272 // the success path uses and process the record as a side effect, then
2273 // hand the original Err back to the client. On the failing conversion
2274 // path no field is written — `dbChannelPut` wrote nothing either.
2275 //
2276 // On SUCCESS this closure is just C `dbPut`: the monitor posts at its
2277 // tail run only when the put fully succeeded (C's `goto done` skips
2278 // them on failure).
2279 let block_result: CaResult<crate::server::record::CommonFieldPutResult> = (|| {
2280 // Coerce value to the field's native DBR type (e.g. String → Double for ao.VAL).
2281 // This matches C EPICS db_put_field() which converts from the CA client's type
2282 // to the record field's native type.
2283 let request = dbput_request(&*instance.record, &field, value)?;
2284
2285 // Pre-write special hook (C EPICS dbPutSpecial pass=0)
2286 instance.record.special(&field, false)?;
2287 pact_exit.arm(special_before_put(&mut instance, &field));
2288
2289 // Capture pre-put value for faac1df1 idempotent-write suppression.
2290 let prev_value = instance.record.get_field(&field);
2291 let old_nord = array_nord_before_put(&instance, &field);
2292
2293 // Try record-specific field first; fall back to common on FieldNotFound.
2294 // For record-owned fields, call on_put() and special() after successful put,
2295 // matching what put_common_field() does for common fields.
2296 use crate::server::record::CommonFieldPutResult;
2297 let common_result = match request {
2298 // C `dbAccess.c:1370-1372` — a zero-element request into a
2299 // scalar field: nothing is written, the record is driven to
2300 // LINK/INVALID, and `dbPut` returns 0. The client's put
2301 // SUCCEEDS; the record's process cycle below commits the alarm
2302 // and posts it, which is how a C IOC surfaces `caput -a`
2303 // of an empty array.
2304 PutRequest::EmptyIntoScalar => {
2305 set_empty_request_alarm(&mut instance);
2306 CommonFieldPutResult::NoChange
2307 }
2308 PutRequest::Write(value) => {
2309 match instance.record.put_field(&field, value.clone()) {
2310 Ok(()) => {
2311 instance.record.on_put(&field);
2312 // C returns the after-put special() status from
2313 // `dbPut` (dbAccess.c:1399-1405); `if (status)
2314 // goto done` then skips both the UDF clear below
2315 // and the field's monitor post, and `dbPutField`
2316 // skips the process. Propagating the error here
2317 // reproduces all three.
2318 let result = special_after_put(
2319 self,
2320 &mut instance,
2321 &field,
2322 &mut special_actions,
2323 )?;
2324 // C `dbAccess.c::dbPut:1410-1411` clears
2325 // `precord->udf = FALSE` synchronously when the
2326 // put target is the record-type's primary value
2327 // field (`dbIsValueField`), and clears NOTHING
2328 // else. The clear happens BEFORE `dbProcess` runs,
2329 // so any reader between the put and the process
2330 // cycle sees the new value with a consistent
2331 // udf=false — but stat/sevr keep their old
2332 // UDF_ALARM until the process cycle recomputes
2333 // them. A value put that drives no process leaves
2334 // the stale UDF alarm, matching C; the process
2335 // path's own `rec_gbl_check_udf` (now a no-op with
2336 // udf clear) + `rec_gbl_reset_alarms` clears it
2337 // when the record does process. The earlier
2338 // synchronous stat/sevr clear here diverged from C.
2339 if instance.record.is_udf_defining_put(&field) {
2340 instance.common.udf = 0;
2341 }
2342 result
2343 }
2344 Err(CaError::FieldNotFound(_)) => {
2345 instance.put_common_field(&field, value)?
2346 }
2347 Err(e) => return Err(e),
2348 }
2349 }
2350 };
2351
2352 // C `add_count` raises the inverted-limits alarm during a SGNL
2353 // SPC_MOD `special()` (histogramRecord.c:329-334). The port raises
2354 // it through `nsta`/`nsev` (CBUG-F12 refused, not C's direct write),
2355 // so this monitor-less special path commits it — check then reset —
2356 // to make STAT=SOFT/INVALID observable, matching the process path.
2357 // Gated on `special_checks_alarms` (histogram SGNL only). No STAT
2358 // post: C's `add_count` posts nothing, and the special path has no
2359 // monitor — the alarm shows on the next caget's field read.
2360 if instance.record.special_checks_alarms(&field) {
2361 let inst = &mut *instance;
2362 inst.record.check_alarms(&mut inst.common);
2363 let _ = crate::server::recgbl::rec_gbl_reset_alarms(&mut inst.common);
2364 }
2365
2366 // Invalidate metadata cache only if the metadata-class
2367 // field's value actually changed (faac1df1).
2368 instance.notify_field_written_if_changed(&field, prev_value.as_ref());
2369
2370 // `putf` is neither set nor cleared anywhere in this block: C's
2371 // `dbPut` does not touch it. It is raised in `put_driven_process`
2372 // (C `dbAccess.c:1274`) immediately before `dbProcess`, stays TRUE
2373 // for the whole process cycle — including an async device round
2374 // trip — and is cleared by the `recGblFwdLink:302` analogue at the
2375 // cycle's tail (`processing.rs:2997` / `complete_async_record_inner`)
2376 // or by the disable-alarm bail (`dbAccess.c:576`).
2377
2378 // C `dbPut:1408-1414`'s field-monitor post, through the one
2379 // owner (`dbput_post_put_field`, shared with `put_pv`). On
2380 // this route the pp-value-field suppression pairs with the
2381 // `should_process` gate below: a suppressed field is exactly
2382 // one the reprocess cycle re-posts via the deadband snapshot.
2383 // (ACKT/ACKS have no arm here: they are SPC_NOMOD, refused by
2384 // the gate above. Alarm acknowledgement arrives as a DBR
2385 // request type, through [`Self::put_alarm_ack_from_ca`].)
2386 dbput_post_put_field(&mut instance, &field);
2387
2388 // The NORD post, through the one owner — C reaches `put_array_info`
2389 // from `dbPut`, so the CA route posts it exactly like the internal
2390 // one. It is NOT covered by the value-field post above: for a
2391 // waveform that post is suppressed (VAL is `pp(TRUE)`), and it is
2392 // not covered by the process cycle either — a `caput -a` to a
2393 // slow-scanned or passive-but-unprocessed waveform posts NORD now
2394 // and VAL only at the next scan.
2395 post_array_info(&mut instance, &old_nord, 0);
2396
2397 // Fields a `special()` changed as a side effect of this put
2398 // (e.g. compress RES reset zeroing NUSE/VAL) get their monitors
2399 // posted here, mirroring the explicit `db_post_events` a C
2400 // `special()` makes — these fields are not pp(TRUE), so no
2401 // process cycle would otherwise post them. Each post carries
2402 // VALUE|LOG unless the record names the field in
2403 // `value_only_change_fields()` — a record whose C `special()`
2404 // posts the field with a literal `DBE_VALUE` (e.g. table SET,
2405 // tableRecord.c:659) gets the LOG bit stripped, honoring the
2406 // same value-only contract as the change-detection path.
2407 //
2408 // C's `monitor()` runs `recGblResetAlarms(prec)` BEFORE those
2409 // `db_post_events`, and OR-adds the alarm bit it returns into the
2410 // value posts (compressRecord.c:103-110). The port mirrors that
2411 // order: commit the alarm here (posting any STAT/SEVR/AMSG/ACKS
2412 // transition through the one owner, `alarm_field_posts`) and carry
2413 // the resulting DBE_ALARM into the side-effect posts below. Records
2414 // whose `special()` does not run `monitor()` return false and skip
2415 // this entirely (no spurious alarm commit on an unrelated put).
2416 let side_effect_alarm_mask = if instance.record.special_commits_alarms(&field) {
2417 commit_special_reset_alarm(&mut instance)
2418 } else {
2419 crate::server::recgbl::EventMask::NONE
2420 };
2421
2422 let side_effect_value_only = instance.record.value_only_change_fields();
2423 for sf in instance.record.monitor_side_effect_fields(&field) {
2424 use crate::server::recgbl::EventMask;
2425 let mask = if side_effect_value_only
2426 .iter()
2427 .any(|f| f.eq_ignore_ascii_case(sf))
2428 {
2429 EventMask::VALUE
2430 } else {
2431 EventMask::VALUE | EventMask::LOG
2432 };
2433 instance.notify_field(sf, mask | side_effect_alarm_mask);
2434 }
2435
2436 // The same `special()` posts, but named by the WRITER instead of
2437 // by a static table: a record whose put handler re-derived a
2438 // partner field marks it — with the mask of the C call site that
2439 // posts it, and only when that field's own comparison moved
2440 // (sseq `special()` posts the re-rendered `STRn` after a `DOn`
2441 // put, `DBE_VALUE`, `only if (strcmp(str, plinkGroup->s))`,
2442 // sseqRecord.c:1108-1116). A static field-name list cannot
2443 // express "only if it changed", so it over-posts; the mark can.
2444 emit_cycle_posts(&mut instance);
2445
2446 Ok(common_result)
2447 })(
2448 );
2449
2450 // Cause B: a put-NOTIFY whose write was rejected must still process.
2451 // C `db_put_process` returned 1 (didPut) despite the failure above, so
2452 // `processNotifyCommon` runs `dbProcess` whenever the gate passes.
2453 // Reuse the SAME `put_drives_processing_of` gate the success tail uses,
2454 // process the record as a side effect, then return the ORIGINAL Err —
2455 // the CA layer maps it to PUTFAIL (C `notifyError`) and `put_accepted`
2456 // stays False, while STAT/SEVR recompute to match C. The notify path
2457 // ONLY: a plain `dbPutField` failure processes nothing (dbAccess.c:1263
2458 // processes only when `dbPut` status==0), so `want_notify == false`
2459 // keeps its Err-without-process behavior. The instance write lock must
2460 // drop before `put_driven_process_already_locked` re-acquires it.
2461 match block_result {
2462 Ok(cr) => Ok(cr),
2463 Err(e) => {
2464 // C `dbPut:83-88` runs `dbPutSpecial(paddr, 1)` UNCONDITIONALLY
2465 // ("Always do special processing if needed") — even when the
2466 // conversion above failed — before the `goto done` that skips
2467 // the udf clear and the field's monitor post. For a compress
2468 // SPC_RESET field that means `special()` still runs `monitor()`
2469 // → `recGblResetAlarms`, committing the born-UDF alarm to
2470 // NO_ALARM though the RES/N put is rejected (a caget then sees
2471 // stat/sevr=NO_ALARM with udf still 1, matching C softIoc).
2472 // Run the after-put `special()` and its alarm commit here, then
2473 // hand back the ORIGINAL Err so the client still sees PUTFAIL.
2474 // Gated on `special_commits_alarms` (compress only) so no other
2475 // special record runs its after-put hook on a failed conversion.
2476 if instance.record.special_commits_alarms(&field) {
2477 let _ = instance.record.special(&field, true);
2478 let alarm_mask = commit_special_reset_alarm(&mut instance);
2479 let value_only = instance.record.value_only_change_fields();
2480 for sf in instance.record.monitor_side_effect_fields(&field) {
2481 use crate::server::recgbl::EventMask;
2482 let mask = if value_only.iter().any(|f| f.eq_ignore_ascii_case(sf)) {
2483 EventMask::VALUE
2484 } else {
2485 EventMask::VALUE | EventMask::LOG
2486 };
2487 instance.notify_field(sf, mask | alarm_mask);
2488 }
2489 }
2490 // The same `dbPutSpecial(paddr, 1)`-on-reject rule for a field
2491 // whose special() raises the inverted-limits alarm (histogram
2492 // SGNL → add_count): C still runs add_count when the SGNL
2493 // conversion fails, so STAT=SOFT/INVALID appears even for a
2494 // rejected `caput .SGNL notanumber`. The port raises it through
2495 // `nsta`/`nsev` (CBUG-F12 refused) and commits it here — check
2496 // then reset — since no process follows.
2497 if instance.record.special_checks_alarms(&field) {
2498 let inst = &mut *instance;
2499 inst.record.check_alarms(&mut inst.common);
2500 let _ = crate::server::recgbl::rec_gbl_reset_alarms(&mut inst.common);
2501 }
2502 // The same `dbPutSpecial(paddr, 1)`-on-reject rule for a field
2503 // whose special() clears UDF: C `mbboDirectRecord.c::special`
2504 // (after==1, B0..B1F, line 290) sets `prec->udf = FALSE`, and
2505 // that special runs UNCONDITIONALLY in `dbPut` (dbAccess.c:1401,
2506 // "Always do special processing") even when the value conversion
2507 // failed — BEFORE the `if (status) goto done`. So a rejected
2508 // `caput -c mbboDirect.Bn 256`/`notanumber` still clears UDF, and
2509 // the notify-process that follows recomputes STAT/SEVR to
2510 // NO_ALARM instead of the born-UDF INVALID (verified live against
2511 // the C softIoc: fresh record → rejected Bn put → NO_ALARM,
2512 // udf=0). The success path clears UDF for this same field set via
2513 // `is_udf_defining_put` (the `udf = 0` at the tail of the put
2514 // body). The primary VAL field is EXCLUDED here: its UDF clear is
2515 // `isValueField` (dbAccess.c:1408), which runs AFTER the status
2516 // check, so a rejected VAL put keeps UDF — matching C. Only
2517 // mbboDirect overrides `is_udf_defining_put` to add non-primary
2518 // fields, so this is a no-op for every other record type.
2519 if instance.record.is_udf_defining_put(&field)
2520 && field != instance.record.primary_field()
2521 {
2522 instance.common.udf = 0;
2523 }
2524 let should_process = want_notify && put_drives_processing_of(&instance, &field);
2525 Err((e, should_process))
2526 }
2527 }
2528 };
2529 // Lock down: arm any put-notify the SNAM park release freed.
2530 drop(pact_exit);
2531
2532 let common_result = match outcome {
2533 Ok(cr) => cr,
2534 Err((e, should_process)) => {
2535 if should_process {
2536 let _ = self.put_driven_process_already_locked(record_name);
2537 }
2538 return Err(e);
2539 }
2540 };
2541 // ASG-field change re-evaluation hook. C
2542 // `asDbLib.c:107-110,144` `asSpcAsCallback` invokes
2543 // `asChangeGroup` → `asAddMemberPvt` → `asComputePvt` for
2544 // every `ASGCLIENT` on `dbPut record.ASG NEW_ASG`. Pre-fix
2545 // Rust mutated `common.asg` directly with no notification,
2546 // so the wire ACCESS_RIGHTS the client saw still reflected
2547 // the OLD ASG until something else triggered re-eval. Now we
2548 // fire a process-wide notifier that the CA server folds into
2549 // its per-client `reeval_access_rights` path.
2550 if field == "ASG" {
2551 crate::server::access_security::notify_asg_field_changed();
2552 }
2553 // record lock released
2554
2555 // C `dbPutField` reaches `dbProcess` only after `dbPut` — and therefore
2556 // after `dbPutSpecial(paddr, 1)` and every `dbPutLink` it made — has run
2557 // to completion. Execute them here, ahead of the `pp(TRUE)` process.
2558 self.run_special_actions(record_name, &rec, std::mem::take(&mut special_actions));
2559
2560 // Update scan index if SCAN or PHAS changed
2561 match common_result {
2562 crate::server::record::CommonFieldPutResult::ScanChanged {
2563 old_scan,
2564 new_scan,
2565 phas,
2566 } => {
2567 self.update_scan_index(record_name, old_scan, new_scan, phas, phas);
2568 }
2569 crate::server::record::CommonFieldPutResult::PhasChanged {
2570 scan: s,
2571 old_phas,
2572 new_phas,
2573 } => {
2574 self.update_scan_index(record_name, s, s, old_phas, new_phas);
2575 }
2576 crate::server::record::CommonFieldPutResult::NoChange => {}
2577 }
2578
2579 // C `dbAccess.c::dbPutField:1263-1268` re-processes the
2580 // record on a put only when the put field is `pp(TRUE)` AND the
2581 // record is Passive (`SCAN == 0`). (The `PROC` field has its own
2582 // always-process intercept above, matching C's
2583 // `pfield == &precord->proc`; alarm-ack fields like ACKT/ACKS are
2584 // not `pp(TRUE)` so they fall out here, matching C's
2585 // `dbrType < DBR_PUT_ACKT`.) Processing on every put would
2586 // double-process scanned records and spuriously process puts to
2587 // non-`pp` fields (extra FLNK / monitors / device writes /
2588 // timestamps). `process_passive_fields()` is total and fail-safe: an
2589 // unmodeled type returns `&[]` (and warns once), so it processes on
2590 // `PROC` only — spurious processing is opt-in (a type must declare its
2591 // pp set), never the default.
2592 let should_process = {
2593 let instance = rec.read();
2594 put_drives_processing_of(&instance, &field)
2595 };
2596
2597 if !should_process {
2598 // No processing cycle, so C never raises `putf` (and this put did
2599 // not either). Report immediate (synchronous) completion to a
2600 // WRITE_NOTIFY caller.
2601 return Ok(crate::server::record::ProcessCompletion::Sync);
2602 }
2603
2604 // Set up the put-notify wait-set BEFORE processing. The wait-set
2605 // fires `completion_tx` only after the originating record AND
2606 // every FLNK/OUT chain target it triggers (sync or async) has
2607 // completed — C `dbNotify.c` `processNotify`/`dbNotifyCompletion`.
2608 // An occupied slot is QUEUED, never refused. C
2609 // `processNotifyCommon` answers "another processNotify owns the
2610 // record" with `ellSafeAdd` onto `restartList` (dbNotify.c:211-217)
2611 // and carries no refusal arm: `S_db_Blocked` is raised by a test
2612 // record's own put hook (test/ioc/db/xRecord.c:89), never by the
2613 // notify machinery, and `ECA_PUTCBINPROG` has exactly one sender in
2614 // all of base — the put-callback timeout at rsrv/camessage.c:1745.
2615 // Overwriting the slot instead would drop the prior Sender, waking
2616 // the prior caller's rx with RecvError that the CA dispatcher treats
2617 // as success, so the install goes through the one owner.
2618 //
2619 // A fire-and-forget put parks NOTHING — C builds a `putNotify`
2620 // only in `dbPutNotify`; `dbPutField` processes the record with
2621 // no notify state at all. It therefore neither conflicts with
2622 // nor disturbs a WRITE_NOTIFY already parked on the record.
2623 let parked = if let Some((completion_tx, completion_rx)) = notify_request.into_completion()
2624 {
2625 // Collect-then-act: clone the handle under a brief map read,
2626 // drop the map lock before the per-record write.
2627 let rec_arc = {
2628 let recs = self.inner.records.read();
2629 recs.get(record_name).cloned()
2630 };
2631 match rec_arc {
2632 Some(rec_arc) => match rec_arc.write().install_or_queue_notify(completion_tx) {
2633 Some(notify) => Some((notify, completion_rx)),
2634 // Queued behind the slot's current owner — see the PROC
2635 // path above. The replay drives the record; returning here
2636 // is what keeps one client request to one process cycle.
2637 None => {
2638 return Ok(crate::server::record::ProcessCompletion::from_signal(
2639 completion_rx,
2640 ));
2641 }
2642 },
2643 None => Some((
2644 crate::server::record::NotifyWaitSet::new(completion_tx),
2645 completion_rx,
2646 )),
2647 }
2648 } else {
2649 None
2650 };
2651
2652 // When a CA put writes directly to VAL on an INPUT record whose
2653 // VAL is the engineering value, the built-in `RVAL → VAL`
2654 // `convert()` must be suppressed for the put-driven process —
2655 // re-deriving VAL from a stale RVAL would clobber the value the
2656 // operator just wrote (the soft ai preset-NaN case, processing.rs
2657 // ~line 677). The framework expresses this by calling
2658 // `set_device_did_compute(true)`.
2659 //
2660 // This MUST be gated on `soft_channel_skips_convert()`. Output
2661 // records (mbbo/mbbo_direct/bo/ao) implement
2662 // `set_device_did_compute` as "skip the VAL → RVAL output
2663 // convert" — the OPPOSITE direction. C `mbboRecord.c::process`
2664 // (line 217), `mbboDirectRecord.c::process` (line 198) and
2665 // `boRecord.c::process` (line 207) call `convert()`
2666 // unconditionally on every non-pact process; a CA VAL-put on an
2667 // output record MUST recompute RVAL/ORAW. Suppressing it there
2668 // left RVAL/ORAW/ORBV stale. Output records return the default
2669 // `false` from `soft_channel_skips_convert()`, so this gate
2670 // matches the identical gates in processing.rs (line 694) and
2671 // record_instance.rs (line 1381).
2672 if field == "VAL" {
2673 // Collect-then-act: clone the handle under a brief map read, drop
2674 // the map lock before the per-record write.
2675 let rec_arc = {
2676 let recs = self.inner.records.read();
2677 recs.get(record_name).cloned()
2678 };
2679 if let Some(rec_arc) = rec_arc {
2680 let mut guard = rec_arc.write();
2681 if guard.record.soft_channel_skips_convert() {
2682 guard.record.set_device_did_compute(true);
2683 }
2684 }
2685 }
2686
2687 // Process the record after field put — through the single owner of C's
2688 // `dbPutField:1269-1277` decision, so an async-active record takes the
2689 // RPRO deferral instead of a doomed re-entrant `dbProcess`.
2690 let _ = self.put_driven_process_already_locked(record_name);
2691
2692 // Is the ORIGINATING record itself still async-pending? Its
2693 // wait-set membership is taken + `leave`d at its own completion
2694 // (sync-end, or later in `complete_async_record_inner`), so a
2695 // lingering `notify` on its instance means its device round-trip
2696 // is still in flight. This gates only the originating record's
2697 // PUTF clear — independent of whether downstream chain targets
2698 // are still pending.
2699 //
2700 // A fire-and-forget put parked nothing, and a `notify` it sees
2701 // on the instance belongs to some other caller's WRITE_NOTIFY —
2702 // not evidence about THIS put. Fall through to the guarded
2703 // clear; its `!is_processing()` gate already preserves PUTF
2704 // across an async-pending device round-trip.
2705 let originating_pending = want_notify && {
2706 let rec = self.inner.records.read();
2707 if let Some(rec_arc) = rec.get(record_name) {
2708 rec_arc.read().notify.is_some()
2709 } else {
2710 false
2711 }
2712 };
2713
2714 // C `recGbl.c::recGblFwdLink:302` clears `putf = FALSE` after
2715 // the forward-link dispatch — the marker only lives for the
2716 // duration of the put's processing cycle. For SYNCHRONOUS
2717 // completions (PACT was cleared by the time
2718 // `process_record_with_links` returns) clear it here. For
2719 // async-pending records, the clearing happens later in
2720 // `complete_async_record_inner` (which runs FLNK as part of
2721 // the completion path) so the PUTF marker survives the
2722 // device-write round trip.
2723 if !originating_pending {
2724 // Collect-then-act: clone the handle under a brief map read, drop
2725 // the map lock before the per-record write.
2726 let rec_arc = {
2727 let recs = self.inner.records.read();
2728 recs.get(record_name).cloned()
2729 };
2730 if let Some(rec_arc) = rec_arc {
2731 let mut guard = rec_arc.write();
2732 if !guard.is_processing() {
2733 guard.common.putf = false;
2734 }
2735 }
2736 }
2737
2738 // CA completion gates on the WHOLE chain, not just the
2739 // originating record: the put-notify must not report
2740 // done until every FLNK/OUT target it drove — including an async
2741 // FLNK target that the originating record's sync cycle merely
2742 // kicked off — has settled. `completed()` is true iff the
2743 // wait-set drained to zero during this call (fully synchronous
2744 // chain); otherwise the receiver fires later from the last
2745 // chain member's `leave`.
2746 match parked {
2747 Some((notify, completion_rx)) => {
2748 if notify.completed() {
2749 Ok(crate::server::record::ProcessCompletion::Sync)
2750 } else {
2751 Ok(crate::server::record::ProcessCompletion::from_signal(
2752 completion_rx,
2753 ))
2754 }
2755 }
2756 None => Ok(crate::server::record::ProcessCompletion::Sync),
2757 }
2758 }
2759
2760 /// Put a PV value without triggering process (for restore).
2761 ///
2762 /// Unlike the `dbPut`-analogue bodies (`put_pv`, `put_pv_and_post`) this
2763 /// entry accepts DBF link fields: its production caller is the autosave
2764 /// restore, whose C analogue (`reboot_restore`) writes via `dbPutField` —
2765 /// and `dbPutField` on a link field is `dbPutFieldLink`, a re-parse
2766 /// write that never processes. That is exactly this body's behavior
2767 /// (`put_common_field`'s INP/OUT/FLNK arms, no process), so the
2768 /// `check_not_link_field` refusal does not apply here.
2769 pub async fn put_pv_no_process(&self, name: &str, value: EpicsValue) -> CaResult<()> {
2770 let (base, field) = super::parse_pv_name(name);
2771 let field = field.to_ascii_uppercase();
2772
2773 let simple = self.inner.simple_pvs.lock().get(name).cloned();
2774 if let Some(pv) = simple {
2775 pv.set(value);
2776 return Ok(());
2777 }
2778
2779 // Records — alias-aware (epics-base PR #336).
2780 if let Some(rec) = self.get_record(base) {
2781 // `put_pv_no_process` is a public record-write API
2782 // (autosave restore). It must take the advisory write gate
2783 // (`dbScanLock` analogue) so an autosave restore cannot
2784 // land between the member writes of a QSRV atomic group or
2785 // a pvalink atomic scan epoch holding `lock_records`.
2786 // `base` is alias-resolved so an alias and its target
2787 // share one gate. Held until return.
2788 let canonical_base: String =
2789 self.resolve_alias(base).unwrap_or_else(|| base.to_string());
2790 let _record_gate = self.lock_record(&canonical_base);
2791
2792 let mut instance = rec.write();
2793
2794 // C `dbPutSpecial(paddr, 0)` — the pre-store pass, which
2795 // `dbPut` runs on every entry path including the `dbPutField`
2796 // this body models (autosave's `reboot_restore`). A non-zero
2797 // status returns before the store (`dbAccess.c:1350-1352`), so a
2798 // restore cannot write a field the record is currently refusing
2799 // (mbboDirect B0..B1F while OMSL=closed_loop,
2800 // `mbboDirectRecord.c:263-269`). The other two `dbPut` bodies in
2801 // this module already ran it; this one did not.
2802 instance.record.special(&field, false)?;
2803
2804 let prev_value = instance.record.get_field(&field);
2805 match instance.record.put_field(&field, value.clone()) {
2806 Ok(()) => {}
2807 Err(CaError::FieldNotFound(_)) => {
2808 instance.put_common_field(&field, value)?;
2809 }
2810 Err(e) => return Err(e),
2811 }
2812 // C `dbAccess.c::dbPut:1414` `if (isValueField) precord->udf =
2813 // FALSE;` — the same clear, through the same predicate, as the
2814 // three other put bodies. An autosave restore of VAL defines the
2815 // record in C, and a record that is left UDF here reports the
2816 // born UDF_ALARM on its first process cycle.
2817 if instance.record.is_udf_defining_put(&field) {
2818 instance.common.udf = 0;
2819 }
2820 // Invalidate metadata cache only if the metadata-class
2821 // field actually changed (faac1df1).
2822 instance.notify_field_written_if_changed(&field, prev_value.as_ref());
2823 // same SPC_AS parity as `put_pv` / the CA-write
2824 // path — autosave-style restores writing `.ASG` at IOC
2825 // startup must still trigger per-client re-eval.
2826 if field == "ASG" {
2827 crate::server::access_security::notify_asg_field_changed();
2828 }
2829 return Ok(());
2830 }
2831
2832 Err(CaError::ChannelNotFound(name.to_string()))
2833 }
2834}
2835
2836/// Project a decoded `DBR_CTRL_*` / `DBR_GR_*` snapshot's metadata fields
2837/// (display / control limits, enum labels) into the shadow-PV
2838/// [`PvMetadata`](crate::server::pv::PvMetadata) the CA gateway installs.
2839/// A non-metadata (TIME/STS) snapshot carries `None` in all three, which
2840/// clears the shadow metadata — but the gateway only ever feeds this a
2841/// control-class snapshot, matching C `setPvData` replacing the attribute
2842/// gdd wholesale from the control get/event.
2843fn metadata_from_snapshot(snapshot: &Snapshot) -> crate::server::pv::PvMetadata {
2844 crate::server::pv::PvMetadata {
2845 display: snapshot.display.clone(),
2846 control: snapshot.control.clone(),
2847 enums: snapshot.enums.clone(),
2848 }
2849}
2850
2851#[cfg(test)]
2852mod tests {
2853 use super::super::PvDatabase;
2854 use super::NotifyArrival;
2855 use crate::types::EpicsValue;
2856
2857 /// Regression: prior to fixing B1, `put_pv_and_post` walked only
2858 /// `inner.records` and returned `ChannelNotFound` for everything
2859 /// `add_pv`-registered. The CA gateway's monitor forwarder uses
2860 /// `add_pv` then expects `put_pv_and_post` to fan-out to
2861 /// downstream subscribers — without the simple-PV branch, every
2862 /// upstream event was silently dropped and the gateway delivered
2863 /// no monitors.
2864 #[epics_macros_rs::epics_test]
2865 async fn put_pv_and_post_handles_simple_pv() {
2866 let db = PvDatabase::new();
2867 db.add_pv("gw:test", EpicsValue::Double(0.0)).await.unwrap();
2868
2869 // Should NOT return ChannelNotFound.
2870 db.put_pv_and_post("gw:test", EpicsValue::Double(42.0))
2871 .await
2872 .expect("simple PV put_pv_and_post must succeed");
2873
2874 // Value actually landed.
2875 let pv = db.find_pv("gw:test").await.expect("PV exists");
2876 assert!(matches!(pv.get(), EpicsValue::Double(v) if v == 42.0));
2877 }
2878
2879 /// Regression: `get_pv`, `put_pv`, `put_pv_and_post`,
2880 /// and `put_pv_no_process` all bypassed `get_record` and walked
2881 /// `self.inner.records` directly, so alias names from epics-base
2882 /// PR #336 silently returned `ChannelNotFound`. A later fix closed
2883 /// `get_record` but the same defect was hiding in field_io.rs.
2884 /// All four CA-server-and-bridge entry points must accept aliases.
2885 #[epics_macros_rs::epics_test]
2886 async fn field_io_entry_points_accept_aliases() {
2887 use crate::server::records::ai::AiRecord;
2888
2889 let db = PvDatabase::new();
2890 db.add_record("CANON", Box::new(AiRecord::new(0.0)))
2891 .await
2892 .unwrap();
2893 db.add_alias("ALT", "CANON").await.unwrap();
2894
2895 // get_pv via alias
2896 db.put_pv("CANON.VAL", EpicsValue::Double(1.5))
2897 .await
2898 .unwrap();
2899 let v = db.get_pv("ALT.VAL").unwrap();
2900 assert!(matches!(v, EpicsValue::Double(x) if x == 1.5));
2901
2902 // put_pv via alias
2903 db.put_pv("ALT.VAL", EpicsValue::Double(7.0)).await.unwrap();
2904 let v = db.get_pv("CANON.VAL").unwrap();
2905 assert!(matches!(v, EpicsValue::Double(x) if x == 7.0));
2906
2907 // put_pv_and_post via alias
2908 db.put_pv_and_post("ALT.VAL", EpicsValue::Double(11.0))
2909 .await
2910 .unwrap();
2911 let v = db.get_pv("CANON.VAL").unwrap();
2912 assert!(matches!(v, EpicsValue::Double(x) if x == 11.0));
2913
2914 // put_pv_no_process via alias
2915 db.put_pv_no_process("ALT.VAL", EpicsValue::Double(13.0))
2916 .await
2917 .unwrap();
2918 let v = db.get_pv("ALT.VAL").unwrap();
2919 assert!(matches!(v, EpicsValue::Double(x) if x == 13.0));
2920 }
2921
2922 /// A `DBR_STRING` menu label written to a `DBF_MENU` field resolves
2923 /// against THAT field's own menu (C `dbConvert` `putStringMenu`), not the
2924 /// field-blind global table that `EpicsValue::convert_to` would consult.
2925 /// Covers the `put_pv` (`put_pv_inner`) and `put_pv_and_post` coercion
2926 /// sites; the CA field-put path (`put_record_field_from_ca_inner`) shares
2927 /// the identical `coerce_write_value` helper.
2928 #[epics_macros_rs::epics_test]
2929 async fn write_path_menu_label_resolves_against_field_menu() {
2930 use crate::server::records::sel::SelRecord;
2931
2932 let db = PvDatabase::new();
2933 db.add_record("SEL", Box::new(SelRecord::default()))
2934 .await
2935 .unwrap();
2936
2937 // put_pv (put_pv_inner): "Specified" is selSELM index 0, NOT the
2938 // menuFanout index 1 the global table would have returned.
2939 db.put_pv("SEL.SELM", EpicsValue::String("Specified".into()))
2940 .await
2941 .unwrap();
2942 assert_eq!(db.get_pv("SEL.SELM").unwrap(), EpicsValue::Enum(0));
2943
2944 // put_pv_and_post: a later choice, proving the whole menu.
2945 db.put_pv_and_post("SEL.SELM", EpicsValue::String("High Signal".into()))
2946 .await
2947 .unwrap();
2948 assert_eq!(db.get_pv("SEL.SELM").unwrap(), EpicsValue::Enum(1));
2949
2950 // A bare numeric string still resolves (C epicsParseUInt16 fallback).
2951 db.put_pv("SEL.SELM", EpicsValue::String("2".into()))
2952 .await
2953 .unwrap();
2954 assert_eq!(db.get_pv("SEL.SELM").unwrap(), EpicsValue::Enum(2));
2955 }
2956
2957 /// `set_pv_metadata` installs the upstream `DBR_CTRL_*` metadata on a
2958 /// shadow simple PV WITHOUT posting any event (the CA gateway's
2959 /// connect-time seed). A later GET-class read must then see the
2960 /// installed limits/units, and a `DBE_PROPERTY` subscriber must NOT
2961 /// have received anything (nothing *changed* yet). An unknown / record
2962 /// name is rejected with `ChannelNotFound`.
2963 #[epics_macros_rs::epics_test]
2964 async fn set_pv_metadata_installs_without_posting() {
2965 use crate::error::CaError;
2966 use crate::server::snapshot::{DisplayInfo, Snapshot};
2967 use crate::types::DbFieldType;
2968 use std::time::SystemTime;
2969
2970 let db = PvDatabase::new();
2971 db.add_pv("gw:meta", EpicsValue::Double(0.0)).await.unwrap();
2972
2973 // A DBE_PROPERTY subscriber attached BEFORE the seed — it must stay
2974 // empty, because seeding metadata is not a property *change*.
2975 const DBE_PROPERTY: u16 = 8;
2976 let pv = db.find_pv("gw:meta").await.expect("PV exists");
2977 let mut prop_rx = pv
2978 .add_subscriber(1, DbFieldType::Double, DBE_PROPERTY)
2979 .expect("subscriber added");
2980
2981 // Build a CTRL-class snapshot carrying display metadata.
2982 let mut ctrl = Snapshot::new(EpicsValue::Double(0.0), 0, 0, SystemTime::UNIX_EPOCH);
2983 ctrl.display = Some(DisplayInfo {
2984 units: "mm".into(),
2985 precision: 3,
2986 upper_disp_limit: 10.0,
2987 lower_disp_limit: -10.0,
2988 ..Default::default()
2989 });
2990
2991 db.set_pv_metadata("gw:meta", &ctrl)
2992 .await
2993 .expect("simple PV set_pv_metadata must succeed");
2994
2995 // The metadata landed on the shadow PV.
2996 let installed = pv.metadata();
2997 assert_eq!(
2998 installed.display.expect("display metadata installed").units,
2999 "mm"
3000 );
3001
3002 // No event was posted (seed != change).
3003 assert!(
3004 prop_rx.try_recv().is_err(),
3005 "set_pv_metadata must not post a DBE_PROPERTY event"
3006 );
3007
3008 // Unknown / non-simple PV is rejected.
3009 assert!(matches!(
3010 db.set_pv_metadata("no:such:pv", &ctrl).await,
3011 Err(CaError::ChannelNotFound(_))
3012 ));
3013 }
3014
3015 /// `post_pv_property` refreshes the shadow metadata AND posts a
3016 /// `DBE_PROPERTY` event carrying the supplied snapshot's metadata,
3017 /// upstream status/severity, and (undefined control-DBR) timestamp — to
3018 /// `DBE_PROPERTY` subscribers only. This is the DB-routing layer the
3019 /// gateway's property monitor drives on every upstream `DBE_PROPERTY`
3020 /// event. An unknown / record name is rejected with `ChannelNotFound`.
3021 #[epics_macros_rs::epics_test]
3022 async fn post_pv_property_refreshes_and_posts_property_event() {
3023 use crate::error::CaError;
3024 use crate::server::snapshot::{DisplayInfo, Snapshot};
3025 use crate::types::{DbFieldType, WallTime};
3026
3027 const DBE_PROPERTY: u16 = 8;
3028 const DBE_VALUE: u16 = 1;
3029 const MAJOR: u16 = 2;
3030 const HIGH: u16 = 3;
3031
3032 let db = PvDatabase::new();
3033 db.add_pv("gw:prop", EpicsValue::Double(0.0)).await.unwrap();
3034 let pv = db.find_pv("gw:prop").await.expect("PV exists");
3035
3036 let mut prop_rx = pv
3037 .add_subscriber(1, DbFieldType::Double, DBE_PROPERTY)
3038 .expect("property subscriber added");
3039 let mut val_rx = pv
3040 .add_subscriber(2, DbFieldType::Double, DBE_VALUE)
3041 .expect("value subscriber added");
3042
3043 // Upstream CTRL event: metadata + MAJOR/HIGH alarm + a fixed past
3044 // timestamp that is unmistakably not a fresh wall clock.
3045 let upstream_ts = WallTime::from_unix(2_000_000, 0);
3046 let mut ctrl = Snapshot::new(EpicsValue::Double(5.0), HIGH, MAJOR, upstream_ts);
3047 ctrl.display = Some(DisplayInfo {
3048 units: "V".into(),
3049 precision: 1,
3050 ..Default::default()
3051 });
3052
3053 db.post_pv_property("gw:prop", ctrl)
3054 .await
3055 .expect("simple PV post_pv_property must succeed");
3056
3057 // The metadata was refreshed on the shadow PV.
3058 assert_eq!(
3059 pv.metadata().display.expect("metadata refreshed").units,
3060 "V"
3061 );
3062
3063 // The DBE_PROPERTY subscriber received the metadata-bearing event,
3064 // with the upstream alarm and timestamp preserved.
3065 let ev = prop_rx
3066 .try_recv()
3067 .expect("DBE_PROPERTY subscriber receives the property event");
3068 assert_eq!(
3069 ev.snapshot
3070 .display
3071 .clone()
3072 .expect("event carries metadata")
3073 .units,
3074 "V"
3075 );
3076 assert_eq!(
3077 ev.snapshot.alarm.severity, MAJOR,
3078 "upstream severity preserved"
3079 );
3080 assert_eq!(ev.snapshot.alarm.status, HIGH, "upstream status preserved");
3081 assert_eq!(
3082 ev.snapshot.timestamp, upstream_ts,
3083 "control-DBR timestamp preserved, not a fresh wall clock"
3084 );
3085
3086 // The DBE_VALUE-only subscriber must NOT receive a property event.
3087 assert!(
3088 val_rx.try_recv().is_err(),
3089 "DBE_VALUE-only subscriber must not receive a property post"
3090 );
3091
3092 // Unknown / non-simple PV is rejected.
3093 let again = Snapshot::new(EpicsValue::Double(0.0), 0, 0, WallTime::UNIX_EPOCH);
3094 assert!(matches!(
3095 db.post_pv_property("no:such:pv", again).await,
3096 Err(CaError::ChannelNotFound(_))
3097 ));
3098 }
3099
3100 /// Regression: `put_record_field_from_ca` (the CA
3101 /// server's main put fast path) must accept aliases. Pre-fix it
3102 /// only consulted `inner.records` directly. Also exercises the
3103 /// canonical-name normalisation that protects subsequent
3104 /// `process_record_with_links` / `update_scan_index` calls.
3105 #[epics_macros_rs::epics_test]
3106 async fn put_record_field_from_ca_accepts_alias() {
3107 use crate::server::records::ai::AiRecord;
3108
3109 let db = PvDatabase::new();
3110 db.add_record("CANON", Box::new(AiRecord::new(0.0)))
3111 .await
3112 .unwrap();
3113 db.add_alias("ALT", "CANON").await.unwrap();
3114
3115 // Put VAL via the alias name.
3116 let _ = db
3117 .put_record_field_from_ca("ALT", "VAL", EpicsValue::Double(2.5))
3118 .await
3119 .expect("put via alias must succeed");
3120
3121 // Read back via canonical to confirm the value landed on the
3122 // right record.
3123 let v = db.get_pv("CANON.VAL").unwrap();
3124 assert!(matches!(v, EpicsValue::Double(x) if x == 2.5));
3125 }
3126
3127 /// Regression: a DBR_PUT_ACKT alarm-acknowledge put posts a record-wide
3128 /// DBE_ALARM (C `dbAccess.c:1299` putAckt
3129 /// `db_post_events(precord, NULL, DBE_ALARM)`), so an alarm-mask monitor
3130 /// on ANY field is notified — and a DBE_VALUE-only monitor is not.
3131 /// Pre-fix the ack field posted only itself with DBE_VALUE|DBE_LOG, so no
3132 /// alarm-mask subscriber observed the acknowledgement, and the post fired
3133 /// on every put regardless of whether `ackt` changed.
3134 #[epics_macros_rs::epics_test]
3135 async fn alarm_ack_put_posts_record_wide_dbe_alarm() {
3136 use crate::server::recgbl::EventMask;
3137 use crate::server::records::ai::AiRecord;
3138 use crate::types::DbFieldType;
3139
3140 let db = PvDatabase::new();
3141 db.add_record("A:REC", Box::new(AiRecord::new(1.0)))
3142 .await
3143 .unwrap();
3144 let rec = db.get_record("A:REC").expect("record exists");
3145
3146 let (mut alarm_rx, mut value_rx) = {
3147 let mut inst = rec.write();
3148 let a = inst
3149 .add_subscriber("VAL", 1, DbFieldType::Double, EventMask::ALARM.bits())
3150 .expect("alarm subscriber");
3151 let v = inst
3152 .add_subscriber("VAL", 2, DbFieldType::Double, EventMask::VALUE.bits())
3153 .expect("value subscriber");
3154 (a, v)
3155 };
3156
3157 // The client acknowledges through its ordinary VAL channel with a
3158 // DBR_PUT_ACKT request type (C `dbAccess.c:1331`). ACKT defaults YES
3159 // (true), so writing 0 (disable transient acknowledgement) is a real
3160 // change.
3161 db.put_alarm_ack_from_ca(
3162 "A:REC",
3163 "VAL",
3164 crate::server::record::AlarmAck::Transient,
3165 0,
3166 )
3167 .await
3168 .expect("ackt put");
3169
3170 // The alarm-mask monitor on VAL receives the record-wide DBE_ALARM.
3171 assert!(
3172 alarm_rx.try_recv().is_ok(),
3173 "DBE_ALARM subscriber must receive the record-wide alarm post"
3174 );
3175 // The DBE_VALUE-only monitor on VAL must NOT: VAL's value is unchanged.
3176 assert!(
3177 value_rx.try_recv().is_err(),
3178 "DBE_VALUE-only subscriber must not receive the alarm post"
3179 );
3180
3181 // Re-putting the same ACKT value is a no-op: C putAckt returns early
3182 // on an unchanged ackt, so no further alarm post fires.
3183 db.put_alarm_ack_from_ca(
3184 "A:REC",
3185 "VAL",
3186 crate::server::record::AlarmAck::Transient,
3187 0,
3188 )
3189 .await
3190 .expect("ackt re-put");
3191 assert!(
3192 alarm_rx.try_recv().is_err(),
3193 "unchanged ACKT must post nothing"
3194 );
3195 }
3196
3197 /// `post_property_fields` writes each field through the internal put and
3198 /// posts a `DBE_PROPERTY` monitor — the C
3199 /// `db_post_events(precord, &precord->val, DBE_PROPERTY)` that asyn's
3200 /// runtime enum re-propagation drives (devAsynInt32.c callbackEnum). A
3201 /// `DBE_VALUE`-only subscriber on the same field must NOT receive it:
3202 /// re-keying enum strings is a property change, not a value change.
3203 #[epics_macros_rs::epics_test]
3204 async fn post_property_fields_writes_and_posts_dbe_property_only() {
3205 use crate::server::recgbl::EventMask;
3206 use crate::server::records::mbbi::MbbiRecord;
3207 use crate::types::DbFieldType;
3208
3209 let db = PvDatabase::new();
3210 db.add_record("M:ENUM", Box::new(MbbiRecord::new(0)))
3211 .await
3212 .unwrap();
3213 let rec = db.get_record("M:ENUM").expect("record exists");
3214
3215 let (mut prop_rx, mut val_rx) = {
3216 let mut inst = rec.write();
3217 let p = inst
3218 .add_subscriber("ZRST", 1, DbFieldType::String, EventMask::PROPERTY.bits())
3219 .expect("property subscriber");
3220 let v = inst
3221 .add_subscriber("ZRST", 2, DbFieldType::String, EventMask::VALUE.bits())
3222 .expect("value subscriber");
3223 (p, v)
3224 };
3225
3226 let posted = db
3227 .post_property_fields(
3228 "M:ENUM",
3229 vec![("ZRST".to_string(), EpicsValue::String("LABEL".into()))],
3230 )
3231 .expect("post_property_fields succeeds");
3232 assert_eq!(posted, vec!["ZRST".to_string()]);
3233
3234 // The field landed on the record.
3235 assert_eq!(
3236 db.get_pv("M:ENUM.ZRST").unwrap(),
3237 EpicsValue::String("LABEL".into())
3238 );
3239
3240 // The DBE_PROPERTY subscriber received the event; the DBE_VALUE-only
3241 // subscriber did not (mask 0x08 vs 0x01, no intersection).
3242 assert!(
3243 prop_rx.try_recv().is_ok(),
3244 "DBE_PROPERTY subscriber must receive the property post"
3245 );
3246 assert!(
3247 val_rx.try_recv().is_err(),
3248 "DBE_VALUE-only subscriber must not receive a property post"
3249 );
3250 }
3251
3252 /// Regression: a direct CA put to a record whose value field VAL is NOT
3253 /// `pp(TRUE)` (calc / calcout / aSub) must still fire a DBE_VALUE monitor.
3254 /// C `dbAccess.c::dbPut:1408-1414` posts the value field immediately
3255 /// unless it is `pp(TRUE)`. The port previously suppressed the immediate
3256 /// post for every `VAL` and — with the `should_process` gate — skipped
3257 /// the reprocess cycle for a non-`pp` VAL, so the operator's write fired
3258 /// no monitor at all. calc's VAL is not in its `pp` field set, so the
3259 /// immediate post is the only event that can fire.
3260 #[epics_macros_rs::epics_test]
3261 async fn ca_put_to_non_pp_val_posts_monitor() {
3262 use crate::server::database::db_access::DbSubscription;
3263 use crate::server::records::calc::CalcRecord;
3264
3265 let db = PvDatabase::new();
3266 db.add_record("CALC1", Box::new(CalcRecord::new("0")))
3267 .await
3268 .unwrap();
3269
3270 let mut sub = DbSubscription::subscribe(&db, "CALC1.VAL")
3271 .await
3272 .expect("subscribe to CALC1.VAL");
3273
3274 db.put_record_field_from_ca("CALC1", "VAL", EpicsValue::Double(5.0))
3275 .await
3276 .expect("CA put to CALC1.VAL must succeed");
3277
3278 let got = crate::runtime::task::timeout(std::time::Duration::from_secs(1), sub.recv_f64())
3279 .await
3280 .expect("a DBE_VALUE monitor must fire for a direct VAL put to a non-pp record");
3281 assert_eq!(got, Some(5.0));
3282 }
3283
3284 /// R8-22 (record-field path): a record monitor whose event queue runs short
3285 /// of room during a burst must receive its EARLIER DISTINCT queued updates
3286 /// and then a tail entry carrying the latest value. C `db_queue_event_log`
3287 /// replaces only `*pLastLog` (`dbEvent.c:812-820`); the earlier entries stay
3288 /// queued and each is delivered by `event_read`.
3289 ///
3290 /// Each non-pp `VAL` put posts exactly one DBE_VALUE monitor with the put
3291 /// value and does NOT reprocess (see `ca_put_to_non_pp_val_posts_monitor`),
3292 /// so N distinct puts produce a strictly increasing 1..=N stream.
3293 ///
3294 /// Before the fix the producer parked the newest value in a side coalesce
3295 /// slot and `next_event`, finding it set, discarded the whole queued backlog
3296 /// — the burst came out as one event instead of {1..=appended-1, N}.
3297 #[epics_macros_rs::epics_test]
3298 async fn r8_22_db_burst_keeps_earlier_distinct_updates() {
3299 use crate::server::database::db_access::DbSubscription;
3300 use crate::server::event_queue::{event_que_size, events_per_que};
3301 use crate::server::records::calc::CalcRecord;
3302
3303 let db = PvDatabase::new();
3304 db.add_record("CALC1", Box::new(CalcRecord::new("0")))
3305 .await
3306 .unwrap();
3307 let mut sub = DbSubscription::subscribe(&db, "CALC1.VAL")
3308 .await
3309 .expect("subscribe to CALC1.VAL");
3310
3311 // With no consumer draining, the first `appended` puts take ring entries
3312 // and every later put replaces the tail entry in place.
3313 let appended = event_que_size() - events_per_que();
3314 let burst = appended + 40;
3315 for i in 1..=burst {
3316 db.put_record_field_from_ca("CALC1", "VAL", EpicsValue::Double(i as f64))
3317 .await
3318 .expect("CA put to CALC1.VAL must succeed");
3319 }
3320
3321 // Drain every immediately-available delivery; the recv past the
3322 // last event has nothing queued and times out, ending collection.
3323 let mut seq = Vec::new();
3324 while let Ok(Some(v)) =
3325 crate::runtime::task::timeout(std::time::Duration::from_millis(200), sub.recv_f64())
3326 .await
3327 {
3328 seq.push(v);
3329 }
3330 let want: Vec<f64> = (1..appended)
3331 .map(|i| i as f64)
3332 .chain(std::iter::once(burst as f64))
3333 .collect();
3334 assert_eq!(
3335 seq, want,
3336 "record burst delivery must be {{earlier distinct backlog…, coalesced tail}}"
3337 );
3338 }
3339
3340 /// The notify slot has exactly two states when a restart replay installs
3341 /// on it, and both must be handled by the one install owner.
3342 ///
3343 /// C cannot reach the occupied one: `restartCheck` (dbNotify.c:149-168)
3344 /// pops the head and assigns `precord->ppn = pfirst` under a single lock,
3345 /// so a restarted notify already owns the record before its callback runs.
3346 /// The port pops under the put gate and installs a moment later, both
3347 /// through the one gate-held owner. Assigning over the slot would drop the
3348 /// prior client's
3349 /// Sender, and its receiver then wakes with `RecvError`, which the CA
3350 /// dispatcher reads as a successful put-callback: the client is told its
3351 /// write completed on a cycle that never ran.
3352 #[epics_macros_rs::epics_test]
3353 async fn a_replay_onto_a_taken_notify_slot_queues_instead_of_overwriting() {
3354 use crate::server::records::ai::AiRecord;
3355
3356 let db = PvDatabase::new();
3357 db.add_record("REPLAY:TAKEN", Box::new(AiRecord::new(0.0)))
3358 .await
3359 .unwrap();
3360 let rec = db.get_record("REPLAY:TAKEN").expect("record exists");
3361
3362 let (owner_tx, _owner_rx) = crate::runtime::sync::oneshot::channel();
3363 let owner = rec
3364 .write()
3365 .install_or_queue_notify(owner_tx)
3366 .expect("a free slot installs");
3367
3368 let (client_tx, _client_rx) = crate::runtime::sync::oneshot::channel();
3369 assert!(
3370 db.install_notify_and_process_already_locked(
3371 "REPLAY:TAKEN",
3372 client_tx,
3373 NotifyArrival::Replay
3374 )
3375 .expect("the record is loaded")
3376 .is_none(),
3377 "the slot is owned, so the replay must drive no process cycle"
3378 );
3379 assert!(
3380 rec.read()
3381 .notify
3382 .as_ref()
3383 .is_some_and(|n| std::sync::Arc::ptr_eq(n, &owner)),
3384 "the owner's wait-set must survive the replay"
3385 );
3386 assert!(
3387 rec.read().notify_restart_pending(),
3388 "the replay must be queued behind the owner, not dropped"
3389 );
3390 }
3391
3392 /// The other boundary value of the same slot: free, so the replay installs
3393 /// and drives the cycle.
3394 #[epics_macros_rs::epics_test]
3395 async fn a_replay_onto_a_free_notify_slot_installs_and_drives_the_cycle() {
3396 use crate::server::records::ai::AiRecord;
3397
3398 let db = PvDatabase::new();
3399 db.add_record("REPLAY:FREE", Box::new(AiRecord::new(0.0)))
3400 .await
3401 .unwrap();
3402 let rec = db.get_record("REPLAY:FREE").expect("record exists");
3403
3404 let (client_tx, _client_rx) = crate::runtime::sync::oneshot::channel();
3405 assert!(
3406 db.install_notify_and_process_already_locked(
3407 "REPLAY:FREE",
3408 client_tx,
3409 NotifyArrival::Replay
3410 )
3411 .expect("the record is loaded")
3412 .is_some(),
3413 "a free slot installs and processes"
3414 );
3415 assert!(
3416 !rec.read().notify_restart_pending(),
3417 "nothing is queued when the replay took the slot"
3418 );
3419 }
3420}