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::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}