1use crate::{
4 network::rpc::Client,
5 runner::{file, response},
6 KeyType,
7};
8use anyhow::anyhow;
9use clap::{ArgGroup, Args, Parser, Subcommand};
10use serde::{Deserialize, Serialize};
11use std::{
12 net::{IpAddr, Ipv6Addr, SocketAddr},
13 path::PathBuf,
14 time::{Duration, SystemTime},
15};
16use tarpc::context;
17
18mod error;
19pub use error::Error;
20mod init;
21pub use init::{handle_init_command, KeyArg, OutputMode};
22pub(crate) mod show;
23pub use show::ConsoleTable;
24
25const DEFAULT_DB_PATH: &str = "homestar.db";
26const TMP_DIR: &str = "/tmp";
27const HELP_TEMPLATE: &str = "{name} {version}
28
29{about}
30
31Usage: {usage}
32
33{all-args}
34";
35
36#[derive(Debug, Parser)]
38#[command(bin_name = "homestar", name = "homestar", author, version, about,
39 long_about = None, help_template = HELP_TEMPLATE)]
40#[clap(group(ArgGroup::new("init_sink").args(&["config", "dry-run"])))]
41#[clap(group(ArgGroup::new("init_key_arg").args(&["key-file", "key-seed"])))]
42pub struct Cli {
43 #[clap(subcommand)]
45 pub command: Command,
46}
47
48#[derive(Debug, Clone, PartialEq, Args)]
50pub struct InitArgs {
51 #[arg(
53 short = 'o',
54 long = "output",
55 value_hint = clap::ValueHint::FilePath,
56 value_name = "OUTPUT",
57 help = "Path to write initialized configuration file (.toml) [optional]",
58 group = "init_sink"
59 )]
60 pub output_path: Option<PathBuf>,
61 #[arg(
63 long = "dry-run",
64 help = "Skip writing to disk",
65 default_value = "false",
66 help = "Skip writing to disk, instead writing configuration to stdout [optional]",
67 group = "init_sink"
68 )]
69 pub dry_run: bool,
70 #[arg(
72 short = 'q',
73 long = "quiet",
74 default_value = "false",
75 help = "Suppress auxiliary output [optional]"
76 )]
77 pub quiet: bool,
78 #[arg(
80 short = 'f',
81 long = "force",
82 default_value = "false",
83 help = "Force destructive operations without prompting [optional]"
84 )]
85 pub force: bool,
86 #[arg(
88 long = "no-input",
89 default_value = "false",
90 help = "Run in non-interactive mode [optional]"
91 )]
92 pub no_input: bool,
93 #[arg(
95 long = "key-type",
96 value_name = "KEY_TYPE",
97 help = "The type of key to use for libp2p [optional]"
98 )]
99 pub key_type: Option<KeyType>,
100 #[arg(
102 long = "key-file",
103 value_name = "KEY_FILE",
104 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]",
105 group = "init_key_arg"
106 )]
107 pub key_file: Option<Option<PathBuf>>,
108 #[arg(
110 long = "key-seed",
111 value_name = "KEY_SEED",
112 help = "The seed to use for generating the key. If left unspecified, a random seed will be chosen [optional]",
113 group = "init_key_arg"
114 )]
115 pub key_seed: Option<Option<String>>,
116}
117
118#[derive(Debug, Clone, PartialEq, Args, Serialize, Deserialize)]
122pub struct RpcArgs {
123 #[clap(
125 long = "host",
126 default_value = "::1",
127 value_hint = clap::ValueHint::Hostname
128 )]
129 host: IpAddr,
130 #[clap(short = 'p', long = "port", default_value_t = 3030)]
132 port: u16,
133 #[clap(long = "timeout", default_value = "60s", value_parser = humantime::parse_duration)]
135 timeout: Duration,
136}
137
138impl Default for RpcArgs {
139 fn default() -> Self {
140 Self {
141 host: Ipv6Addr::LOCALHOST.into(),
142 port: 3030,
143 timeout: Duration::from_secs(60),
144 }
145 }
146}
147
148#[derive(Debug, Subcommand)]
150pub enum Command {
151 Init(InitArgs),
153 Start {
155 #[arg(
157 long = "db",
158 value_name = "DB",
159 env = "DATABASE_PATH",
160 value_hint = clap::ValueHint::AnyPath,
161 value_name = "DATABASE_PATH",
162 default_value = DEFAULT_DB_PATH,
163 help = "Database path (SQLite) [optional]"
164 )]
165 database_url: Option<String>,
166 #[arg(
168 short = 'c',
169 long = "config",
170 value_hint = clap::ValueHint::FilePath,
171 value_name = "CONFIG",
172 help = "Runtime configuration file (.toml) [optional]"
173 )]
174 runtime_config: Option<PathBuf>,
175 #[arg(
177 short = 'd',
178 long = "daemonize",
179 default_value = "false",
180 help = "Daemonize the runtime"
181 )]
182 daemonize: bool,
183 #[arg(
185 long = "daemon_dir",
186 default_value = TMP_DIR,
187 value_hint = clap::ValueHint::DirPath,
188 value_name = "DIR",
189 help = "Directory to place daemon file(s)"
190 )]
191 daemon_dir: PathBuf,
192 },
193 Stop(RpcArgs),
195 Ping(RpcArgs),
197 Run {
199 #[clap(flatten)]
201 args: RpcArgs,
202 #[arg(
204 short = 'n',
205 long = "name",
206 value_name = "NAME",
207 help = "Local name given to a workflow (optional)"
208 )]
209 name: Option<String>,
210 #[arg(
214 value_hint = clap::ValueHint::FilePath,
215 value_name = "FILE",
216 value_parser = clap::value_parser!(file::ReadWorkflow),
217 index = 1,
218 required = true,
219 help = r#"IPVM-configured workflow file to run.
220Supported:
221 - JSON (.json)"#
222 )]
223 workflow: file::ReadWorkflow,
224 },
225 Node {
227 #[clap(flatten)]
229 args: RpcArgs,
230 },
231 Info,
233}
234
235impl Command {
236 fn name(&self) -> &'static str {
237 match self {
238 Command::Init { .. } => "init",
239 Command::Start { .. } => "start",
240 Command::Stop { .. } => "stop",
241 Command::Ping { .. } => "ping",
242 Command::Run { .. } => "run",
243 Command::Node { .. } => "node",
244 Command::Info => "info",
245 }
246 }
247
248 pub fn handle_rpc_command(self) -> Result<(), Error> {
250 let rt = tokio::runtime::Builder::new_current_thread()
252 .enable_all()
253 .build()?;
254
255 match self {
256 Command::Ping(args) => {
257 let (client, response) = rt.block_on(async {
258 let client = args.client().await?;
259 let response = client.ping().await?;
260 Ok::<(Client, String), Error>((client, response))
261 })?;
262
263 let response = response::Ping::new(client.addr(), response);
264 response.echo_table()?;
265 Ok(())
266 }
267 Command::Stop(args) => rt.block_on(async {
268 let client = args.client().await?;
269 client.stop().await??;
270 Ok(())
271 }),
272 Command::Run {
273 args,
274 name,
275 workflow: workflow_file,
276 } => {
277 let response = rt.block_on(async {
278 let client = args.client().await?;
279 let response = client.run(name.map(|n| n.into()), workflow_file).await??;
280 Ok::<Box<response::AckWorkflow>, Error>(response)
281 })?;
282
283 response.echo_table()?;
284 Ok(())
285 }
286 Command::Node { args } => {
287 let response = rt.block_on(async {
288 let client = args.client().await?;
289 let response = client.node_info().await??;
290 Ok::<response::AckNodeInfo, Error>(response)
291 })?;
292
293 response.echo_table()?;
294 Ok(())
295 }
296 _ => Err(anyhow!("Invalid command {}", self.name()).into()),
297 }
298 }
299}
300
301impl RpcArgs {
302 async fn client(&self) -> Result<Client, Error> {
303 let addr = SocketAddr::new(self.host, self.port);
304 let mut ctx = context::current();
305 ctx.deadline = SystemTime::now() + self.timeout;
306 let client = Client::new(addr, ctx).await?;
307 Ok(client)
308 }
309}