From d0db32fa6acb6efb5269de9793a63de92ef9eb4a Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 26 Jun 2026 12:20:37 +0900 Subject: [PATCH] fix: add worker runtime REST process binary --- crates/worker-runtime/Cargo.toml | 5 + crates/worker-runtime/src/main.rs | 346 ++++++++++++++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 crates/worker-runtime/src/main.rs diff --git a/crates/worker-runtime/Cargo.toml b/crates/worker-runtime/Cargo.toml index 2cbeefd7..d8588ab2 100644 --- a/crates/worker-runtime/Cargo.toml +++ b/crates/worker-runtime/Cargo.toml @@ -5,6 +5,11 @@ version = "0.1.0" edition.workspace = true license.workspace = true +[[bin]] +name = "worker-runtime-rest-server" +path = "src/main.rs" +required-features = ["http-server"] + [features] default = [] fs-store = ["dep:serde_json"] diff --git a/crates/worker-runtime/src/main.rs b/crates/worker-runtime/src/main.rs new file mode 100644 index 00000000..8b511d58 --- /dev/null +++ b/crates/worker-runtime/src/main.rs @@ -0,0 +1,346 @@ +//! Minimal Runtime REST process wrapper. +//! +//! This binary is available only when the `http-server` feature is enabled. It +//! starts a Runtime-local command API intended for a trusted backend/proxy; +//! browsers must not connect to this Runtime process directly. + +use std::collections::VecDeque; +use std::env; +use std::error::Error; +use std::fmt; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::process::ExitCode; + +use worker_runtime::http_server::{ + RuntimeHttpServer, RuntimeHttpServerConfig, RuntimeHttpServerError, RuntimeHttpStoreSelection, +}; +use worker_runtime::identity::RuntimeId; + +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("worker-runtime-rest-server: {error}"); + if let ProcessError::Usage(_) = error { + eprintln!(); + eprintln!("{}", usage()); + ExitCode::from(2) + } else { + ExitCode::FAILURE + } + } + } +} + +fn run() -> Result<(), ProcessError> { + let Some(config) = parse_args(env::args().skip(1))? else { + println!("{}", usage()); + return Ok(()); + }; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_io() + .build()?; + runtime.block_on(async move { + let server = RuntimeHttpServer::bind(config).await?; + let local_addr = server.local_addr()?; + eprintln!( + "worker-runtime REST server listening on {local_addr}; intended client is a trusted backend/proxy, not a browser" + ); + server.serve().await + })?; + Ok(()) +} + +fn parse_args(args: I) -> Result, ProcessError> +where + I: IntoIterator, + S: Into, +{ + let mut config = RuntimeHttpServerConfig::default(); + let mut store = StoreArg::Memory; + let mut args = args.into_iter().map(Into::into).collect::>(); + + while let Some(arg) = args.pop_front() { + let (flag, inline_value) = split_flag_value(arg)?; + match flag.as_str() { + "--help" | "-h" => return Ok(None), + "--bind" => { + let value = take_value(&flag, inline_value, &mut args)?; + config.bind_addr = value.parse::().map_err(|error| { + ProcessError::usage(format!("invalid --bind socket address `{value}`: {error}")) + })?; + } + "--runtime-id" => { + let value = take_value(&flag, inline_value, &mut args)?; + config.runtime_id = Some(RuntimeId::new(value).ok_or_else(|| { + ProcessError::usage("--runtime-id must not be empty".to_string()) + })?); + } + "--display-name" => { + config.display_name = Some(take_value(&flag, inline_value, &mut args)?); + } + "--store" => { + let value = take_value(&flag, inline_value, &mut args)?; + store = match value.as_str() { + "memory" => StoreArg::Memory, + "fs" | "fs-store" => StoreArg::Fs { root: None }, + _ => { + return Err(ProcessError::usage(format!( + "unsupported --store `{value}`; expected `memory` or `fs`" + ))); + } + }; + } + "--fs-root" => { + let value = take_value(&flag, inline_value, &mut args)?; + store = StoreArg::Fs { + root: Some(PathBuf::from(value)), + }; + } + "--local-token" => { + let value = take_value(&flag, inline_value, &mut args)?; + if value.is_empty() { + return Err(ProcessError::usage( + "--local-token must not be empty when provided".to_string(), + )); + } + config.local_token = Some(value); + } + "--local-token-env" => { + let name = take_value(&flag, inline_value, &mut args)?; + let value = env::var(&name).map_err(|error| { + ProcessError::usage(format!( + "failed to read --local-token-env `{name}`: {error}" + )) + })?; + if value.is_empty() { + return Err(ProcessError::usage(format!( + "--local-token-env `{name}` resolved to an empty value" + ))); + } + config.local_token = Some(value); + } + "--max-transcript-projection-items" => { + config.limits.max_transcript_projection_items = + parse_usize_flag(&flag, take_value(&flag, inline_value, &mut args)?)?; + } + "--max-event-batch-items" => { + config.limits.max_event_batch_items = + parse_usize_flag(&flag, take_value(&flag, inline_value, &mut args)?)?; + } + _ => { + return Err(ProcessError::usage(format!("unknown argument `{flag}`"))); + } + } + } + + apply_store_selection(&mut config, store)?; + Ok(Some(config)) +} + +fn split_flag_value(arg: String) -> Result<(String, Option), ProcessError> { + if !arg.starts_with('-') { + return Err(ProcessError::usage(format!( + "unexpected positional argument `{arg}`" + ))); + } + if let Some((flag, value)) = arg.split_once('=') { + Ok((flag.to_string(), Some(value.to_string()))) + } else { + Ok((arg, None)) + } +} + +fn take_value( + flag: &str, + inline_value: Option, + args: &mut VecDeque, +) -> Result { + if let Some(value) = inline_value { + return Ok(value); + } + args.pop_front() + .ok_or_else(|| ProcessError::usage(format!("{flag} requires a value"))) +} + +fn parse_usize_flag(flag: &str, value: String) -> Result { + value + .parse::() + .map_err(|error| ProcessError::usage(format!("invalid {flag} value `{value}`: {error}"))) +} + +fn apply_store_selection( + config: &mut RuntimeHttpServerConfig, + store: StoreArg, +) -> Result<(), ProcessError> { + match store { + StoreArg::Memory => { + config.store = RuntimeHttpStoreSelection::Memory; + Ok(()) + } + StoreArg::Fs { root } => apply_fs_store_selection(config, root), + } +} + +#[cfg(feature = "fs-store")] +fn apply_fs_store_selection( + config: &mut RuntimeHttpServerConfig, + root: Option, +) -> Result<(), ProcessError> { + let root = root + .ok_or_else(|| ProcessError::usage("--store fs requires --fs-root ".to_string()))?; + config.store = RuntimeHttpStoreSelection::Fs { root }; + Ok(()) +} + +#[cfg(not(feature = "fs-store"))] +fn apply_fs_store_selection( + _config: &mut RuntimeHttpServerConfig, + root: Option, +) -> Result<(), ProcessError> { + let _ = root; + Err(ProcessError::usage( + "fs store selection requires building worker-runtime with features `http-server,fs-store`" + .to_string(), + )) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum StoreArg { + Memory, + Fs { root: Option }, +} + +#[derive(Debug)] +enum ProcessError { + Usage(String), + Server(RuntimeHttpServerError), + Io(std::io::Error), +} + +impl ProcessError { + fn usage(message: String) -> Self { + Self::Usage(message) + } +} + +impl fmt::Display for ProcessError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Usage(message) => message.fmt(f), + Self::Server(error) => error.fmt(f), + Self::Io(error) => error.fmt(f), + } + } +} + +impl Error for ProcessError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Usage(_) => None, + Self::Server(error) => Some(error), + Self::Io(error) => Some(error), + } + } +} + +impl From for ProcessError { + fn from(error: RuntimeHttpServerError) -> Self { + Self::Server(error) + } +} + +impl From for ProcessError { + fn from(error: std::io::Error) -> Self { + Self::Io(error) + } +} + +fn usage() -> &'static str { + "Usage: worker-runtime-rest-server [OPTIONS]\n\n\ +Starts the worker-runtime REST command API for a trusted backend/proxy.\n\ +Browsers must not connect to this Runtime process directly.\n\n\ +Options:\n\ + --bind Bind socket address (default: 127.0.0.1:0)\n\ + --runtime-id Runtime authority id (default: generated)\n\ + --display-name Runtime display name\n\ + --store Store selection (default: memory)\n\ + --fs-root Filesystem store root; requires fs-store feature\n\ + --local-token Minimal local bearer token placeholder\n\ + --local-token-env Read local bearer token placeholder from env\n\ + --max-transcript-projection-items Override transcript projection limit\n\ + --max-event-batch-items Override event batch limit\n\ + -h, --help Show this help" +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_memory_runtime_process_config() { + let config = parse_args([ + "--bind", + "127.0.0.1:48181", + "--runtime-id=runtime-review", + "--display-name", + "review runtime", + "--store", + "memory", + "--local-token", + "local-placeholder", + "--max-transcript-projection-items", + "32", + "--max-event-batch-items=16", + ]) + .unwrap() + .unwrap(); + + assert_eq!( + config.bind_addr, + "127.0.0.1:48181".parse::().unwrap() + ); + assert_eq!( + config.runtime_id.as_ref().map(RuntimeId::as_str), + Some("runtime-review") + ); + assert_eq!(config.display_name.as_deref(), Some("review runtime")); + assert!(matches!(config.store, RuntimeHttpStoreSelection::Memory)); + assert_eq!(config.local_token.as_deref(), Some("local-placeholder")); + assert_eq!(config.limits.max_transcript_projection_items, 32); + assert_eq!(config.limits.max_event_batch_items, 16); + } + + #[cfg(feature = "fs-store")] + #[test] + fn parses_fs_store_runtime_process_config_when_feature_enabled() { + let config = parse_args(["--fs-root", "/tmp/yoi-worker-runtime-store"]) + .unwrap() + .unwrap(); + + assert!(matches!( + config.store, + RuntimeHttpStoreSelection::Fs { ref root } + if root == &PathBuf::from("/tmp/yoi-worker-runtime-store") + )); + } + + #[cfg(not(feature = "fs-store"))] + #[test] + fn rejects_fs_store_runtime_process_config_without_feature() { + let error = parse_args(["--store", "fs", "--fs-root", "/tmp/store"]).unwrap_err(); + + assert!( + error + .to_string() + .contains("requires building worker-runtime with features") + ); + } + + #[test] + fn help_does_not_start_server() { + assert!(parse_args(["--help"]).unwrap().is_none()); + } +}