use std::future::Future;
use std::path::Path;
use std::pin::Pin;
use std::sync::atomic::AtomicBool;
use bacnet_client::client::BACnetClient;
use bacnet_transport::port::TransportPort;
use serde_json::{Map, Value, json};
use super::audit::{Audit, Context, utc_now};
use super::ledger::{Entry, Ledger, Origin};
use super::lock::{WorkspaceLock, lock};
use super::plan::{self, PRE_READ, PreRead};
use super::send::{find, item_json, send};
use super::{Gate, Level, Outcome, Planned, Preview, WriteSpec, run_id};
use crate::engine::{Halt, Reader, Wanted, unreachable};
use crate::limits::RequestLimits;
pub const PASSIVE: &str = "passive session: nothing is transmitted";
pub const STALE: &str = "changed since preview, review again";
pub const UNKNOWN_PRE_IMAGE: &str =
"the old value was not read before the write, so there is nothing to put back";
pub const ENDED: &str = "the session ended first";
pub type Waiting<'a> = Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
pub trait Caller: Sync {
fn level(&self) -> Level;
fn refusal(&self) -> Option<String>;
fn fields(&self) -> Map<String, Value> {
Map::new()
}
fn gone(&self) -> bool {
false
}
fn going(&self) -> Waiting<'_> {
Box::pin(std::future::pending())
}
fn ledger_changed<'a>(&'a self, _entries: &'a [Entry]) -> Waiting<'a> {
Box::pin(async {})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sent {
pub label: String,
pub outcome: Outcome,
pub entry: Option<u64>,
}
pub struct Desk {
ledger: Option<Ledger>,
audit: Option<Audit>,
_lock: Option<WorkspaceLock>,
missing: String,
}
pub const NO_WORKSPACE: &str =
"write sessions need a profile: its workspace holds the ledger and the audit log";
impl Desk {
pub fn open(workspace: Option<&Path>, context: Context, origin: Origin) -> Self {
let mut desk = Self {
ledger: None,
audit: None,
_lock: None,
missing: String::new(),
};
let Some(dir) = workspace else {
desk.missing = NO_WORKSPACE.into();
return desk;
};
match Audit::open(dir, context) {
Ok(audit) => {
desk.audit =
Some(audit.written_by(origin.front_end.clone(), origin.principal.clone()));
}
Err(error) => {
tracing::error!("{error}");
desk.missing = error;
return desk;
}
}
let front_end = origin.front_end.name().to_string();
match lock(dir, &front_end).and_then(|held| Ok((held, Ledger::open(dir)?))) {
Ok((held, ledger)) => {
desk._lock = Some(held);
desk.ledger = Some(ledger.written_by(origin));
}
Err(error) => {
tracing::warn!("{error}");
desk.missing = error;
}
}
desk
}
pub fn ledger(&self) -> Option<&Ledger> {
self.ledger.as_ref()
}
pub fn keep_all(&mut self) -> bool {
match &mut self.ledger {
Some(ledger) => {
ledger.keep_all();
self.save()
}
None => false,
}
}
pub fn entries(&self) -> Vec<Entry> {
self.ledger
.as_ref()
.map(|ledger| ledger.entries().to_vec())
.unwrap_or_default()
}
pub fn missing(&self) -> Option<&str> {
self.ledger.is_none().then_some(self.missing.as_str())
}
pub async fn record(&self, caller: &dyn Caller, event: &str, mut fields: Value) {
if let Value::Object(map) = &mut fields {
for (key, value) in caller.fields() {
map.entry(key).or_insert(value);
}
}
if let Some(audit) = &self.audit
&& let Err(error) = audit.record(caller.level(), event, fields).await
{
tracing::error!("{error}");
}
}
pub fn save(&self) -> bool {
let Some(ledger) = &self.ledger else {
return false;
};
let saved = ledger.save();
if let Err(error) = &saved {
tracing::error!("{error}");
}
saved.is_ok()
}
pub async fn settle_abandoned(&mut self, caller: &dyn Caller) {
let Some(ledger) = &mut self.ledger else {
return;
};
let settled = ledger.settle_abandoned();
if settled.is_empty() {
return;
}
self.save();
for entry in settled {
let fields = json!({
"target": entry.label(),
"priority": entry.priority,
"outcome": { "result": "unknown", "why": "session ended" },
});
self.record(caller, "unknown", fields).await;
}
}
pub fn write_refusal(
&self,
caller: &dyn Caller,
preview: &Preview,
dangerous: bool,
) -> Option<String> {
if let Some(why) = caller.refusal() {
return Some(why);
}
if let Some(why) = self.missing() {
return Some(why.to_string());
}
if let Gate::Blocked(why) = &preview.gate {
return Some(why.clone());
}
let six = preview.items.iter().any(|i| i.priority == Some(6));
if six && !dangerous {
return Some(plan::PRIORITY_6.into());
}
if preview.batch {
let specs: Vec<&WriteSpec> = preview.items.iter().map(|i| &i.spec).collect();
return plan::repeated_target(&specs);
}
None
}
pub async fn prepare<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
rate: &RequestLimits,
caller: &dyn Caller,
specs: &[WriteSpec],
batch: bool,
dangerous: bool,
) -> Result<Preview, String> {
let ledger = self.ledger.as_ref();
prepare(client, rate, caller, specs, batch, dangerous, ledger).await
}
pub(crate) async fn not_sent(
&self,
caller: &dyn Caller,
preview: &Preview,
why: &str,
) -> Vec<Sent> {
let outcome = Outcome::NotSent(why.into());
for item in &preview.items {
self.audit_write(caller, preview, item, "write", &outcome)
.await;
}
preview
.items
.iter()
.map(|item| Sent {
label: item.label(),
outcome: outcome.clone(),
entry: None,
})
.collect()
}
pub async fn execute<T: TransportPort + 'static>(
&mut self,
client: &BACnetClient<T>,
rate: &RequestLimits,
caller: &dyn Caller,
preview: &Preview,
dangerous: bool,
) -> Vec<Sent> {
if caller.gone() {
return self.not_sent(caller, preview, ENDED).await;
}
let specs: Vec<WriteSpec> = preview.items.iter().map(|i| i.spec.clone()).collect();
let fresh = self
.prepare(client, rate, caller, &specs, preview.batch, dangerous)
.await;
if !fresh.as_ref().is_ok_and(|fresh| preview.unchanged(fresh)) {
let items: Vec<Value> = preview.items.iter().map(item_json).collect();
let detail = fresh
.err()
.unwrap_or_else(|| "the pre-image changed".into());
let fields = json!({ "items": items, "why": "stale", "detail": detail });
self.record(caller, "declined", fields).await;
return preview
.items
.iter()
.map(|item| Sent {
label: item.label(),
outcome: Outcome::NotSent(STALE.into()),
entry: None,
})
.collect();
}
self.commit(client, rate, caller, preview).await
}
pub async fn commit<T: TransportPort + 'static>(
&mut self,
client: &BACnetClient<T>,
rate: &RequestLimits,
caller: &dyn Caller,
preview: &Preview,
) -> Vec<Sent> {
if let Some(why) = caller.refusal() {
return self.not_sent(caller, preview, &why).await;
}
let Some(ledger) = &mut self.ledger else {
return Vec::new();
};
let backup = ledger.entries().to_vec();
let at = utc_now();
let ids: Vec<u64> = preview
.items
.iter()
.map(|item| ledger.begin(item, run_id(), &at))
.collect();
if !self.save() {
if let Some(ledger) = &mut self.ledger {
for id in &ids {
ledger.finish(*id, Some(false), &backup);
}
}
return self
.not_sent(caller, preview, "the ledger could not be saved")
.await;
}
for item in &preview.items {
self.audit_write(
caller,
preview,
item,
"sending",
&Outcome::NotSent(String::new()),
)
.await;
}
caller.ledger_changed(&self.entries()).await;
let turn = rate.turn(preview.device).await;
let outcomes = send(client, preview).await;
drop(turn);
for ((item, id), outcome) in preview.items.iter().zip(&ids).zip(&outcomes) {
if let Some(ledger) = &mut self.ledger {
ledger.finish(*id, outcome.applied(), &backup);
}
let event = if item.relinquishes() {
"relinquish"
} else {
"write"
};
self.audit_write(caller, preview, item, event, outcome)
.await;
}
self.save();
let held = |id: &u64| {
self.ledger
.as_ref()
.is_some_and(|l| l.entries().iter().any(|e| e.id == *id))
};
preview
.items
.iter()
.zip(ids)
.zip(outcomes)
.map(|((item, id), outcome)| Sent {
label: item.label(),
outcome,
entry: held(&id).then_some(id),
})
.collect()
}
pub(crate) async fn audit_write(
&self,
caller: &dyn Caller,
preview: &Preview,
item: &Planned,
event: &str,
outcome: &Outcome,
) {
let mut fields = item_json(item);
if let Value::Object(map) = &mut fields {
map.insert("service".into(), json!(preview.service()));
map.insert("tier".into(), json!(preview.gate.name()));
if event != "sending" {
map.insert("outcome".into(), outcome.json());
}
}
self.record(caller, event, fields).await;
}
pub async fn revert<T: TransportPort + 'static>(
&mut self,
client: &BACnetClient<T>,
rate: &RequestLimits,
caller: &dyn Caller,
chosen: impl Fn(&Entry) -> bool,
) -> Vec<(Entry, Outcome)> {
let entries: Vec<Entry> = self
.ledger
.as_ref()
.map(Ledger::newest_first)
.unwrap_or_default()
.into_iter()
.filter(|entry| chosen(entry))
.collect();
let mut done = Vec::with_capacity(entries.len());
for entry in entries {
let outcome = match (entry.undo(), entry.object()) {
_ if caller.gone() => Outcome::NotSent(ENDED.into()),
(Some(undo), Some(object)) => {
find(client, entry.device).await;
let turn = rate.turn(entry.device).await;
let result = client
.write_property_to_device(
entry.device,
object,
entry.property(),
entry.index,
undo.bytes.clone(),
entry.priority,
)
.await;
drop(turn);
Outcome::of(&result)
}
_ => Outcome::NotSent(UNKNOWN_PRE_IMAGE.into()),
};
if outcome.applied() == Some(true) {
if let Some(ledger) = &mut self.ledger {
ledger.remove(entry.id);
}
self.save();
}
let fields = json!({
"target": entry.label(),
"service": "WriteProperty",
"priority": entry.priority,
"params": { "value": entry.undo().map(|u| u.text) },
"pre_image": { "written": entry.written.text },
"outcome": outcome.json(),
});
self.record(caller, "revert", fields).await;
done.push((entry, outcome));
}
done
}
}
pub async fn prepare<T: TransportPort + 'static>(
client: &BACnetClient<T>,
rate: &RequestLimits,
caller: &dyn Caller,
specs: &[WriteSpec],
batch: bool,
dangerous: bool,
ledger: Option<&Ledger>,
) -> Result<Preview, String> {
let device = specs.first().ok_or("nothing to write")?.device;
if specs.iter().any(|spec| spec.device != device) {
return Err("a batch goes to one device".into());
}
let cancel = AtomicBool::new(false);
let mut reader = Reader {
client,
device,
rate,
cancel: &cancel,
rpm: true,
};
let mut reads = Vec::with_capacity(specs.len());
for spec in specs {
let mut wanted = PRE_READ.to_vec();
if !wanted.contains(&spec.property) && spec.index.is_none() {
wanted.push(spec.property);
}
let read = tokio::select! {
biased;
() = caller.going() => return Err("the session ended".into()),
read = reader.properties(spec.object, Wanted::List(&wanted)) => read,
};
let rows = match read {
Ok((rows, _)) => rows,
Err(Halt::Device(error)) => {
return Err(format!("reading {}: {error}", spec.object));
}
Err(Halt::Cancelled) => return Err("cancelled".into()),
};
let element = match spec.index {
None | Some(0) => None,
Some(index) => {
let read = tokio::select! {
biased;
() = caller.going() => return Err("the session ended".into()),
read = reader.rp(spec.object, spec.property, Some(index)) => read,
};
Some(match read {
Ok(bytes) => Ok(bytes),
Err(Halt::Device(error)) if unreachable(&error) => {
return Err(format!("reading {}: {error}", spec.object));
}
Err(Halt::Device(error)) => Err(error.to_string()),
Err(Halt::Cancelled) => return Err("cancelled".into()),
})
}
};
reads.push(PreRead { rows, element });
}
let ours = |spec: &WriteSpec, priority, now: &[u8]| {
ledger.is_some_and(|l| l.owns(spec, priority, now))
};
plan::preview(specs, &reads, ours, dangerous, batch)
}
#[cfg(test)]
#[path = "desk_tests.rs"]
mod tests;