1use core::ffi::c_void;
10use nros_core::lifecycle::{
11 LifecycleState, LifecycleTransition, TransitionResult, apply_transition, can_transition,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum LifecycleError {
17 InvalidTransition {
19 from: LifecycleState,
21 transition: LifecycleTransition,
23 },
24 CallbackFailed {
26 transition: LifecycleTransition,
28 result: TransitionResult,
30 },
31 NodeFinalized,
33}
34
35pub type LifecycleCallbackFn = fn() -> TransitionResult;
41
42pub struct LifecyclePollingNode {
60 state: LifecycleState,
61 on_configure: Option<LifecycleCallbackFn>,
62 on_activate: Option<LifecycleCallbackFn>,
63 on_deactivate: Option<LifecycleCallbackFn>,
64 on_cleanup: Option<LifecycleCallbackFn>,
65 on_shutdown: Option<LifecycleCallbackFn>,
66 on_error: Option<LifecycleCallbackFn>,
67}
68
69impl LifecyclePollingNode {
70 pub const fn new() -> Self {
74 Self {
75 state: LifecycleState::Unconfigured,
76 on_configure: None,
77 on_activate: None,
78 on_deactivate: None,
79 on_cleanup: None,
80 on_shutdown: None,
81 on_error: None,
82 }
83 }
84
85 pub const fn state(&self) -> LifecycleState {
87 self.state
88 }
89
90 pub fn trigger_transition(
92 &mut self,
93 transition: LifecycleTransition,
94 ) -> Result<LifecycleState, LifecycleError> {
95 if self.state.is_terminal() {
96 return Err(LifecycleError::NodeFinalized);
97 }
98
99 if !can_transition(self.state, transition) {
100 return Err(LifecycleError::InvalidTransition {
101 from: self.state,
102 transition,
103 });
104 }
105
106 let result = self.invoke_callback(transition);
107 self.state = apply_transition(self.state, transition, result);
108
109 if result == TransitionResult::Success {
110 Ok(self.state)
111 } else {
112 Err(LifecycleError::CallbackFailed { transition, result })
113 }
114 }
115
116 pub fn configure(&mut self) -> Result<LifecycleState, LifecycleError> {
118 self.trigger_transition(LifecycleTransition::Configure)
119 }
120
121 pub fn activate(&mut self) -> Result<LifecycleState, LifecycleError> {
123 self.trigger_transition(LifecycleTransition::Activate)
124 }
125
126 pub fn deactivate(&mut self) -> Result<LifecycleState, LifecycleError> {
128 self.trigger_transition(LifecycleTransition::Deactivate)
129 }
130
131 pub fn cleanup(&mut self) -> Result<LifecycleState, LifecycleError> {
133 self.trigger_transition(LifecycleTransition::Cleanup)
134 }
135
136 pub fn shutdown(&mut self) -> Result<LifecycleState, LifecycleError> {
138 let transition = match self.state {
139 LifecycleState::Unconfigured => LifecycleTransition::ShutdownUnconfigured,
140 LifecycleState::Inactive => LifecycleTransition::ShutdownInactive,
141 LifecycleState::Active => LifecycleTransition::ShutdownActive,
142 LifecycleState::Finalized => return Err(LifecycleError::NodeFinalized),
143 LifecycleState::ErrorProcessing => {
144 return Err(LifecycleError::InvalidTransition {
145 from: self.state,
146 transition: LifecycleTransition::ShutdownUnconfigured,
147 });
148 }
149 };
150 self.trigger_transition(transition)
151 }
152
153 pub fn bring_up(&mut self) -> Result<LifecycleState, LifecycleError> {
155 self.configure()?;
156 self.activate()
157 }
158
159 pub fn register_on_configure(&mut self, cb: LifecycleCallbackFn) {
161 self.on_configure = Some(cb);
162 }
163
164 pub fn register_on_activate(&mut self, cb: LifecycleCallbackFn) {
166 self.on_activate = Some(cb);
167 }
168
169 pub fn register_on_deactivate(&mut self, cb: LifecycleCallbackFn) {
171 self.on_deactivate = Some(cb);
172 }
173
174 pub fn register_on_cleanup(&mut self, cb: LifecycleCallbackFn) {
176 self.on_cleanup = Some(cb);
177 }
178
179 pub fn register_on_shutdown(&mut self, cb: LifecycleCallbackFn) {
181 self.on_shutdown = Some(cb);
182 }
183
184 pub fn register_on_error(&mut self, cb: LifecycleCallbackFn) {
186 self.on_error = Some(cb);
187 }
188
189 fn invoke_callback(&mut self, transition: LifecycleTransition) -> TransitionResult {
190 let cb = match transition {
191 LifecycleTransition::Configure => self.on_configure,
192 LifecycleTransition::Activate => self.on_activate,
193 LifecycleTransition::Deactivate => self.on_deactivate,
194 LifecycleTransition::Cleanup => self.on_cleanup,
195 LifecycleTransition::ShutdownUnconfigured
196 | LifecycleTransition::ShutdownInactive
197 | LifecycleTransition::ShutdownActive => self.on_shutdown,
198 LifecycleTransition::ErrorRecovery => self.on_error,
199 };
200
201 match cb {
202 Some(f) => f(),
203 None => TransitionResult::Success,
204 }
205 }
206}
207
208impl Default for LifecyclePollingNode {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214pub type LifecycleCallbackFnCtx = unsafe extern "C" fn(ctx: *mut c_void) -> u8;
225
226pub struct LifecyclePollingNodeCtx {
234 state: LifecycleState,
235 on_configure: Option<LifecycleCallbackFnCtx>,
236 on_activate: Option<LifecycleCallbackFnCtx>,
237 on_deactivate: Option<LifecycleCallbackFnCtx>,
238 on_cleanup: Option<LifecycleCallbackFnCtx>,
239 on_shutdown: Option<LifecycleCallbackFnCtx>,
240 on_error: Option<LifecycleCallbackFnCtx>,
241 context: *mut c_void,
242}
243
244impl LifecyclePollingNodeCtx {
248 pub const fn new() -> Self {
250 Self {
251 state: LifecycleState::Unconfigured,
252 on_configure: None,
253 on_activate: None,
254 on_deactivate: None,
255 on_cleanup: None,
256 on_shutdown: None,
257 on_error: None,
258 context: core::ptr::null_mut(),
259 }
260 }
261
262 pub const fn state(&self) -> LifecycleState {
264 self.state
265 }
266
267 pub fn set_context(&mut self, ctx: *mut c_void) {
269 self.context = ctx;
270 }
271
272 pub fn context(&self) -> *mut c_void {
274 self.context
275 }
276
277 pub fn register(&mut self, slot: LifecycleCallbackSlot, cb: Option<LifecycleCallbackFnCtx>) {
279 match slot {
280 LifecycleCallbackSlot::Configure => self.on_configure = cb,
281 LifecycleCallbackSlot::Activate => self.on_activate = cb,
282 LifecycleCallbackSlot::Deactivate => self.on_deactivate = cb,
283 LifecycleCallbackSlot::Cleanup => self.on_cleanup = cb,
284 LifecycleCallbackSlot::Shutdown => self.on_shutdown = cb,
285 LifecycleCallbackSlot::Error => self.on_error = cb,
286 }
287 }
288
289 pub fn clear_callbacks(&mut self) {
291 self.on_configure = None;
292 self.on_activate = None;
293 self.on_deactivate = None;
294 self.on_cleanup = None;
295 self.on_shutdown = None;
296 self.on_error = None;
297 self.context = core::ptr::null_mut();
298 }
299
300 pub fn finalize(&mut self) {
302 self.state = LifecycleState::Finalized;
303 }
304
305 pub unsafe fn trigger_transition(
312 &mut self,
313 transition: LifecycleTransition,
314 ) -> Result<LifecycleState, LifecycleError> {
315 if self.state.is_terminal() {
316 return Err(LifecycleError::NodeFinalized);
317 }
318
319 if !can_transition(self.state, transition) {
320 return Err(LifecycleError::InvalidTransition {
321 from: self.state,
322 transition,
323 });
324 }
325
326 let cb = match transition {
327 LifecycleTransition::Configure => self.on_configure,
328 LifecycleTransition::Activate => self.on_activate,
329 LifecycleTransition::Deactivate => self.on_deactivate,
330 LifecycleTransition::Cleanup => self.on_cleanup,
331 LifecycleTransition::ShutdownUnconfigured
332 | LifecycleTransition::ShutdownInactive
333 | LifecycleTransition::ShutdownActive => self.on_shutdown,
334 LifecycleTransition::ErrorRecovery => self.on_error,
335 };
336
337 let result = match cb {
338 Some(f) => {
339 let raw = unsafe { f(self.context) };
340 TransitionResult::from_u8(raw).unwrap_or(TransitionResult::Error)
341 }
342 None => TransitionResult::Success,
343 };
344
345 self.state = apply_transition(self.state, transition, result);
346
347 if result == TransitionResult::Success {
348 Ok(self.state)
349 } else {
350 Err(LifecycleError::CallbackFailed { transition, result })
351 }
352 }
353}
354
355impl Default for LifecyclePollingNodeCtx {
356 fn default() -> Self {
357 Self::new()
358 }
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363pub enum LifecycleCallbackSlot {
364 Configure,
366 Activate,
368 Deactivate,
370 Cleanup,
372 Shutdown,
374 Error,
376}
377
378pub trait LifecycleCallbacks {
409 fn on_configure(&mut self) -> TransitionResult {
411 TransitionResult::Success
412 }
413 fn on_activate(&mut self) -> TransitionResult {
415 TransitionResult::Success
416 }
417 fn on_deactivate(&mut self) -> TransitionResult {
419 TransitionResult::Success
420 }
421 fn on_cleanup(&mut self) -> TransitionResult {
423 TransitionResult::Success
424 }
425 fn on_shutdown(&mut self) -> TransitionResult {
427 TransitionResult::Success
428 }
429 fn on_error(&mut self) -> TransitionResult {
431 TransitionResult::Failure
432 }
433}
434
435pub mod trampolines {
441 use super::{LifecycleCallbacks, TransitionResult};
442 use core::ffi::c_void;
443
444 macro_rules! trampoline {
445 ($name:ident, $method:ident) => {
446 pub unsafe extern "C" fn $name<T: LifecycleCallbacks>(ctx: *mut c_void) -> u8 {
451 if ctx.is_null() {
452 return TransitionResult::Error as u8;
453 }
454 let node = unsafe { &mut *(ctx as *mut T) };
458 node.$method() as u8
459 }
460 };
461 }
462
463 trampoline!(on_configure, on_configure);
464 trampoline!(on_activate, on_activate);
465 trampoline!(on_deactivate, on_deactivate);
466 trampoline!(on_cleanup, on_cleanup);
467 trampoline!(on_shutdown, on_shutdown);
468 trampoline!(on_error, on_error);
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 struct DemoNode {
480 configured: bool,
481 }
482 impl LifecycleCallbacks for DemoNode {
483 fn on_configure(&mut self) -> TransitionResult {
484 self.configured = true;
485 TransitionResult::Success
486 }
487 fn on_error(&mut self) -> TransitionResult {
488 TransitionResult::Error
489 }
490 }
492
493 #[test]
494 fn trampoline_dispatches_to_trait_impl_and_defaults() {
495 let mut n = DemoNode { configured: false };
496 let ctx = &mut n as *mut DemoNode as *mut c_void;
497
498 let rc = unsafe { trampolines::on_configure::<DemoNode>(ctx) };
500 assert_eq!(rc, TransitionResult::Success as u8);
501 assert!(n.configured);
502
503 let rc = unsafe { trampolines::on_activate::<DemoNode>(&mut n as *mut _ as *mut c_void) };
505 assert_eq!(rc, TransitionResult::Success as u8);
506
507 let rc = unsafe { trampolines::on_error::<DemoNode>(&mut n as *mut _ as *mut c_void) };
509 assert_eq!(rc, TransitionResult::Error as u8);
510
511 let rc = unsafe { trampolines::on_configure::<DemoNode>(core::ptr::null_mut()) };
513 assert_eq!(rc, TransitionResult::Error as u8);
514 }
515
516 #[test]
517 fn ctx_state_machine_drives_trait_through_a_transition() {
518 let mut n = DemoNode { configured: false };
521 let mut sm = LifecyclePollingNodeCtx::new();
522 sm.set_context(&mut n as *mut DemoNode as *mut c_void);
523 sm.register(
524 LifecycleCallbackSlot::Configure,
525 Some(trampolines::on_configure::<DemoNode>),
526 );
527
528 let new_state =
530 unsafe { sm.trigger_transition(LifecycleTransition::Configure) }.expect("configure");
531
532 assert!(n.configured, "the trait's on_configure ran");
533 assert_eq!(new_state, LifecycleState::Inactive);
534 }
535
536 #[test]
537 fn raw_extern_c_callbacks_drive_the_ctx_state_machine() {
538 unsafe extern "C" fn cb_success(_ctx: *mut c_void) -> u8 {
543 TransitionResult::Success as u8
544 }
545 let mut sm = LifecyclePollingNodeCtx::new();
546 sm.register(LifecycleCallbackSlot::Configure, Some(cb_success));
547 sm.register(LifecycleCallbackSlot::Activate, Some(cb_success));
548
549 let s =
551 unsafe { sm.trigger_transition(LifecycleTransition::Configure) }.expect("configure");
552 assert_eq!(s, LifecycleState::Inactive);
553 let s = unsafe { sm.trigger_transition(LifecycleTransition::Activate) }.expect("activate");
554 assert_eq!(s, LifecycleState::Active);
555 }
556
557 #[test]
562 fn test_polling_node_initial_state() {
563 let node = LifecyclePollingNode::new();
564 assert_eq!(node.state(), LifecycleState::Unconfigured);
565 }
566
567 #[test]
568 fn test_polling_node_default() {
569 let node = LifecyclePollingNode::default();
570 assert_eq!(node.state(), LifecycleState::Unconfigured);
571 }
572
573 #[test]
574 fn test_polling_node_happy_path() {
575 let mut node = LifecyclePollingNode::new();
576
577 assert_eq!(node.configure().unwrap(), LifecycleState::Inactive);
578 assert_eq!(node.activate().unwrap(), LifecycleState::Active);
579 assert_eq!(node.deactivate().unwrap(), LifecycleState::Inactive);
580 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
581 }
582
583 #[test]
584 fn test_polling_node_cleanup_cycle() {
585 let mut node = LifecyclePollingNode::new();
586
587 node.configure().unwrap();
588 assert_eq!(node.cleanup().unwrap(), LifecycleState::Unconfigured);
589
590 assert_eq!(node.configure().unwrap(), LifecycleState::Inactive);
592 }
593
594 #[test]
595 fn test_polling_node_invalid_transition() {
596 let mut node = LifecyclePollingNode::new();
597
598 let err = node.activate().unwrap_err();
599 assert_eq!(
600 err,
601 LifecycleError::InvalidTransition {
602 from: LifecycleState::Unconfigured,
603 transition: LifecycleTransition::Activate,
604 }
605 );
606 }
607
608 #[test]
609 fn test_polling_node_finalized_rejection() {
610 let mut node = LifecyclePollingNode::new();
611 node.shutdown().unwrap();
612
613 assert_eq!(node.configure().unwrap_err(), LifecycleError::NodeFinalized);
614 assert_eq!(node.shutdown().unwrap_err(), LifecycleError::NodeFinalized);
615 }
616
617 fn on_configure_success() -> TransitionResult {
618 TransitionResult::Success
619 }
620
621 fn on_configure_failure() -> TransitionResult {
622 TransitionResult::Failure
623 }
624
625 fn on_configure_error() -> TransitionResult {
626 TransitionResult::Error
627 }
628
629 #[test]
630 fn test_polling_node_callback_success() {
631 let mut node = LifecyclePollingNode::new();
632 node.register_on_configure(on_configure_success);
633
634 assert_eq!(node.configure().unwrap(), LifecycleState::Inactive);
635 }
636
637 #[test]
638 fn test_polling_node_callback_failure_rollback() {
639 let mut node = LifecyclePollingNode::new();
640 node.register_on_configure(on_configure_failure);
641
642 let err = node.configure().unwrap_err();
643 assert_eq!(
644 err,
645 LifecycleError::CallbackFailed {
646 transition: LifecycleTransition::Configure,
647 result: TransitionResult::Failure,
648 }
649 );
650 assert_eq!(node.state(), LifecycleState::Unconfigured);
652 }
653
654 #[test]
655 fn test_polling_node_callback_error() {
656 let mut node = LifecyclePollingNode::new();
657 node.register_on_configure(on_configure_error);
658
659 let err = node.configure().unwrap_err();
660 assert_eq!(
661 err,
662 LifecycleError::CallbackFailed {
663 transition: LifecycleTransition::Configure,
664 result: TransitionResult::Error,
665 }
666 );
667 assert_eq!(node.state(), LifecycleState::ErrorProcessing);
669 }
670
671 #[test]
672 fn test_polling_node_error_recovery() {
673 let mut node = LifecyclePollingNode::new();
674 node.register_on_configure(on_configure_error);
675
676 let _ = node.configure();
677 assert_eq!(node.state(), LifecycleState::ErrorProcessing);
678
679 assert!(node.shutdown().is_err());
681
682 node.register_on_error(on_configure_success);
684 let result = node.trigger_transition(LifecycleTransition::ErrorRecovery);
685 assert_eq!(result.unwrap(), LifecycleState::Unconfigured);
686 }
687
688 #[test]
689 fn test_polling_node_bring_up() {
690 let mut node = LifecyclePollingNode::new();
691 assert_eq!(node.bring_up().unwrap(), LifecycleState::Active);
692 }
693
694 #[test]
695 fn test_polling_node_bring_up_stops_on_configure_failure() {
696 let mut node = LifecyclePollingNode::new();
697 node.register_on_configure(on_configure_failure);
698
699 let err = node.bring_up().unwrap_err();
700 assert_eq!(
701 err,
702 LifecycleError::CallbackFailed {
703 transition: LifecycleTransition::Configure,
704 result: TransitionResult::Failure,
705 }
706 );
707 assert_eq!(node.state(), LifecycleState::Unconfigured);
709 }
710
711 #[test]
712 fn test_polling_node_shutdown_from_each_state() {
713 let mut node = LifecyclePollingNode::new();
715 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
716
717 let mut node = LifecyclePollingNode::new();
719 node.configure().unwrap();
720 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
721
722 let mut node = LifecyclePollingNode::new();
724 node.bring_up().unwrap();
725 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
726 }
727
728 #[test]
729 fn test_polling_node_no_callback_defaults_success() {
730 let mut node = LifecyclePollingNode::new();
732 assert_eq!(node.configure().unwrap(), LifecycleState::Inactive);
733 assert_eq!(node.activate().unwrap(), LifecycleState::Active);
734 assert_eq!(node.deactivate().unwrap(), LifecycleState::Inactive);
735 assert_eq!(node.cleanup().unwrap(), LifecycleState::Unconfigured);
736 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
737 }
738
739 fn on_shutdown_success() -> TransitionResult {
740 TransitionResult::Success
741 }
742
743 #[test]
744 fn test_polling_node_shutdown_callback_invoked() {
745 let mut node = LifecyclePollingNode::new();
746 node.register_on_shutdown(on_shutdown_success);
747
748 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
750 }
751
752 unsafe extern "C" fn ctx_cb_success(_: *mut c_void) -> u8 {
757 TransitionResult::Success as u8
758 }
759 unsafe extern "C" fn ctx_cb_failure(_: *mut c_void) -> u8 {
760 TransitionResult::Failure as u8
761 }
762 unsafe extern "C" fn ctx_cb_error(_: *mut c_void) -> u8 {
763 TransitionResult::Error as u8
764 }
765
766 #[test]
767 fn test_ctx_node_happy_path() {
768 unsafe {
769 let mut node = LifecyclePollingNodeCtx::new();
770 node.register(LifecycleCallbackSlot::Configure, Some(ctx_cb_success));
771 node.register(LifecycleCallbackSlot::Activate, Some(ctx_cb_success));
772 node.register(LifecycleCallbackSlot::Deactivate, Some(ctx_cb_success));
773 node.register(LifecycleCallbackSlot::Shutdown, Some(ctx_cb_success));
774
775 assert_eq!(
776 node.trigger_transition(LifecycleTransition::Configure)
777 .unwrap(),
778 LifecycleState::Inactive
779 );
780 assert_eq!(
781 node.trigger_transition(LifecycleTransition::Activate)
782 .unwrap(),
783 LifecycleState::Active
784 );
785 assert_eq!(
786 node.trigger_transition(LifecycleTransition::Deactivate)
787 .unwrap(),
788 LifecycleState::Inactive
789 );
790 assert_eq!(
791 node.trigger_transition(LifecycleTransition::ShutdownInactive)
792 .unwrap(),
793 LifecycleState::Finalized
794 );
795 }
796 }
797
798 #[test]
799 fn test_ctx_node_invalid_transition() {
800 unsafe {
801 let mut node = LifecyclePollingNodeCtx::new();
802 let err = node
803 .trigger_transition(LifecycleTransition::Activate)
804 .unwrap_err();
805 assert_eq!(
806 err,
807 LifecycleError::InvalidTransition {
808 from: LifecycleState::Unconfigured,
809 transition: LifecycleTransition::Activate,
810 }
811 );
812 assert_eq!(node.state(), LifecycleState::Unconfigured);
813 }
814 }
815
816 #[test]
817 fn test_ctx_node_callback_failure_rolls_back() {
818 unsafe {
819 let mut node = LifecyclePollingNodeCtx::new();
820 node.register(LifecycleCallbackSlot::Configure, Some(ctx_cb_failure));
821 assert!(
822 node.trigger_transition(LifecycleTransition::Configure)
823 .is_err()
824 );
825 assert_eq!(node.state(), LifecycleState::Unconfigured);
826 }
827 }
828
829 #[test]
830 fn test_ctx_node_callback_error_enters_error_processing() {
831 unsafe {
832 let mut node = LifecyclePollingNodeCtx::new();
833 node.register(LifecycleCallbackSlot::Configure, Some(ctx_cb_error));
834 assert!(
835 node.trigger_transition(LifecycleTransition::Configure)
836 .is_err()
837 );
838 assert_eq!(node.state(), LifecycleState::ErrorProcessing);
839 }
840 }
841
842 #[test]
843 fn test_ctx_node_finalized_rejects() {
844 unsafe {
845 let mut node = LifecyclePollingNodeCtx::new();
846 node.finalize();
847 let err = node
848 .trigger_transition(LifecycleTransition::Configure)
849 .unwrap_err();
850 assert_eq!(err, LifecycleError::NodeFinalized);
851 }
852 }
853
854 #[test]
855 fn test_ctx_node_context_passed() {
856 use core::sync::atomic::{AtomicU32, Ordering};
857 static SEEN: AtomicU32 = AtomicU32::new(0);
858 unsafe extern "C" fn cb_record(ctx: *mut c_void) -> u8 {
859 SEEN.store(ctx as usize as u32, Ordering::Relaxed);
860 TransitionResult::Success as u8
861 }
862
863 unsafe {
864 let mut node = LifecyclePollingNodeCtx::new();
865 node.set_context(0xBEEFu32 as usize as *mut c_void);
866 node.register(LifecycleCallbackSlot::Configure, Some(cb_record));
867 let _ = node.trigger_transition(LifecycleTransition::Configure);
868 assert_eq!(SEEN.load(Ordering::Relaxed), 0xBEEF);
869 }
870 }
871
872 #[test]
873 fn test_ctx_node_clear_callbacks_resets() {
874 unsafe {
875 let mut node = LifecyclePollingNodeCtx::new();
876 node.set_context(core::ptr::dangling_mut::<c_void>());
877 node.register(LifecycleCallbackSlot::Configure, Some(ctx_cb_success));
878 node.clear_callbacks();
879 assert!(node.context().is_null());
880 assert_eq!(
882 node.trigger_transition(LifecycleTransition::Configure)
883 .unwrap(),
884 LifecycleState::Inactive
885 );
886 }
887 }
888}