Skip to main content

unifier/cli/
mod.rs

1//! CLI dispatch.
2
3mod defs;
4
5use clap::Parser;
6use defs::{
7    ChrootCommands, Cli, Commands, DaemonCommands, LogCommands, NamespaceCommands, SqlCommands,
8    TickCommands, TripleCommands, WebCommands,
9};
10
11use crate::chroot::{init_chroot, list_chroots};
12use crate::daemon::{self};
13use crate::home::UnifierHome;
14use crate::namespace;
15use crate::postbox::{
16    ack, delete_key, get_key, list_dir, poll_cron, poll_mailbox, post_cron, put_key, send_from,
17    Message,
18};
19use crate::log as spanlog;
20use crate::sqlite::{self, SqlOutcome};
21use crate::Result;
22
23#[cfg(unix)]
24use crate::daemon::{
25    response_found, response_messages, response_ok, response_uuid, response_value, Client, Request,
26};
27
28pub fn run() -> Result<()> {
29    let cli = Cli::parse();
30    let home = UnifierHome::resolve(cli.home, cli.chroot)?;
31    let ns = cli.namespace;
32
33    match cli.cmd {
34        Commands::Daemon(DaemonCommands::Start) => {
35            daemon::start(&home, false)?;
36            println!("daemon started");
37            Ok(())
38        }
39        Commands::Daemon(DaemonCommands::Run) => {
40            #[cfg(unix)]
41            {
42                daemon::run_server(home)
43            }
44            #[cfg(not(unix))]
45            {
46                Err(crate::Error::msg("hot daemon requires a Unix platform"))
47            }
48        }
49        Commands::Daemon(DaemonCommands::Stop) => {
50            daemon::stop(&home)?;
51            println!("daemon stopped");
52            Ok(())
53        }
54        Commands::Daemon(DaemonCommands::Status) => daemon::status(&home),
55        Commands::Daemon(DaemonCommands::Flush) => daemon::flush(&home),
56        Commands::Daemon(DaemonCommands::Watch) => {
57            #[cfg(unix)]
58            {
59                daemon::ensure_running(&home)?;
60                daemon::watch(&home)
61            }
62            #[cfg(not(unix))]
63            {
64                Err(crate::Error::msg("hot daemon requires a Unix platform"))
65            }
66        }
67        Commands::Daemon(DaemonCommands::Gc { dry_run }) => {
68            #[cfg(unix)]
69            {
70                let _ = home;
71                daemon::gc(dry_run)
72            }
73            #[cfg(not(unix))]
74            {
75                let _ = (home, dry_run);
76                Err(crate::Error::msg("hot daemon requires a Unix platform"))
77            }
78        }
79        Commands::Chroot(ChrootCommands::Init { name }) => {
80            init_chroot(home.global_path(), &name)?;
81            println!(
82                "chroot initialized: {}",
83                home.global_path().join("chroots").join(name).display()
84            );
85            Ok(())
86        }
87        Commands::Chroot(ChrootCommands::List) => {
88            for name in list_chroots(home.global_path())? {
89                println!("{name}");
90            }
91            Ok(())
92        }
93        Commands::Namespace(cmd) => {
94            home.ensure()?;
95            dispatch_namespace(&home, cmd, ns.as_deref())
96        }
97        Commands::Sql(cmd) => {
98            home.ensure()?;
99            dispatch_sql(&home, cmd)
100        }
101        Commands::Triple(cmd) => {
102            home.ensure()?;
103            dispatch_triple(&home, cmd)
104        }
105        Commands::Log(cmd) => {
106            home.ensure()?;
107            dispatch_log(&home, cmd)
108        }
109        Commands::Serve {
110            name,
111            file,
112            content_type,
113            ttl,
114            wrap,
115            title,
116        } => {
117            home.ensure()?;
118            dispatch_serve(
119                &home,
120                ServeOpts {
121                    name,
122                    file,
123                    content_type,
124                    ttl,
125                    wrap,
126                    title,
127                    no_daemon: cli.no_daemon,
128                },
129            )
130        }
131        Commands::Web(cmd) => {
132            home.ensure()?;
133            dispatch_web(&home, cmd, cli.no_daemon)
134        }
135        cmd => {
136            home.ensure()?;
137            if cli.no_daemon {
138                dispatch_data(&home, cmd, ns.as_deref())
139            } else {
140                #[cfg(unix)]
141                {
142                    daemon::ensure_running(&home)?;
143                    dispatch_via_daemon(&home, cmd, ns.as_deref())
144                }
145                #[cfg(not(unix))]
146                {
147                    dispatch_data(&home, cmd, ns.as_deref())
148                }
149            }
150        }
151    }
152}
153
154fn dispatch_namespace(
155    home: &UnifierHome,
156    cmd: NamespaceCommands,
157    override_ns: Option<&str>,
158) -> Result<()> {
159    match cmd {
160        NamespaceCommands::Set { name } => {
161            namespace::set(home, &name)?;
162            println!("{name}");
163            Ok(())
164        }
165        NamespaceCommands::Get => match namespace::effective(home, override_ns)? {
166            Some(name) => {
167                println!("{name}");
168                Ok(())
169            }
170            None => Err(crate::Error::msg("no namespace")),
171        },
172        NamespaceCommands::Clear => {
173            if namespace::clear(home)? {
174                Ok(())
175            } else {
176                Err(crate::Error::msg("no namespace"))
177            }
178        }
179    }
180}
181
182fn qualify(home: &UnifierHome, ns: Option<&str>, key: String) -> Result<String> {
183    namespace::qualify_key(home, ns, &key)
184}
185
186fn dispatch_sql(home: &UnifierHome, cmd: SqlCommands) -> Result<()> {
187    match cmd {
188        SqlCommands::List => {
189            for name in sqlite::list_databases(home)? {
190                println!("{name}");
191            }
192            Ok(())
193        }
194        SqlCommands::Create { name } => {
195            let path = sqlite::create_db(home, &name)?;
196            println!("{}", path.display());
197            Ok(())
198        }
199        SqlCommands::Tables { database, schema } => {
200            for table in sqlite::list_tables(home, &database)? {
201                println!("{}", table.name);
202                if schema {
203                    for col in &table.columns {
204                        if col.decl_type.is_empty() {
205                            println!("  {}", col.name);
206                        } else {
207                            println!("  {} {}", col.name, col.decl_type);
208                        }
209                    }
210                }
211            }
212            Ok(())
213        }
214        SqlCommands::Exec { database, sql } => match sqlite::exec_sql(home, &database, &sql)? {
215            SqlOutcome::Query(result) => {
216                if !result.columns.is_empty() {
217                    println!("{}", result.columns.join("\t"));
218                }
219                for row in result.rows {
220                    println!("{}", row.join("\t"));
221                }
222                Ok(())
223            }
224            SqlOutcome::Exec(result) => {
225                println!("ok {}", result.rows_affected);
226                Ok(())
227            }
228        },
229    }
230}
231
232fn dispatch_triple(home: &UnifierHome, cmd: TripleCommands) -> Result<()> {
233    match cmd {
234        TripleCommands::Add {
235            subject,
236            predicate,
237            object,
238        } => {
239            sqlite::insert_triple(home, &subject, &predicate, &object)?;
240            Ok(())
241        }
242        TripleCommands::Query {
243            subject,
244            predicate,
245            object,
246        } => {
247            let rows = sqlite::query_triples(
248                home,
249                subject.as_deref(),
250                predicate.as_deref(),
251                object.as_deref(),
252            )?;
253            for (s, p, o) in rows {
254                println!("{s}\t{p}\t{o}");
255            }
256            Ok(())
257        }
258    }
259}
260
261fn dispatch_data(home: &UnifierHome, cmd: Commands, ns: Option<&str>) -> Result<()> {
262    match cmd {
263        Commands::Put { key, value } => {
264            let key = qualify(home, ns, key)?;
265            put_key(home, &key, &value)?;
266            Ok(())
267        }
268        Commands::Get { key } => {
269            let key = qualify(home, ns, key)?;
270            match get_key(home, &key)? {
271                Some(v) => {
272                    println!("{v}");
273                    Ok(())
274                }
275                None => Err(crate::Error::msg(format!("key not found: {key}"))),
276            }
277        }
278        Commands::Del { key } => {
279            let key = qualify(home, ns, key)?;
280            if delete_key(home, &key)? {
281                Ok(())
282            } else {
283                Err(crate::Error::msg(format!("key not found: {key}")))
284            }
285        }
286        Commands::Send {
287            from,
288            recipient,
289            message,
290        } => {
291            let from = from.unwrap_or_else(|| crate::envelope::DEFAULT_SENDER.to_string());
292            let id = send_from(home, &from, &recipient, &message)?;
293            println!("{}", id.hyphenated());
294            Ok(())
295        }
296        Commands::Cron { schedule, message } => {
297            let id = post_cron(home, &schedule, &message)?;
298            println!("{}", id.hyphenated());
299            Ok(())
300        }
301        Commands::Poll {
302            recipient,
303            ack: do_ack,
304        } => {
305            let messages = poll_mailbox(home, &recipient)?;
306            print_messages(&messages);
307            if do_ack {
308                for msg in &messages {
309                    ack(home, &msg.id.hyphenated().to_string())?;
310                }
311            }
312            Ok(())
313        }
314        Commands::PollCron { ack: do_ack } => {
315            let messages = poll_cron(home)?;
316            print_messages(&messages);
317            if do_ack {
318                for msg in &messages {
319                    ack(home, &msg.id.hyphenated().to_string())?;
320                }
321            }
322            Ok(())
323        }
324        Commands::List { path } => {
325            let messages = list_dir(home, &path)?;
326            print_messages(&messages);
327            Ok(())
328        }
329        Commands::Ack { id_or_path } => {
330            if ack(home, &id_or_path)? {
331                Ok(())
332            } else {
333                Err(crate::Error::msg(format!(
334                    "message not found: {id_or_path}"
335                )))
336            }
337        }
338        Commands::Root => {
339            println!("{}", home.path().display());
340            Ok(())
341        }
342        Commands::Event { .. } | Commands::Message { .. } | Commands::Tick(_) => {
343            Err(crate::Error::msg(
344                "event, message, and tick commands require the hot daemon; omit --no-daemon",
345            ))
346        }
347        Commands::Daemon(_)
348        | Commands::Chroot(_)
349        | Commands::Namespace(_)
350        | Commands::Sql(_)
351        | Commands::Triple(_)
352        | Commands::Log(_)
353        | Commands::Serve { .. }
354        | Commands::Web(_) => {
355            unreachable!("handled in run()")
356        }
357    }
358}
359
360#[cfg(unix)]
361fn dispatch_via_daemon(home: &UnifierHome, cmd: Commands, ns: Option<&str>) -> Result<()> {
362    let mut client = Client::connect(home)?;
363    match cmd {
364        Commands::Put { key, value } => {
365            let key = qualify(home, ns, key)?;
366            response_ok(client.request(Request::Put { key, value })?)?;
367            Ok(())
368        }
369        Commands::Get { key } => {
370            let key = qualify(home, ns, key)?;
371            match response_value(client.request(Request::Get { key })?)? {
372                Some(v) => {
373                    println!("{v}");
374                    Ok(())
375                }
376                None => Err(crate::Error::msg("key not found")),
377            }
378        }
379        Commands::Del { key } => {
380            let key = qualify(home, ns, key)?;
381            response_ok(client.request(Request::Del { key })?)?;
382            Ok(())
383        }
384        Commands::Send {
385            from,
386            recipient,
387            message,
388        } => {
389            let id = response_uuid(client.request(Request::Send {
390                from,
391                recipient,
392                message,
393            })?)?;
394            println!("{}", id.hyphenated());
395            Ok(())
396        }
397        Commands::Cron { schedule, message } => {
398            let id = response_uuid(client.request(Request::Cron { schedule, message })?)?;
399            println!("{}", id.hyphenated());
400            Ok(())
401        }
402        Commands::Poll {
403            recipient,
404            ack: do_ack,
405        } => {
406            let messages = response_messages(client.request(Request::Poll { recipient })?)?;
407            print_messages(&messages);
408            if do_ack {
409                for msg in &messages {
410                    let found = response_found(client.request(Request::Ack {
411                        id_or_path: msg.id.hyphenated().to_string(),
412                    })?)?;
413                    if !found {
414                        return Err(crate::Error::msg(format!(
415                            "message not found: {}",
416                            msg.id.hyphenated()
417                        )));
418                    }
419                }
420            }
421            Ok(())
422        }
423        Commands::PollCron { ack: do_ack } => {
424            let messages = response_messages(client.request(Request::PollCron)?)?;
425            print_messages(&messages);
426            if do_ack {
427                for msg in &messages {
428                    response_found(client.request(Request::Ack {
429                        id_or_path: msg.id.hyphenated().to_string(),
430                    })?)?;
431                }
432            }
433            Ok(())
434        }
435        Commands::List { path } => {
436            let messages = response_messages(client.request(Request::List { path })?)?;
437            print_messages(&messages);
438            Ok(())
439        }
440        Commands::Ack { id_or_path } => {
441            if response_found(client.request(Request::Ack { id_or_path })?)? {
442                Ok(())
443            } else {
444                Err(crate::Error::msg("message not found"))
445            }
446        }
447        Commands::Root => {
448            println!("{}", home.path().display());
449            Ok(())
450        }
451        Commands::Event { payload, ttl } => {
452            let id = response_uuid(client.request(Request::Event { payload, ttl })?)?;
453            println!("{}", id.hyphenated());
454            Ok(())
455        }
456        Commands::Message {
457            from,
458            recipient,
459            payload,
460        } => {
461            let id = response_uuid(client.request(Request::AgentMessage {
462                from,
463                to: recipient,
464                payload,
465            })?)?;
466            println!("{}", id.hyphenated());
467            Ok(())
468        }
469        Commands::Tick(cmd) => dispatch_tick_via_daemon(home, &mut client, cmd, ns),
470        Commands::Daemon(_)
471        | Commands::Chroot(_)
472        | Commands::Namespace(_)
473        | Commands::Sql(_)
474        | Commands::Triple(_)
475        | Commands::Log(_)
476        | Commands::Serve { .. }
477        | Commands::Web(_) => {
478            unreachable!("handled in run()")
479        }
480    }
481}
482
483#[cfg(unix)]
484fn dispatch_tick_via_daemon(
485    home: &UnifierHome,
486    client: &mut Client,
487    cmd: TickCommands,
488    ns: Option<&str>,
489) -> Result<()> {
490    use crate::daemon::{response_found, response_ok, Response};
491
492    match cmd {
493        TickCommands::Start { label } => {
494            let resp = client.request(Request::TickStart { label })?;
495            match resp {
496                Response::Ok { tick: Some(t), .. } => {
497                    println!("tick {t} started");
498                    Ok(())
499                }
500                Response::Ok {
501                    queued: Some(pos), ..
502                } => {
503                    println!("tick start queued at position {pos}");
504                    Ok(())
505                }
506                Response::Err { error } => Err(crate::Error::msg(error)),
507                _ => Err(crate::Error::msg("unexpected tick start response")),
508            }
509        }
510        TickCommands::End => {
511            let resp = client.request(Request::TickEnd)?;
512            match resp {
513                Response::Ok { tick: Some(t), .. } => {
514                    println!("tick {t} committed");
515                    Ok(())
516                }
517                Response::Err { error } => Err(crate::Error::msg(error)),
518                _ => Err(crate::Error::msg("unexpected tick end response")),
519            }
520        }
521        TickCommands::Status => {
522            let resp = client.request(Request::TickStatus)?;
523            match resp {
524                Response::Ok { value: Some(v), .. } => {
525                    println!("{v}");
526                    Ok(())
527                }
528                Response::Err { error } => Err(crate::Error::msg(error)),
529                _ => Err(crate::Error::msg("unexpected tick status response")),
530            }
531        }
532        TickCommands::Lock { key } => {
533            let key = qualify(home, ns, key)?;
534            response_ok(client.request(Request::TickLock { key })?)?;
535            Ok(())
536        }
537        TickCommands::Unlock { key } => {
538            let key = qualify(home, ns, key)?;
539            if response_found(client.request(Request::TickUnlock { key })?)? {
540                Ok(())
541            } else {
542                Err(crate::Error::msg("lock not held"))
543            }
544        }
545    }
546}
547
548struct ServeOpts {
549    name: Option<String>,
550    file: Option<std::path::PathBuf>,
551    content_type: Option<String>,
552    ttl: Option<u64>,
553    wrap: bool,
554    title: String,
555    no_daemon: bool,
556}
557
558fn dispatch_serve(home: &UnifierHome, opts: ServeOpts) -> Result<()> {
559    #[cfg(not(unix))]
560    {
561        let _ = (home, opts);
562        return Err(crate::Error::msg("web serve requires a Unix platform"));
563    }
564
565    #[cfg(unix)]
566    {
567        use std::io::Read;
568        use uuid::Uuid;
569
570        if !opts.no_daemon {
571            daemon::ensure_running(home)?;
572            wait_for_http_port(home)?;
573        } else if daemon::www::base_url(home).is_none() {
574            return Err(crate::Error::msg(
575                "web server is not listening; omit --no-daemon so the daemon can start it",
576            ));
577        }
578
579        let mut raw = Vec::new();
580        if let Some(path) = opts.file {
581            raw = std::fs::read(&path)?;
582        } else {
583            std::io::stdin().read_to_end(&mut raw)?;
584        }
585
586        let body = if opts.wrap {
587            let text = String::from_utf8_lossy(&raw);
588            daemon::www::wrap_html(&opts.title, &text).into_bytes()
589        } else {
590            raw
591        };
592
593        let name = opts
594            .name
595            .unwrap_or_else(|| Uuid::new_v4().hyphenated().to_string());
596        daemon::www::publish(home, &name, &body, opts.content_type.as_deref(), opts.ttl)?;
597        let url = daemon::www::entry_url(home, &name)?;
598        println!("{url}");
599        Ok(())
600    }
601}
602
603fn dispatch_web(home: &UnifierHome, cmd: WebCommands, no_daemon: bool) -> Result<()> {
604    #[cfg(not(unix))]
605    {
606        let _ = (home, cmd, no_daemon);
607        return Err(crate::Error::msg("web commands require a Unix platform"));
608    }
609
610    #[cfg(unix)]
611    {
612        if !no_daemon {
613            daemon::ensure_running(home)?;
614            wait_for_http_port(home)?;
615        }
616
617        match cmd {
618            WebCommands::List => {
619                if !no_daemon {
620                    let mut client = Client::connect(home)?;
621                    match client.request(Request::WebList)? {
622                        crate::daemon::Response::Ok {
623                            value: Some(v), ..
624                        } => {
625                            if !v.is_empty() {
626                                println!("{v}");
627                            }
628                            Ok(())
629                        }
630                        crate::daemon::Response::Ok { .. } => Ok(()),
631                        crate::daemon::Response::Err { error } => Err(crate::Error::msg(error)),
632                    }
633                } else {
634                    for e in daemon::www::list(home)? {
635                        let url = daemon::www::entry_url(home, &e.name).unwrap_or_default();
636                        println!("{}\t{}\t{}\t{}", e.name, e.content_type, e.bytes, url);
637                    }
638                    Ok(())
639                }
640            }
641            WebCommands::Url { name } => {
642                let url = daemon::www::entry_url(home, &name)?;
643                if daemon::www::load_meta(home, &name)?.is_none() {
644                    return Err(crate::Error::msg(format!("web file not found: {name}")));
645                }
646                println!("{url}");
647                Ok(())
648            }
649            WebCommands::Rm { name } => {
650                let found = if !no_daemon {
651                    let mut client = Client::connect(home)?;
652                    response_found(client.request(Request::WebRm { name: name.clone() })?)?
653                } else {
654                    daemon::www::remove(home, &name)?
655                };
656                if found {
657                    Ok(())
658                } else {
659                    Err(crate::Error::msg(format!("web file not found: {name}")))
660                }
661            }
662            WebCommands::Status => {
663                let url = if !no_daemon {
664                    let mut client = Client::connect(home)?;
665                    response_value(client.request(Request::WebStatus)?)?
666                        .ok_or_else(|| crate::Error::msg("web server is not listening"))?
667                } else {
668                    daemon::www::base_url(home)
669                        .ok_or_else(|| crate::Error::msg("web server is not listening"))?
670                };
671                println!("{url}");
672                Ok(())
673            }
674        }
675    }
676}
677
678#[cfg(unix)]
679fn wait_for_http_port(home: &UnifierHome) -> Result<()> {
680    for _ in 0..100 {
681        if daemon::www::base_url(home).is_some() {
682            return Ok(());
683        }
684        std::thread::sleep(std::time::Duration::from_millis(50));
685    }
686    Err(crate::Error::msg("web server failed to start"))
687}
688
689fn dispatch_log(home: &UnifierHome, cmd: LogCommands) -> Result<()> {
690    match cmd {
691        LogCommands::Start { name, parent, fields } => {
692            let parent_id = parent.as_deref().map(parse_uuid).transpose()?;
693            let fields = spanlog::parse_fields(&fields)?;
694            let id = spanlog::span_start(home, &name, parent_id, fields)?;
695            println!("{}", id.hyphenated());
696            Ok(())
697        }
698        LogCommands::End { id } => {
699            let id = parse_uuid(&id)?;
700            spanlog::span_end(home, id)?;
701            println!("span {id} ended");
702            Ok(())
703        }
704        LogCommands::Event { span, message, fields } => {
705            let span_id = parse_uuid(&span)?;
706            let fields = spanlog::parse_fields(&fields)?;
707            spanlog::log_event(home, span_id, &message, fields)?;
708            Ok(())
709        }
710        LogCommands::Field { span, fields } => {
711            let span_id = parse_uuid(&span)?;
712            let fields = spanlog::parse_fields(&fields)?;
713            spanlog::span_set_fields(home, span_id, fields)?;
714            Ok(())
715        }
716        LogCommands::Tree { span } => {
717            let root_id = span.as_deref().map(parse_uuid).transpose()?;
718            let spans = spanlog::load_all_spans(home)?;
719            let tree = spanlog::render_tree(&spans, root_id);
720            print!("{tree}");
721            Ok(())
722        }
723        LogCommands::List => {
724            let spans = spanlog::load_all_spans(home)?;
725            for span in &spans {
726                let status = if span.ended_at.is_some() { "ended" } else { "open" };
727                println!("{}\t{}\t{}", span.id.hyphenated(), span.name, status);
728            }
729            Ok(())
730        }
731    }
732}
733
734fn parse_uuid(s: &str) -> Result<uuid::Uuid> {
735    uuid::Uuid::parse_str(s).map_err(|_| crate::Error::msg(format!("invalid UUID: {s}")))
736}
737
738fn print_messages(messages: &[Message]) {
739    for (i, msg) in messages.iter().enumerate() {
740        println!("{} {}", msg.id.hyphenated(), msg.path.display());
741        println!("{}", msg.body);
742        if i + 1 < messages.len() {
743            println!("---");
744        }
745    }
746}