use clap::Parser;
use notify::{RecursiveMode, Watcher};
use tokio::sync::Mutex;
use std::{path::Path, sync::Arc};
use std::time::{Duration, Instant};
use tokio::{signal, sync::mpsc};
use tokio_util::sync::CancellationToken;
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use crate::app::App;
use crate::route_builder::config::{Config, ServerConfig};
pub mod route_builder;
pub mod handlers;
pub mod app;
pub mod link;
pub mod pages;
pub mod upload_configuration;
const DEFAULT_PORT: u16 = 4520;
const DEFAULT_FOLDER: &str = "mocks";
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
#[arg(short, long, default_value_t = DEFAULT_PORT)]
port: u16,
#[arg(short, long, default_value = DEFAULT_FOLDER)]
folder: String,
#[arg(short, long)]
disable_cors: bool,
#[arg(short, long)]
allowed_origin: Option<String>,
}
enum SessionResult {
Restart,
Shutdown,
}
fn is_upload_folder(folder: &str) -> bool {
folder.contains("{upload}")
}
async fn run_app_session(config: Config) -> SessionResult {
let token = CancellationToken::new();
let app = App::new(config);
let app_arc = Arc::new(Mutex::new(app));
let main_logic = {
let app_ref = Arc::clone(&app_arc);
async move {
let mut app = app_ref.lock().await;
app.initialize().await
}
};
let app_finisher_task = tokio::spawn({
let token_clone = token.clone();
let app_ref = Arc::clone(&app_arc);
async move {
token_clone.cancelled().await;
let mut app = app_ref.lock().await;
app.finish();
}
});
tracing::info!("RS-MOCK-SERVER started. Watching for file changes in '{}'...", app_arc.lock().await.get_folder());
let (tx, mut rx) = mpsc::channel(1);
let last_send_time = Arc::new(Mutex::new(Instant::now() - Duration::from_millis(1000)));
let debounce_duration = Duration::from_millis(300);
let mut watcher = notify::recommended_watcher(
move |res: Result<notify::Event, notify::Error>| {
if let Ok(event) = res {
for path in &event.paths {
if is_upload_folder(path.to_str().unwrap()) {
if !path.is_dir() {
return;
}
}
}
println!("event {:?}", event.paths.iter().map(|f|f.to_str().unwrap_or("")).collect::<Vec<&str>>().join("|"));
let now = std::time::Instant::now();
let mut last_time = last_send_time.blocking_lock();
if now.duration_since(*last_time) >= debounce_duration {
*last_time = now;
let _ = tx.blocking_send(());
}
}
}
).unwrap();
watcher.watch(Path::new(&app_arc.lock().await.get_folder()), RecursiveMode::Recursive).unwrap();
let result = tokio::select! {
_ = main_logic => {
tracing::warn!("Main logic completed unexpectedly. Shutting down.");
SessionResult::Shutdown
},
_ = rx.recv() => {
tracing::info!("File change detected. Restarting application...");
SessionResult::Restart
},
_ = signal::ctrl_c() => {
tracing::info!("Ctrl+C received. Shutting down.");
SessionResult::Shutdown
}
};
token.cancel();
let _ = app_finisher_task.await;
tracing::info!("Application instance shut down gracefully.");
result
}
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
.with(fmt::layer())
.init();
let args = Args::parse();
let config = if let Ok(file) = std::fs::read_to_string("./rs-mock-server.toml") {
match Config::try_from(file.as_str()) {
Ok(config) => config,
Err(err) => {
println!("Error: {}", err);
return;
},
}
} else {
Config {
server: Some(ServerConfig {
port: Some(args.port),
folder: Some(args.folder),
allowed_origin: args.allowed_origin,
enable_cors: Some(!args.disable_cors),
}),
..Default::default()
}
};
while let SessionResult::Restart = run_app_session(config.clone()).await {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}