iocaine 2.4.1

The deadliest poison known to AI
Documentation
// SPDX-FileCopyrightText: 2025 Gergely Nagy
// SPDX-FileContributor: Gergely Nagy
//
// SPDX-License-Identifier: MIT

#[cfg(all(not(target_env = "musl"), feature = "jemalloc"))]
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;

use anyhow::Result;
use clap::{Parser, Subcommand};
use curl::easy::Easy;
use tokio_listener::ListenerAddress;

use iocaine::{app::Iocaine, config::Config, sex_dungeon};

#[derive(Debug, Parser, Default)]
#[command(version, about)]
pub struct Args {
    /// Configuration file to use.
    #[arg(short = 'c', long, default_value_t = Self::xdg_config_file())]
    pub config_file: String,

    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Debug, Subcommand)]
enum Commands {
    /// Start the iocaine server. This is the default, if no command is specified.
    Start,
    /// Attempt to reload the configuration of a running iocaine server.
    Reload,
    /// Run the request-handler test suite, if any.
    Test,
}

impl Args {
    fn xdg_config_file() -> String {
        "config.toml".to_owned()
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    #[cfg(all(feature = "tokio-console", not(tokio_unstable)))]
    compile_error!(
        "`tokio-console` requires manually enabling the `--cfg tokio_unstable` rust flag during compilation!"
    );

    #[cfg(all(feature = "tokio-console", tokio_unstable))]
    console_subscriber::init();

    #[cfg(not(all(feature = "tokio-console", tokio_unstable)))]
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
        )
        .with_writer(std::io::stderr)
        .init();

    let args = Args::parse();
    let command = args.command.unwrap_or(Commands::Start);

    tracing::debug!(config_file = &args.config_file, "loading configuration");
    let config = Config::load(&args.config_file).unwrap_or_else(|err| panic!("{}", err));

    match command {
        Commands::Start => {
            tracing::info!("starting iocaine");
            let app = Iocaine::new(config)?;
            app.run().await?;
        }
        Commands::Reload => {
            reload(&config, &args.config_file)?;
        }
        Commands::Test => {
            run_tests(&config)?;
        }
    }

    Ok(())
}

fn reload(config: &Config, config_file: &str) -> Result<()> {
    if config.server.control.is_none() {
        return Ok(());
    }
    let socket_path = config.server.control.clone().unwrap().bind;

    let mut client = Easy::new();
    client.url("http://iocaine/config/load")?;

    match socket_path {
        ListenerAddress::Tcp(addr) => {
            client.url(&format!("http://{addr}/config/load"))?;
        }
        ListenerAddress::Path(path) => {
            client.unix_socket_path(Some(path))?;
        }
        #[cfg(target_os = "linux")]
        ListenerAddress::Abstract(s) => {
            client.abstract_unix_socket(s.as_bytes())?;
        }
        _ => {
            return Err(anyhow::anyhow!("Unsupported control socket type"));
        }
    }

    client.post(true).unwrap();
    client.post_fields_copy(format!(r#"{{"path": "{config_file}"}}"#).as_bytes())?;
    let mut list = curl::easy::List::new();
    list.append("content-type: application/json")?;
    client.http_headers(list)?;
    client.perform()?;

    match client.response_code()? {
        202 => Ok(()),
        409 => Err(anyhow::anyhow!("New configuration is incompatible")),
        422 => Err(anyhow::anyhow!(
            "New configuration could not be loaded or parsed"
        )),
        v => Err(anyhow::anyhow!("Unexpected response status: {v}")),
    }
}

fn run_tests(config: &Config) -> Result<()> {
    let Some(path) = &config.server.request_handler.path else {
        return Ok(());
    };

    let mut npc = sex_dungeon::create(
        config.server.request_handler.language,
        &config.server.request_handler.options,
        path,
        None,
    )?;
    npc.run_tests()
}