mod auth;
mod cert;
mod config;
mod daemon;
mod handler;
mod listener;
mod storage;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "pwr-server", version, about, long_about = None)]
struct Cli {
#[arg(short, long, default_value = "/etc/pwr/server.toml")]
config: PathBuf,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Init {
#[arg(long)]
hostname: Option<String>,
},
Start {
#[arg(long, short = 'f')]
foreground: bool,
},
Stop,
Status,
}
fn main() {
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install rustls ring crypto provider");
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "pwr_server=info".into()),
)
.with_target(false)
.init();
let cli = Cli::parse();
let result = match cli.command {
Commands::Init { hostname } => cmd_init(&cli.config, hostname),
Commands::Start { foreground } => cmd_start(&cli.config, foreground),
Commands::Stop => cmd_stop(),
Commands::Status => cmd_status(&cli.config),
};
if let Err(e) = result {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
fn cmd_init(cli_config: &PathBuf, hostname: Option<String>) -> Result<(), String> {
let hostname = hostname.unwrap_or_else(|| {
std::fs::read_to_string("/etc/hostname")
.ok()
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "pwr-server".into())
});
let (config_dir, storage_dir) = if cli_config.as_os_str() != "/etc/pwr/server.toml" {
let dir = cli_config
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let storage = config::user_data_dir();
(dir, storage)
} else {
let base = config::resolve_config_base();
let is_system = base == config::system_config_dir();
let storage = if is_system {
config::system_data_dir()
} else {
config::user_data_dir()
};
(base, storage)
};
println!("Initializing pwr-server on '{}'...", hostname);
if !config::is_path_writable(&config_dir) {
println!(" (using {} — system paths not writable)", config_dir.display());
}
cert::init_server(&config_dir, &storage_dir, &hostname)?;
Ok(())
}
fn cmd_start(config_path: &PathBuf, foreground: bool) -> Result<(), String> {
let path = config::find_config(Some(config_path))
.ok_or_else(|| format!("Config not found at {}", config_path.display()))?;
let server_config = config::load_config(&path)?;
tracing::info!("Starting pwr-server v{}", env!("CARGO_PKG_VERSION"));
tracing::info!("Config: {}", path.display());
tracing::info!(
"Storage: {}",
server_config.storage_base_path.display()
);
tracing::info!("Bind: {}", server_config.bind_addr());
if !foreground {
let pid_file = daemon::pid_file_for_config(&path);
daemon::daemonize(&pid_file)?;
tracing::info!("Daemonized successfully (PID file: {})", pid_file.display());
}
listener::run(server_config)?;
tracing::info!("Server stopped");
Ok(())
}
fn cmd_status(config_path: &PathBuf) -> Result<(), String> {
let path = config::find_config(Some(config_path))
.ok_or_else(|| format!("No config found at {}", config_path.display()))?;
let cfg = config::load_config(&path)?;
println!("pwr-server configuration:");
println!(" Config file: {}", path.display());
println!(" Listen: {}", cfg.bind_addr());
println!(" Storage: {}", cfg.storage_base_path.display());
println!(" Max size: {} GB", cfg.max_project_size_gb);
println!(" Max conns: {}", cfg.max_connections);
println!(" Idle timeout: {}s", cfg.idle_timeout_secs);
println!(" TLS cert: {}", cfg.tls_cert_path.display());
println!(" TLS key: {}", cfg.tls_key_path.display());
if cfg.tls_cert_path.exists() {
println!(" TLS: certificate present");
} else {
println!(" TLS: certificate MISSING — run 'init' first");
}
match std::net::TcpStream::connect_timeout(
&cfg.bind_addr().parse().unwrap(),
std::time::Duration::from_secs(2),
) {
Ok(_) => println!(" Server: RUNNING (port {})", cfg.listen_port),
Err(_) => println!(" Server: not reachable"),
}
Ok(())
}
fn cmd_stop() -> Result<(), String> {
let candidates = [
std::path::PathBuf::from(daemon::SYSTEM_PID_FILE),
daemon::user_pid_file(),
];
let mut errors = Vec::new();
for pid_file in &candidates {
match daemon::stop_daemon(pid_file) {
Ok(summary) => {
println!("{}", summary);
return Ok(());
}
Err(e) => errors.push(e),
}
}
for e in &errors {
eprintln!("{}", e);
}
Ok(())
}