use crate::{
network::rpc::Client,
runner::{file, response},
KeyType,
};
use anyhow::anyhow;
use clap::{ArgGroup, Args, Parser, Subcommand};
use serde::{Deserialize, Serialize};
use std::{
net::{IpAddr, Ipv6Addr, SocketAddr},
path::PathBuf,
time::{Duration, SystemTime},
};
use tarpc::context;
mod error;
pub use error::Error;
mod init;
pub use init::{handle_init_command, KeyArg, OutputMode};
pub(crate) mod show;
pub use show::ConsoleTable;
const DEFAULT_DB_PATH: &str = "homestar.db";
const TMP_DIR: &str = "/tmp";
const HELP_TEMPLATE: &str = "{name} {version}
{about}
Usage: {usage}
{all-args}
";
#[derive(Debug, Parser)]
#[command(bin_name = "homestar", name = "homestar", author, version, about,
long_about = None, help_template = HELP_TEMPLATE)]
#[clap(group(ArgGroup::new("init_sink").args(&["config", "dry-run"])))]
#[clap(group(ArgGroup::new("init_key_arg").args(&["key-file", "key-seed"])))]
pub struct Cli {
#[clap(subcommand)]
pub command: Command,
}
#[derive(Debug, Clone, PartialEq, Args)]
pub struct InitArgs {
#[arg(
short = 'o',
long = "output",
value_hint = clap::ValueHint::FilePath,
value_name = "OUTPUT",
help = "Path to write initialized configuration file (.toml) [optional]",
group = "init_sink"
)]
pub output_path: Option<PathBuf>,
#[arg(
long = "dry-run",
help = "Skip writing to disk",
default_value = "false",
help = "Skip writing to disk, instead writing configuration to stdout [optional]",
group = "init_sink"
)]
pub dry_run: bool,
#[arg(
short = 'q',
long = "quiet",
default_value = "false",
help = "Suppress auxiliary output [optional]"
)]
pub quiet: bool,
#[arg(
short = 'f',
long = "force",
default_value = "false",
help = "Force destructive operations without prompting [optional]"
)]
pub force: bool,
#[arg(
long = "no-input",
default_value = "false",
help = "Run in non-interactive mode [optional]"
)]
pub no_input: bool,
#[arg(
long = "key-type",
value_name = "KEY_TYPE",
help = "The type of key to use for libp2p [optional]"
)]
pub key_type: Option<KeyType>,
#[arg(
long = "key-file",
value_name = "KEY_FILE",
help = "The path to the key file. A key will be generated if the file does not exist, and if left unspecified, a default path will be used. [optional]",
group = "init_key_arg"
)]
pub key_file: Option<Option<PathBuf>>,
#[arg(
long = "key-seed",
value_name = "KEY_SEED",
help = "The seed to use for generating the key. If left unspecified, a random seed will be chosen [optional]",
group = "init_key_arg"
)]
pub key_seed: Option<Option<String>>,
}
#[derive(Debug, Clone, PartialEq, Args, Serialize, Deserialize)]
pub struct RpcArgs {
#[clap(
long = "host",
default_value = "::1",
value_hint = clap::ValueHint::Hostname
)]
host: IpAddr,
#[clap(short = 'p', long = "port", default_value_t = 3030)]
port: u16,
#[clap(long = "timeout", default_value = "60s", value_parser = humantime::parse_duration)]
timeout: Duration,
}
impl Default for RpcArgs {
fn default() -> Self {
Self {
host: Ipv6Addr::LOCALHOST.into(),
port: 3030,
timeout: Duration::from_secs(60),
}
}
}
#[derive(Debug, Subcommand)]
pub enum Command {
Init(InitArgs),
Start {
#[arg(
long = "db",
value_name = "DB",
env = "DATABASE_PATH",
value_hint = clap::ValueHint::AnyPath,
value_name = "DATABASE_PATH",
default_value = DEFAULT_DB_PATH,
help = "Database path (SQLite) [optional]"
)]
database_url: Option<String>,
#[arg(
short = 'c',
long = "config",
value_hint = clap::ValueHint::FilePath,
value_name = "CONFIG",
help = "Runtime configuration file (.toml) [optional]"
)]
runtime_config: Option<PathBuf>,
#[arg(
short = 'd',
long = "daemonize",
default_value = "false",
help = "Daemonize the runtime"
)]
daemonize: bool,
#[arg(
long = "daemon_dir",
default_value = TMP_DIR,
value_hint = clap::ValueHint::DirPath,
value_name = "DIR",
help = "Directory to place daemon file(s)"
)]
daemon_dir: PathBuf,
},
Stop(RpcArgs),
Ping(RpcArgs),
Run {
#[clap(flatten)]
args: RpcArgs,
#[arg(
short = 'n',
long = "name",
value_name = "NAME",
help = "Local name given to a workflow (optional)"
)]
name: Option<String>,
#[arg(
value_hint = clap::ValueHint::FilePath,
value_name = "FILE",
value_parser = clap::value_parser!(file::ReadWorkflow),
index = 1,
required = true,
help = r#"IPVM-configured workflow file to run.
Supported:
- JSON (.json)"#
)]
workflow: file::ReadWorkflow,
},
Node {
#[clap(flatten)]
args: RpcArgs,
},
Info,
}
impl Command {
fn name(&self) -> &'static str {
match self {
Command::Init { .. } => "init",
Command::Start { .. } => "start",
Command::Stop { .. } => "stop",
Command::Ping { .. } => "ping",
Command::Run { .. } => "run",
Command::Node { .. } => "node",
Command::Info => "info",
}
}
pub fn handle_rpc_command(self) -> Result<(), Error> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
match self {
Command::Ping(args) => {
let (client, response) = rt.block_on(async {
let client = args.client().await?;
let response = client.ping().await?;
Ok::<(Client, String), Error>((client, response))
})?;
let response = response::Ping::new(client.addr(), response);
response.echo_table()?;
Ok(())
}
Command::Stop(args) => rt.block_on(async {
let client = args.client().await?;
client.stop().await??;
Ok(())
}),
Command::Run {
args,
name,
workflow: workflow_file,
} => {
let response = rt.block_on(async {
let client = args.client().await?;
let response = client.run(name.map(|n| n.into()), workflow_file).await??;
Ok::<Box<response::AckWorkflow>, Error>(response)
})?;
response.echo_table()?;
Ok(())
}
Command::Node { args } => {
let response = rt.block_on(async {
let client = args.client().await?;
let response = client.node_info().await??;
Ok::<response::AckNodeInfo, Error>(response)
})?;
response.echo_table()?;
Ok(())
}
_ => Err(anyhow!("Invalid command {}", self.name()).into()),
}
}
}
impl RpcArgs {
async fn client(&self) -> Result<Client, Error> {
let addr = SocketAddr::new(self.host, self.port);
let mut ctx = context::current();
ctx.deadline = SystemTime::now() + self.timeout;
let client = Client::new(addr, ctx).await?;
Ok(client)
}
}