1/*
  2 * SPDX-FileCopyrightText: 2026 Copyright (c) Contributors to the Eclipse Foundation
  3 *
  4 * See the NOTICE file(s) distributed with this work for additional
  5 * information regarding copyright ownership.
  6 *
  7 * This program and the accompanying materials are made available under the
  8 * terms of the Apache License Version 2.0 which is available at
  9 * https://www.apache.org/licenses/LICENSE-2.0
 10 *
 11 * SPDX-License-Identifier: Apache-2.0
 12 */
 13
 14//! Default communication plugin: applies `init_mode` policy.
 15//!
 16//! [`DefaultCommunicationPlugin`] is the only place `init_mode` policy is
 17//! applied. The framework itself never inspects `init_mode`, except for the
 18//! `Always`-mode startup call.
 19//!
 20//! Required mode semantics:
 21//!
 22//! | Mode        | `activate()`                   | first ECU request               | `trigger_detection()`   |
 23//! |-------------|--------------------------------|---------------------------------|-------------------------|
 24//! | `Always`    | joins or repeats current state | requests use current readiness  | detect while enabled    |
 25//! | `OnDemand`  | initializes the whole vehicle  | returns pending, triggers once  | detect while enabled    |
 26//! | `Disabled`  | rejected, no network activity  | returns pending, never triggers | detect while enabled    |
 27//!
 28//! Only the first two columns vary by mode. `trigger_detection()` never
 29//! consults `init_mode`, so under `Disabled` communication has to be enabled out
 30//! of band before anything can detect.
 31//!
 32//! Other plugins can be substituted via `Setup::with_communication_plugin`.
 33
 34use std::sync::Arc;
 35
 36use async_trait::async_trait;
 37use cda_interfaces::communication_control::{
 38    ActivationCause, CommunicationError, CommunicationGuard, CommunicationInitMode,
 39    CommunicationLifecycle, CommunicationOperation, CommunicationOperationFailure,
 40    CommunicationState, CommunicationVariantDetection, DetectionCause, VariantDetectionMode,
 41};
 42
 43use super::{CommunicationPlugin, CommunicationPluginBuilder};
 44use crate::lifecycle::{
 45    controller::CommunicationHandle,
 46    disable::{DisableError, DisableLease, DisableReason},
 47};
 48
 49/// Default communication plugin: applies `init_mode` policy on top of the
 50/// framework-serialized lifecycle handle.
[docs] 51/// [[ dimpl~default-communication-policy, Default deferred communication policy, dimpl ]]
 52pub struct DefaultCommunicationPlugin {
 53    handle: CommunicationHandle,
 54    mode: CommunicationInitMode,
 55}
 56
 57impl DefaultCommunicationPlugin {
 58    /// Creates the default plugin for the given `init_mode`. It delegates all
 59    /// lifecycle operations to the framework handle and worker.
 60    #[must_use]
 61    pub fn new(handle: CommunicationHandle, mode: CommunicationInitMode) -> Self {
 62        Self { handle, mode }
 63    }
 64}
 65
 66#[async_trait]
 67impl CommunicationPlugin for DefaultCommunicationPlugin {
 68    fn state(&self) -> CommunicationState {
 69        self.handle.state()
 70    }
 71
 72    fn acquire(&self) -> Result<CommunicationGuard, CommunicationError> {
 73        self.handle.acquire()
 74    }
 75
 76    async fn activate(
 77        &self,
 78        cause: ActivationCause,
 79    ) -> Result<CommunicationState, CommunicationOperationFailure> {
 80        match self.mode {
 81            CommunicationInitMode::Always | CommunicationInitMode::OnDemand => {
 82                tracing::debug!(?cause, mode = ?self.mode, "Activating communication");
 83                self.handle.enable_and_detect().await
 84            }
 85            // `Disabled` authorizes no bring-up through this plugin.
 86            CommunicationInitMode::Disabled => {
 87                tracing::debug!(?cause, "activate rejected: init_mode is Disabled");
 88                Err(CommunicationOperationFailure::ModeDisabled {
 89                    operation: CommunicationOperation::EnableAndDetect,
 90                })
 91            }
 92        }
 93    }
 94
 95    fn request_activate(&self, cause: ActivationCause) -> CommunicationState {
 96        match self.mode {
 97            CommunicationInitMode::Always | CommunicationInitMode::OnDemand => {
 98                tracing::debug!(?cause, mode = ?self.mode, "Requesting activation (non-blocking)");
 99                self.handle.request_enable_and_detect()
100            }
101            // See the `Disabled` arm of `activate` above.
102            CommunicationInitMode::Disabled => {
103                tracing::debug!(?cause, "request_activate rejected: init_mode is Disabled");
104                self.handle.state()
105            }
106        }
107    }
108
109    async fn trigger_detection(
110        &self,
111        cause: DetectionCause,
112    ) -> Result<CommunicationState, CommunicationOperationFailure> {
113        // TODO: topology rediscovery (#490).
114
115        tracing::debug!(?cause, mode = ?self.mode, "Triggering whole-vehicle detection");
116
117        self.handle.redetect().await
118    }
119
120    async fn disable(&self, reason: DisableReason) -> Result<DisableLease, DisableError> {
121        self.handle.disable(reason).await
122    }
123
124    async fn register_lifecycle_hook(
125        &self,
126        initializer: Arc<dyn CommunicationLifecycle>,
127    ) -> Result<(), CommunicationOperationFailure> {
128        self.handle.register_lifecycle_hook(initializer).await
129    }
130
131    async fn register_variant_detection(
132        &self,
133        detector: Arc<dyn CommunicationVariantDetection>,
134    ) -> Result<(), CommunicationOperationFailure> {
135        self.handle.register_variant_detection(detector).await
136    }
137
138    fn variant_detection(&self) -> VariantDetectionMode {
139        self.handle.variant_detection()
140    }
141}
142
143#[async_trait]
144impl cda_interfaces::Shutdown for DefaultCommunicationPlugin {
145    async fn shutdown(&self) {
146        self.handle.shutdown().await;
147    }
148}
149
150/// Factory for the default communication plugin.
151#[derive(Default)]
152pub struct DefaultCommunicationPluginBuilder;
153
154impl CommunicationPluginBuilder for DefaultCommunicationPluginBuilder {
155    type Plugin = DefaultCommunicationPlugin;
156    type Error = std::convert::Infallible;
157
158    fn build(
159        self,
160        handle: CommunicationHandle,
161        mode: CommunicationInitMode,
162    ) -> impl Future<Output = Result<Self::Plugin, Self::Error>> + Send {
163        std::future::ready(Ok(DefaultCommunicationPlugin::new(handle, mode)))
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
170
171    use async_trait::async_trait;
172    use cda_interfaces::communication_control::{
173        CommControlError, TransportControl, TransportState,
174    };
175
176    use super::*;
177    use crate::lifecycle::controller::test_utils::communication_handle_new;
178
179    struct RecordingControl {
180        enable_count: AtomicUsize,
181        disable_count: AtomicUsize,
182        fail_enable: AtomicBool,
183    }
184
185    #[async_trait]
186    impl TransportControl for RecordingControl {
187        async fn enable(&self) -> Result<(), CommControlError> {
188            if self.fail_enable.load(Ordering::Relaxed) {
189                return Err(CommControlError::InitFailed("simulated failure".to_owned()));
190            }
191            self.enable_count.fetch_add(1, Ordering::Relaxed);
192            Ok(())
193        }
194        async fn disable(&self) -> Result<(), CommControlError> {
195            self.disable_count.fetch_add(1, Ordering::Relaxed);
196            Ok(())
197        }
198        async fn state(&self) -> TransportState {
199            TransportState::Disabled
200        }
201    }
202
203    /// Returns the plugin, its transport, and a second handle to the same
204    /// runtime.
205    ///
206    /// The extra handle stands in for whatever brings communication up out of
207    /// band, which under `init_mode = Disabled` is the only way to reach an
208    /// enabled transport.
209    fn plugin_with_mode(
210        mode: CommunicationInitMode,
211    ) -> (
212        DefaultCommunicationPlugin,
213        Arc<RecordingControl>,
214        CommunicationHandle,
215    ) {
216        let control = Arc::new(RecordingControl {
217            enable_count: AtomicUsize::new(0),
218            disable_count: AtomicUsize::new(0),
219            fail_enable: AtomicBool::new(false),
220        });
221        let handle = communication_handle_new(Arc::clone(&control) as Arc<dyn TransportControl>);
222        (
223            DefaultCommunicationPlugin::new(handle.clone(), mode),
224            control,
225            handle,
226        )
227    }
228
229    /// Both admitting modes bring the transport up for the cause they admit.
230    /// `Disabled` refuses, covered separately below.
[docs]231    /// [[ test~deferred-admitting-modes-activate, Eager and on-demand modes admit communication activation, test ]]
232    #[tokio::test]
233    async fn admitting_modes_activate_the_transport() {
234        for (mode, cause) in [
235            (CommunicationInitMode::Always, ActivationCause::Startup),
236            (CommunicationInitMode::OnDemand, ActivationCause::Explicit),
237        ] {
238            let (plugin, control, _handle) = plugin_with_mode(mode);
239            assert_eq!(
240                plugin.activate(cause).await,
241                Ok(CommunicationState::Enabled),
242                "{mode:?}"
243            );
244            assert_eq!(control.enable_count.load(Ordering::Relaxed), 1, "{mode:?}");
245        }
246    }
247
248    /// Ordinary `activate()` in `Disabled` mode is rejected without any network
249    /// activity. The transport must never see an `enable()` call.
[docs]250    /// [[ test~deferred-disabled-rejects-activation, Disabled mode rejects communication activation, test ]]
251    #[tokio::test]
252    async fn disabled_mode_activate_rejected_without_network_activity() {
253        let (plugin, control, _handle) = plugin_with_mode(CommunicationInitMode::Disabled);
254        assert_eq!(
255            plugin.activate(ActivationCause::Explicit).await,
256            Err(CommunicationOperationFailure::ModeDisabled {
257                operation: CommunicationOperation::EnableAndDetect
258            })
259        );
260        assert_eq!(
261            control.enable_count.load(Ordering::Relaxed),
262            0,
263            "Disabled mode must not touch the transport on an ordinary activate()"
264        );
265        assert_eq!(plugin.state(), CommunicationState::Disabled);
266    }
267
[docs]268    /// [[ test~deferred-request-activation-policy, Nonblocking activation follows the configured initialization mode, test ]]
269    #[tokio::test]
270    async fn request_activate_follows_init_mode() {
271        for mode in [
272            CommunicationInitMode::Always,
273            CommunicationInitMode::OnDemand,
274        ] {
275            let (plugin, control, handle) = plugin_with_mode(mode);
276            assert!(matches!(
277                plugin.request_activate(ActivationCause::DiagnosticRequest),
278                CommunicationState::Enabling(CommunicationOperation::EnableAndDetect)
279                    | CommunicationState::Enabled
280            ));
281            tokio::time::timeout(std::time::Duration::from_secs(5), async {
282                while handle.state() != CommunicationState::Enabled {
283                    tokio::task::yield_now().await;
284                }
285            })
286            .await
287            .expect("activation did not complete");
288            assert_eq!(control.enable_count.load(Ordering::Relaxed), 1, "{mode:?}");
289        }
290
291        let (plugin, control, _handle) = plugin_with_mode(CommunicationInitMode::Disabled);
292        assert_eq!(
293            plugin.request_activate(ActivationCause::DiagnosticRequest),
294            CommunicationState::Disabled
295        );
296        tokio::task::yield_now().await;
297        assert_eq!(control.enable_count.load(Ordering::Relaxed), 0);
298    }
299
300    /// `trigger_detection()` never brings a transport up, so `Disabled` mode is
301    /// inert on every path this plugin exposes: widening a detection request
302    /// into an activation would produce exactly the unauthorized network traffic
303    /// the mode exists to prevent.
304    #[tokio::test]
305    async fn disabled_mode_is_inert_on_every_path_including_detection() {
306        let (plugin, control, handle) = plugin_with_mode(CommunicationInitMode::Disabled);
307
308        // Ordinary activation is inert.
309        assert!(plugin.activate(ActivationCause::Explicit).await.is_err());
310        assert_eq!(control.enable_count.load(Ordering::Relaxed), 0);
311        assert_eq!(plugin.state(), CommunicationState::Disabled);
312
313        // So is detection: refused without touching the transport.
314        assert_eq!(
315            plugin.trigger_detection(DetectionCause::Explicit).await,
316            Err(CommunicationOperationFailure::TransitionFailure {
317                operation: CommunicationOperation::Detect
318            })
319        );
320        assert_eq!(
321            control.enable_count.load(Ordering::Relaxed),
322            0,
323            "a detection request must never enable the transport"
324        );
325        assert_eq!(plugin.state(), CommunicationState::Disabled);
326
327        // Once something else enables communication, detection is served.
328        assert_eq!(handle.enable().await, Ok(CommunicationState::Enabled));
329        assert_eq!(
330            plugin.trigger_detection(DetectionCause::Explicit).await,
331            Ok(CommunicationState::Enabled)
332        );
333        assert_eq!(control.enable_count.load(Ordering::Relaxed), 1);
334    }
335
336    /// From `Enabled`, `trigger_detection` detects without touching the
337    /// transport, in every `init_mode`.
338    #[tokio::test]
339    async fn trigger_detection_redetects_while_enabled_in_every_init_mode() {
340        for mode in [
341            CommunicationInitMode::Always,
342            CommunicationInitMode::OnDemand,
343            CommunicationInitMode::Disabled,
344        ] {
345            let (plugin, control, handle) = plugin_with_mode(mode);
346
347            // Out of band, since `Disabled` authorizes no bring-up here.
348            assert_eq!(
349                handle.enable_and_detect().await,
350                Ok(CommunicationState::Enabled)
351            );
352            assert_eq!(control.enable_count.load(Ordering::Relaxed), 1, "{mode:?}");
353
354            assert_eq!(
355                plugin.trigger_detection(DetectionCause::Explicit).await,
356                Ok(CommunicationState::Enabled),
357                "{mode:?}: detection must be allowed while already enabled"
358            );
359            assert_eq!(
360                control.enable_count.load(Ordering::Relaxed),
361                1,
362                "{mode:?}: re-detection must not re-enable the transport"
363            );
364            assert_eq!(control.disable_count.load(Ordering::Relaxed), 0, "{mode:?}");
365        }
366    }
367
368    /// `Error(_)` is a downed transport too, so detection is refused there as
369    /// well. Recovery is an activation, which only `activate()` performs.
370    #[tokio::test]
371    async fn trigger_detection_refused_from_error_in_every_init_mode() {
372        for mode in [
373            CommunicationInitMode::Always,
374            CommunicationInitMode::OnDemand,
375            CommunicationInitMode::Disabled,
376        ] {
377            let (plugin, control, handle) = plugin_with_mode(mode);
378            control.fail_enable.store(true, Ordering::Relaxed);
379            assert!(handle.enable_and_detect().await.is_err(), "{mode:?}");
380            assert!(matches!(plugin.state(), CommunicationState::Error(_)));
381
382            control.fail_enable.store(false, Ordering::Relaxed);
383            assert_eq!(
384                plugin.trigger_detection(DetectionCause::Explicit).await,
385                Err(CommunicationOperationFailure::TransitionFailure {
386                    operation: CommunicationOperation::Detect
387                }),
388                "{mode:?}: detection must not recover a broken transport"
389            );
390            assert_eq!(
391                control.enable_count.load(Ordering::Relaxed),
392                0,
393                "{mode:?}: no successful enable may have happened"
394            );
395        }
396    }
397
398    /// An exclusive disable lease outranks detection. The request is refused
399    /// rather than re-enabling a transport somebody else took down.
400    #[tokio::test]
401    async fn trigger_detection_rejected_while_disable_lease_held() {
402        let (plugin, control, _handle) = plugin_with_mode(CommunicationInitMode::OnDemand);
403        assert_eq!(
404            plugin.activate(ActivationCause::Explicit).await,
405            Ok(CommunicationState::Enabled)
406        );
407        let lease = plugin.disable(DisableReason::RuntimeUpdate).await.unwrap();
408
409        assert_eq!(
410            plugin.trigger_detection(DetectionCause::Explicit).await,
411            Err(CommunicationOperationFailure::DisableLeaseHeld {
412                operation: CommunicationOperation::Detect
413            })
414        );
415        assert_eq!(control.enable_count.load(Ordering::Relaxed), 1);
416
417        assert_eq!(lease.release().await, Ok(CommunicationState::Enabled));
418    }
419
420    /// Repeated `trigger_detection()` calls join the in-flight variant detection
421    /// rather than starting a second one.
422    #[tokio::test]
423    async fn repeated_trigger_detection_coalesces() {
424        let (plugin, control, handle) = plugin_with_mode(CommunicationInitMode::Disabled);
425        assert_eq!(
426            handle.enable_and_detect().await,
427            Ok(CommunicationState::Enabled)
428        );
429
430        let plugin = Arc::new(plugin);
431        let a = {
432            let plugin = Arc::clone(&plugin);
433            tokio::spawn(async move { plugin.trigger_detection(DetectionCause::Explicit).await })
434        };
435        let b = {
436            let plugin = Arc::clone(&plugin);
437            tokio::spawn(async move { plugin.trigger_detection(DetectionCause::Explicit).await })
438        };
439        assert_eq!(a.await.unwrap(), Ok(CommunicationState::Enabled));
440        assert_eq!(b.await.unwrap(), Ok(CommunicationState::Enabled));
441        assert_eq!(control.enable_count.load(Ordering::Relaxed), 1);
442    }
443}