From 1a0fcafaf14750f258b8edb182810c0cd4466877 Mon Sep 17 00:00:00 2001 From: Alexis-Bernard Date: Wed, 22 Mar 2023 15:43:39 +0100 Subject: [PATCH] docs: internal_api & main Signed-off-by: Alexis-Bernard fix: deleted files are back Signed-off-by: WoodenMaiden --- agent/lib/src/config.rs | 66 +++++++++++++++++++++++++++ agent/lib/src/external_api/comms.rs | 64 ++++++++++++++++++++++++++ agent/lib/src/external_api/service.rs | 62 +++++++++++++------------ agent/lib/src/internal_api/mod.rs | 2 + agent/lib/src/internal_api/model.rs | 14 ++++++ agent/lib/src/internal_api/service.rs | 50 ++++++++++++++++++++ agent/src/main.rs | 18 ++++++++ 7 files changed, 246 insertions(+), 30 deletions(-) create mode 100644 agent/lib/src/config.rs create mode 100644 agent/lib/src/external_api/comms.rs create mode 100644 agent/lib/src/internal_api/mod.rs diff --git a/agent/lib/src/config.rs b/agent/lib/src/config.rs new file mode 100644 index 0000000..3f9e042 --- /dev/null +++ b/agent/lib/src/config.rs @@ -0,0 +1,66 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::{ + fs::File, + io::{self, BufReader}, +}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AgentConfigError { + #[error("cannot load config file")] + Load(#[from] io::Error), + #[error("cannot parse config file")] + Parse(#[from] serde_yaml::Error), + #[error("unsupported config kind")] + KindNotSupported, + #[error("unsupported config api version")] + VersionNotSupported, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +#[allow(non_snake_case)] +pub struct AgentConfig { + /// The api version of the agent config file + pub apiVersion: String, + /// The kind of the agent config file + pub kind: String, + /// The serial configuration + pub serial: SerialConfig, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct SerialConfig { + /// The path to the serial port + pub path: String, + /// The baud rate to use for the serial port + pub baud_rate: u32, +} + +impl AgentConfig { + /// Load a AgentConfig from a file. + /// + /// Arguments: + /// + /// * `path`: The path to the config file. + /// + /// Returns: + /// + /// A Result + pub fn load(path: &str) -> Result { + let file = File::open(path).map_err(AgentConfigError::Load)?; + let reader = BufReader::new(file); + let config: AgentConfig = + serde_yaml::from_reader(reader).map_err(AgentConfigError::Parse)?; + + if config.kind != "AgentConfig" { + return Err(AgentConfigError::KindNotSupported.into()); + } + + if config.apiVersion != "lambdo.io/v1alpha1" { + return Err(AgentConfigError::VersionNotSupported.into()); + } + + Ok(config) + } +} \ No newline at end of file diff --git a/agent/lib/src/external_api/comms.rs b/agent/lib/src/external_api/comms.rs new file mode 100644 index 0000000..a538a5d --- /dev/null +++ b/agent/lib/src/external_api/comms.rs @@ -0,0 +1,64 @@ +// This message is sent to the API server to indicate wether +// the agent is ready or not to receive messages. + +pub const MESSAGE_SIZE_NB_BYTES: usize = 8; + +pub struct Message { + pub message_size: [u8; MESSAGE_SIZE_NB_BYTES], // These are characters e.g. 00002048 + pub message: Vec // stringified json, vec because size is unknown +} + +impl Message { + pub fn new(message_to_send: String) -> Self { + let mut message_size = [0; MESSAGE_SIZE_NB_BYTES]; + let message = message_to_send.as_bytes().to_vec(); + + let string_size = format!("{:0>8}", message.len()); + //We can't call directly as bytes as both &str and String sizes are not known at + //compile time unlike message_size + + for (i, c) in string_size.chars().enumerate() { + message_size[i] = c as u8; + } + + Self { + message_size, + message + } + } + + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&self.message_size); + bytes.extend_from_slice(&self.message); + bytes + } + + +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_message_well_encoded() { + let message_data = "Hello world".to_string(); + let message = Message::new(message_data); + assert_eq!(message.message, [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]); + assert_eq!(message.message_size, [48, 48, 48, 48, 48, 48, 49, 49]); + + assert_eq!(message.to_bytes(), [48, 48, 48, 48, 48, 48, 49, 49, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]); + } + + + #[test] + fn message_size_badly_encoded() { + let message_data = "Hello world".to_string(); + let message = Message::new(message_data); + assert_eq!(message.message, [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]); + assert_ne!(message.message_size, [48, 48, 48, 48, 48, 48, 49, 50]); // should be 11, is 12 + } + +} + diff --git a/agent/lib/src/external_api/service.rs b/agent/lib/src/external_api/service.rs index eadeb09..7643348 100644 --- a/agent/lib/src/external_api/service.rs +++ b/agent/lib/src/external_api/service.rs @@ -1,30 +1,32 @@ use anyhow::{anyhow, Result}; -use log::{error, info, debug, trace}; +use log::{debug, error, info, trace}; + +use serialport::SerialPort; -use super::model::{RequestMessage, ResponseMessage, StatusMessage}; use super::comms::{Message, MESSAGE_SIZE_NB_BYTES}; +use super::model::{RequestMessage, ResponseMessage, StatusMessage}; pub struct ExternalApi { serial_path: String, serial_baud_rate: u32, + + serial_port: Box, // So we don't open it multiple times } impl ExternalApi { pub fn new(serial_path: String, serial_baud_rate: u32) -> Self { Self { - serial_path, + serial_path: serial_path.clone(), serial_baud_rate, + serial_port: serialport::new(serial_path, serial_baud_rate) + .open() + .unwrap(), } } pub fn read_from_serial(&mut self) -> Result { info!("Reading from serial port: {}", self.serial_path); - // Open the serial port - let mut serial = serialport::new(&self.serial_path, self.serial_baud_rate) - .open() - .map_err(|e| anyhow!("Failed to open serial port: {}", e))?; - // Create a buffer to hold the data let mut data_size: usize = 0; let mut size_buffer: [u8; MESSAGE_SIZE_NB_BYTES] = [0; MESSAGE_SIZE_NB_BYTES]; @@ -36,9 +38,9 @@ impl ExternalApi { // Create the final vector to hold the data let mut data_received: Vec = Vec::new(); - //we read the buffer and retrieve the first 8 bytes which are the size of the message + //we read the buffer and retrieve the first 8 bytes which are the size of the message while !got_size { - match serial.read(&mut size_buffer) { + match self.serial_port.read(&mut size_buffer) { Ok(t) => { if t == 0 { break; @@ -52,18 +54,15 @@ impl ExternalApi { if bytes_read >= MESSAGE_SIZE_NB_BYTES { got_size = true; - - let size_string = String::from_utf8( - data_received.clone() - ).map_err( - |e| anyhow!("Failed to get message size as string: {}", e) - )?; - + + let size_string = String::from_utf8(data_received.clone()) + .map_err(|e| anyhow!("Failed to get message size as string: {}", e))?; + trace!("Size string: {}", size_string); - - data_size = size_string.parse::().map_err( - |e| anyhow!("Failed to parse length of message: {}", e) - )?; + + data_size = size_string + .parse::() + .map_err(|e| anyhow!("Failed to parse length of message: {}", e))?; data_received.drain(..MESSAGE_SIZE_NB_BYTES); } @@ -76,7 +75,7 @@ impl ExternalApi { bytes_read = 0; while bytes_read < data_size { - match serial.read(&mut buf) { + match self.serial_port.read(&mut buf) { Ok(t) => { if t > 0 { bytes_read += t; @@ -97,7 +96,7 @@ impl ExternalApi { info!("Code entry: {:?}", code_entry); // Flush the serial port - serial + self.serial_port .flush() .map_err(|e| anyhow!("Failed to flush serial port: {}", e))?; @@ -137,22 +136,25 @@ impl ExternalApi { } pub fn write_to_serial(&mut self, data: &str) -> Result<()> { - // Open the serial port - let mut serial = serialport::new(&self.serial_path, self.serial_baud_rate) - .open() - .map_err(|e| anyhow!("Failed to open serial port: {}", e))?; + info!("Writing to serial port: {}", self.serial_path); - // Conver the string to a byte array + // Convert the string to a byte array let message = Message::new(data.to_string()).to_bytes(); let buf = message.as_slice(); // Write the byte array to the serial port - serial + self.serial_port .write_all(buf) .map_err(|e| anyhow!("Failed to write to serial port: {}", e))?; + // In order to still be readable by ``readline`` on the other side, we add a carriage return + // (not included in the message size) + self.serial_port + .write("\r\n".as_bytes()) + .map_err(|e| anyhow!("Failed to write to serial port: {}", e))?; + // Flush the serial port - serial + self.serial_port .flush() .map_err(|e| anyhow!("Failed to flush serial port: {}", e))?; Ok(()) diff --git a/agent/lib/src/internal_api/mod.rs b/agent/lib/src/internal_api/mod.rs new file mode 100644 index 0000000..0948cae --- /dev/null +++ b/agent/lib/src/internal_api/mod.rs @@ -0,0 +1,2 @@ +pub mod model; +pub mod service; \ No newline at end of file diff --git a/agent/lib/src/internal_api/model.rs b/agent/lib/src/internal_api/model.rs index 0ea8bf9..47358be 100644 --- a/agent/lib/src/internal_api/model.rs +++ b/agent/lib/src/internal_api/model.rs @@ -2,6 +2,13 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; +/// A struct to represent a file in the request message +/// +/// # Attributes +/// +/// * `path` - The path of the file +/// * `file_name` - The name of the file +/// * `content` - The content of the file #[derive(Deserialize, Serialize, Debug)] pub struct FileModel { pub path: PathBuf, @@ -19,6 +26,13 @@ impl FileModel { } } +/// A struct to represent the result of a command +/// +/// # Attributes +/// +/// * `stdout` - The stdout of the command +/// * `stderr` - The stderr of the command +/// * `exit_code` - The exit code of the command #[derive(Deserialize, Serialize, Debug)] pub struct CodeReturn { pub stdout: String, diff --git a/agent/lib/src/internal_api/service.rs b/agent/lib/src/internal_api/service.rs index 662d04a..dcfef32 100644 --- a/agent/lib/src/internal_api/service.rs +++ b/agent/lib/src/internal_api/service.rs @@ -12,17 +12,33 @@ use std::{ process::Command, }; +/// The path where the workspace will be created const WORKSPACE_PATH: &str = "/tmp"; +/// The internal API pub struct InternalApi { pub request_message: RequestMessage, } impl InternalApi { + /// Create a new instance of InternalApi + /// + /// # Arguments + /// + /// * `request_message` - The request message + /// + /// # Returns + /// + /// * `Self` - The new instance of InternalApi pub fn new(request_message: RequestMessage) -> Self { Self { request_message } } + /// Create the workspace for the code execution + /// + /// # Returns + /// + /// * `Result<()>` - Nothing or an error pub fn create_workspace(&mut self) -> Result<()> { info!("Creating workspace for code execution"); @@ -101,6 +117,11 @@ impl InternalApi { Ok(()) } + /// Run all the steps of the request message + /// + /// # Returns + /// + /// * `Result` - The response message or an error pub fn run(&mut self) -> Result { info!("Running all steps"); let mut steps: Vec = Vec::new(); @@ -135,6 +156,15 @@ impl InternalApi { Ok(response_message) } + /// Run a command + /// + /// # Arguments + /// + /// * `command` - The command to run + /// + /// # Returns + /// + /// * `Result` - The code return or an error pub fn run_one(&mut self, command: &str) -> Result { info!("Running command : {}", command); @@ -167,6 +197,15 @@ mod tests { use std::fs::File; use std::io::Read; + /// Generate a random integer + /// + /// # Arguments + /// + /// * `max` - The maximum value + /// + /// # Returns + /// + /// * `usize` - The random integer fn random_usize(max: usize) -> usize { let mut f = File::open("/dev/urandom").unwrap(); let mut buf = [0u8; 1]; @@ -180,6 +219,15 @@ mod tests { } } + /// Generate a random string + /// + /// # Arguments + /// + /// * `len` - The length of the string + /// + /// # Returns + /// + /// * `String` - The random string fn native_rand_string(len: usize) -> String { let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; let mut string = String::new(); @@ -191,6 +239,7 @@ mod tests { string } + /// Test the creation of a file #[test] fn workload_runs_correctly() { let files: Vec = Vec::new(); @@ -218,6 +267,7 @@ mod tests { assert!(res.data.steps[0].enable_output); } + /// Test the execution of a command with a workspace #[test] fn workspace_created_sucessfully() { let mut base_dir = PathBuf::from(WORKSPACE_PATH); diff --git a/agent/src/main.rs b/agent/src/main.rs index 5ca0bc6..5597b73 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -5,6 +5,7 @@ use anyhow::{anyhow, Result}; use clap::Parser; use log::{debug, info, trace}; +/// Agent CLI options #[derive(Parser)] #[clap( version = "0.1", @@ -17,13 +18,19 @@ pub struct AgentOpts { config: String, } +/// Main function fn main() -> Result<()> { + // Initialize logger env_logger::init(); + info!("Starting agent"); + // Parse CLI options let options = AgentOpts::parse(); debug!("loading config file at {}", options.config); + + // Load config file let config = AgentConfig::load(options.config.as_str())?; trace!( @@ -31,14 +38,25 @@ fn main() -> Result<()> { config ); + // Initialize external API let mut external_api = ExternalApi::new(config.serial.path, config.serial.baud_rate); + // Send status message to serial port external_api.send_status_message()?; + // Read request message from serial port let request_message = external_api.read_from_serial()?; + + // Initialize internal API let mut internal_api = InternalApi::new(request_message); + + // Create the workspace internal_api.create_workspace()?; + + // Run the steps of the request message let response_message = internal_api.run().map_err(|e| anyhow!("{:?}", e))?; + + // Send response message to serial port external_api.send_response_message(response_message)?; info!("Stopping agent");