Skip to main content

bacnet_server/server/
reinitialize.rs

1//! ReinitializeDevice (Clause 16.4): the application handler that carries a
2//! request out, the requester context it receives, the builder setters for
3//! the handler and the password, and the guarded reply the full server and
4//! endpoint sessions share.
5
6use std::panic::{catch_unwind, AssertUnwindSafe};
7
8use bacnet_transport::port::{DirectScIdentity, TransportProvenance};
9use bacnet_types::enums::ReinitializedState;
10
11use super::requests::confirmed_response::error_apdu_from_error;
12use super::*;
13
14/// Carries out a ReinitializeDevice request (Clause 16.4) on a full server or
15/// an endpoint session.
16///
17/// It is called only for a request that decoded, passed its password check
18/// and names a state the clause defines. `Ok(())` sends the SimpleACK; an
19/// `Err` is sent in its place. The rules:
20///
21/// - **Reply first, restart later.** The SimpleACK goes out only after the
22///   handler returns, so it must not restart the device inline. Accept or
23///   refuse, prepare, and schedule the restart (or the backup or restore
24///   step) to run after the reply: Clause 16.4.2 has the device acknowledge
25///   the request before it shuts down.
26/// - **No reply signal yet.** Nothing tells the application when the
27///   SimpleACK has left (#1565), so any delay before restarting is best
28///   effort, and on MS/TP it can need longer: a postponed reply waits for
29///   the token. [`BACnetServer::stop`] seals responses and aborts request
30///   tasks before joining them, so a restart path that stops the server
31///   before the reply has left drops the SimpleACK. On a full server, accepted
32///   WARMSTART/COLDSTART ends DISABLE_INITIATION immediately (Clause 16.1.2),
33///   cancels its timer and resumes held COV, even if the reply later fails.
34/// - **Be quick.** It runs synchronously on a runtime worker with the object
35///   database write-locked, so every other request waits for it. Hand slow
36///   work, such as writing backup files, to a task. On an endpoint session a
37///   blocking handler stalls the whole session.
38/// - **Database edits are raw.** On a full server, changes made through the
39///   `&mut ObjectDatabase` skip what [`BACnetServer::write_local`] adds
40///   around a write: the Object_Name uniqueness check, the COV fanout, the
41///   event pass, Schedule and Command follow-ups, and the Audit record. A
42///   change that needs them, as ACTIVATE_CHANGES work on Network Port or
43///   Device properties may, belongs in `write_local`, called after the
44///   reply from a task the handler schedules (for example by sending the
45///   change to the code that holds the server).
46/// - **Gate the requester.** Without `reinit_password` any peer reaches the
47///   handler. [`MutationPolicy`](crate::mutation::MutationPolicy) and the
48///   mutation authorizer don't cover ReinitializeDevice, so check the
49///   [`ReinitializeContext`] to restrict sources.
50/// - **Refuse with [`Error::Protocol`].** Its class and code go back in an
51///   Error PDU, for example DEVICE / CONFIGURATION_IN_PROGRESS (Clause
52///   16.4.1.3.1). Once the handler has run, the request may no longer be
53///   rejected (Clause 20.1.8), so an [`Error::Reject`], or any error other
54///   than `Protocol` or `Structured`, is sent as SERVICES / OTHER. A panic is
55///   caught (with unwind builds) and also answered SERVICES / OTHER, since
56///   the handler may have partly acted.
57pub type ReinitializeHandler =
58    Arc<dyn Fn(&ReinitializeContext, &mut ObjectDatabase) -> Result<(), Error> + Send + Sync>;
59
60/// The ReinitializeDevice request a [`ReinitializeHandler`] is asked to carry
61/// out, and who sent it.
62///
63/// The addresses are claims, not an authenticated operator. For routed
64/// traffic `source_mac` is the last router, so restrict routed requesters by
65/// `source_network`; only [`direct_sc_identity`](Self::direct_sc_identity) is
66/// verified. Neither the mutation policy nor the mutation authorizer sees
67/// this service, so a handler that serves only some peers checks them here.
68///
69/// The server builds the context; [`new`](Self::new) lets a test build one
70/// to call a handler directly.
71#[derive(Clone, PartialEq, Eq)]
72#[non_exhaustive]
73pub struct ReinitializeContext {
74    /// The requested state, one of those Clause 16.4 defines.
75    pub state: ReinitializedState,
76    /// Immediate data-link peer (or router) address.
77    pub source_mac: MacAddr,
78    /// Originating NPDU source when the request was routed.
79    pub source_network: Option<NpduAddress>,
80    /// Immutable ingress snapshot, retained after the admitting connection closes.
81    pub provenance: TransportProvenance,
82    /// Confirmed-request invoke identifier.
83    pub invoke_id: u8,
84}
85
86impl ReinitializeContext {
87    /// A context with these fields, for calling a handler outside the server,
88    /// such as in its unit tests. Outside `bacnet-transport` only
89    /// [`TransportProvenance::unverified`] can be built.
90    pub fn new(
91        state: ReinitializedState,
92        source_mac: MacAddr,
93        source_network: Option<NpduAddress>,
94        provenance: TransportProvenance,
95        invoke_id: u8,
96    ) -> Self {
97        Self {
98            state,
99            source_mac,
100            source_network,
101            provenance,
102            invoke_id,
103        }
104    }
105
106    /// Original verified direct-SC leaf and incarnation, separate from claims.
107    pub fn direct_sc_identity(&self) -> Option<DirectScIdentity> {
108        self.provenance.direct_sc_identity()
109    }
110}
111
112impl std::fmt::Debug for ReinitializeContext {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        f.debug_struct("ReinitializeContext")
115            .field("state", &self.state)
116            .field("source_mac_len", &self.source_mac.len())
117            .field(
118                "source_network",
119                &self
120                    .source_network
121                    .as_ref()
122                    .map(|s| (s.network, s.mac_address.len())),
123            )
124            .field("provenance", &self.provenance)
125            .field("invoke_id", &self.invoke_id)
126            .finish_non_exhaustive()
127    }
128}
129
130/// Where a ReinitializeDevice request came from: its [`ReinitializeContext`]
131/// short of the state, which decoding supplies.
132pub(in crate::server) struct Requester {
133    source_mac: MacAddr,
134    source_network: Option<NpduAddress>,
135    provenance: TransportProvenance,
136}
137
138impl Requester {
139    pub(in crate::server) fn new(
140        source_mac: &[u8],
141        source_network: Option<&NpduAddress>,
142        provenance: TransportProvenance,
143    ) -> Self {
144        Self {
145            source_mac: MacAddr::from_slice(source_mac),
146            source_network: source_network.cloned(),
147            provenance,
148        }
149    }
150}
151
152/// Answer a ReinitializeDevice request: decode it, check its password and its
153/// state, then run `handler` with the database write-locked. A valid request
154/// with no handler is refused with SERVICES / SERVICE_REQUEST_DENIED.
155///
156/// `still_open` runs once the write lock is held, so an owner that closed
157/// while the request waited refuses it before the handler runs. The guard is
158/// released before `on_accepted` runs. That callback runs synchronously on
159/// success, before reply construction and with no intervening suspension.
160pub(in crate::server) async fn response(
161    db: &RwLock<ObjectDatabase>,
162    request: &ConfirmedRequestPdu,
163    password: &Option<String>,
164    handler: Option<&ReinitializeHandler>,
165    requester: Requester,
166    still_open: impl FnOnce() -> Result<(), Error>,
167    on_accepted: impl FnOnce(ReinitializedState),
168) -> Apdu {
169    let outcome = match handlers::handle_reinitialize_device(&request.service_request, password) {
170        Ok(state) => match handler {
171            Some(handler) => {
172                let context = ReinitializeContext::new(
173                    state,
174                    requester.source_mac,
175                    requester.source_network,
176                    requester.provenance,
177                    request.invoke_id,
178                );
179                let mut db = db.write().await;
180                still_open()
181                    .and_then(|()| invoke(handler, &context, &mut db))
182                    .map(|()| state)
183            }
184            None => Err(services_error(ErrorCode::SERVICE_REQUEST_DENIED)),
185        },
186        Err(error) => Err(error),
187    };
188    match outcome {
189        Ok(state) => {
190            on_accepted(state);
191            Apdu::SimpleAck(SimpleAck {
192                invoke_id: request.invoke_id,
193                service_choice: request.service_choice,
194            })
195        }
196        Err(error) => error_apdu_from_error(request.invoke_id, request.service_choice, &error),
197    }
198}
199
200/// Full-server acceptance also owns the DCC restart transition. Acquire the
201/// timer slot before the handler's database lock, as DCC replacement/expiry do.
202pub(in crate::server) async fn server_response<T: TransportPort + 'static>(
203    services: &super::request_services::RequestServices<T>,
204    request: &ConfirmedRequestPdu,
205    requester: Requester,
206) -> Apdu {
207    let cov_resume = services.cov_table.read().await.timed().clone();
208    let mut slot = services.dcc_timer.lock().await;
209    let mut restart_accepted = false;
210    let reply = response(
211        &services.db,
212        request,
213        &services.config.reinit_password,
214        services.config.on_reinitialize.as_ref(),
215        requester,
216        || Ok(()),
217        |state| {
218            if matches!(
219                state,
220                ReinitializedState::WARMSTART | ReinitializedState::COLDSTART
221            ) {
222                super::dcc_timer::enable_for_restart(&slot, &services.comm_state, &cov_resume);
223                restart_accepted = true;
224            }
225        },
226    )
227    .await;
228    if restart_accepted {
229        // The commit already happened. Cancellation during this borrowed join
230        // retains the aborted handle for later replacement/shutdown cleanup.
231        super::dcc_timer::cancel(&mut slot).await;
232    }
233    reply
234}
235
236/// Run `handler` under a panic guard. Execution has begun once it is called,
237/// so the request may no longer draw a Reject (Clause 20.1.8): a panic, a
238/// Reject, or an error with no class and code is answered SERVICES / OTHER.
239fn invoke(
240    handler: &ReinitializeHandler,
241    context: &ReinitializeContext,
242    db: &mut ObjectDatabase,
243) -> Result<(), Error> {
244    match catch_unwind(AssertUnwindSafe(|| handler(context, db))) {
245        Ok(Ok(())) => Ok(()),
246        Ok(Err(error @ (Error::Protocol { .. } | Error::Structured { .. }))) => Err(error),
247        Ok(Err(error)) => {
248            debug!(%error, "ReinitializeDevice handler error answered SERVICES / OTHER");
249            Err(services_error(ErrorCode::OTHER))
250        }
251        Err(_) => {
252            warn!(
253                state = context.state.to_raw(),
254                invoke_id = context.invoke_id,
255                "ReinitializeDevice handler panicked; answered SERVICES / OTHER"
256            );
257            Err(services_error(ErrorCode::OTHER))
258        }
259    }
260}
261
262fn services_error(code: ErrorCode) -> Error {
263    Error::Protocol {
264        class: ErrorClass::SERVICES.to_raw() as u32,
265        code: code.to_raw() as u32,
266    }
267}
268
269impl<T: TransportPort + 'static> ServerBuilder<T> {
270    /// Set the password required for ReinitializeDevice requests.
271    pub fn reinit_password(mut self, password: impl Into<String>) -> Self {
272        self.config.reinit_password = Some(password.into());
273        self
274    }
275
276    /// Set the ReinitializeDevice handler. It must reply before restarting and
277    /// stay quick; see [`ReinitializeHandler`].
278    pub fn on_reinitialize<F>(mut self, handler: F) -> Self
279    where
280        F: Fn(&ReinitializeContext, &mut ObjectDatabase) -> Result<(), Error>
281            + Send
282            + Sync
283            + 'static,
284    {
285        self.config.on_reinitialize = Some(Arc::new(handler));
286        self
287    }
288}
289
290impl BipServerBuilder {
291    /// Set the password required for ReinitializeDevice requests.
292    pub fn reinit_password(mut self, password: impl Into<String>) -> Self {
293        self.config.reinit_password = Some(password.into());
294        self
295    }
296
297    /// Set the ReinitializeDevice handler. It must reply before restarting and
298    /// stay quick; see [`ReinitializeHandler`].
299    pub fn on_reinitialize<F>(mut self, handler: F) -> Self
300    where
301        F: Fn(&ReinitializeContext, &mut ObjectDatabase) -> Result<(), Error>
302            + Send
303            + Sync
304            + 'static,
305    {
306        self.config.on_reinitialize = Some(Arc::new(handler));
307        self
308    }
309}
310
311#[cfg(feature = "sc-tls")]
312impl super::ScServerBuilder {
313    /// Set the password required for ReinitializeDevice requests.
314    pub fn reinit_password(mut self, password: impl Into<String>) -> Self {
315        self.config.reinit_password = Some(password.into());
316        self
317    }
318
319    /// Set the ReinitializeDevice handler. It must reply before restarting and
320    /// stay quick; see [`ReinitializeHandler`].
321    pub fn on_reinitialize<F>(mut self, handler: F) -> Self
322    where
323        F: Fn(&ReinitializeContext, &mut ObjectDatabase) -> Result<(), Error>
324            + Send
325            + Sync
326            + 'static,
327    {
328        self.config.on_reinitialize = Some(Arc::new(handler));
329        self
330    }
331}