mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
Releasing a reservation before spawn or recovery allowed unrelated tests to take the same port. Transfer the socket across exec through a private Unix test-only protocol, retaining a parent copy across Grasp restarts. Validate the inherited descriptor and keep it out of Git and SSH descendants. Offline identity fixtures retain an accept-and-close endpoint until recovery. Mock relay listeners and upgraded connections remain owned until shutdown. Use real HTTP or SDK readiness instead of grace sleeps; startup errors in the owned-listener path are not retried. Normal server startup and deployment configuration are unchanged. The capability probe permits older external harnesses to remain compatible. Validation: listener adoption unit tests passed; relay_identity passed with recovery coverage, MockRelay shutdown passed, and a real subprocess restart plus a cross-repository ngit harness smoke test passed. Full platform builds remain host validation. Assisted-by: Codex (GPT-6)
179 lines
6.3 KiB
Rust
179 lines
6.3 KiB
Rust
use std::{ffi::OsStr, io::IsTerminal};
|
|
|
|
use anyhow::Result;
|
|
use clap::Parser;
|
|
use tokio::signal;
|
|
use tracing::info;
|
|
use tracing_subscriber::{filter::FilterExt, layer::SubscriberExt, EnvFilter, Layer};
|
|
|
|
use ngit_grasp::{
|
|
cleanup_empty_repos,
|
|
config::Config,
|
|
logging::{effective_log_filter, SuppressRoutineDisconnects},
|
|
nostr,
|
|
server::RelayServer,
|
|
};
|
|
|
|
mod docs_export;
|
|
#[cfg(unix)]
|
|
mod test_listener;
|
|
|
|
/// Top-level CLI dispatcher.
|
|
///
|
|
/// With no subcommand the binary runs the relay (all relay flags apply).
|
|
/// With a subcommand it runs the requested maintenance tool instead.
|
|
#[derive(Debug, Parser)]
|
|
#[command(author, version, about = "ngit-grasp GRASP relay", long_about = None)]
|
|
#[command(propagate_version = true)]
|
|
enum Cli {
|
|
/// Run the GRASP relay server (default when no subcommand is given).
|
|
#[command(name = "serve")]
|
|
Serve(Box<Config>),
|
|
|
|
/// Remove kind 30617/30618 events whose bare git repository is empty or missing.
|
|
///
|
|
/// Runs in dry-run mode by default. Pass --execute to make changes.
|
|
/// Stop the relay service before running with --execute.
|
|
CleanupEmptyRepos(cleanup_empty_repos::CleanupArgs),
|
|
|
|
/// Permanently eject deleted repository data from holding/archive stores.
|
|
///
|
|
/// This is an operator/admin maintenance command and is idempotent.
|
|
HoldingEject(nostr::lifecycle::HoldingEjectArgs),
|
|
|
|
/// Queue storage- and event-authorization integrity checks in the running relay.
|
|
IntegrityCheck(ngit_grasp::git::integrity::IntegrityCheckArgs),
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
// Harnesses probe this before handing a reserved Unix listener to the child.
|
|
if std::env::args_os().nth(1).as_deref() == Some(OsStr::new("--internal-test-listener-support"))
|
|
{
|
|
#[cfg(unix)]
|
|
println!("ngit-test-listener-v1");
|
|
return Ok(());
|
|
}
|
|
|
|
// Documentation builds need only the static command model. Dispatch before
|
|
// dotenv, clap parsing, secret discovery, filesystem access, or relay
|
|
// startup so exporting metadata is safe in an isolated build environment.
|
|
if std::env::args_os().nth(1).as_deref() == Some(OsStr::new(docs_export::INTERNAL_COMMAND)) {
|
|
docs_export::write_stdout()?;
|
|
return Ok(());
|
|
}
|
|
|
|
// Load .env file before clap parses, so env vars are available.
|
|
dotenvy::dotenv().ok();
|
|
|
|
// Peek at argv[1] to decide whether a subcommand was explicitly provided.
|
|
// If not, prepend the implicit "serve" subcommand so that clap routes to Cli::Serve
|
|
// and all relay flags are parsed normally (preserving backward compatibility).
|
|
let mut args: Vec<String> = std::env::args().collect();
|
|
let known_subcommands = [
|
|
"serve",
|
|
"cleanup-empty-repos",
|
|
"holding-eject",
|
|
"integrity-check",
|
|
"help",
|
|
];
|
|
let has_subcommand = args.get(1).is_some_and(|a| {
|
|
known_subcommands.contains(&a.as_str())
|
|
|| matches!(a.as_str(), "-h" | "--help" | "-V" | "--version")
|
|
});
|
|
if !has_subcommand {
|
|
args.insert(1, "serve".to_string());
|
|
}
|
|
|
|
match Cli::parse_from(args) {
|
|
Cli::CleanupEmptyRepos(cleanup_args) => cleanup_empty_repos::run(&cleanup_args).await,
|
|
Cli::HoldingEject(eject_args) => nostr::lifecycle::run_holding_eject(eject_args).await,
|
|
Cli::IntegrityCheck(integrity_args) => {
|
|
let path = ngit_grasp::git::integrity::enqueue_manual_check(&integrity_args)?;
|
|
println!(
|
|
"Queued {} for identifier '{}' at {}",
|
|
if integrity_args.repair {
|
|
"storage and authorization-integrity check and repair"
|
|
} else {
|
|
"storage and authorization-integrity check"
|
|
},
|
|
integrity_args.identifier,
|
|
path.display()
|
|
);
|
|
Ok(())
|
|
}
|
|
Cli::Serve(config) => {
|
|
let mut config = *config;
|
|
config.relay_owner_nsec = Some(Config::load_relay_owner_key()?);
|
|
run_relay(config).await
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run the relay until an OS shutdown signal arrives.
|
|
///
|
|
/// All relay wiring lives in [`ngit_grasp::server::RelayServer`];
|
|
/// this function only owns concerns specific to the standalone binary:
|
|
/// the global tracing subscriber and signal handling.
|
|
async fn run_relay(config: Config) -> Result<()> {
|
|
let effective_filter = effective_log_filter(&config.log_level);
|
|
let filter = EnvFilter::new(&effective_filter).and(SuppressRoutineDisconnects);
|
|
// Only colorize when stdout is a terminal: ANSI escape codes in redirected
|
|
// logs corrupt journald/file output and break log-scraping consumers.
|
|
let subscriber = tracing_subscriber::registry().with(
|
|
tracing_subscriber::fmt::layer()
|
|
.with_ansi(std::io::stdout().is_terminal())
|
|
.with_filter(filter),
|
|
);
|
|
tracing::subscriber::set_global_default(subscriber)?;
|
|
|
|
info!(
|
|
configured_log_level = %config.log_level,
|
|
effective_log_filter = %effective_filter,
|
|
"Starting ngit-grasp"
|
|
);
|
|
|
|
#[cfg(unix)]
|
|
let server = match test_listener::from_env()? {
|
|
Some(listener) => RelayServer::start_with_listener(config, listener).await?,
|
|
None => RelayServer::start(config).await?,
|
|
};
|
|
#[cfg(not(unix))]
|
|
let server = RelayServer::start(config).await?;
|
|
|
|
server.run_until(shutdown_signal()).await
|
|
}
|
|
|
|
/// Resolves when the process receives SIGINT (Ctrl+C) or, on unix, SIGTERM.
|
|
async fn shutdown_signal() {
|
|
#[cfg(unix)]
|
|
{
|
|
use tokio::signal::unix::{signal as unix_signal, SignalKind};
|
|
let mut sigterm = match unix_signal(SignalKind::terminate()) {
|
|
Ok(sigterm) => sigterm,
|
|
Err(e) => {
|
|
tracing::error!("Failed to install SIGTERM handler: {}", e);
|
|
// Fall back to Ctrl+C only.
|
|
let _ = signal::ctrl_c().await;
|
|
info!("Received SIGINT (Ctrl+C), cleaning up...");
|
|
return;
|
|
}
|
|
};
|
|
|
|
tokio::select! {
|
|
_ = signal::ctrl_c() => {
|
|
info!("Received SIGINT (Ctrl+C), cleaning up...");
|
|
}
|
|
_ = sigterm.recv() => {
|
|
info!("Received SIGTERM, cleaning up...");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
{
|
|
let _ = signal::ctrl_c().await;
|
|
info!("Received SIGINT (Ctrl+C), cleaning up...");
|
|
}
|
|
}
|