Skip to main content

ej_builder_sdk/
lib.rs

1//! Builder SDK for the EJ framework.
2//!
3//! Provides communication interface between builders and the EJ dispatcher.
4//!
5//! # Usage
6//!
7//! ```rust, no_run
8//! use ej_builder_sdk::{BuilderSdk, BuilderEvent};
9//!
10//! #[tokio::main]
11//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
12//!     let sdk = BuilderSdk::init(|sdk, event| async move {
13//!         match event {
14//!             BuilderEvent::Exit => {
15//!                 // Cleanup logic here
16//!                 println!("Received exit signal for: ");
17//!                 println!("{} {} ({:?})", sdk.board_name(), sdk.board_config_name(), sdk.action());
18//!                 std::process::exit(0);
19//!             }
20//!         }
21//!     }).await.unwrap();
22//!     
23//!     // Builder logic here
24//!     Ok(())
25//! }
26//! ```
27
28use std::{env::args, path::PathBuf};
29
30use serde::{Deserialize, Serialize};
31use tokio::{
32    io::{AsyncReadExt, AsyncWriteExt},
33    net::{
34        UnixStream,
35        unix::{OwnedReadHalf, OwnedWriteHalf},
36    },
37    signal::unix::{SignalKind, signal},
38};
39use tracing::info;
40
41use crate::prelude::*;
42pub mod error;
43pub mod prelude;
44
45/// Events sent from the dispatcher to the builder.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub enum BuilderEvent {
48    /// Request to exit the builder.
49    Exit,
50}
51
52/// Responses sent from the builder to the dispatcher.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum BuilderResponse {
55    /// Acknowledge receipt of an event.
56    Ack,
57}
58#[derive(Debug, Clone, Copy)]
59pub enum Action {
60    Build,
61    Run,
62}
63
64impl TryFrom<&str> for Action {
65    type Error = Error;
66
67    fn try_from(value: &str) -> Result<Self> {
68        if value == "build" {
69            return Ok(Action::Build);
70        }
71        if value == "run" {
72            return Ok(Action::Run);
73        }
74        Err(Error::InvalidAction(String::from(value)))
75    }
76}
77
78impl From<Action> for &'static str {
79    fn from(value: Action) -> Self {
80        match value {
81            Action::Build => "build",
82            Action::Run => "run",
83        }
84    }
85}
86
87impl From<Action> for String {
88    fn from(value: Action) -> Self {
89        let value: &str = value.into();
90        Self::from(value)
91    }
92}
93
94/// Builder SDK for communicating with the EJ dispatcher.
95///
96/// Handles Unix socket communication and event processing between
97/// the builder and dispatcher.
98#[derive(Debug, Clone)]
99pub struct BuilderSdk {
100    /// The board name.
101    board_name: String,
102    /// The board configuration name.
103    board_config_name: String,
104    /// The path to the config.toml file.
105    config_path: String,
106    /// The action the script should take.
107    action: Action,
108}
109
110impl BuilderSdk {
111    /// Initialize the builder SDK and start event processing.
112    ///
113    /// Sets up Unix socket communication with the dispatcher and starts
114    /// an async event loop to handle incoming events.
115    ///
116    /// # Arguments
117    ///
118    /// * `event_callback` - Function called when events are received
119    ///
120    /// # Examples
121    ///
122    /// ```rust,no_run
123    /// use ej_builder_sdk::{BuilderSdk, BuilderEvent};
124    /// # tokio_test::block_on(async {
125    /// let sdk = BuilderSdk::init(|sdk, event| async move {
126    ///     println!("{:?} {} {} ({:?})", event, sdk.board_name(), sdk.board_config_name(), sdk.action());
127    ///     match event {
128    ///         BuilderEvent::Exit => std::process::exit(0),
129    ///     }
130    /// }).await.unwrap();
131    /// # });
132    /// ```
133    pub async fn init<F, Fut>(event_callback: F) -> Result<Self>
134    where
135        F: Fn(Self, BuilderEvent) -> Fut + Send + Sync + 'static,
136        Fut: Future<Output = Result<()>> + Send + 'static,
137    {
138        let args: Vec<String> = std::env::args().into_iter().collect();
139        if args.len() < 6 {
140            return Err(Error::MissingArgs(6, args.len()));
141        }
142
143        let action: Action = TryFrom::<&str>::try_from(&args[1])?;
144
145        let stream = UnixStream::connect(&args[5]).await?;
146        let sdk = Self {
147            config_path: args[2].clone(),
148            board_name: args[3].clone(),
149            board_config_name: args[4].clone(),
150            action,
151        };
152        let sdk_loop = sdk.clone();
153        let mut sigint = signal(SignalKind::interrupt())?;
154        tokio::spawn(async move {
155            while sigint.recv().await.is_some() {
156                info!("SIGINT received");
157            }
158        });
159
160        tokio::spawn(async move { sdk_loop.start_event_loop(stream, event_callback).await });
161        Ok(sdk)
162    }
163    /// Get the action this script should take
164    pub fn action(&self) -> Action {
165        self.action
166    }
167    /// Get the path to the config.toml file.
168    pub fn config_path(&self) -> PathBuf {
169        PathBuf::from(&self.config_path)
170    }
171    /// Get the board name.
172    pub fn board_name(&self) -> &str {
173        &self.board_name
174    }
175    /// Get the board configuration name.
176    pub fn board_config_name(&self) -> &str {
177        &self.board_config_name
178    }
179    /// Parse event data from JSON string.
180    fn parse_event(payload: &str) -> Result<BuilderEvent> {
181        Ok(serde_json::from_str(payload)?)
182    }
183    /// Start the event loop for processing dispatcher messages.
184    async fn start_event_loop<F, Fut>(self, stream: UnixStream, cb: F) -> Result<()>
185    where
186        F: Fn(Self, BuilderEvent) -> Fut + Send + Sync + 'static,
187        Fut: Future<Output = Result<()>> + Send + 'static,
188    {
189        let mut payload = String::new();
190        let (mut rx, mut tx) = stream.into_split();
191
192        loop {
193            tokio::select! {
194                read_result = rx.read_to_string(&mut payload)  => {
195                    match read_result {
196                        Ok(0) => break,
197                        Ok(n) => {
198                            let event = BuilderSdk::parse_event(&payload)?;
199                            info!("Received event from builder {:?}", event);
200                            cb(self.clone(), event).await;
201                            info!("Acking event to builder");
202                            let response = serde_json::to_string(&BuilderResponse::Ack)?;
203                            tx.write_all(response.as_bytes()).await;
204                            tx.write_all(b"\n").await;
205                            tx.flush().await;
206                        }
207                        Err(e) => return Err(Error::from(e)),
208                    }
209                }
210
211                _ = tokio::signal::ctrl_c() => {
212                    info!("Received Ctrl+C, shutting down...");
213                    cb(self.clone(), BuilderEvent::Exit).await; // call callback with shutdown event
214                    break;
215                }
216            }
217        }
218        Ok(())
219    }
220}