1mod 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::log as spanlog;
15use crate::namespace;
16use crate::postbox::{
17 ack, delete_key, get_key, list_dir, poll_cron, poll_mailbox, post_cron, put_key, send_from,
18 Message,
19};
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, Request, Response};
491
492 let mut tick_client = Client::connect_tick(home).ok();
494 let client = tick_client.as_mut().unwrap_or(client);
495
496 match cmd {
497 TickCommands::Start { label } => {
498 let resp = client.request(Request::TickStart { label })?;
499 match resp {
500 Response::Ok {
501 tick: Some(t),
502 phase,
503 label,
504 ..
505 } => {
506 let phase = phase.unwrap_or_else(|| "start".into());
507 if let Some(label) = label {
508 println!("tick {t} started phase={phase} label={label}");
509 } else {
510 println!("tick {t} started phase={phase}");
511 }
512 Ok(())
513 }
514 Response::Ok {
515 queued: Some(pos), ..
516 } => {
517 println!("tick start queued at position {pos}");
518 Ok(())
519 }
520 Response::Err { error } => Err(crate::Error::msg(error)),
521 _ => Err(crate::Error::msg("unexpected tick start response")),
522 }
523 }
524 TickCommands::End => {
525 let resp = client.request(Request::TickEnd)?;
526 match resp {
527 Response::Ok { tick: Some(t), .. } => {
528 println!("tick {t} committed");
529 Ok(())
530 }
531 Response::Err { error } => Err(crate::Error::msg(error)),
532 _ => Err(crate::Error::msg("unexpected tick end response")),
533 }
534 }
535 TickCommands::Status => {
536 let resp = client.request(Request::TickStatus)?;
537 match resp {
538 Response::Ok { value: Some(v), .. } => {
539 println!("{v}");
540 Ok(())
541 }
542 Response::Err { error } => Err(crate::Error::msg(error)),
543 _ => Err(crate::Error::msg("unexpected tick status response")),
544 }
545 }
546 TickCommands::Phase { phase } => {
547 let resp = client.request(Request::TickPhase { phase })?;
548 match resp {
549 Response::Ok {
550 tick: Some(t),
551 phase: Some(p),
552 ..
553 } => {
554 println!("tick {t} phase={p}");
555 Ok(())
556 }
557 Response::Err { error } => Err(crate::Error::msg(error)),
558 _ => Err(crate::Error::msg("unexpected tick phase response")),
559 }
560 }
561 TickCommands::Lock { key } => {
562 let key = qualify(home, ns, key)?;
563 response_ok(client.request(Request::TickLock { key })?)?;
564 Ok(())
565 }
566 TickCommands::Unlock { key } => {
567 let key = qualify(home, ns, key)?;
568 if response_found(client.request(Request::TickUnlock { key })?)? {
569 Ok(())
570 } else {
571 Err(crate::Error::msg("lock not held"))
572 }
573 }
574 }
575}
576
577struct ServeOpts {
578 name: Option<String>,
579 file: Option<std::path::PathBuf>,
580 content_type: Option<String>,
581 ttl: Option<u64>,
582 wrap: bool,
583 title: String,
584 no_daemon: bool,
585}
586
587fn dispatch_serve(home: &UnifierHome, opts: ServeOpts) -> Result<()> {
588 #[cfg(not(unix))]
589 {
590 let _ = (home, opts);
591 return Err(crate::Error::msg("web serve requires a Unix platform"));
592 }
593
594 #[cfg(unix)]
595 {
596 use std::io::Read;
597 use uuid::Uuid;
598
599 if !opts.no_daemon {
600 daemon::ensure_running(home)?;
601 wait_for_http_port(home)?;
602 } else if daemon::www::base_url(home).is_none() {
603 return Err(crate::Error::msg(
604 "web server is not listening; omit --no-daemon so the daemon can start it",
605 ));
606 }
607
608 let mut raw = Vec::new();
609 if let Some(path) = opts.file {
610 raw = std::fs::read(&path)?;
611 } else {
612 std::io::stdin().read_to_end(&mut raw)?;
613 }
614
615 let body = if opts.wrap {
616 let text = String::from_utf8_lossy(&raw);
617 daemon::www::wrap_html(&opts.title, &text).into_bytes()
618 } else {
619 raw
620 };
621
622 let name = opts
623 .name
624 .unwrap_or_else(|| Uuid::new_v4().hyphenated().to_string());
625 daemon::www::publish(home, &name, &body, opts.content_type.as_deref(), opts.ttl)?;
626 let url = daemon::www::entry_url(home, &name)?;
627 println!("{url}");
628 Ok(())
629 }
630}
631
632fn dispatch_web(home: &UnifierHome, cmd: WebCommands, no_daemon: bool) -> Result<()> {
633 #[cfg(not(unix))]
634 {
635 let _ = (home, cmd, no_daemon);
636 return Err(crate::Error::msg("web commands require a Unix platform"));
637 }
638
639 #[cfg(unix)]
640 {
641 if !no_daemon {
642 daemon::ensure_running(home)?;
643 wait_for_http_port(home)?;
644 }
645
646 match cmd {
647 WebCommands::List => {
648 if !no_daemon {
649 let mut client = Client::connect(home)?;
650 match client.request(Request::WebList)? {
651 crate::daemon::Response::Ok { value: Some(v), .. } => {
652 if !v.is_empty() {
653 println!("{v}");
654 }
655 Ok(())
656 }
657 crate::daemon::Response::Ok { .. } => Ok(()),
658 crate::daemon::Response::Err { error } => Err(crate::Error::msg(error)),
659 }
660 } else {
661 for e in daemon::www::list(home)? {
662 let url = daemon::www::entry_url(home, &e.name).unwrap_or_default();
663 println!("{}\t{}\t{}\t{}", e.name, e.content_type, e.bytes, url);
664 }
665 Ok(())
666 }
667 }
668 WebCommands::Url { name } => {
669 let url = daemon::www::entry_url(home, &name)?;
670 if daemon::www::load_meta(home, &name)?.is_none() {
671 return Err(crate::Error::msg(format!("web file not found: {name}")));
672 }
673 println!("{url}");
674 Ok(())
675 }
676 WebCommands::KeyUrl { key } => {
677 let url = daemon::www::key_url(home, &key)?;
678 println!("{url}");
679 Ok(())
680 }
681 WebCommands::Rm { name } => {
682 let found = if !no_daemon {
683 let mut client = Client::connect(home)?;
684 response_found(client.request(Request::WebRm { name: name.clone() })?)?
685 } else {
686 daemon::www::remove(home, &name)?
687 };
688 if found {
689 Ok(())
690 } else {
691 Err(crate::Error::msg(format!("web file not found: {name}")))
692 }
693 }
694 WebCommands::Status => {
695 let url = if !no_daemon {
696 let mut client = Client::connect(home)?;
697 response_value(client.request(Request::WebStatus)?)?
698 .ok_or_else(|| crate::Error::msg("web server is not listening"))?
699 } else {
700 daemon::www::base_url(home)
701 .ok_or_else(|| crate::Error::msg("web server is not listening"))?
702 };
703 println!("{url}");
704 Ok(())
705 }
706 }
707 }
708}
709
710#[cfg(unix)]
711fn wait_for_http_port(home: &UnifierHome) -> Result<()> {
712 for _ in 0..100 {
713 if daemon::www::base_url(home).is_some() {
714 return Ok(());
715 }
716 std::thread::sleep(std::time::Duration::from_millis(50));
717 }
718 Err(crate::Error::msg("web server failed to start"))
719}
720
721fn dispatch_log(home: &UnifierHome, cmd: LogCommands) -> Result<()> {
722 match cmd {
723 LogCommands::Start {
724 name,
725 parent,
726 fields,
727 } => {
728 let parent_id = parent.as_deref().map(parse_uuid).transpose()?;
729 let fields = spanlog::parse_fields(&fields)?;
730 let id = spanlog::span_start(home, &name, parent_id, fields)?;
731 println!("{}", id.hyphenated());
732 Ok(())
733 }
734 LogCommands::End { id } => {
735 let id = parse_uuid(&id)?;
736 spanlog::span_end(home, id)?;
737 println!("span {id} ended");
738 Ok(())
739 }
740 LogCommands::Event {
741 span,
742 message,
743 fields,
744 } => {
745 let span_id = parse_uuid(&span)?;
746 let fields = spanlog::parse_fields(&fields)?;
747 spanlog::log_event(home, span_id, &message, fields)?;
748 Ok(())
749 }
750 LogCommands::Field { span, fields } => {
751 let span_id = parse_uuid(&span)?;
752 let fields = spanlog::parse_fields(&fields)?;
753 spanlog::span_set_fields(home, span_id, fields)?;
754 Ok(())
755 }
756 LogCommands::Tree { span } => {
757 let root_id = span.as_deref().map(parse_uuid).transpose()?;
758 let spans = spanlog::load_all_spans(home)?;
759 let tree = spanlog::render_tree(&spans, root_id);
760 print!("{tree}");
761 Ok(())
762 }
763 LogCommands::List => {
764 let spans = spanlog::load_all_spans(home)?;
765 for span in &spans {
766 let status = if span.ended_at.is_some() {
767 "ended"
768 } else {
769 "open"
770 };
771 println!("{}\t{}\t{}", span.id.hyphenated(), span.name, status);
772 }
773 Ok(())
774 }
775 }
776}
777
778fn parse_uuid(s: &str) -> Result<uuid::Uuid> {
779 uuid::Uuid::parse_str(s).map_err(|_| crate::Error::msg(format!("invalid UUID: {s}")))
780}
781
782fn print_messages(messages: &[Message]) {
783 for (i, msg) in messages.iter().enumerate() {
784 println!("{} {}", msg.id.hyphenated(), msg.path.display());
785 println!("{}", msg.body);
786 if i + 1 < messages.len() {
787 println!("---");
788 }
789 }
790}