diff --git a/crates/api-manage/src/handlers/v0/workers.rs b/crates/api-manage/src/handlers/v0/workers.rs index 81e88c61..ed247ca1 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,14 @@ 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..d7424f20 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(WorkerSet::default()); } /// Identify if a route can manage a certain URL and generates @@ -37,13 +40,36 @@ 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, +} + +/// Structure that holds the map of workers from their identifier to +/// their worker::Worker structure containing the runtime information, +/// such as the WebAssembly module itself as well as the WebAssembly +/// runtime. +/// +/// This structure hides the global (but internal) hash map from +/// other crates. +#[derive(Default)] +pub struct WorkerSet { + workers: HashMap, +} + +impl WorkerSet { + pub fn get(&self, worker_id: &str) -> Option<&Worker> { + self.workers.get(worker_id) + } + + pub fn register(&mut self, worker_id: String, worker: Worker) { + self.workers.insert(worker_id, worker); + } } impl Route { @@ -57,12 +83,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") + .register(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 12453017..a70f6dfe 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, DataConnectors}; use actix_web::{ http::StatusCode, - web::{self, Bytes, Data}, + web::{Bytes, Data}, HttpRequest, HttpResponse, }; -use std::{fs::File, io::Write, sync::RwLock}; -use wws_router::Routes; +use std::{io::Write, sync::RwLock}; +use wws_router::WORKERS; use wws_worker::io::WasmOutput; const CORS_HEADER: &str = "Access-Control-Allow-Origin"; @@ -31,23 +31,17 @@ const CORS_HEADER: &str = "Access-Control-Allow-Origin"; /// /// 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, - cors_origins: web::Data>>, -) -> HttpResponse { - let routes = req.app_data::>().unwrap(); - let stderr_file = req.app_data::>>().unwrap(); +pub async fn handle_worker(req: HttpRequest, body: Bytes) -> HttpResponse { + let app_data = req + .app_data::>() + .expect("error fetching app data"); let data_connectors = req .app_data::>>() - .unwrap() - .clone(); - // We will improve error handling + .expect("error fetching data connectors"); 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 @@ -58,16 +52,26 @@ pub async fn handle_worker( } } + 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 = 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()) @@ -75,21 +79,19 @@ pub async fn handle_worker( 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); } @@ -104,7 +106,7 @@ pub async fn handle_worker( builder.insert_header(("Content-Type", "text/html")); // Check if cors config has any origins to register - if let Some(origins) = cors_origins.as_ref() { + if let Some(origins) = app_data.cors_origins.as_ref() { // Check if worker has overridden the header, if not if !handler_result.headers.contains_key(CORS_HEADER) { // insert those origins in 'Access-Control-Allow-Origin' header @@ -123,7 +125,7 @@ pub async fn handle_worker( if handler_success && kv_namespace.is_some() { 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 94b027dc..36bc1b26 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,88 +16,141 @@ 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: Option, + pub stderr: Option, + pub cors_origins: Option>, +} + +#[derive(Default)] +pub struct AppData { + routes: Routes, + root_path: PathBuf, + stderr: Option, + cors_origins: 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, + root_path: serve_options.root_path.clone(), + stderr, + cors_origins: serve_options.cors_origins.clone(), + }) + } +} + /// 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>, - cors_origins: Option>, -) -> Result { +pub async fn serve(serve_options: ServeOptions) -> 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); - } + let data_connectors = Data::new(RwLock::new(DataConnectors::default())); - let cors_data = Data::new(cors_origins); + 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 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(&cors_data)); + .app_data(Data::clone(&app_data)) + .app_data(Data::clone(&data_connectors)); // Configure panel - if panel { + if serve_options.panel == Some(Panel::Enabled) { app = app.configure(config_panel_handlers); app = app.configure(config_manage_api_handlers); } + let workers = WORKERS + .read() + .expect("error locking worker lock for reading"); + // 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() { + 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 80c0c9cf..8d3aa240 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)] @@ -195,21 +195,21 @@ async fn main() -> std::io::Result<()> { ); } - let server = serve( - &project_path, - routes, - &args.hostname, - args.port, - args.enable_panel, - None, - args.cors, - ) + let server = serve(ServeOptions { + root_path: project_path, + base_routes: routes, + hostname: args.hostname.clone(), + port: args.port, + panel: Some(args.enable_panel.into()), + stderr: None, + cors_origins: args.cors, + }) .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