use crate::error::{Result, WasmrunError};
use crate::logging::{LogEntry, LogSource, LogTrailSystem};
use crate::runtime::multilang_kernel::{MultiLanguageKernel, OsRunConfig};
use crate::runtime::project_files::ProjectFilesCollector;
use crate::runtime::runtime_cache::RuntimeCache;
use crate::runtime::tunnel::BoreClient;
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, RwLock};
use tiny_http::{Header, Method, Request, Response, Server};
const TEMPLATE_INDEX_HTML: &str = include_str!("../../templates/os/index.html");
const TEMPLATE_OS_JS: &str = include_str!("../../templates/os/os.js");
const TEMPLATE_INDEX_CSS: &str = include_str!("../../templates/os/index.css");
const TEMPLATE_LOGGING_JS: &str = include_str!("../../templates/os/logging.js");
const TEMPLATE_LOGS_HTML: &str = include_str!("../../templates/os/logs.html");
const ASSET_LOGO_PNG: &[u8] = include_bytes!("../../templates/assets/logo.png");
const ASSET_LOGO_TEXT_PNG: &[u8] = include_bytes!("../../templates/assets/logo-text.png");
pub struct OsServer {
kernel: Arc<RwLock<MultiLanguageKernel>>,
config: OsRunConfig,
project_pid: Arc<RwLock<Option<u32>>>,
template_cache: HashMap<String, String>,
log_system: Arc<LogTrailSystem>,
tunnel_client: Arc<RwLock<Option<BoreClient>>>,
runtime_cache: RuntimeCache,
cors_origin: String,
}
impl OsServer {
pub fn new(kernel: MultiLanguageKernel, config: OsRunConfig) -> Result<Self> {
let log_system = kernel.log_system();
let cors_origin = if config.allow_cors {
"*".to_string()
} else {
format!("http://127.0.0.1:{}", config.port.unwrap_or(8420))
};
let runtime_cache = RuntimeCache::new()?;
let mut server = Self {
kernel: Arc::new(RwLock::new(kernel)),
config,
project_pid: Arc::new(RwLock::new(None)),
template_cache: HashMap::new(),
log_system,
tunnel_client: Arc::new(RwLock::new(None)),
runtime_cache,
cors_origin,
};
server.load_templates()?;
Ok(server)
}
fn load_templates(&mut self) -> Result<()> {
let project_name = Path::new(&self.config.project_path)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let detected_language = self.detect_project_language()?;
let language = self
.config
.language
.as_deref()
.unwrap_or(&detected_language);
let port_str = self.config.port.unwrap_or(8420).to_string();
let index_content = TEMPLATE_INDEX_HTML
.replace("$PROJECT_NAME$", &project_name)
.replace("$LANGUAGE$", language)
.replace("$PROJECT_PATH$", &self.config.project_path)
.replace("$PORT$", &port_str)
.replace(
"<!-- @style-placeholder -->",
"<link rel=\"stylesheet\" href=\"/index.css\">",
)
.replace(
"<!-- @script-placeholder -->",
"<script src=\"/os.js\"></script>",
);
self.template_cache
.insert("index.html".to_string(), index_content);
self.template_cache
.insert("os.js".to_string(), TEMPLATE_OS_JS.to_string());
self.template_cache
.insert("index.css".to_string(), TEMPLATE_INDEX_CSS.to_string());
self.template_cache
.insert("logging.js".to_string(), TEMPLATE_LOGGING_JS.to_string());
self.template_cache.insert(
"logs.html".to_string(),
TEMPLATE_LOGS_HTML.replace("$PORT$", &port_str),
);
self.log_system.log(LogEntry::info(
LogSource::Kernel,
"OS mode templates loaded",
));
println!("✅ OS mode templates loaded");
Ok(())
}
fn cors_header(&self) -> Header {
Header::from_bytes(
&b"Access-Control-Allow-Origin"[..],
self.cors_origin.as_bytes(),
)
.unwrap()
}
fn detect_project_language(&self) -> Result<String> {
if Path::new(&self.config.project_path)
.join("package.json")
.exists()
{
return Ok("nodejs".to_string());
}
if Path::new(&self.config.project_path)
.join("Cargo.toml")
.exists()
{
return Ok("rust".to_string());
}
if Path::new(&self.config.project_path).join("go.mod").exists() {
return Ok("go".to_string());
}
let project_path = Path::new(&self.config.project_path);
if project_path.join("requirements.txt").exists()
|| project_path.join("pyproject.toml").exists()
{
return Ok("python".to_string());
}
Ok("unknown".to_string())
}
pub fn start(self, port: u16) -> Result<()> {
let server = Server::http(format!("127.0.0.1:{port}"))
.map_err(|e| WasmrunError::from(format!("Failed to start HTTP server: {e}")))?;
self.log_system.log(LogEntry::info(
LogSource::Kernel,
format!("OS Mode server listening on http://127.0.0.1:{port}"),
));
println!("🌐 OS Mode server listening on http://127.0.0.1:{port}");
self.start_project()?;
for request in server.incoming_requests() {
match self.handle_request(request) {
Ok(_) => {}
Err(e) => eprintln!("Request handling error: {e}"),
}
}
Ok(())
}
fn start_project(&self) -> Result<()> {
let mut project_pid = self.project_pid.write().unwrap();
match self.run_project_in_kernel() {
Ok(pid) => {
*project_pid = Some(pid);
Ok(())
}
Err(e) => {
self.log_system.log(LogEntry::error(
LogSource::Kernel,
format!("Failed to start project in kernel: {e}"),
));
eprintln!("⚠️ Failed to start project in kernel: {e}");
Ok(())
}
}
}
fn run_project_in_kernel(&self) -> Result<u32> {
let mut kernel = self.kernel.write().unwrap();
if let Err(e) = kernel.mount_project(&self.config.project_path) {
eprintln!("⚠️ Failed to mount project directory: {e}");
}
match kernel.auto_detect_and_run(self.config.clone()) {
Ok(pid) => {
self.log_system.log(
LogEntry::info(
LogSource::Kernel,
format!("Project started with PID: {pid}"),
)
.with_pid(pid),
);
println!("✅ Project started with PID: {pid}");
Ok(pid)
}
Err(e) => Err(WasmrunError::from(e.to_string())),
}
}
fn handle_request(&self, request: Request) -> Result<()> {
let method = request.method().clone();
let url = request.url().to_string();
match (method, url.as_str()) {
(Method::Options, _) => {
let response = Response::from_string("")
.with_header(self.cors_header())
.with_header(
Header::from_bytes(
&b"Access-Control-Allow-Methods"[..],
&b"GET, POST, DELETE, OPTIONS"[..],
)
.unwrap(),
)
.with_header(
Header::from_bytes(
&b"Access-Control-Allow-Headers"[..],
&b"Content-Type"[..],
)
.unwrap(),
)
.with_status_code(tiny_http::StatusCode(204));
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
(Method::Get, "/") => {
if let Some(content) = self.template_cache.get("index.html") {
let response = Response::from_string(content).with_header(
Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..])
.unwrap(),
);
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
} else {
self.send_404(request)?;
}
}
(Method::Get, "/os.js") => {
if let Some(content) = self.template_cache.get("os.js") {
let response = Response::from_string(content).with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/javascript"[..])
.unwrap(),
);
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
} else {
self.send_404(request)?;
}
}
(Method::Get, "/index.css") => {
if let Some(content) = self.template_cache.get("index.css") {
let response = Response::from_string(content).with_header(
Header::from_bytes(&b"Content-Type"[..], &b"text/css"[..]).unwrap(),
);
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
} else {
self.send_404(request)?;
}
}
(Method::Get, "/logging.js") => {
if let Some(content) = self.template_cache.get("logging.js") {
let response = Response::from_string(content).with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/javascript"[..])
.unwrap(),
);
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
} else {
self.send_404(request)?;
}
}
(Method::Get, "/logs") => {
if let Some(content) = self.template_cache.get("logs.html") {
let response = Response::from_string(content).with_header(
Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..])
.unwrap(),
);
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
} else {
self.send_404(request)?;
}
}
(Method::Get, "/ws") => {
let response = Response::from_string("WebSocket not implemented yet").with_header(
Header::from_bytes(&b"Content-Type"[..], &b"text/plain"[..]).unwrap(),
);
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
(Method::Get, path) if path.starts_with("/api/runtime/") => {
let language = &path[13..]; self.handle_runtime_request(request, language)?;
}
(Method::Get, "/api/runtimes") => {
self.handle_runtimes_list_request(request)?;
}
(Method::Get, "/api/project/files") => {
self.handle_project_files_request(request)?;
}
(Method::Get, "/api/kernel/stats") => {
self.handle_kernel_stats_request(request)?;
}
(Method::Get, "/api/fs/stats") => {
self.handle_fs_stats_request(request)?;
}
(Method::Get, path) if path.starts_with("/api/fs/read/") => {
let file_path = &path[13..]; self.handle_fs_read_request(request, file_path)?;
}
(Method::Get, path) if path.starts_with("/api/fs/list/") => {
let dir_path = &path[13..]; self.handle_fs_list_request(request, dir_path)?;
}
(Method::Post, path) if path.starts_with("/api/fs/write/") => {
let file_path = &path[14..]; self.handle_fs_write_request(request, file_path)?;
}
(Method::Post, path) if path.starts_with("/api/fs/mkdir/") => {
let dir_path = &path[14..]; self.handle_fs_mkdir_request(request, dir_path)?;
}
(Method::Post, path) if path.starts_with("/api/fs/delete/") => {
let file_path = &path[15..]; self.handle_fs_delete_request(request, file_path)?;
}
(Method::Post, "/api/kernel/start") => {
self.handle_start_project(request)?;
}
(Method::Post, "/api/kernel/restart") => {
self.handle_restart_project(request)?;
}
(Method::Get, path)
if path.starts_with("/api/processes/") && path.ends_with("/ports") =>
{
let parts: Vec<&str> = path.split('/').collect();
if parts.len() >= 4 {
if let Ok(pid) = parts[3].parse::<u32>() {
self.handle_list_ports_request(request, pid)?;
} else {
self.send_error(request, "Invalid PID")?;
}
} else {
self.send_404(request)?;
}
}
(Method::Post, path)
if path.starts_with("/api/processes/") && path.contains("/forward") =>
{
let parts: Vec<&str> = path.split('/').collect();
if parts.len() >= 4 {
if let Ok(pid) = parts[3].parse::<u32>() {
self.handle_create_port_forward_request(request, pid)?;
} else {
self.send_error(request, "Invalid PID")?;
}
} else {
self.send_404(request)?;
}
}
(Method::Delete, path)
if path.starts_with("/api/processes/") && path.contains("/forward/") =>
{
let parts: Vec<&str> = path.split('/').collect();
if parts.len() >= 6 {
if let (Ok(pid), Ok(guest_port)) =
(parts[3].parse::<u32>(), parts[5].parse::<u16>())
{
self.handle_delete_port_forward_request(request, pid, guest_port)?;
} else {
self.send_error(request, "Invalid PID or port")?;
}
} else {
self.send_404(request)?;
}
}
(Method::Get, "/api/logs") => {
self.handle_logs_request(request)?;
}
(Method::Get, "/api/logs/recent") => {
self.handle_recent_logs_request(request)?;
}
(Method::Post, "/api/tunnel/start") => {
self.handle_tunnel_start_request(request)?;
}
(Method::Get, "/api/tunnel/status") => {
self.handle_tunnel_status_request(request)?;
}
(Method::Post, "/api/tunnel/stop") => {
self.handle_tunnel_stop_request(request)?;
}
(Method::Get, path) if path.starts_with("/assets/") => {
self.serve_asset(request, &path[8..])?; }
(Method::Get, path) if path.starts_with("/app/") => {
let project_path = &path[5..]; self.proxy_to_dev_server(request, project_path)?;
}
_ => {
self.send_404(request)?;
}
}
Ok(())
}
fn handle_start_project(&self, request: Request) -> Result<()> {
let mut project_pid = self.project_pid.write().unwrap();
let response_json = if project_pid.is_some() {
serde_json::json!({ "success": false, "error": "Project is already running" })
} else {
match self.run_project_in_kernel() {
Ok(pid) => {
*project_pid = Some(pid);
serde_json::json!({ "success": true, "pid": pid })
}
Err(e) => {
serde_json::json!({ "success": false, "error": e.to_string() })
}
}
};
let status = if response_json["success"].as_bool() == Some(true) {
200
} else if project_pid.is_some() {
409
} else {
500
};
let response = Response::from_string(response_json.to_string())
.with_status_code(tiny_http::StatusCode(status))
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
Ok(())
}
fn handle_restart_project(&self, request: Request) -> Result<()> {
let mut project_pid = self.project_pid.write().unwrap();
if let Some(pid) = *project_pid {
let mut kernel = self.kernel.write().unwrap();
let _ = kernel.kill_process(pid);
}
*project_pid = None;
let response_json = match self.run_project_in_kernel() {
Ok(pid) => {
*project_pid = Some(pid);
serde_json::json!({ "success": true, "pid": pid })
}
Err(e) => {
serde_json::json!({ "success": false, "error": e.to_string() })
}
};
let status = if response_json["success"].as_bool() == Some(true) {
200
} else {
500
};
let response = Response::from_string(response_json.to_string())
.with_status_code(tiny_http::StatusCode(status))
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
Ok(())
}
fn handle_kernel_stats_request(&self, request: Request) -> Result<()> {
let kernel = self.kernel.read().unwrap();
let stats = kernel.get_statistics();
let project_pid = *self.project_pid.read().unwrap();
let stats_json = serde_json::json!({
"status": "running",
"active_processes": stats.active_processes,
"total_memory_usage": stats.total_memory_usage,
"active_runtimes": stats.active_runtimes,
"active_dev_servers": stats.active_dev_servers,
"project_pid": project_pid,
"os": stats.os,
"arch": stats.arch,
"kernel_version": stats.kernel_version,
"wasi_capabilities": stats.wasi_capabilities,
"filesystem_mounts": stats.filesystem_mounts,
"supported_languages": stats.supported_languages,
});
let response = Response::from_string(stats_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
Ok(())
}
fn handle_list_ports_request(&self, request: Request, pid: u32) -> Result<()> {
let kernel = self.kernel.read().unwrap();
let all_network_stats = kernel.get_network_stats();
if let Some(network_stats) = all_network_stats.get(&pid) {
let mappings = if let Some(ns) = kernel.get_network_namespace(pid) {
ns.list_port_mappings()
} else {
vec![]
};
let response_json = serde_json::json!({
"success": true,
"pid": pid,
"port_mappings": mappings.iter().map(|m| {
serde_json::json!({
"guest_port": m.guest_port,
"host_port": m.host_port,
"protocol": format!("{:?}", m.protocol),
"created_at": m.created_at.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default().as_secs()
})
}).collect::<Vec<_>>(),
"network_stats": {
"base_port": network_stats.base_port,
"allocated_ports": network_stats.allocated_ports,
"total_connections": network_stats.total_connections,
"active_connections": network_stats.active_connections,
"listening_sockets": network_stats.listening_sockets,
}
});
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
} else {
self.send_error(request, &format!("Process with PID {pid} not found"))?;
}
Ok(())
}
fn handle_create_port_forward_request(&self, mut request: Request, pid: u32) -> Result<()> {
let mut content = String::new();
let mut reader = request.as_reader();
if let Err(e) = std::io::Read::read_to_string(&mut reader, &mut content) {
return self.send_error(request, &format!("Failed to read request body: {e}"));
}
let body: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(e) => return self.send_error(request, &format!("Invalid JSON: {e}")),
};
let guest_port = match body.get("guest_port").and_then(|v| v.as_u64()) {
Some(p) if p <= u16::MAX as u64 => p as u16,
_ => return self.send_error(request, "Invalid guest_port"),
};
let protocol = match body.get("protocol").and_then(|v| v.as_str()) {
Some("tcp") | Some("Tcp") => crate::runtime::network_namespace::SocketProtocol::Tcp,
Some("udp") | Some("Udp") => crate::runtime::network_namespace::SocketProtocol::Udp,
_ => return self.send_error(request, "Invalid protocol (must be 'tcp' or 'udp')"),
};
let kernel = self.kernel.read().unwrap();
if let Some(ns) = kernel.get_network_namespace(pid) {
match ns.allocate_port(guest_port, protocol) {
Ok(host_port) => {
let response_json = serde_json::json!({
"success": true,
"pid": pid,
"guest_port": guest_port,
"host_port": host_port,
"protocol": format!("{:?}", protocol)
});
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..])
.unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
Err(e) => {
self.send_error(request, &format!("Failed to allocate port: {e}"))?;
}
}
} else {
self.send_error(request, &format!("Process with PID {pid} not found"))?;
}
Ok(())
}
fn handle_delete_port_forward_request(
&self,
request: Request,
pid: u32,
guest_port: u16,
) -> Result<()> {
let kernel = self.kernel.read().unwrap();
if let Some(ns) = kernel.get_network_namespace(pid) {
match ns.deallocate_port(guest_port) {
Ok(()) => {
let response_json = serde_json::json!({
"success": true,
"pid": pid,
"guest_port": guest_port,
"message": "Port mapping removed successfully"
});
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..])
.unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
Err(e) => {
self.send_error(request, &format!("Failed to remove port mapping: {e}"))?;
}
}
} else {
self.send_error(request, &format!("Process with PID {pid} not found"))?;
}
Ok(())
}
fn handle_runtime_request(&self, request: Request, language: &str) -> Result<()> {
if language.is_empty() || language.contains("..") || language.contains('/') {
return self.send_error(request, "Invalid language identifier");
}
let wasmhub_lang = crate::runtime::runtime_cache::wasmhub_language(language);
match self.runtime_cache.get_runtime(wasmhub_lang) {
Ok(wasm_bytes) => {
self.log_system.log(LogEntry::info(
LogSource::Kernel,
format!("Serving {language} runtime ({} bytes)", wasm_bytes.len()),
));
let response = Response::from_data(wasm_bytes)
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/wasm"[..]).unwrap(),
)
.with_header(
Header::from_bytes(&b"Cache-Control"[..], &b"public, max-age=86400"[..])
.unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
Err(e) => {
self.log_system.log(LogEntry::error(
LogSource::Kernel,
format!("Failed to fetch {language} runtime: {e}"),
));
let error_json = serde_json::json!({
"success": false,
"error": format!("Runtime not available for '{language}': {e}")
});
let response = Response::from_string(error_json.to_string())
.with_status_code(tiny_http::StatusCode(404))
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
}
Ok(())
}
fn handle_runtimes_list_request(&self, request: Request) -> Result<()> {
let detected_language = self.detect_project_language()?;
let wasmhub_lang = crate::runtime::runtime_cache::wasmhub_language(&detected_language);
let mut response_json = serde_json::json!({
"detected_language": detected_language,
"wasmhub_runtime": wasmhub_lang,
"cached": self.runtime_cache.is_cached(wasmhub_lang),
"cached_version": self.runtime_cache.cached_version(wasmhub_lang),
});
if let Ok(manifest) = self.runtime_cache.fetch_manifest() {
response_json["wasmhub_version"] = serde_json::Value::String(manifest.version);
response_json["available_languages"] =
serde_json::json!(manifest.languages.keys().collect::<Vec<_>>());
}
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
Ok(())
}
fn handle_project_files_request(&self, request: Request) -> Result<()> {
let collector = match ProjectFilesCollector::new(&self.config.project_path) {
Ok(c) => c,
Err(e) => {
self.log_system.log(LogEntry::error(
LogSource::Kernel,
format!("Failed to read project files: {e}"),
));
return self.send_error(request, &format!("Failed to read project files: {e}"));
}
};
match collector.collect() {
Ok(bundle) => {
self.log_system.log(LogEntry::info(
LogSource::Kernel,
format!(
"Serving {} project files ({} bytes)",
bundle.file_count, bundle.total_size
),
));
let response_json = serde_json::json!({
"success": true,
"files": bundle.files,
"file_count": bundle.file_count,
"total_size": bundle.total_size,
"project_path": bundle.project_path,
"skipped": bundle.skipped,
});
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
Err(e) => {
self.log_system.log(LogEntry::error(
LogSource::Kernel,
format!("Failed to collect project files: {e}"),
));
let error_json = serde_json::json!({
"success": false,
"error": format!("Failed to collect project files: {e}")
});
let response = Response::from_string(error_json.to_string())
.with_status_code(tiny_http::StatusCode(500))
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
}
Ok(())
}
fn send_error(&self, request: Request, error_msg: &str) -> Result<()> {
let response_json = serde_json::json!({
"success": false,
"error": error_msg
});
let response = Response::from_string(response_json.to_string())
.with_status_code(tiny_http::StatusCode(400))
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
Ok(())
}
fn proxy_to_dev_server(&self, request: Request, path: &str) -> Result<()> {
let project_pid = *self.project_pid.read().unwrap();
if let Some(pid) = project_pid {
let kernel = self.kernel.read().unwrap();
let dev_server_port = kernel.get_dev_server_status(pid).and_then(|status| {
if let crate::runtime::registry::DevServerStatus::Running(port) = status {
Some(port)
} else {
None
}
});
if let Some(port) = dev_server_port {
let target_url = format!(
"http://127.0.0.1:{}{}",
port,
if path.is_empty() { "/" } else { path }
);
match self.fetch_from_dev_server(&target_url) {
Ok((content, content_type)) => {
let response = Response::from_string(content).with_header(
Header::from_bytes(&b"Content-Type"[..], content_type.as_bytes())
.unwrap(),
);
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
Err(e) => {
let error_html = format!(
"<html><body><h1>Dev Server Error</h1><p>{e}</p></body></html>"
);
let response = Response::from_string(error_html).with_header(
Header::from_bytes(&b"Content-Type"[..], &b"text/html"[..]).unwrap(),
);
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
}
} else {
let error_html = format!(
"<html><body><h1>No Dev Server</h1><p>No dev server running for PID {pid}</p></body></html>"
);
let response = Response::from_string(error_html).with_header(
Header::from_bytes(&b"Content-Type"[..], &b"text/html"[..]).unwrap(),
);
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
} else {
let error_html = "<html><body><h1>No Project Running</h1><p>No project is currently running</p></body></html>";
let response = Response::from_string(error_html)
.with_header(Header::from_bytes(&b"Content-Type"[..], &b"text/html"[..]).unwrap());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
Ok(())
}
fn fetch_from_dev_server(&self, url: &str) -> Result<(String, String)> {
use std::io::Read;
use std::net::TcpStream;
let url_without_scheme = url.strip_prefix("http://").unwrap_or(url);
let parts: Vec<&str> = url_without_scheme.splitn(2, '/').collect();
let host = parts[0];
let path = if parts.len() > 1 {
format!("/{}", parts[1])
} else {
"/".to_string()
};
let mut stream = TcpStream::connect(host)
.map_err(|e| WasmrunError::from(format!("Failed to connect to dev server: {e}")))?;
let request = format!("GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n");
std::io::Write::write_all(&mut stream, request.as_bytes())
.map_err(|e| WasmrunError::from(format!("Failed to send request: {e}")))?;
let mut response = String::new();
stream
.read_to_string(&mut response)
.map_err(|e| WasmrunError::from(format!("Failed to read response: {e}")))?;
if let Some(header_end) = response.find("\r\n\r\n") {
let headers = &response[..header_end];
let body = &response[header_end + 4..];
let content_type = headers
.lines()
.find(|line| line.to_lowercase().starts_with("content-type:"))
.and_then(|line| line.split(':').nth(1))
.map(|ct| ct.trim().to_string())
.unwrap_or_else(|| "text/html".to_string());
Ok((body.to_string(), content_type))
} else {
Err(WasmrunError::from("Invalid HTTP response"))
}
}
fn serve_asset(&self, request: Request, asset_path: &str) -> Result<()> {
let (content, content_type): (&[u8], &str) = match asset_path {
"logo.png" => (ASSET_LOGO_PNG, "image/png"),
"logo-text.png" => (ASSET_LOGO_TEXT_PNG, "image/png"),
_ => return self.send_404(request),
};
let response = Response::from_data(content.to_vec()).with_header(
Header::from_bytes(&b"Content-Type"[..], content_type.as_bytes()).unwrap(),
);
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
Ok(())
}
fn send_404(&self, request: Request) -> Result<()> {
let not_found = "
<html>
<head><title>404 - Not Found</title></head>
<body>
<h1>404 - Not Found</h1>
<p>The requested resource was not found on this server.</p>
</body>
</html>
";
let response = Response::from_string(not_found)
.with_status_code(tiny_http::StatusCode(404))
.with_header(Header::from_bytes(&b"Content-Type"[..], &b"text/html"[..]).unwrap());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
Ok(())
}
fn handle_fs_stats_request(&self, request: Request) -> Result<()> {
let kernel = self.kernel.read().unwrap();
let wasi_fs = kernel.wasi_filesystem();
let stats = wasi_fs.get_stats();
let stats_json =
serde_json::to_string(&stats).map_err(|e| WasmrunError::from(e.to_string()))?;
let response = Response::from_string(stats_json)
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
Ok(())
}
fn handle_fs_read_request(&self, request: Request, file_path: &str) -> Result<()> {
let kernel = self.kernel.read().unwrap();
let wasi_fs = kernel.wasi_filesystem();
let normalized_path = if file_path.starts_with('/') {
file_path.to_string()
} else {
format!("/{file_path}")
};
match wasi_fs.read_file(&normalized_path) {
Ok(content) => {
let is_text = content
.iter()
.all(|&b| b.is_ascii() || b == b'\n' || b == b'\r' || b == b'\t');
let response_json = if is_text {
serde_json::json!({
"success": true,
"path": file_path,
"content": String::from_utf8_lossy(&content),
"size": content.len(),
"type": "text"
})
} else {
let hex_content: String = content.iter().map(|b| format!("{b:02x}")).collect();
serde_json::json!({
"success": true,
"path": file_path,
"content": hex_content,
"size": content.len(),
"type": "binary"
})
};
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
Err(e) => {
let error_json = serde_json::json!({
"success": false,
"error": e.to_string()
});
let response = Response::from_string(error_json.to_string())
.with_status_code(tiny_http::StatusCode(404))
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
}
Ok(())
}
fn handle_fs_list_request(&self, request: Request, dir_path: &str) -> Result<()> {
let kernel = self.kernel.read().unwrap();
let wasi_fs = kernel.wasi_filesystem();
let normalized_path = if dir_path.starts_with('/') {
dir_path.to_string()
} else {
format!("/{dir_path}")
};
match wasi_fs.path_readdir(&normalized_path) {
Ok(entries) => {
let response_json = serde_json::json!({
"success": true,
"path": dir_path,
"entries": entries
});
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
Err(e) => {
let error_json = serde_json::json!({
"success": false,
"error": e.to_string()
});
let response = Response::from_string(error_json.to_string())
.with_status_code(tiny_http::StatusCode(404))
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
}
Ok(())
}
fn handle_fs_write_request(&self, mut request: Request, file_path: &str) -> Result<()> {
let mut body = Vec::new();
let mut reader = request.as_reader();
std::io::Read::read_to_end(&mut reader, &mut body)
.map_err(|e| WasmrunError::from(e.to_string()))?;
let kernel = self.kernel.read().unwrap();
let wasi_fs = kernel.wasi_filesystem();
let normalized_path = if file_path.starts_with('/') {
file_path.to_string()
} else {
format!("/{file_path}")
};
match wasi_fs.write_file(&normalized_path, &body) {
Ok(_) => {
let response_json = serde_json::json!({
"success": true,
"path": file_path,
"size": body.len()
});
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
Err(e) => {
let error_json = serde_json::json!({
"success": false,
"error": e.to_string()
});
let response = Response::from_string(error_json.to_string())
.with_status_code(tiny_http::StatusCode(500))
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
}
Ok(())
}
fn handle_fs_mkdir_request(&self, request: Request, dir_path: &str) -> Result<()> {
let kernel = self.kernel.read().unwrap();
let wasi_fs = kernel.wasi_filesystem();
let normalized_path = if dir_path.starts_with('/') {
dir_path.to_string()
} else {
format!("/{dir_path}")
};
match wasi_fs.path_create_directory(&normalized_path) {
Ok(_) => {
let response_json = serde_json::json!({
"success": true,
"path": dir_path
});
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
Err(e) => {
let error_json = serde_json::json!({
"success": false,
"error": e.to_string()
});
let response = Response::from_string(error_json.to_string())
.with_status_code(tiny_http::StatusCode(500))
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
}
Ok(())
}
fn handle_fs_delete_request(&self, request: Request, file_path: &str) -> Result<()> {
let kernel = self.kernel.read().unwrap();
let wasi_fs = kernel.wasi_filesystem();
let normalized_path = if file_path.starts_with('/') {
file_path.to_string()
} else {
format!("/{file_path}")
};
match wasi_fs.path_unlink_file(&normalized_path) {
Ok(_) => {
let response_json = serde_json::json!({
"success": true,
"path": file_path
});
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
Err(e) => {
let error_json = serde_json::json!({
"success": false,
"error": e.to_string()
});
let response = Response::from_string(error_json.to_string())
.with_status_code(tiny_http::StatusCode(500))
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
}
Ok(())
}
fn handle_logs_request(&self, request: Request) -> Result<()> {
let logs = self.log_system.get_all();
let response_json = serde_json::json!({
"success": true,
"count": logs.len(),
"logs": logs
});
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
Ok(())
}
fn handle_recent_logs_request(&self, request: Request) -> Result<()> {
let count = 100;
let logs = self.log_system.get_recent(count);
let response_json = serde_json::json!({
"success": true,
"count": logs.len(),
"logs": logs
});
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
Ok(())
}
fn handle_tunnel_start_request(&self, request: Request) -> Result<()> {
let mut tunnel_guard = self.tunnel_client.write().unwrap();
if tunnel_guard.is_some() {
let response_json = serde_json::json!({
"success": false,
"error": "Tunnel is already running"
});
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
return Ok(());
}
let server = self
.config
.tunnel_server
.clone()
.unwrap_or_else(|| "bore.pub:7835".to_string());
let secret = self.config.tunnel_secret.clone();
let local_port = self.config.port.unwrap_or(8420);
let mut client = BoreClient::new(server, secret, local_port);
match client.connect() {
Ok(public_url) => {
let response_json = serde_json::json!({
"success": true,
"public_url": public_url,
"status": "Connected"
});
*tunnel_guard = Some(client);
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
Err(e) => {
let response_json = serde_json::json!({
"success": false,
"error": e
});
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
}
}
Ok(())
}
fn handle_tunnel_status_request(&self, request: Request) -> Result<()> {
let tunnel_guard = self.tunnel_client.read().unwrap();
let response_json = if let Some(ref client) = *tunnel_guard {
let status = client.get_status();
let status_str = match status {
crate::runtime::tunnel::bore::TunnelStatus::Disconnected => "Disconnected",
crate::runtime::tunnel::bore::TunnelStatus::Connecting => "Connecting",
crate::runtime::tunnel::bore::TunnelStatus::Connected => "Connected",
crate::runtime::tunnel::bore::TunnelStatus::Reconnecting => "Reconnecting",
crate::runtime::tunnel::bore::TunnelStatus::Failed => "Failed",
};
serde_json::json!({
"success": true,
"status": status_str,
"public_url": client.get_public_url(),
"public_port": client.get_public_port()
})
} else {
serde_json::json!({
"success": true,
"status": "Not started"
})
};
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
Ok(())
}
fn handle_tunnel_stop_request(&self, request: Request) -> Result<()> {
let mut tunnel_guard = self.tunnel_client.write().unwrap();
let response_json = if let Some(ref client) = *tunnel_guard {
client.stop();
*tunnel_guard = None;
serde_json::json!({
"success": true,
"message": "Tunnel stopped"
})
} else {
serde_json::json!({
"success": false,
"error": "No tunnel is running"
})
};
let response = Response::from_string(response_json.to_string())
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
)
.with_header(self.cors_header());
request
.respond(response)
.map_err(|e| WasmrunError::from(e.to_string()))?;
Ok(())
}
}