use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf;
use std::time::Duration;
use crate::Server;
use tracing::info;
#[cfg(feature = "ssr")]
use tracing::warn;
const DEFAULT_SERVE_PORT: u16 = 3000;
#[derive(clap::Args, Debug)]
pub struct ServeArgs {
workload: Option<PathBuf>,
#[arg(long)]
bundle: Option<PathBuf>,
#[arg(long)]
listen: Option<SocketAddr>,
#[arg(long)]
idle_ttl: Option<u64>,
#[arg(long, default_value_t = DEFAULT_SERVE_PORT)]
port: u16,
#[arg(long, default_value = "0.0.0.0")]
host: IpAddr,
#[arg(long)]
revalidate: bool,
#[arg(long, default_value = "mesofact.config.toml")]
publish_config: PathBuf,
#[arg(long, env = "MESOFACT_MIRROR_KEY")]
mirror_key: Option<String>,
#[arg(long = "allow-route", value_name = "ROUTE")]
allow_route: Vec<String>,
#[arg(long, conflicts_with_all = ["workload", "publish_config", "allow_route"])]
tenants: Option<PathBuf>,
}
impl ServeArgs {
fn bind_addr(&self) -> SocketAddr {
self.listen
.unwrap_or_else(|| SocketAddr::new(self.host, self.port))
}
}
pub async fn run(args: ServeArgs) -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new("mesofact=info,mesofact_dev=info,tower_http=info")
}),
)
.init();
if let Some(bundle) = args.bundle.as_ref() {
let bundle_abs = bundle.canonicalize().unwrap_or_else(|_| bundle.clone());
let server = Server::from_bundle(&bundle_abs)?;
let idle_ttl = args.idle_ttl.filter(|s| *s > 0).map(Duration::from_secs);
let listener = match socket_activation_listener()? {
Some(l) => {
info!(bundle = %bundle_abs.display(), "mesofact serve: serving bundle on inherited LISTEN_FDS socket");
l
}
None => {
let addr = args.bind_addr();
info!(%addr, bundle = %bundle_abs.display(), "mesofact-serve listening (bundle, static v0)");
tokio::net::TcpListener::bind(addr).await?
}
};
return server.serve_on_listener(listener, idle_ttl).await;
}
run_workload_modes(args).await
}
#[cfg(unix)]
fn socket_activation_listener() -> anyhow::Result<Option<tokio::net::TcpListener>> {
use std::os::fd::FromRawFd;
let n_fds: i32 = std::env::var("LISTEN_FDS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
if n_fds < 1 {
return Ok(None);
}
if let Ok(pid) = std::env::var("LISTEN_PID") {
if pid.parse::<u32>().ok() != Some(std::process::id()) {
return Ok(None);
}
}
const SD_LISTEN_FDS_START: i32 = 3;
let std_listener = unsafe { std::net::TcpListener::from_raw_fd(SD_LISTEN_FDS_START) };
std_listener
.set_nonblocking(true)
.map_err(|e| anyhow::anyhow!("set_nonblocking on inherited LISTEN_FDS socket: {e}"))?;
let listener = tokio::net::TcpListener::from_std(std_listener)
.map_err(|e| anyhow::anyhow!("adopting inherited LISTEN_FDS socket: {e}"))?;
Ok(Some(listener))
}
#[cfg(not(unix))]
fn socket_activation_listener() -> anyhow::Result<Option<tokio::net::TcpListener>> {
Ok(None)
}
#[cfg(feature = "ssr")]
async fn run_workload_modes(args: ServeArgs) -> anyhow::Result<()> {
use crate::{revalidate, ssr, tenants, SsrSpawnOptions};
let addr = args.bind_addr();
if let Some(tenants_dir) = args.tenants.as_ref() {
if !args.revalidate {
warn!("--tenants implies the revalidate receiver; running multi-tenant receiver");
}
if args.mirror_key.is_some() {
warn!(
"--mirror-key / MESOFACT_MIRROR_KEY is ignored in --tenants mode — \
each tenant's bearer comes from its own mirror_key_env",
);
}
let files = tenants::load_tenants(tenants_dir)?;
let resolved = tenants::resolve_tenants(files, |name| std::env::var(name).ok());
let registry = tenants::TenantRegistry::new(resolved);
if registry.is_empty() {
anyhow::bail!(
"--tenants {} contains no tenants/<id>.toml files — a receiver with an \
empty registry rejects every poke",
tenants_dir.display()
);
}
registry.validate()?;
info!(tenants = registry.len(), dir = %tenants_dir.display(), "multi-tenant revalidate receiver");
return tenants::serve(registry, addr.ip(), addr.port()).await;
}
if args.revalidate {
let workload = args
.workload
.clone()
.ok_or_else(|| anyhow::anyhow!("--revalidate needs a <workload> dir (or use --tenants)"))?;
let workload_abs = workload.canonicalize().unwrap_or(workload);
return revalidate::serve(
revalidate::RevalidateConfig {
workload: workload_abs,
publish_config: args.publish_config,
mirror_key: args.mirror_key,
routes: args.allow_route,
},
addr.ip(),
addr.port(),
)
.await;
}
let workload = args
.workload
.clone()
.ok_or_else(|| anyhow::anyhow!("a <workload> dir is required for the SSR host (or --bundle for static serving)"))?;
let server = Server::from_workload(&workload)?;
let workload_abs = workload.canonicalize().unwrap_or(workload);
let opts = SsrSpawnOptions::new(
workload_abs.clone(),
workload_abs.join("dist"),
workload_abs.join(".mesofact-serve"),
)
.with_env(std::env::vars().collect());
let server = match ssr::spawn(opts).await? {
Some(child) => {
info!(prefixes = ?child.prefixes(), "mesofact-serve ssr runtime attached");
server.with_ssr(child)
}
None => {
warn!("mesofact-serve: no SSR routes (or no manifest) — serving static only");
server
}
};
let addr = args.bind_addr();
info!(%addr, workload = %workload_abs.display(), "mesofact-serve listening");
server.serve_on(addr).await
}
#[cfg(not(feature = "ssr"))]
async fn run_workload_modes(args: ServeArgs) -> anyhow::Result<()> {
if args.revalidate || args.tenants.is_some() {
anyhow::bail!(
"--revalidate / --tenants need the `ssr` build feature (V8); this is a static-only build"
);
}
let workload = args
.workload
.clone()
.ok_or_else(|| anyhow::anyhow!("a <workload> dir is required (or --bundle to serve a W272 bundle)"))?;
let server = Server::from_workload(&workload)?;
let addr = args.bind_addr();
info!(%addr, workload = %workload.display(), "mesofact-serve listening (static only, no ssr)");
server.serve_on(addr).await
}