unifier-cli 0.2.0

Filesystem postbox for inter-process communication via a Unix tree
Documentation
//! CLI dispatch.

mod defs;

use clap::Parser;
use defs::{ChrootCommands, Cli, Commands, DaemonCommands, TickCommands};

use crate::chroot::{init_chroot, list_chroots};
use crate::daemon::{self};
use crate::home::UnifierHome;
use crate::postbox::{
    ack, delete_key, get_key, list_dir, poll_cron, poll_mailbox, post_cron, put_key, send_from,
    Message,
};
use crate::Result;

#[cfg(unix)]
use crate::daemon::{Client, Request, response_found, response_messages, response_ok, response_uuid, response_value};

pub fn run() -> Result<()> {
    let cli = Cli::parse();
    let home = UnifierHome::resolve(cli.home, cli.chroot)?;

    match &cli.cmd {
        Commands::Daemon(DaemonCommands::Start) => {
            daemon::start(&home, false)?;
            println!("daemon started");
            Ok(())
        }
        Commands::Daemon(DaemonCommands::Run) => {
            #[cfg(unix)]
            {
                daemon::run_server(home)
            }
            #[cfg(not(unix))]
            {
                Err(crate::Error::msg("hot daemon requires a Unix platform"))
            }
        }
        Commands::Daemon(DaemonCommands::Stop) => {
            daemon::stop(&home)?;
            println!("daemon stopped");
            Ok(())
        }
        Commands::Daemon(DaemonCommands::Status) => daemon::status(&home),
        Commands::Daemon(DaemonCommands::Flush) => daemon::flush(&home),
        Commands::Daemon(DaemonCommands::Watch) => {
            #[cfg(unix)]
            {
                daemon::ensure_running(&home)?;
                daemon::watch(&home)
            }
            #[cfg(not(unix))]
            {
                Err(crate::Error::msg("hot daemon requires a Unix platform"))
            }
        }
        Commands::Chroot(ChrootCommands::Init { name }) => {
            init_chroot(home.global_path(), name)?;
            println!(
                "chroot initialized: {}",
                home.global_path().join("chroots").join(name).display()
            );
            Ok(())
        }
        Commands::Chroot(ChrootCommands::List) => {
            for name in list_chroots(home.global_path())? {
                println!("{name}");
            }
            Ok(())
        }
        _ => {
            home.ensure()?;
            if cli.no_daemon {
                dispatch_data(&home, cli.cmd)
            } else {
                #[cfg(unix)]
                {
                    daemon::ensure_running(&home)?;
                    dispatch_via_daemon(&home, cli.cmd)
                }
                #[cfg(not(unix))]
                {
                    dispatch_data(&home, cli.cmd)
                }
            }
        }
    }
}

fn dispatch_data(home: &UnifierHome, cmd: Commands) -> Result<()> {
    match cmd {
        Commands::Put { key, value } => {
            put_key(home, &key, &value)?;
            Ok(())
        }
        Commands::Get { key } => match get_key(home, &key)? {
            Some(v) => {
                println!("{v}");
                Ok(())
            }
            None => Err(crate::Error::msg(format!("key not found: {key}"))),
        },
        Commands::Del { key } => {
            if delete_key(home, &key)? {
                Ok(())
            } else {
                Err(crate::Error::msg(format!("key not found: {key}")))
            }
        }
        Commands::Send {
            from,
            recipient,
            message,
        } => {
            let from = from.unwrap_or_else(|| crate::envelope::DEFAULT_SENDER.to_string());
            let id = send_from(home, &from, &recipient, &message)?;
            println!("{}", id.hyphenated());
            Ok(())
        }
        Commands::Cron { schedule, message } => {
            let id = post_cron(home, &schedule, &message)?;
            println!("{}", id.hyphenated());
            Ok(())
        }
        Commands::Poll { recipient, ack: do_ack } => {
            let messages = poll_mailbox(home, &recipient)?;
            print_messages(&messages);
            if do_ack {
                for msg in &messages {
                    ack(home, &msg.id.hyphenated().to_string())?;
                }
            }
            Ok(())
        }
        Commands::PollCron { ack: do_ack } => {
            let messages = poll_cron(home)?;
            print_messages(&messages);
            if do_ack {
                for msg in &messages {
                    ack(home, &msg.id.hyphenated().to_string())?;
                }
            }
            Ok(())
        }
        Commands::List { path } => {
            let messages = list_dir(home, &path)?;
            print_messages(&messages);
            Ok(())
        }
        Commands::Ack { id_or_path } => {
            if ack(home, &id_or_path)? {
                Ok(())
            } else {
                Err(crate::Error::msg(format!("message not found: {id_or_path}")))
            }
        }
        Commands::Root => {
            println!("{}", home.path().display());
            Ok(())
        }
        Commands::Event { .. } | Commands::Message { .. } | Commands::Tick(_) => {
            Err(crate::Error::msg(
                "event, message, and tick commands require the hot daemon; omit --no-daemon",
            ))
        }
        Commands::Daemon(_) | Commands::Chroot(_) => unreachable!("handled in run()"),
    }
}

#[cfg(unix)]
fn dispatch_via_daemon(home: &UnifierHome, cmd: Commands) -> Result<()> {
    let mut client = Client::connect(home)?;
    match cmd {
        Commands::Put { key, value } => {
            response_ok(client.request(Request::Put { key, value })?)?;
            Ok(())
        }
        Commands::Get { key } => match response_value(client.request(Request::Get { key })?)? {
            Some(v) => {
                println!("{v}");
                Ok(())
            }
            None => Err(crate::Error::msg("key not found")),
        },
        Commands::Del { key } => {
            response_ok(client.request(Request::Del { key })?)?;
            Ok(())
        }
        Commands::Send {
            from,
            recipient,
            message,
        } => {
            let id = response_uuid(client.request(Request::Send {
                from,
                recipient,
                message,
            })?)?;
            println!("{}", id.hyphenated());
            Ok(())
        }
        Commands::Cron { schedule, message } => {
            let id = response_uuid(client.request(Request::Cron { schedule, message })?)?;
            println!("{}", id.hyphenated());
            Ok(())
        }
        Commands::Poll { recipient, ack: do_ack } => {
            let messages = response_messages(client.request(Request::Poll { recipient })?)?;
            print_messages(&messages);
            if do_ack {
                for msg in &messages {
                    let found = response_found(
                        client.request(Request::Ack {
                            id_or_path: msg.id.hyphenated().to_string(),
                        })?,
                    )?;
                    if !found {
                        return Err(crate::Error::msg(format!(
                            "message not found: {}",
                            msg.id.hyphenated()
                        )));
                    }
                }
            }
            Ok(())
        }
        Commands::PollCron { ack: do_ack } => {
            let messages = response_messages(client.request(Request::PollCron)?)?;
            print_messages(&messages);
            if do_ack {
                for msg in &messages {
                    response_found(client.request(Request::Ack {
                        id_or_path: msg.id.hyphenated().to_string(),
                    })?)?;
                }
            }
            Ok(())
        }
        Commands::List { path } => {
            let messages = response_messages(client.request(Request::List { path })?)?;
            print_messages(&messages);
            Ok(())
        }
        Commands::Ack { id_or_path } => {
            if response_found(client.request(Request::Ack { id_or_path })?)? {
                Ok(())
            } else {
                Err(crate::Error::msg("message not found"))
            }
        }
        Commands::Root => {
            println!("{}", home.path().display());
            Ok(())
        }
        Commands::Event { payload } => {
            let id = response_uuid(client.request(Request::Event { payload })?)?;
            println!("{}", id.hyphenated());
            Ok(())
        }
        Commands::Message { from, recipient, payload } => {
            let id = response_uuid(client.request(Request::AgentMessage {
                from,
                to: recipient,
                payload,
            })?)?;
            println!("{}", id.hyphenated());
            Ok(())
        }
        Commands::Tick(cmd) => dispatch_tick_via_daemon(&mut client, cmd),
        Commands::Daemon(_) | Commands::Chroot(_) => unreachable!("handled in run()"),
    }
}

#[cfg(unix)]
fn dispatch_tick_via_daemon(client: &mut Client, cmd: TickCommands) -> Result<()> {
    use crate::daemon::{response_found, response_ok, Response};

    match cmd {
        TickCommands::Start { label } => {
            let resp = client.request(Request::TickStart { label })?;
            match resp {
                Response::Ok {
                    tick: Some(t), ..
                } => {
                    println!("tick {t} started");
                    Ok(())
                }
                Response::Ok {
                    queued: Some(pos), ..
                } => {
                    println!("tick start queued at position {pos}");
                    Ok(())
                }
                Response::Err { error } => Err(crate::Error::msg(error)),
                _ => Err(crate::Error::msg("unexpected tick start response")),
            }
        }
        TickCommands::End => {
            let resp = client.request(Request::TickEnd)?;
            match resp {
                Response::Ok {
                    tick: Some(t), ..
                } => {
                    println!("tick {t} committed");
                    Ok(())
                }
                Response::Err { error } => Err(crate::Error::msg(error)),
                _ => Err(crate::Error::msg("unexpected tick end response")),
            }
        }
        TickCommands::Status => {
            let resp = client.request(Request::TickStatus)?;
            match resp {
                Response::Ok { value: Some(v), .. } => {
                    println!("{v}");
                    Ok(())
                }
                Response::Err { error } => Err(crate::Error::msg(error)),
                _ => Err(crate::Error::msg("unexpected tick status response")),
            }
        }
        TickCommands::Lock { key } => {
            response_ok(client.request(Request::TickLock { key })?)?;
            Ok(())
        }
        TickCommands::Unlock { key } => {
            if response_found(client.request(Request::TickUnlock { key })?)? {
                Ok(())
            } else {
                Err(crate::Error::msg("lock not held"))
            }
        }
    }
}

fn print_messages(messages: &[Message]) {
    for (i, msg) in messages.iter().enumerate() {
        println!("{} {}", msg.id.hyphenated(), msg.path.display());
        println!("{}", msg.body);
        if i + 1 < messages.len() {
            println!("---");
        }
    }
}