Documentation
/*
==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--

R50

Copyright (C) 2018-2019, 2021-2025  Anonymous

There are several releases over multiple years,
they are listed as ranges, such as: "2018-2019".

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
*/

//! # Server

#![warn(missing_docs)]
#![cfg(any(target_os = "linux", target_os = "l4re"))]

// TODO: remove these when related APIs are stabilized
#![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");

/// # Main
fn main() {
    if let Err(err) = run() {
        eprintln!("{}", err);
        process::exit(1);
    }
}

/// # Runs the program
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")),
    }
}

/// # Ensures arguments are empty
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)))
    }
}

/// # Makes version string
fn make_version_string<'a>() -> Cow<'a, str> {
    format!("{} {} {:?}", r50::NAME, r50::VERSION, r50::RELEASE_DATE).into()
}

/// # Prints version
fn print_version() -> Result<()> {
    println!("{}", make_version_string());
    Ok(())
}

/// # Prints licenses
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(())
}

/// # Prints help
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()
}

/// # Prints sample service file
fn print_sample_service_file(args: Args) -> Result<()> {
    ensure_args_are_empty(args)?;
    println!("{}", SAMPLE_SERVICE_FILE);
    Ok(())
}

/// # Runs main job
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(())
}