#![warn(missing_docs)]
#![cfg(any(target_os = "linux", target_os = "l4re"))]
#![feature(peer_credentials_unix_socket)]
#![feature(tcp_quickack)]
mod client_handler;
mod shared;
use {
core::borrow::Borrow,
std::{
borrow::Cow,
io::{Error, ErrorKind},
os::{
linux::net::SocketAddrExt,
unix::net::{SocketAddr, UnixListener},
},
process,
},
self::{
client_handler::ClientHandler,
shared::{DEBUG_SERVER_ADDRESS, SERVER_ADDRESS},
},
blackhole::BlackHole,
dia_args::Args,
nairud::MapKind,
r50::Result,
};
const MAP_KIND: MapKind = MapKind::HashMap;
const CMD_HELP: &str = "help";
const CMD_HELP_DOCS: Cow<str> = Cow::Borrowed("Prints help and exits.");
const CMD_VERSION: &str = "version";
const CMD_VERSION_DOCS: Cow<str> = Cow::Borrowed("Prints version and exits.");
const CMD_LICENSES: &str = "licenses";
const CMD_LICENSES_DOCS: Cow<str> = Cow::Borrowed("Prints licenses and exits.");
const CMD_PRINT_SAMPLE_SERVICE_FILE: &str = "print-sample-service-file";
const CMD_PRINT_SAMPLE_SERVICE_FILE_DOCS: Cow<str> = Cow::Borrowed("Prints sample service file.");
const CMD_RUN: &str = "run";
const CMD_RUN_DOCS: Cow<str> = Cow::Borrowed("Runs main job.");
const ARG_DEBUG: &[&str] = &["--debug"];
const ARG_DEBUG_DOCS: Cow<str> = Cow::Borrowed("Debug mode. When on, the server will bind to a debug address.");
const ARG_DEBUG_DEFAULT: bool = false;
const ARG_MAX_ACTIVE_CLIENTS: &[&str] = &["--max-active-clients"];
const ARG_MAX_ACTIVE_CLIENTS_DOCS: Cow<str> = Cow::Borrowed(concat!(
"Maximum active clients which can be handled at a time.\n\n",
"This option only affects new connecting clients. It does *not* affect sub process(es) spawned by clients which are already running.",
));
const ARG_MAX_ACTIVE_CLIENTS_DEFAULT: u32 = 256;
const ARG_MAX_ACTIVE_CLIENTS_MIN: u32 = 64;
const ARG_MAX_ACTIVE_CLIENTS_MAX: u32 = 2048;
const SAMPLE_SERVICE_FILE: &str = include_str!("sample.service");
fn main() {
if let Err(err) = run() {
eprintln!("{}", err);
process::exit(1);
}
}
fn run() -> Result<()> {
let args = dia_args::parse()?;
match args.cmd() {
Some(CMD_HELP) => {
ensure_args_are_empty(args.try_into_sub_cmd()?.1)?;
print_help()
},
Some(CMD_VERSION) => {
ensure_args_are_empty(args.try_into_sub_cmd()?.1)?;
print_version()
},
Some(CMD_LICENSES) => print_licenses(args.try_into_sub_cmd()?.1),
Some(CMD_PRINT_SAMPLE_SERVICE_FILE) => print_sample_service_file(args.try_into_sub_cmd()?.1),
Some(CMD_RUN) => run_main_job(args.try_into_sub_cmd()?.1),
Some(other) => Err(Error::new(ErrorKind::InvalidInput, format!("Unknown command: {:?}", other))),
None => Err(Error::new(ErrorKind::Other, "Mising command")),
}
}
fn ensure_args_are_empty<A>(args: A) -> Result<()> where A: Borrow<Args> {
let args = args.borrow();
if args.is_empty() {
Ok(())
} else {
Err(Error::new(ErrorKind::InvalidInput, format!("Unknown arguments: {:?}", args)))
}
}
fn make_version_string<'a>() -> Cow<'a, str> {
format!("{} {} {:?}", r50::NAME, r50::VERSION, r50::RELEASE_DATE).into()
}
fn print_version() -> Result<()> {
println!("{}", make_version_string());
Ok(())
}
fn print_licenses(args: Args) -> Result<()> {
ensure_args_are_empty(args)?;
dia_args::lock_write_out(format!(
"{copying}\n\n================================================================\n\n{copying_lesser}\n",
copying=include_str!("../../../COPYING"),
copying_lesser=include_str!("../../../COPYING.LESSER"),
));
Ok(())
}
fn print_help() -> Result<()> {
use dia_args::docs::{Cmd, Docs, Option};
let docs = Cow::Owned(format!(
concat!(
"This is {} server. It binds to an abstract Linux socket and listens for clients.\n\n",
"Each client is expected to send:\n\n",
"- One single command (with optional arguments).\n",
"- Its credentials: process ID, user ID, group ID.\n",
"- Its standard streams: input, output, error.\n",
"- Its current working directory and environment variables.\n\n",
"The command will be run by server under client's credentials, with standard streams routed to client's. The new process ID will",
" be sent back to client. So when the user uses Ctrl-C, client can *forward* it to that process ID.\n\n",
"The idea is to group client processes under one single process: the server. This helps with resource management. For instance,",
" you can run this program with {:?} command to generate a sample service file.",
),
r50::NAME, CMD_PRINT_SAMPLE_SERVICE_FILE,
));
let commands = Some(dia_args::make_cmds![
Cmd::new(CMD_HELP, CMD_HELP_DOCS, None),
Cmd::new(CMD_VERSION, CMD_VERSION_DOCS, None),
Cmd::new(CMD_LICENSES, CMD_LICENSES_DOCS, None),
Cmd::new(CMD_PRINT_SAMPLE_SERVICE_FILE, CMD_PRINT_SAMPLE_SERVICE_FILE_DOCS, None),
Cmd::new(CMD_RUN, CMD_RUN_DOCS, Some(dia_args::make_options![
Option::new(ARG_DEBUG, false, &[], Some(ARG_DEBUG_DEFAULT), ARG_DEBUG_DOCS),
Option::new(ARG_MAX_ACTIVE_CLIENTS, false, &[], Some(ARG_MAX_ACTIVE_CLIENTS_DEFAULT), ARG_MAX_ACTIVE_CLIENTS_DOCS),
])),
]);
let mut docs = Docs::new(make_version_string(), docs);
docs.commands = commands;
docs.project = shared::project();
docs.print()
}
fn print_sample_service_file(args: Args) -> Result<()> {
ensure_args_are_empty(args)?;
println!("{}", SAMPLE_SERVICE_FILE);
Ok(())
}
fn run_main_job(mut args: Args) -> Result<()> {
let debug = args.take(ARG_DEBUG)?.unwrap_or(ARG_DEBUG_DEFAULT);
let max_active_clients = match args.take(ARG_MAX_ACTIVE_CLIENTS)?.unwrap_or(ARG_MAX_ACTIVE_CLIENTS_DEFAULT) {
some @ ARG_MAX_ACTIVE_CLIENTS_MIN..=ARG_MAX_ACTIVE_CLIENTS_MAX => some,
_ => return Err(Error::new(
ErrorKind::InvalidInput,
format!("{:?} must be in range: [{}..{}]", ARG_MAX_ACTIVE_CLIENTS, ARG_MAX_ACTIVE_CLIENTS_MIN, ARG_MAX_ACTIVE_CLIENTS_MAX),
)),
};
ensure_args_are_empty(args)?;
let server_address = if debug { DEBUG_SERVER_ADDRESS } else { SERVER_ADDRESS };
let server = match UnixListener::bind_addr(&SocketAddr::from_abstract_name(&server_address)?) {
Ok(server) => server,
Err(err) => return Err(Error::new(
ErrorKind::Other, format!("Failed starting server at {:?}: {:?}", zeros::bytes_to_hex(server_address), err),
)),
};
println!("Running at: {}", zeros::bytes_to_hex(server_address));
let blackhole = BlackHole::make_with_active_limit(
max_active_clients,
usize::try_from(max_active_clients)
.map_err(|_| Error::new(ErrorKind::Other, format!("Failed to convert {} to usize", max_active_clients)))?,
)?;
for client in server.incoming() {
match client {
Ok(client) => match blackhole.throw(ClientHandler::new(client)) {
Ok(job) => if job.is_some() {
dia_args::lock_write_err("Black hole is full, discarding 1 new client...\n");
},
Err(err) => return Err(Error::new(ErrorKind::Other, format!("Black hole... exploded: {:?}", err))),
},
Err(err) => dia_args::lock_write_err(format!("Failed accepting new client: {:?}\n", err)),
};
}
Ok(())
}