use oxipage_core::builder::BuildExt;
use oxipage_core::config::Config;
use oxipage_core::extension::Extension;
use oxipage_core::registry::ExtensionRegistry;
use oxipage_core::state::AppState;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing_subscriber::EnvFilter;
pub fn all_extensions() -> Vec<Arc<dyn Extension>> {
vec![
Arc::new(oxipage_ext_profile::ProfileExtension),
Arc::new(oxipage_ext_blog::BlogExtension),
Arc::new(oxipage_ext_projects::ProjectsExtension),
Arc::new(oxipage_ext_links::LinksExtension),
Arc::new(oxipage_ext_novels::NovelsExtension),
Arc::new(oxipage_ext_movies::MoviesExtension),
Arc::new(oxipage_ext_books::BooksExtension),
Arc::new(oxipage_ext_scraps::ScrapsExtension),
Arc::new(oxipage_ext_activity::ActivityExtension),
]
}
pub fn all_builders() -> Vec<Box<dyn BuildExt>> {
vec![
Box::new(oxipage_ext_profile::ProfileExtension),
Box::new(oxipage_ext_blog::BlogExtension),
Box::new(oxipage_ext_projects::ProjectsExtension),
Box::new(oxipage_ext_links::LinksExtension),
Box::new(oxipage_ext_novels::NovelsExtension),
Box::new(oxipage_ext_movies::MoviesExtension),
Box::new(oxipage_ext_books::BooksExtension),
Box::new(oxipage_ext_scraps::ScrapsExtension),
Box::new(oxipage_ext_activity::ActivityExtension),
]
}
pub async fn run_console() -> anyhow::Result<()> {
run_console_with_extensions(all_extensions()).await
}
pub async fn run_console_with_extensions(all: Vec<Arc<dyn Extension>>) -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
let config_path = std::env::var("OXIPAGE_CONFIG")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("oxipage.toml"));
let config = if config_path.exists() {
Config::load(&config_path)?
} else {
tracing::warn!(path = %config_path.display(), "config file not found; using defaults");
let mut cfg = Config::default();
cfg.apply_env_overrides();
cfg
};
let config = Arc::new(config);
let all = {
#[cfg(feature = "wasm")]
{
let mut all = all;
all.extend(oxipage_wasm::load_all_from_dir(
&config.server.data_dir.join("extensions"),
));
all
}
#[cfg(not(feature = "wasm"))]
{
all
}
};
let registry = Arc::new(ExtensionRegistry::new(all));
let toml_enabled = config.extensions.enabled.clone();
let db_path = config.server.data_dir.join("oxipage.db");
let db = oxipage_core::db::connect(&db_path).await?;
registry.run_migrations(&db, &toml_enabled).await?;
if oxipage_core::setup::is_setup_needed(&db).await {
let url = format!("http://{}:{}/setup", config.server.host, config.server.port);
tracing::info!("first boot detected — opening setup wizard at {url}");
open_browser(&url);
}
let wasm_loader: Option<Arc<dyn oxipage_core::extension::WasmLoader>> = {
#[cfg(feature = "wasm")]
{
Some(Arc::new(oxipage_wasm::WasmLoaderImpl))
}
#[cfg(not(feature = "wasm"))]
{
None
}
};
let state = AppState {
db,
config: config.clone(),
registry: registry.clone(),
wasm_loader,
site_override: Arc::new(RwLock::new(None)),
builders: Arc::new(all_builders()),
};
for ext in registry.iter() {
let status = registry.status_of(ext.id()).await;
let active = status.map(|s| s.active()).unwrap_or(false);
if active {
ext.on_startup(&state).await?;
} else if status.map(|s| !s.purged).unwrap_or(false) {
if let Err(e) = ext.on_disable(&state).await {
tracing::warn!(extension = ext.id(), error = %e, "on_disable failed at boot");
}
}
}
let mut scheduler = oxipage_core::scheduler::Scheduler::new();
for ext in registry.iter() {
if registry.is_active(ext.id()).await {
for job in ext.background_jobs() {
scheduler.register(job);
}
}
}
let app = oxipage_core::http::build_app(state);
let addr = SocketAddr::new(config.server.host.parse()?, config.server.port);
let listener = tokio::net::TcpListener::bind(addr).await?;
tracing::info!("oxipage listening on http://{addr}");
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown_signal())
.await?;
Ok(())
}
fn open_browser(url: &str) {
let cmd = if cfg!(target_os = "macos") {
"open"
} else if cfg!(target_os = "windows") {
"start"
} else {
"xdg-open"
};
if let Err(e) = std::process::Command::new(cmd).arg(url).spawn() {
tracing::warn!("could not open browser: {e}");
}
}
async fn shutdown_signal() {
let ctrl_c = tokio::signal::ctrl_c();
#[cfg(unix)]
let terminate = async {
use tokio::signal::unix::{SignalKind, signal};
match signal(SignalKind::terminate()) {
Ok(mut s) => {
let _ = s.recv().await;
}
Err(e) => {
tracing::warn!(error = %e, "failed to install SIGTERM handler");
std::future::pending::<()>().await;
}
}
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
tracing::info!("shutdown signal received");
}
pub mod admin;