Files
tranquil-pds/crates/tranquil-server/src/main.rs
T
Lewis 05246aec58 feat(tranquil-store): gc and hardening
Lewis: May this revision serve well! <lu5a@proton.me>
2026-04-10 13:57:47 +03:00

358 lines
12 KiB
Rust

use clap::{Parser, Subcommand};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use tracing::{error, info, warn};
use tranquil_pds::BUILD_VERSION;
use tranquil_pds::comms::{CommsService, DiscordSender, EmailSender, SignalSender, TelegramSender};
use tranquil_pds::crawlers::{Crawlers, start_crawlers_service};
use tranquil_pds::scheduled::{
backfill_record_blobs, backfill_repo_rev, backfill_user_blocks, start_scheduled_tasks,
};
use tranquil_pds::state::AppState;
#[derive(Parser)]
#[command(name = "tranquil-pds", version = BUILD_VERSION, about = "Tranquil AT Protocol PDS")]
struct Cli {
#[arg(short, long, value_name = "FILE", env = "TRANQUIL_PDS_CONFIG")]
config: Option<PathBuf>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
Validate {
#[arg(long)]
ignore_secrets: bool,
},
ConfigTemplate,
}
#[tokio::main]
async fn main() -> ExitCode {
dotenvy::dotenv().ok();
let cli = Cli::parse();
if let Some(command) = &cli.command {
return match command {
Command::ConfigTemplate => {
print!("{}", tranquil_config::template());
ExitCode::SUCCESS
}
Command::Validate { ignore_secrets } => {
let config = match tranquil_config::load(cli.config.as_ref()) {
Ok(c) => c,
Err(e) => {
eprintln!("Failed to load configuration: {e:#}");
return ExitCode::FAILURE;
}
};
match config.validate(*ignore_secrets) {
Ok(()) => {
println!("Configuration is valid.");
ExitCode::SUCCESS
}
Err(e) => {
eprint!("{e}");
ExitCode::FAILURE
}
}
}
};
}
tracing_subscriber::fmt::init();
let config = match tranquil_config::load(cli.config.as_ref()) {
Ok(c) => c,
Err(e) => {
error!("Failed to load configuration: {e:#}");
return ExitCode::FAILURE;
}
};
if let Err(e) = config.validate(false) {
error!("{e}");
return ExitCode::FAILURE;
}
tranquil_config::init(config);
tranquil_pds::metrics::init_metrics();
match run().await {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
error!("Fatal error: {}", e);
ExitCode::FAILURE
}
}
}
async fn run() -> Result<(), Box<dyn std::error::Error>> {
let shutdown = CancellationToken::new();
let shutdown_for_panic = shutdown.clone();
let default_panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
error!("PANIC: {}", info);
shutdown_for_panic.cancel();
default_panic_hook(info);
}));
spawn_signal_handler(shutdown.clone());
let mut state = AppState::new(shutdown.clone()).await?;
let signal_sender = if tranquil_config::get().signal.enabled {
let slot = Arc::new(tranquil_signal::SignalSlot::default());
state = state.with_signal_sender(slot.clone());
if let Some(provider) = &state.signal_store_provider
&& let Some(client) = provider.load_signal_client(shutdown.clone()).await
{
slot.set_client(client).await;
info!("Signal device already linked");
}
Some(SignalSender::new(slot))
} else {
None
};
tranquil_sync::listener::start_sequencer_listener(state.clone()).await;
let backfill_repo_repo = state.repos.repo.clone();
let backfill_block_store = state.block_store.clone();
tokio::spawn(async move {
tokio::join!(
backfill_repo_rev(backfill_repo_repo.clone(), backfill_block_store.clone()),
backfill_user_blocks(backfill_repo_repo.clone(), backfill_block_store.clone()),
backfill_record_blobs(backfill_repo_repo, backfill_block_store),
);
});
let mut comms_service = CommsService::new(state.repos.infra.clone());
let mut deferred_discord_endpoint: Option<(DiscordSender, String, String)> = None;
let cfg = tranquil_config::get();
if let Some(email_sender) = EmailSender::from_config(cfg) {
info!("Email comms enabled");
comms_service = comms_service.register_sender(email_sender);
} else {
warn!("Email comms disabled (MAIL_FROM_ADDRESS not set)");
}
if let Some(discord_sender) = DiscordSender::from_config(cfg) {
info!("Discord comms enabled");
match discord_sender.resolve_bot_username().await {
Ok(username) => {
info!(bot_username = %username, "Resolved Discord bot username");
tranquil_pds::util::set_discord_bot_username(username);
}
Err(e) => {
warn!("Failed to resolve Discord bot username: {}", e);
}
}
match discord_sender.resolve_application_info().await {
Ok((app_id, verify_key)) => {
info!(app_id = %app_id, "Resolved Discord application info");
tranquil_pds::util::set_discord_app_id(app_id.clone());
match hex::decode(&verify_key)
.ok()
.and_then(|bytes| <[u8; 32]>::try_from(bytes.as_slice()).ok())
.and_then(|bytes| ed25519_dalek::VerifyingKey::from_bytes(&bytes).ok())
{
Some(public_key) => {
tranquil_pds::util::set_discord_public_key(public_key);
info!("Discord Ed25519 public key loaded");
let hostname = &tranquil_config::get().server.hostname;
let webhook_url = format!("https://{}/webhook/discord", hostname);
match discord_sender.register_slash_command(&app_id).await {
Ok(()) => info!("Discord /start slash command registered"),
Err(e) => warn!("Failed to register Discord slash command: {}", e),
}
deferred_discord_endpoint =
Some((discord_sender.clone(), app_id, webhook_url));
}
None => {
warn!("Failed to parse Discord verify_key as Ed25519 public key");
}
}
}
Err(e) => {
warn!("Failed to resolve Discord application info: {}", e);
}
}
comms_service = comms_service.register_sender(discord_sender);
}
if let Some(telegram_sender) = TelegramSender::from_config(cfg) {
let secret_token = tranquil_config::get()
.telegram
.webhook_secret
.clone()
.expect("telegram.webhook_secret checked during config validation");
info!("Telegram comms enabled");
match telegram_sender.resolve_bot_username().await {
Ok(username) => {
info!(bot_username = %username, "Resolved Telegram bot username");
tranquil_pds::util::set_telegram_bot_username(username);
let hostname = tranquil_config::get().server.hostname.clone();
let webhook_url = format!("https://{}/webhook/telegram", hostname);
match telegram_sender
.set_webhook(&webhook_url, Some(&secret_token))
.await
{
Ok(()) => info!(url = %webhook_url, "Telegram webhook registered"),
Err(e) => warn!("Failed to register Telegram webhook: {}", e),
}
}
Err(e) => {
warn!("Failed to resolve Telegram bot username: {}", e);
}
}
comms_service = comms_service.register_sender(telegram_sender);
}
if let Some(sender) = signal_sender {
info!("Signal comms enabled");
comms_service = comms_service.register_sender(sender);
}
let comms_handle = tokio::spawn(comms_service.run(shutdown.clone()));
let crawlers_handle = if let Some(crawlers) = Crawlers::from_config(cfg) {
let crawlers = Arc::new(
crawlers.with_circuit_breaker(state.circuit_breakers.relay_notification.clone()),
);
let firehose_rx = state.firehose_tx.subscribe();
info!("Crawlers notification service enabled");
Some(tokio::spawn(start_crawlers_service(
crawlers,
firehose_rx,
shutdown.clone(),
)))
} else {
warn!("Crawlers notification service disabled (PDS_HOSTNAME or CRAWLERS not set)");
None
};
let scheduled_handle = tokio::spawn(start_scheduled_tasks(
state.repos.user.clone(),
state.repos.blob.clone(),
state.blob_store.clone(),
state.repos.sso.clone(),
state.repos.repo.clone(),
state.block_store.clone(),
state.eventlog_segments_dir.clone(),
shutdown.clone(),
));
let app = tranquil_pds::app_with_routes(
state,
tranquil_pds::ExternalRoutes {
xrpc: tranquil_api::api_routes().merge(tranquil_sync::sync_routes()),
oauth: tranquil_oauth_server::oauth_routes(),
well_known: tranquil_oauth_server::well_known_oauth_routes()
.merge(tranquil_api::well_known_api_routes()),
extra: tranquil_api::misc_routes()
.merge(tranquil_api::webhook_routes())
.merge(tranquil_oauth_server::frontend_client_metadata_route()),
},
);
let cfg = tranquil_config::get();
let host = &cfg.server.host;
let port = cfg.server.port;
let addr: SocketAddr = format!("{}:{}", host, port)
.parse()
.map_err(|e| format!("Invalid SERVER_HOST or SERVER_PORT: {}", e))?;
info!("tranquil-pds {} listening on {}", BUILD_VERSION, addr);
let listener = tokio::net::TcpListener::bind(addr)
.await
.map_err(|e| format!("Failed to bind to {}: {}", addr, e))?;
let server_handle = tokio::spawn(async move {
axum::serve(listener, app)
.with_graceful_shutdown(shutdown.clone().cancelled_owned())
.await
});
if let Some((sender, app_id, webhook_url)) = deferred_discord_endpoint {
tokio::spawn(async move {
match sender
.set_interactions_endpoint(&app_id, &webhook_url)
.await
{
Ok(()) => info!(url = %webhook_url, "Discord interactions endpoint registered"),
Err(e) => warn!("Failed to set Discord interactions endpoint: {}", e),
}
});
}
let server_result = server_handle
.await
.map_err(|e| format!("Server task panicked: {}", e))?;
comms_handle.await.ok();
if let Some(handle) = crawlers_handle {
handle.await.ok();
}
scheduled_handle.await.ok();
if let Err(e) = server_result {
return Err(format!("Server error: {}", e).into());
}
Ok(())
}
fn spawn_signal_handler(shutdown: CancellationToken) {
tokio::spawn(async move {
let ctrl_c = async {
match tokio::signal::ctrl_c().await {
Ok(()) => {}
Err(e) => {
error!("Failed to install Ctrl+C handler: {}", e);
std::future::pending::<()>().await;
}
}
};
#[cfg(unix)]
let terminate = async {
match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
Ok(mut signal) => {
signal.recv().await;
}
Err(e) => {
error!("Failed to install SIGTERM handler: {}", e);
std::future::pending::<()>().await;
}
}
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
info!("Shutdown signal received, stopping services...");
shutdown.cancel();
});
}