From ce600658a95c10a905e30e9e34b48a78c193526b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Fern=C3=A1ndez=20L=C3=B3pez?= Date: Thu, 20 Jul 2023 13:28:53 +0200 Subject: [PATCH] feat: use more descriptive types for enabling features on serve --- crates/api-manage/src/handlers/v0/workers.rs | 10 +- crates/api-manage/src/models/worker.rs | 14 +- crates/router/src/lib.rs | 7 +- crates/router/src/route.rs | 17 ++- crates/server/src/handlers/assets.rs | 11 +- crates/server/src/handlers/not_found.rs | 7 +- crates/server/src/handlers/worker.rs | 63 +++++---- crates/server/src/lib.rs | 134 +++++++++++++------ src/main.rs | 20 +-- 9 files changed, 186 insertions(+), 97 deletions(-) diff --git a/crates/api-manage/src/handlers/v0/workers.rs b/crates/api-manage/src/handlers/v0/workers.rs index 81e88c61..83a7b644 100644 --- a/crates/api-manage/src/handlers/v0/workers.rs +++ b/crates/api-manage/src/handlers/v0/workers.rs @@ -7,7 +7,7 @@ use actix_web::{ web::{Data, Json, Path}, HttpResponse, Responder, Result, }; -use wws_router::Routes; +use wws_router::{Routes, WORKERS}; /// Return the list of loaded workers. #[utoipa::path( @@ -34,11 +34,15 @@ pub async fn handle_api_workers(routes: Data) -> Result )] #[get("/_api/v0/workers/{id}")] pub async fn handle_api_worker(routes: Data, path: Path) -> HttpResponse { + let workers = WORKERS + .read() + .expect("error locking worker lock for reading"); let worker = routes .routes .iter() - .find(|r| &r.worker.id == path.as_ref()) - .map(|r| &r.worker); + .find(|r| &r.worker == path.as_ref()) + .map(|r| workers.get(&r.worker)) + .expect("unexpected missing worker"); if let Some(worker) = worker { HttpResponse::Ok().json(WorkerConfig::from(worker)) diff --git a/crates/api-manage/src/models/worker.rs b/crates/api-manage/src/models/worker.rs index 5d02dac4..1f96a3bf 100644 --- a/crates/api-manage/src/models/worker.rs +++ b/crates/api-manage/src/models/worker.rs @@ -3,7 +3,7 @@ use serde::Serialize; use utoipa::ToSchema; -use wws_router::Route; +use wws_router::{Route, WORKERS}; #[derive(Serialize, ToSchema)] /// Defines a worker in a given application. @@ -23,10 +23,18 @@ pub struct Worker { impl From<&Route> for Worker { fn from(value: &Route) -> Self { - let name = value.worker.config.name.as_ref(); + let workers = WORKERS + .read() + .expect("error locking worker lock for reading"); + let name = workers + .get(&value.worker) + .expect("unexpected missing worker") + .config + .name + .as_ref(); Self { - id: value.worker.id.clone(), + id: value.worker.clone(), name: name.unwrap_or(&String::from("default")).to_string(), path: value.path.clone(), filepath: value.handler.to_string_lossy().to_string(), diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index e04dbbe9..8216e835 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -15,9 +15,10 @@ use std::path::{Path, PathBuf}; use std::time::Instant; use wws_config::Config; -pub use route::Route; +pub use route::{Route, WORKERS}; /// Contains all registered routes +#[derive(Clone, Default)] pub struct Routes { pub routes: Vec, pub prefix: String, @@ -54,6 +55,10 @@ impl Routes { Self { routes, prefix } } + pub fn iter(&self) -> impl Iterator { + self.routes.iter() + } + /// Based on a set of routes and a given path, it provides the best /// match based on the parametrized URL score. See the [`Route::can_manage_path`] /// method to understand how to calculate the score. diff --git a/crates/router/src/route.rs b/crates/router/src/route.rs index 24c83055..06ed266e 100644 --- a/crates/router/src/route.rs +++ b/crates/router/src/route.rs @@ -4,8 +4,10 @@ use lazy_static::lazy_static; use regex::Regex; use std::{ + collections::HashMap, ffi::OsStr, path::{Component, Path, PathBuf}, + sync::RwLock, }; use wws_config::Config as ProjectConfig; use wws_worker::Worker; @@ -13,6 +15,7 @@ use wws_worker::Worker; lazy_static! { static ref PARAMETER_REGEX: Regex = Regex::new(r"\[\w+\]").unwrap(); static ref DYNAMIC_ROUTE_REGEX: Regex = Regex::new(r".*\[\w+\].*").unwrap(); + pub static ref WORKERS: RwLock> = RwLock::new(HashMap::default()); } /// Identify if a route can manage a certain URL and generates @@ -37,13 +40,14 @@ pub enum RouteAffinity { /// api/index.wasm => /api /// api/v2/ping.wasm => /api/v2/ping /// ``` +#[derive(Clone)] pub struct Route { /// The wasm module that will manage the route pub handler: PathBuf, /// The URL path pub path: String, /// The associated worker - pub worker: Worker, + pub worker: String, } impl Route { @@ -57,12 +61,19 @@ impl Route { prefix: &str, project_config: &ProjectConfig, ) -> Self { - let worker = Worker::new(base_path, &filepath, project_config).unwrap(); + let worker = + Worker::new(base_path, &filepath, project_config).expect("error creating worker"); + let worker_id = worker.id.clone(); + + WORKERS + .write() + .expect("error locking worker lock for writing") + .insert(worker_id.clone(), worker); Self { path: Self::retrieve_route(base_path, &filepath, prefix), handler: filepath, - worker, + worker: worker_id.clone(), } } diff --git a/crates/server/src/handlers/assets.rs b/crates/server/src/handlers/assets.rs index af2691ae..d7cf18ff 100644 --- a/crates/server/src/handlers/assets.rs +++ b/crates/server/src/handlers/assets.rs @@ -1,12 +1,10 @@ // Copyright 2022 VMware, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::AppData; use actix_files::NamedFile; use actix_web::{web::Data, HttpRequest}; -use std::{ - io::{Error, ErrorKind}, - path::PathBuf, -}; +use std::io::{Error, ErrorKind}; /// Find a static HTML file in the `public` folder. This function is used /// when there's no direct file to be served. It will look for certain patterns @@ -14,7 +12,10 @@ use std::{ /// /// If no file is present, it will try to get a default "public/404.html" pub async fn handle_assets(req: &HttpRequest) -> Result { - let root_path = req.app_data::>().unwrap(); + let root_path = &req + .app_data::>() + .expect("error fetching app data") + .root_path; let uri_path = req.path(); // File path. This is required for the wasm_handler as dynamic routes may capture static files diff --git a/crates/server/src/handlers/not_found.rs b/crates/server/src/handlers/not_found.rs index 71e18095..a2fd56d6 100644 --- a/crates/server/src/handlers/not_found.rs +++ b/crates/server/src/handlers/not_found.rs @@ -1,14 +1,17 @@ // Copyright 2022 VMware, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::AppData; use actix_files::NamedFile; use actix_web::{web::Data, HttpRequest, HttpResponse}; -use std::path::PathBuf; /// This method tries to render a custom 404 error file from the static /// folder. If not, it will render an empty 404 pub async fn handle_not_found(req: &HttpRequest) -> HttpResponse { - let root_path = req.app_data::>().unwrap(); + let root_path = &req + .app_data::>() + .expect("error fetching app data") + .root_path; let public_404_path = root_path.join("public").join("404.html"); if let Ok(file) = NamedFile::open_async(public_404_path).await { diff --git a/crates/server/src/handlers/worker.rs b/crates/server/src/handlers/worker.rs index 75369e15..f266eab1 100644 --- a/crates/server/src/handlers/worker.rs +++ b/crates/server/src/handlers/worker.rs @@ -2,14 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 use super::{assets::handle_assets, not_found::handle_not_found}; -use crate::DataConnectors; +use crate::AppData; use actix_web::{ http::StatusCode, web::{Bytes, Data}, HttpRequest, HttpResponse, }; -use std::{fs::File, io::Write, sync::RwLock}; -use wws_router::Routes; +use std::io::Write; +use wws_router::WORKERS; use wws_worker::io::WasmOutput; /// Process an HTTP request by passing it to the right Runner. The Runner @@ -30,18 +30,13 @@ use wws_worker::io::WasmOutput; /// For these reasons, we are selecting the right handler at this point and not /// allowing Actix to select it for us. pub async fn handle_worker(req: HttpRequest, body: Bytes) -> HttpResponse { - let routes = req.app_data::>().unwrap(); - let stderr_file = req.app_data::>>().unwrap(); - let data_connectors = req - .app_data::>>() - .unwrap() - .clone(); - // We will improve error handling + let app_data = req + .app_data::>() + .expect("error fetching app data"); let result: HttpResponse; // First, we need to identify the best suited route - let selected_route = routes.retrieve_best_route(req.path()); - + let selected_route = app_data.routes.retrieve_best_route(req.path()); if let Some(route) = selected_route { // First, check if there's an existing static file. Static assets have more priority // than dynamic routes. However, I cannot set the static assets as the first service @@ -52,16 +47,27 @@ pub async fn handle_worker(req: HttpRequest, body: Bytes) -> HttpResponse { } } + let workers = WORKERS + .read() + .expect("error locking worker lock for reading"); + + let worker = workers + .get(&route.worker) + .expect("unexpected missing worker"); + // Let's continue let body_str = String::from_utf8(body.to_vec()).unwrap_or_else(|_| String::from("")); // Init from configuration - let vars = &route.worker.config.vars; - let kv_namespace = route.worker.config.data_kv_namespace(); + let vars = &worker.config.vars; + let kv_namespace = worker.config.data_kv_namespace(); let store = match &kv_namespace { Some(namespace) => { - let connector = data_connectors.read().unwrap(); + let connector = app_data + .data_connectors + .read() + .expect("error locking data connectors lock for reading"); let kv_store = connector.kv.find_store(namespace); kv_store.map(|store| store.clone()) @@ -69,21 +75,19 @@ pub async fn handle_worker(req: HttpRequest, body: Bytes) -> HttpResponse { None => None, }; + let stderr_file = app_data + .stderr + .as_ref() + .map(|file| file.try_clone().expect("error setting up stderr")); + let (handler_result, handler_success) = - match route - .worker - .run(&req, &body_str, store, vars, stderr_file.get_ref()) - { + match worker.run(&req, &body_str, store, vars, &stderr_file) { Ok(output) => (output, true), Err(error) => { - if let Some(stderr_file) = stderr_file.get_ref() { - if let Ok(mut stderr_file) = stderr_file.try_clone() { - stderr_file - .write_all(error.to_string().as_bytes()) - .expect("Failed to write error to stderr_file"); - } else { - eprintln!("{}", error); - } + if let Some(mut stderr_file) = stderr_file { + stderr_file + .write_all(error.to_string().as_bytes()) + .expect("Failed to write error to stderr_file"); } else { eprintln!("{}", error); } @@ -105,9 +109,10 @@ pub async fn handle_worker(req: HttpRequest, body: Bytes) -> HttpResponse { // Write to the state if required if handler_success && kv_namespace.is_some() { - data_connectors + app_data + .data_connectors .write() - .unwrap() + .expect("error locking data connectors lock for writing") .kv .replace_store(&kv_namespace.unwrap(), &handler_result.kv) } diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index ecce2418..ba66f896 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 mod errors; -use errors::Result; +use errors::{Result, ServeError}; mod handlers; @@ -16,84 +16,136 @@ use actix_web::{ use handlers::assets::handle_assets; use handlers::not_found::handle_not_found; use handlers::worker::handle_worker; -use std::fs::OpenOptions; -use std::path::Path; -use std::sync::RwLock; +use std::{ + fs::{File, OpenOptions}, + path::PathBuf, + sync::RwLock, +}; use wws_api_manage::config_manage_api_handlers; use wws_data_kv::KV; use wws_panel::config_panel_handlers; -use wws_router::Routes; +use wws_router::{Routes, WORKERS}; + +#[derive(Clone, PartialEq)] +pub enum Panel { + Enabled, + Disabled, +} + +impl From for Panel { + fn from(panel_enabled: bool) -> Self { + if panel_enabled { + Panel::Enabled + } else { + Panel::Disabled + } + } +} #[derive(Default)] pub(crate) struct DataConnectors { kv: KV, } +#[derive(Clone)] +pub struct ServeOptions { + pub root_path: PathBuf, + pub base_routes: Routes, + pub hostname: String, + pub port: u16, + pub panel: Panel, + pub stderr: Option, +} + +#[derive(Default)] +pub struct AppData { + routes: Routes, + data_connectors: RwLock, + root_path: PathBuf, + stderr: Option, +} + +impl TryFrom for AppData { + type Error = ServeError; + + fn try_from(serve_options: ServeOptions) -> Result { + let stderr = if let Some(stderr) = serve_options.stderr { + Some( + OpenOptions::new() + .append(true) + .open(stderr) + .map_err(|_| ServeError::InitializeServerError)?, + ) + } else { + None + }; + + Ok(AppData { + routes: serve_options.base_routes, + data_connectors: RwLock::new(DataConnectors::default()), + root_path: serve_options.root_path.clone(), + stderr, + }) + } +} + /// Initializes an actix-web server based on the given configuration and /// path. It will configure the different handlers to manage static /// assets and workers. -pub async fn serve( - root_path: &Path, - base_routes: Routes, - hostname: &str, - port: u16, - panel: bool, - stderr: Option<&Path>, -) -> Result { - // Initializes the data connectors. For now, just KV - let data = Data::new(RwLock::new(DataConnectors::default())); - let routes = Data::new(base_routes); - let root_path = Data::new(root_path.to_owned()); - let stderr_file; - - // Configure stderr - if let Some(path) = stderr { - let file = OpenOptions::new() - .read(true) - .write(true) - .open(path) - .map_err(|_| errors::ServeError::InitializeServerError)?; - - stderr_file = Data::new(Some(file)); - } else { - stderr_file = Data::new(None); - } +pub async fn serve(serve_options: ServeOptions) -> Result { + let (hostname, port) = (serve_options.hostname.clone(), serve_options.port); + let serve_options = serve_options.clone(); let server = HttpServer::new(move || { + // Initializes the app data for handlers + let app_data: Data = Data::new( + >::try_into(serve_options.clone()) + .expect("failed initializing server"), + ); + let workers = WORKERS + .read() + .expect("error locking worker lock for reading"); + let mut app = App::new() // enable logger .wrap(middleware::Logger::default()) // Clean path before sending it to the service .wrap(middleware::NormalizePath::trim()) - .app_data(Data::clone(&routes)) - .app_data(Data::clone(&data)) - .app_data(Data::clone(&root_path)) - .app_data(Data::clone(&stderr_file)); + .app_data(Data::clone(&app_data)); // Configure panel - if panel { + if serve_options.panel == Panel::Enabled { app = app.configure(config_panel_handlers); app = app.configure(config_manage_api_handlers); } // Append routes to the current service - for route in routes.routes.iter() { + for route in app_data.routes.iter() { app = app.service(web::resource(route.actix_path()).to(handle_worker)); + let worker = workers + .get(&route.worker) + .expect("unexpected missing worker"); + // Configure KV - if let Some(namespace) = route.worker.config.data_kv_namespace() { - data.write().unwrap().kv.create_store(&namespace); + if let Some(namespace) = worker.config.data_kv_namespace() { + app_data + .data_connectors + .write() + .expect("cannot retrieve shared data") + .kv + .create_store(&namespace); } } // Serve static files from the static folder - let mut static_prefix = routes.prefix.clone(); + let mut static_prefix = app_data.routes.prefix.clone(); if static_prefix.is_empty() { static_prefix = String::from("/"); } app = app.service( - Files::new(&static_prefix, root_path.join("public")) + Files::new(&static_prefix, app_data.root_path.join("public")) .index_file("index.html") // This handler check if there's an HTML file in the public folder that // can reply to the given request. For example, if someone request /about, diff --git a/src/main.rs b/src/main.rs index 999ac0a1..557dbe62 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ use std::process::exit; use wws_config::Config; use wws_project::{identify_type, prepare_project, ProjectType}; use wws_router::Routes; -use wws_server::serve; +use wws_server::{serve, ServeOptions}; // Arguments #[derive(Parser, Debug)] @@ -191,20 +191,20 @@ async fn main() -> std::io::Result<()> { ); } - let server = serve( - &project_path, - routes, - &args.hostname, - args.port, - args.enable_panel, - None, - ) + let server = serve(ServeOptions { + root_path: project_path, + base_routes: routes, + hostname: args.hostname.clone(), + port: args.port, + panel: args.enable_panel.into(), + stderr: None, + }) .await .map_err(|err| Error::new(ErrorKind::AddrInUse, err))?; println!( "🚀 Start serving requests at http://{}:{}\n", - &args.hostname, args.port + args.hostname, args.port ); // Run the server