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.
This commit is contained in:
2025-12-11 17:43:40 -06:00
parent fd474767ae
commit 2532a21772
7 changed files with 67 additions and 18 deletions

1
Cargo.lock generated
View File

@@ -359,6 +359,7 @@ dependencies = [
"salvo", "salvo",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.17",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tracing", "tracing",

View File

@@ -44,12 +44,12 @@ frontend-build:
# Development server with hot reload # Development server with hot reload
dev: dev:
@echo "Starting development server..." @echo "Starting development server..."
cargo watch -x run cargo watch -x run --bin backend
# Simple development run (no hot reload) # Simple development run (no hot reload)
run: run:
@echo "Starting server..." @echo "Starting server..."
cargo run cargo run --bin backend
# Build release # Build release
build: build:

View File

@@ -19,6 +19,7 @@ reqwest = { workspace = true, features = ["json", "rustls-tls"] }
salvo.workspace = true salvo.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
thiserror = "2.0.17"
tokio.workspace = true tokio.workspace = true
tokio-stream.workspace = true tokio-stream.workspace = true
tracing.workspace = true tracing.workspace = true

19
backend/src/errors.rs Normal file
View File

@@ -0,0 +1,19 @@
use std::path::PathBuf;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, AppError>;
#[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 },
}

View File

@@ -1,4 +1,5 @@
pub mod config; pub mod config;
pub mod errors;
pub mod handlers; pub mod handlers;
pub mod models; pub mod models;
pub mod railway; pub mod railway;

View File

@@ -60,9 +60,16 @@ async fn main() {
} }
} }
store.add_executable("Windows", "./demo.exe"); for (exe_type, exe_path) in [
store.add_executable("Linux", "./demo-linux"); ("Windows", "./demo.exe"),
// store.add_executable("MacOS", "./demo-macos"); ("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 drop(store); // critical: Drop the lock to avoid deadlock, otherwise the server will hang

View File

@@ -1,10 +1,11 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path; use std::path::{Path, PathBuf};
use std::sync::LazyLock; use std::sync::LazyLock;
use salvo::{http::cookie::Cookie, Response}; use salvo::{http::cookie::Cookie, Response};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::errors::{AppError, Result};
use crate::models::{BuildLogs, Executable, ExecutableJson, Session}; use crate::models::{BuildLogs, Executable, ExecutableJson, Session};
pub static STORE: LazyLock<Mutex<State>> = LazyLock::new(|| Mutex::new(State::new())); pub static STORE: LazyLock<Mutex<State>> = LazyLock::new(|| Mutex::new(State::new()));
@@ -27,30 +28,49 @@ impl State {
} }
} }
pub fn add_executable(&mut self, exe_type: &str, exe_path: &str) { pub fn add_executable(&mut self, exe_type: &str, exe_path: &str) -> Result<()> {
let data = std::fs::read(exe_path).expect("Unable to read file"); 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 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 key_end = key_start + pattern.len();
let path = path::Path::new(&exe_path); let extension = path
let name = path.file_stem().unwrap().to_str().unwrap(); .extension()
let extension = match path.extension() { .and_then(|s| s.to_str())
Some(s) => s.to_str().unwrap(), .unwrap_or_default()
None => "", .to_string();
};
let exe = Executable { let exe = Executable {
data, data,
filename: path.file_name().unwrap().to_str().unwrap().to_string(), filename: path
name: name.to_string(), .file_name()
extension: extension.to_string(), .and_then(|s| s.to_str())
.unwrap_or_default()
.to_string(),
name,
extension,
key_start, key_start,
key_end, key_end,
}; };
self.executables.insert(exe_type.to_string(), exe); self.executables.insert(exe_type.to_string(), exe);
Ok(())
} }
pub async fn new_session(&mut self, res: &mut Response) -> u32 { pub async fn new_session(&mut self, res: &mut Response) -> u32 {