diff --git a/Cargo.lock b/Cargo.lock index ebf2c04c..6d803d05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" dependencies = [ "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -203,7 +203,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -294,7 +294,7 @@ checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -604,7 +604,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -895,7 +895,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 1.0.107", ] [[package]] @@ -1876,7 +1876,7 @@ checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2017,7 +2017,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "version_check", ] @@ -2034,9 +2034,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.51" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] @@ -2086,9 +2086,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.23" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -2388,7 +2388,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2552,6 +2552,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "system-interface" version = "0.25.4" @@ -2630,7 +2641,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2788,7 +2799,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2865,6 +2876,32 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utoipa" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ae74ef183fae36d650f063ae7bde1cacbe1cd7e72b617cbe1e985551878b98" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea8ac818da7e746a63285594cce8a96f5e00ee31994e655bd827569cb8b137b" +dependencies = [ + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.18", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -2968,7 +3005,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-shared", ] @@ -3002,7 +3039,7 @@ checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3139,7 +3176,7 @@ dependencies = [ "anyhow", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasmtime-component-util", "wasmtime-wit-bindgen", "wit-parser", @@ -3404,7 +3441,7 @@ dependencies = [ "proc-macro2", "quote", "shellexpand", - "syn", + "syn 1.0.107", "witx", ] @@ -3416,7 +3453,7 @@ checksum = "7dd1d09a625f96effa501cdff06192eb6a89eeadd4fd4e2489e0c6907f604307" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wiggle-generate", ] @@ -3654,10 +3691,30 @@ dependencies = [ "quote", "serde", "serde_json", - "syn", + "syn 1.0.107", "wasi", ] +[[package]] +name = "wws-api-manage" +version = "1.2.0" +dependencies = [ + "actix-web", + "serde", + "serde_json", + "utoipa", + "wws-router", + "wws-worker", +] + +[[package]] +name = "wws-api-manage-openapi" +version = "1.2.0" +dependencies = [ + "utoipa", + "wws-api-manage", +] + [[package]] name = "wws-config" version = "1.2.0" @@ -3728,6 +3785,7 @@ dependencies = [ "actix-files", "actix-web", "anyhow", + "wws-api-manage", "wws-data-kv", "wws-router", "wws-worker", @@ -3750,6 +3808,7 @@ dependencies = [ "base64 0.21.0", "serde", "serde_json", + "sha256", "toml 0.7.2", "wasi-common", "wasmtime", diff --git a/Cargo.toml b/Cargo.toml index c30a2eb4..85572292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,8 @@ openssl = { version = "=0.10.48", features = ["vendored"] } [workspace] members = [ + "crates/api-manage", + "crates/api-manage-openapi", "crates/config", "crates/data-kv", "crates/project", @@ -79,6 +81,8 @@ wws-server = { path = "./crates/server" } wws-store = { path = "./crates/store" } wws-worker = { path = "./crates/worker" } wws-project = { path = "./crates/project" } +wws-api-manage = { path = "./crates/api-manage" } +wws-api-manage-openapi = { path = "./crates/api-manage-openapi" } wasmtime = "6.0.2" wasmtime-wasi = "6.0.2" wasi-common = "6.0.2" diff --git a/crates/api-manage-openapi/Cargo.toml b/crates/api-manage-openapi/Cargo.toml new file mode 100644 index 00000000..4616d06d --- /dev/null +++ b/crates/api-manage-openapi/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "wws-api-manage-openapi" +version = { workspace = true } +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +[dependencies] + +[build-dependencies] +utoipa = { version = "3.3.0", features = ["actix_extras"] } +wws-api-manage = { path = "../api-manage" } diff --git a/crates/api-manage-openapi/build.rs b/crates/api-manage-openapi/build.rs new file mode 100644 index 00000000..ddccf5da --- /dev/null +++ b/crates/api-manage-openapi/build.rs @@ -0,0 +1,14 @@ +// Copyright 2023 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Trick to generate the OpenAPI spec on build time. +// See: https://github.com/juhaku/utoipa/issues/214#issuecomment-1179589373 + +use std::fs; +use utoipa::OpenApi; +use wws_api_manage::ApiDoc; + +fn main() { + let spec = ApiDoc::openapi().to_pretty_json().unwrap(); + fs::write("./src/openapi.json", spec).expect("Error writing the OpenAPI documentation"); +} diff --git a/crates/api-manage-openapi/src/lib.rs b/crates/api-manage-openapi/src/lib.rs new file mode 100644 index 00000000..b3ce5c4d --- /dev/null +++ b/crates/api-manage-openapi/src/lib.rs @@ -0,0 +1,5 @@ +// Copyright 2023 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Contains the Open API Spec of the wws management API. +pub static OPEN_API_SPEC: &str = include_str!("./openapi.json"); diff --git a/crates/api-manage-openapi/src/openapi.json b/crates/api-manage-openapi/src/openapi.json new file mode 100644 index 00000000..a5d30664 --- /dev/null +++ b/crates/api-manage-openapi/src/openapi.json @@ -0,0 +1,114 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Wasm Workers Server Management API", + "description": "Exposes methods to read current workers, services and to configure and run projects", + "contact": {}, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/vmware-labs/wasm-workers-server/blob/main/LICENSE" + }, + "version": "1" + }, + "paths": { + "/_api/v0/workers": { + "get": { + "tags": [ + "handlers::v0::workers" + ], + "summary": "Return the list of loaded workers.", + "description": "Return the list of loaded workers.", + "operationId": "handle_api_workers", + "responses": { + "200": { + "description": "Returns all the workers", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Worker" + } + } + } + } + } + } + } + }, + "/_api/v0/workers/{id}": { + "get": { + "tags": [ + "handlers::v0::workers" + ], + "summary": "Return the details of a specific worker. It includes all the configuration details", + "description": "Return the details of a specific worker. It includes all the configuration details", + "operationId": "handle_api_worker", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Worker identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Return the configuration associated to the given worker", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkerConfig" + } + } + } + } + }, + "404": { + "description": "The worker is not present" + } + } + } + } + }, + "components": { + "schemas": { + "Worker": { + "type": "object", + "description": "Defines a worker in a given application.", + "required": [ + "id", + "name", + "path", + "filepath" + ], + "properties": { + "filepath": { + "type": "string", + "description": "Associated source code / wasm module to this worker", + "example": "/app/api/hello.js" + }, + "id": { + "type": "string", + "description": "Worker identifier" + }, + "name": { + "type": "string", + "description": "The associated name to this worker", + "example": "default" + }, + "path": { + "type": "string", + "description": "API path for this specific worker.", + "example": "/api/hello" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/api-manage/Cargo.toml b/crates/api-manage/Cargo.toml new file mode 100644 index 00000000..4df68c1f --- /dev/null +++ b/crates/api-manage/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "wws-api-manage" +version = { workspace = true } +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +[dependencies] +actix-web = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +wws-router = { workspace = true } +wws-worker = { workspace = true } +utoipa = { version = "3.3.0", features = ["actix_extras"] } diff --git a/crates/api-manage/src/handlers/mod.rs b/crates/api-manage/src/handlers/mod.rs new file mode 100644 index 00000000..53b2adfa --- /dev/null +++ b/crates/api-manage/src/handlers/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod v0; diff --git a/crates/api-manage/src/handlers/v0/mod.rs b/crates/api-manage/src/handlers/v0/mod.rs new file mode 100644 index 00000000..c73cb3ff --- /dev/null +++ b/crates/api-manage/src/handlers/v0/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +pub mod workers; diff --git a/crates/api-manage/src/handlers/v0/workers.rs b/crates/api-manage/src/handlers/v0/workers.rs new file mode 100644 index 00000000..81e88c61 --- /dev/null +++ b/crates/api-manage/src/handlers/v0/workers.rs @@ -0,0 +1,48 @@ +// Copyright 2023 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::models::{Worker, WorkerConfig}; +use actix_web::{ + get, + web::{Data, Json, Path}, + HttpResponse, Responder, Result, +}; +use wws_router::Routes; + +/// Return the list of loaded workers. +#[utoipa::path( + responses( + (status = 200, description = "Returns all the workers", body = [Worker]) + ) +)] +#[get("/_api/v0/workers")] +pub async fn handle_api_workers(routes: Data) -> Result { + let workers: Vec = routes.routes.iter().map(Worker::from).collect(); + + Ok(Json(workers)) +} + +/// Return the details of a specific worker. It includes all the configuration details +#[utoipa::path( + responses( + (status = 200, description = "Return the configuration associated to the given worker", body = [WorkerConfig]), + (status = 404, description = "The worker is not present") + ), + params( + ("id" = String, Path, description = "Worker identifier"), + ) +)] +#[get("/_api/v0/workers/{id}")] +pub async fn handle_api_worker(routes: Data, path: Path) -> HttpResponse { + let worker = routes + .routes + .iter() + .find(|r| &r.worker.id == path.as_ref()) + .map(|r| &r.worker); + + if let Some(worker) = worker { + HttpResponse::Ok().json(WorkerConfig::from(worker)) + } else { + HttpResponse::NotFound().json("{}") + } +} diff --git a/crates/api-manage/src/lib.rs b/crates/api-manage/src/lib.rs new file mode 100644 index 00000000..2e02175c --- /dev/null +++ b/crates/api-manage/src/lib.rs @@ -0,0 +1,36 @@ +// Copyright 2023 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +mod handlers; +mod models; + +use actix_web::web::ServiceConfig; +use models::Worker; +use utoipa::OpenApi; + +/// Add the administration panel HTTP handlers to an existing +/// Actix application. +pub fn config_manage_api_handlers(cfg: &mut ServiceConfig) { + cfg.service(handlers::v0::workers::handle_api_workers); + cfg.service(handlers::v0::workers::handle_api_worker); +} + +#[derive(OpenApi)] +#[openapi( + info( + title = "Wasm Workers Server Management API", + description = "Exposes methods to read current workers, services and to configure and run projects", + license( + name = "Apache 2.0", + url = "https://github.com/vmware-labs/wasm-workers-server/blob/main/LICENSE" + ), + contact(), + version = "1" + ), + paths( + handlers::v0::workers::handle_api_workers, + handlers::v0::workers::handle_api_worker + ), + components(schemas(Worker)) +)] +pub struct ApiDoc; diff --git a/crates/api-manage/src/models/mod.rs b/crates/api-manage/src/models/mod.rs new file mode 100644 index 00000000..8aaa1a83 --- /dev/null +++ b/crates/api-manage/src/models/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2023 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +mod worker; +mod worker_config; + +pub use worker::Worker; +pub use worker_config::WorkerConfig; diff --git a/crates/api-manage/src/models/worker.rs b/crates/api-manage/src/models/worker.rs new file mode 100644 index 00000000..5d02dac4 --- /dev/null +++ b/crates/api-manage/src/models/worker.rs @@ -0,0 +1,35 @@ +// Copyright 2023 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use serde::Serialize; +use utoipa::ToSchema; +use wws_router::Route; + +#[derive(Serialize, ToSchema)] +/// Defines a worker in a given application. +pub struct Worker { + /// Worker identifier + id: String, + /// The associated name to this worker + #[schema(example = "default")] + name: String, + /// API path for this specific worker. + #[schema(example = "/api/hello")] + path: String, + /// Associated source code / wasm module to this worker + #[schema(example = "/app/api/hello.js")] + filepath: String, +} + +impl From<&Route> for Worker { + fn from(value: &Route) -> Self { + let name = value.worker.config.name.as_ref(); + + Self { + id: value.worker.id.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/api-manage/src/models/worker_config.rs b/crates/api-manage/src/models/worker_config.rs new file mode 100644 index 00000000..45dc93da --- /dev/null +++ b/crates/api-manage/src/models/worker_config.rs @@ -0,0 +1,97 @@ +// Copyright 2023 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; + +use serde::Serialize; +use utoipa::ToSchema; +use wws_worker::{ + config::{ConfigData, Folder}, + Worker, +}; + +#[derive(Serialize, ToSchema)] +/// Defines a worker in a given application. +pub struct WorkerConfig { + /// The worker identifier + #[schema(example = "default")] + id: String, + /// The associated name to this worker + #[schema(example = "default")] + name: String, + /// Version of the configuration file + #[schema(example = "/api/hello")] + version: String, + /// Associated data configuration + pub data: WorkerConfigData, + /// Mounted folders + pub folders: Vec, + /// Environment variables + pub vars: HashMap, +} + +impl From<&Worker> for WorkerConfig { + fn from(value: &Worker) -> Self { + let config = &value.config; + + let folders = config + .folders + .as_ref() + .map(|f| { + f.iter() + .map(WorkerFolder::from) + .collect::>() + }) + .unwrap_or(Vec::new()); + + Self { + id: value.id.clone(), + name: config + .name + .as_ref() + .unwrap_or(&String::from("default")) + .to_string(), + version: config.version.clone(), + data: WorkerConfigData::from(config.data.as_ref()), + folders, + vars: config.vars.clone(), + } + } +} + +#[derive(Serialize, ToSchema)] +/// Data configuration for this specific worker +pub struct WorkerConfigData { + /// Key/Value namespace this worker can read/write + kv: Option, +} + +impl From> for WorkerConfigData { + fn from(value: Option<&ConfigData>) -> Self { + Self { + kv: value + .map(|data| data.kv.as_ref().map(|kv| kv.namespace.clone())) + .unwrap_or(None), + } + } +} + +#[derive(Serialize, ToSchema)] +/// Data configuration for this specific worker +pub struct WorkerFolder { + /// Filesystem path to mount in the worker + #[schema(example = "/tmp/worker-dir")] + from: String, + /// Worker internal location for this specific folder + #[schema(example = "/tmp")] + to: String, +} + +impl From<&Folder> for WorkerFolder { + fn from(value: &Folder) -> Self { + Self { + from: value.from.to_string_lossy().to_string(), + to: value.to.clone(), + } + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 2bc134bd..e04dbbe9 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -10,11 +10,13 @@ mod files; mod route; use files::Files; -use route::{Route, RouteAffinity}; +use route::RouteAffinity; use std::path::{Path, PathBuf}; use std::time::Instant; use wws_config::Config; +pub use route::Route; + /// Contains all registered routes pub struct Routes { pub routes: Vec, diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 404ad6bf..d9660ea3 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -9,7 +9,8 @@ repository = { workspace = true } [dependencies] anyhow = { workspace = true } actix-web = { workspace = true } +wws-api-manage = { workspace = true } wws-data-kv = { workspace = true } wws-router = { workspace = true } wws-worker = { workspace = true } -actix-files = "0.6.2" \ No newline at end of file +actix-files = "0.6.2" diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index f85591ce..97915023 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -17,6 +17,7 @@ use handlers::worker::handle_worker; use std::fs::OpenOptions; use std::path::Path; use std::sync::RwLock; +use wws_api_manage::config_manage_api_handlers; use wws_data_kv::KV; use wws_router::Routes; @@ -33,6 +34,7 @@ pub async fn serve( base_routes: Routes, hostname: &str, port: u16, + panel: bool, stderr: Option<&Path>, ) -> Result { // Initializes the data connectors. For now, just KV @@ -61,6 +63,11 @@ pub async fn serve( .app_data(Data::clone(&root_path)) .app_data(Data::clone(&stderr_file)); + // Configure panel + if panel { + app = app.configure(config_manage_api_handlers); + } + // Append routes to the current service for route in routes.routes.iter() { app = app.service(web::resource(route.actix_path()).to(handle_worker)); diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index 66ca6048..36431f44 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -21,4 +21,5 @@ wasi-common = { workspace = true } wws-config = { workspace = true } wws-data-kv = { workspace = true } wws-runtimes = { workspace = true } -base64 = "0.21.0" \ No newline at end of file +base64 = "0.21.0" +sha256 = "1.1.1" diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index 0f9b126c..42635342 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -9,6 +9,7 @@ use actix_web::HttpRequest; use anyhow::{anyhow, Result}; use config::Config; use io::{WasmInput, WasmOutput}; +use sha256::digest as sha256_digest; use std::fs::{self, File}; use std::path::PathBuf; use std::{collections::HashMap, path::Path}; @@ -22,6 +23,8 @@ use wws_runtimes::{init_runtime, Runtime}; /// This struct will process requests by preparing the environment /// with the runtime and running it in Wasmtime pub struct Worker { + /// Worker identifier + pub id: String, /// Wasmtime engine to run this worker engine: Engine, /// Wasm Module @@ -37,6 +40,9 @@ pub struct Worker { impl Worker { /// Creates a new Worker pub fn new(project_root: &Path, path: &Path, project_config: &ProjectConfig) -> Result { + // Compute the identifier + let id = sha256_digest(project_root.join(path).to_string_lossy().as_bytes()); + // Load configuration let mut config_path = path.to_path_buf(); config_path.set_extension("toml"); @@ -59,6 +65,7 @@ impl Worker { runtime.prepare()?; Ok(Self { + id, engine, module, runtime, diff --git a/src/main.rs b/src/main.rs index acd0243e..c78f1de3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,6 +61,10 @@ pub struct Args { #[arg(long)] git_folder: Option, + /// Enable the administration panel + #[arg(long)] + enable_panel: bool, + /// Manage language runtimes in your project #[command(subcommand)] commands: Option
, @@ -180,9 +184,23 @@ async fn main() -> std::io::Result<()> { ); } - let server = serve(&project_path, routes, &args.hostname, args.port, None) - .await - .map_err(|err| Error::new(ErrorKind::AddrInUse, err))?; + if args.enable_panel { + println!( + "🎛️ The admin panel is available at http://{}:{}/_panel/", + &args.hostname, args.port + ); + } + + let server = serve( + &project_path, + routes, + &args.hostname, + args.port, + args.enable_panel, + None, + ) + .await + .map_err(|err| Error::new(ErrorKind::AddrInUse, err))?; println!( "🚀 Start serving requests at http://{}:{}\n",