From 2532a217722dbb7e851f5cab77c871f61c920b68 Mon Sep 17 00:00:00 2001 From: Xevion Date: Thu, 11 Dec 2025 17:43:40 -0600 Subject: [PATCH] feat(backend): add thiserror-based error handling Introduce AppError enum to replace panic-based error handling in executable loading and state management. Adds proper error propagation with descriptive error messages for missing executables, key patterns, and environment variables. --- Cargo.lock | 1 + Justfile | 4 ++-- backend/Cargo.toml | 1 + backend/src/errors.rs | 19 ++++++++++++++++++ backend/src/lib.rs | 1 + backend/src/main.rs | 13 +++++++++--- backend/src/state.rs | 46 +++++++++++++++++++++++++++++++------------ 7 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 backend/src/errors.rs diff --git a/Cargo.lock b/Cargo.lock index 7f33ca6..b832adb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -359,6 +359,7 @@ dependencies = [ "salvo", "serde", "serde_json", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", diff --git a/Justfile b/Justfile index 61f194f..0640993 100644 --- a/Justfile +++ b/Justfile @@ -44,12 +44,12 @@ frontend-build: # Development server with hot reload dev: @echo "Starting development server..." - cargo watch -x run + cargo watch -x run --bin backend # Simple development run (no hot reload) run: @echo "Starting server..." - cargo run + cargo run --bin backend # Build release build: diff --git a/backend/Cargo.toml b/backend/Cargo.toml index b4241dc..798fc17 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -19,6 +19,7 @@ reqwest = { workspace = true, features = ["json", "rustls-tls"] } salvo.workspace = true serde.workspace = true serde_json.workspace = true +thiserror = "2.0.17" tokio.workspace = true tokio-stream.workspace = true tracing.workspace = true diff --git a/backend/src/errors.rs b/backend/src/errors.rs new file mode 100644 index 0000000..871408c --- /dev/null +++ b/backend/src/errors.rs @@ -0,0 +1,19 @@ +use std::path::PathBuf; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("executable not found at '{path}'")] + ExecutableNotFound { path: PathBuf }, + + #[error("key pattern not found in executable '{name}'")] + KeyPatternNotFound { name: String }, + + #[error("missing required environment variable '{name}'")] + MissingEnvVar { name: String }, + + #[error("configuration error: {message}")] + Config { message: String }, +} diff --git a/backend/src/lib.rs b/backend/src/lib.rs index fdabce7..483a5e6 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -1,4 +1,5 @@ pub mod config; +pub mod errors; pub mod handlers; pub mod models; pub mod railway; diff --git a/backend/src/main.rs b/backend/src/main.rs index f8570fb..88199d7 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -60,9 +60,16 @@ async fn main() { } } - store.add_executable("Windows", "./demo.exe"); - store.add_executable("Linux", "./demo-linux"); - // store.add_executable("MacOS", "./demo-macos"); + for (exe_type, exe_path) in [ + ("Windows", "./demo.exe"), + ("Linux", "./demo-linux"), + // ("MacOS", "./demo-macos"), + ] { + if let Err(e) = store.add_executable(exe_type, exe_path) { + tracing::error!("{}", e); + std::process::exit(1); + } + } drop(store); // critical: Drop the lock to avoid deadlock, otherwise the server will hang diff --git a/backend/src/state.rs b/backend/src/state.rs index 83f2f58..79eb0db 100644 --- a/backend/src/state.rs +++ b/backend/src/state.rs @@ -1,10 +1,11 @@ use std::collections::HashMap; -use std::path; +use std::path::{Path, PathBuf}; use std::sync::LazyLock; use salvo::{http::cookie::Cookie, Response}; use tokio::sync::Mutex; +use crate::errors::{AppError, Result}; use crate::models::{BuildLogs, Executable, ExecutableJson, Session}; pub static STORE: LazyLock> = LazyLock::new(|| Mutex::new(State::new())); @@ -27,30 +28,49 @@ impl State { } } - pub fn add_executable(&mut self, exe_type: &str, exe_path: &str) { - let data = std::fs::read(exe_path).expect("Unable to read file"); + pub fn add_executable(&mut self, exe_type: &str, exe_path: &str) -> Result<()> { + let path = Path::new(exe_path); + + let data = std::fs::read(path).map_err(|_| AppError::ExecutableNotFound { + path: PathBuf::from(exe_path), + })?; let pattern = "a".repeat(1024); - let key_start = Executable::search_pattern(&data, pattern.as_bytes(), 0).unwrap(); + let name = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or_default() + .to_string(); + + let key_start = + Executable::search_pattern(&data, pattern.as_bytes(), 0).ok_or_else(|| { + AppError::KeyPatternNotFound { + name: name.clone(), + } + })?; let key_end = key_start + pattern.len(); - let path = path::Path::new(&exe_path); - let name = path.file_stem().unwrap().to_str().unwrap(); - let extension = match path.extension() { - Some(s) => s.to_str().unwrap(), - None => "", - }; + let extension = path + .extension() + .and_then(|s| s.to_str()) + .unwrap_or_default() + .to_string(); let exe = Executable { data, - filename: path.file_name().unwrap().to_str().unwrap().to_string(), - name: name.to_string(), - extension: extension.to_string(), + filename: path + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or_default() + .to_string(), + name, + extension, key_start, key_end, }; self.executables.insert(exe_type.to_string(), exe); + Ok(()) } pub async fn new_session(&mut self, res: &mut Response) -> u32 {