Skip to main content

unifier/cli/
mod.rs

1//! CLI dispatch.
2
3mod defs;
4
5use clap::Parser;
6use defs::{ChrootCommands, Cli, Commands, DaemonCommands, NamespaceCommands, TickCommands};
7
8use crate::chroot::{init_chroot, list_chroots};
9use crate::daemon::{self};
10use crate::home::UnifierHome;
11use crate::namespace;
12use crate::postbox::{
13    ack, delete_key, get_key, list_dir, poll_cron, poll_mailbox, post_cron, put_key, send_from,
14    Message,
15};
16use crate::Result;
17
18#[cfg(unix)]
19use crate::daemon::{
20    response_found, response_messages, response_ok, response_uuid, response_value, Client, Request,
21};
22
23pub fn run() -> Result<()> {
24    let cli = Cli::parse();
25    let home = UnifierHome::resolve(cli.home, cli.chroot)?;
26    let ns = cli.namespace;
27
28    match cli.cmd {
29        Commands::Daemon(DaemonCommands::Start) => {
30            daemon::start(&home, false)?;
31            println!("daemon started");
32            Ok(())
33        }
34        Commands::Daemon(DaemonCommands::Run) => {
35            #[cfg(unix)]
36            {
37                daemon::run_server(home)
38            }
39            #[cfg(not(unix))]
40            {
41                Err(crate::Error::msg("hot daemon requires a Unix platform"))
42            }
43        }
44        Commands::Daemon(DaemonCommands::Stop) => {
45            daemon::stop(&home)?;
46            println!("daemon stopped");
47            Ok(())
48        }
49        Commands::Daemon(DaemonCommands::Status) => daemon::status(&home),
50        Commands::Daemon(DaemonCommands::Flush) => daemon::flush(&home),
51        Commands::Daemon(DaemonCommands::Watch) => {
52            #[cfg(unix)]
53            {
54                daemon::ensure_running(&home)?;
55                daemon::watch(&home)
56            }
57            #[cfg(not(unix))]
58            {
59                Err(crate::Error::msg("hot daemon requires a Unix platform"))
60            }
61        }
62        Commands::Chroot(ChrootCommands::Init { name }) => {
63            init_chroot(home.global_path(), &name)?;
64            println!(
65                "chroot initialized: {}",
66                home.global_path().join("chroots").join(name).display()
67            );
68            Ok(())
69        }
70        Commands::Chroot(ChrootCommands::List) => {
71            for name in list_chroots(home.global_path())? {
72                println!("{name}");
73            }
74            Ok(())
75        }
76        Commands::Namespace(cmd) => {
77            home.ensure()?;
78            dispatch_namespace(&home, cmd, ns.as_deref())
79        }
80        cmd => {
81            home.ensure()?;
82            if cli.no_daemon {
83                dispatch_data(&home, cmd, ns.as_deref())
84            } else {
85                #[cfg(unix)]
86                {
87                    daemon::ensure_running(&home)?;
88                    dispatch_via_daemon(&home, cmd, ns.as_deref())
89                }
90                #[cfg(not(unix))]
91                {
92                    dispatch_data(&home, cmd, ns.as_deref())
93                }
94            }
95        }
96    }
97}
98
99fn dispatch_namespace(
100    home: &UnifierHome,
101    cmd: NamespaceCommands,
102    override_ns: Option<&str>,
103) -> Result<()> {
104    match cmd {
105        NamespaceCommands::Set { name } => {
106            namespace::set(home, &name)?;
107            println!("{name}");
108            Ok(())
109        }
110        NamespaceCommands::Get => match namespace::effective(home, override_ns)? {
111            Some(name) => {
112                println!("{name}");
113                Ok(())
114            }
115            None => Err(crate::Error::msg("no namespace")),
116        },
117        NamespaceCommands::Clear => {
118            if namespace::clear(home)? {
119                Ok(())
120            } else {
121                Err(crate::Error::msg("no namespace"))
122            }
123        }
124    }
125}
126
127fn qualify(home: &UnifierHome, ns: Option<&str>, key: String) -> Result<String> {
128    namespace::qualify_key(home, ns, &key)
129}
130
131fn dispatch_data(home: &UnifierHome, cmd: Commands, ns: Option<&str>) -> Result<()> {
132    match cmd {
133        Commands::Put { key, value } => {
134            let key = qualify(home, ns, key)?;
135            put_key(home, &key, &value)?;
136            Ok(())
137        }
138        Commands::Get { key } => {
139            let key = qualify(home, ns, key)?;
140            match get_key(home, &key)? {
141                Some(v) => {
142                    println!("{v}");
143                    Ok(())
144                }
145                None => Err(crate::Error::msg(format!("key not found: {key}"))),
146            }
147        }
148        Commands::Del { key } => {
149            let key = qualify(home, ns, key)?;
150            if delete_key(home, &key)? {
151                Ok(())
152            } else {
153                Err(crate::Error::msg(format!("key not found: {key}")))
154            }
155        }
156        Commands::Send {
157            from,
158            recipient,
159            message,
160        } => {
161            let from = from.unwrap_or_else(|| crate::envelope::DEFAULT_SENDER.to_string());
162            let id = send_from(home, &from, &recipient, &message)?;
163            println!("{}", id.hyphenated());
164            Ok(())
165        }
166        Commands::Cron { schedule, message } => {
167            let id = post_cron(home, &schedule, &message)?;
168            println!("{}", id.hyphenated());
169            Ok(())
170        }
171        Commands::Poll {
172            recipient,
173            ack: do_ack,
174        } => {
175            let messages = poll_mailbox(home, &recipient)?;
176            print_messages(&messages);
177            if do_ack {
178                for msg in &messages {
179                    ack(home, &msg.id.hyphenated().to_string())?;
180                }
181            }
182            Ok(())
183        }
184        Commands::PollCron { ack: do_ack } => {
185            let messages = poll_cron(home)?;
186            print_messages(&messages);
187            if do_ack {
188                for msg in &messages {
189                    ack(home, &msg.id.hyphenated().to_string())?;
190                }
191            }
192            Ok(())
193        }
194        Commands::List { path } => {
195            let messages = list_dir(home, &path)?;
196            print_messages(&messages);
197            Ok(())
198        }
199        Commands::Ack { id_or_path } => {
200            if ack(home, &id_or_path)? {
201                Ok(())
202            } else {
203                Err(crate::Error::msg(format!(
204                    "message not found: {id_or_path}"
205                )))
206            }
207        }
208        Commands::Root => {
209            println!("{}", home.path().display());
210            Ok(())
211        }
212        Commands::Event { .. } | Commands::Message { .. } | Commands::Tick(_) => {
213            Err(crate::Error::msg(
214                "event, message, and tick commands require the hot daemon; omit --no-daemon",
215            ))
216        }
217        Commands::Daemon(_) | Commands::Chroot(_) | Commands::Namespace(_) => {
218            unreachable!("handled in run()")
219        }
220    }
221}
222
223#[cfg(unix)]
224fn dispatch_via_daemon(home: &UnifierHome, cmd: Commands, ns: Option<&str>) -> Result<()> {
225    let mut client = Client::connect(home)?;
226    match cmd {
227        Commands::Put { key, value } => {
228            let key = qualify(home, ns, key)?;
229            response_ok(client.request(Request::Put { key, value })?)?;
230            Ok(())
231        }
232        Commands::Get { key } => {
233            let key = qualify(home, ns, key)?;
234            match response_value(client.request(Request::Get { key })?)? {
235                Some(v) => {
236                    println!("{v}");
237                    Ok(())
238                }
239                None => Err(crate::Error::msg("key not found")),
240            }
241        }
242        Commands::Del { key } => {
243            let key = qualify(home, ns, key)?;
244            response_ok(client.request(Request::Del { key })?)?;
245            Ok(())
246        }
247        Commands::Send {
248            from,
249            recipient,
250            message,
251        } => {
252            let id = response_uuid(client.request(Request::Send {
253                from,
254                recipient,
255                message,
256            })?)?;
257            println!("{}", id.hyphenated());
258            Ok(())
259        }
260        Commands::Cron { schedule, message } => {
261            let id = response_uuid(client.request(Request::Cron { schedule, message })?)?;
262            println!("{}", id.hyphenated());
263            Ok(())
264        }
265        Commands::Poll {
266            recipient,
267            ack: do_ack,
268        } => {
269            let messages = response_messages(client.request(Request::Poll { recipient })?)?;
270            print_messages(&messages);
271            if do_ack {
272                for msg in &messages {
273                    let found = response_found(client.request(Request::Ack {
274                        id_or_path: msg.id.hyphenated().to_string(),
275                    })?)?;
276                    if !found {
277                        return Err(crate::Error::msg(format!(
278                            "message not found: {}",
279                            msg.id.hyphenated()
280                        )));
281                    }
282                }
283            }
284            Ok(())
285        }
286        Commands::PollCron { ack: do_ack } => {
287            let messages = response_messages(client.request(Request::PollCron)?)?;
288            print_messages(&messages);
289            if do_ack {
290                for msg in &messages {
291                    response_found(client.request(Request::Ack {
292                        id_or_path: msg.id.hyphenated().to_string(),
293                    })?)?;
294                }
295            }
296            Ok(())
297        }
298        Commands::List { path } => {
299            let messages = response_messages(client.request(Request::List { path })?)?;
300            print_messages(&messages);
301            Ok(())
302        }
303        Commands::Ack { id_or_path } => {
304            if response_found(client.request(Request::Ack { id_or_path })?)? {
305                Ok(())
306            } else {
307                Err(crate::Error::msg("message not found"))
308            }
309        }
310        Commands::Root => {
311            println!("{}", home.path().display());
312            Ok(())
313        }
314        Commands::Event { payload } => {
315            let id = response_uuid(client.request(Request::Event { payload })?)?;
316            println!("{}", id.hyphenated());
317            Ok(())
318        }
319        Commands::Message {
320            from,
321            recipient,
322            payload,
323        } => {
324            let id = response_uuid(client.request(Request::AgentMessage {
325                from,
326                to: recipient,
327                payload,
328            })?)?;
329            println!("{}", id.hyphenated());
330            Ok(())
331        }
332        Commands::Tick(cmd) => dispatch_tick_via_daemon(home, &mut client, cmd, ns),
333        Commands::Daemon(_) | Commands::Chroot(_) | Commands::Namespace(_) => {
334            unreachable!("handled in run()")
335        }
336    }
337}
338
339#[cfg(unix)]
340fn dispatch_tick_via_daemon(
341    home: &UnifierHome,
342    client: &mut Client,
343    cmd: TickCommands,
344    ns: Option<&str>,
345) -> Result<()> {
346    use crate::daemon::{response_found, response_ok, Response};
347
348    match cmd {
349        TickCommands::Start { label } => {
350            let resp = client.request(Request::TickStart { label })?;
351            match resp {
352                Response::Ok { tick: Some(t), .. } => {
353                    println!("tick {t} started");
354                    Ok(())
355                }
356                Response::Ok {
357                    queued: Some(pos), ..
358                } => {
359                    println!("tick start queued at position {pos}");
360                    Ok(())
361                }
362                Response::Err { error } => Err(crate::Error::msg(error)),
363                _ => Err(crate::Error::msg("unexpected tick start response")),
364            }
365        }
366        TickCommands::End => {
367            let resp = client.request(Request::TickEnd)?;
368            match resp {
369                Response::Ok { tick: Some(t), .. } => {
370                    println!("tick {t} committed");
371                    Ok(())
372                }
373                Response::Err { error } => Err(crate::Error::msg(error)),
374                _ => Err(crate::Error::msg("unexpected tick end response")),
375            }
376        }
377        TickCommands::Status => {
378            let resp = client.request(Request::TickStatus)?;
379            match resp {
380                Response::Ok { value: Some(v), .. } => {
381                    println!("{v}");
382                    Ok(())
383                }
384                Response::Err { error } => Err(crate::Error::msg(error)),
385                _ => Err(crate::Error::msg("unexpected tick status response")),
386            }
387        }
388        TickCommands::Lock { key } => {
389            let key = qualify(home, ns, key)?;
390            response_ok(client.request(Request::TickLock { key })?)?;
391            Ok(())
392        }
393        TickCommands::Unlock { key } => {
394            let key = qualify(home, ns, key)?;
395            if response_found(client.request(Request::TickUnlock { key })?)? {
396                Ok(())
397            } else {
398                Err(crate::Error::msg("lock not held"))
399            }
400        }
401    }
402}
403
404fn print_messages(messages: &[Message]) {
405    for (i, msg) in messages.iter().enumerate() {
406        println!("{} {}", msg.id.hyphenated(), msg.path.display());
407        println!("{}", msg.body);
408        if i + 1 < messages.len() {
409            println!("---");
410        }
411    }
412}