1use std::fs;
4use std::io::{BufRead, BufReader, Write};
5use std::os::unix::net::{UnixListener, UnixStream};
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::Arc;
8use std::thread;
9use std::time::{Duration, Instant};
10
11use crate::daemon::idle::{
12 idle_after, idle_stop_after, max_load, should_idle_flush, should_idle_stop, system_is_idle,
13};
14
15use crate::daemon::notify::{event_name, EventHub, Notice};
16use crate::daemon::paths::{
17 daemon_dir, events_socket_path, pid_path, socket_path, tick_socket_path,
18};
19use crate::daemon::protocol::{
20 decode_request, encode_response, is_tick_socket_request, ok_empty, MessageDto, Request,
21 Response,
22};
23use crate::daemon::www;
24use crate::envelope::DEFAULT_SENDER;
25use crate::error::{Error, Result};
26use crate::home::UnifierHome;
27use crate::store::HotStore;
28use crate::tick::TickStartOutcome;
29
30static SIGNALLED: AtomicBool = AtomicBool::new(false);
33
34extern "C" fn on_terminate(_sig: libc::c_int) {
35 SIGNALLED.store(true, Ordering::SeqCst);
36}
37
38fn install_signal_handlers() {
39 let handler = on_terminate as extern "C" fn(libc::c_int);
40 for sig in [libc::SIGTERM, libc::SIGINT, libc::SIGHUP] {
41 unsafe { libc::signal(sig, handler as libc::sighandler_t) };
42 }
43}
44
45fn flush_on_exit(home: &UnifierHome, store: &Arc<std::sync::Mutex<HotStore>>) {
47 let Ok(mut store) = store.lock() else {
48 return;
49 };
50 if !store.is_dirty() {
51 return;
52 }
53 if let Err(e) = store.flush(home) {
54 eprintln!("shutdown flush error: {e}");
55 }
56}
57
58pub fn run(home: UnifierHome) -> Result<()> {
59 let shutdown = Arc::new(AtomicBool::new(false));
60 install_signal_handlers();
61 home.ensure()?;
62 fs::create_dir_all(daemon_dir(&home))?;
63
64 let events_sock = events_socket_path(&home);
65 if events_sock.exists() {
66 fs::remove_file(&events_sock)?;
67 }
68 let events_listener = UnixListener::bind(&events_sock)?;
69 events_listener.set_nonblocking(true)?;
70 let hub = Arc::new(EventHub::default());
71
72 let sock = socket_path(&home);
73 if sock.exists() {
74 fs::remove_file(&sock)?;
75 }
76 let listener = UnixListener::bind(&sock)?;
77 listener.set_nonblocking(true)?;
78
79 let tick_sock = tick_socket_path(&home);
80 if tick_sock.exists() {
81 fs::remove_file(&tick_sock)?;
82 }
83 let tick_listener = UnixListener::bind(&tick_sock)?;
84 tick_listener.set_nonblocking(true)?;
85
86 let store = Arc::new(std::sync::Mutex::new(HotStore::load(&home)?));
87 write_pid(&home)?;
88
89 let http_activity = Arc::new(AtomicBool::new(false));
90 let http_port = match www::spawn(
91 home.clone(),
92 Arc::clone(&store),
93 Arc::clone(&shutdown),
94 Arc::clone(&http_activity),
95 ) {
96 Ok(port) => {
97 eprintln!("www listening on http://127.0.0.1:{port}");
98 Some(port)
99 }
100 Err(e) => {
101 eprintln!("www server disabled: {e}");
102 None
103 }
104 };
105 let _ = http_port;
106
107 let idle_after = idle_after();
108 let idle_stop_after = idle_stop_after();
109 let max_load = max_load();
110 let mut last_activity = Instant::now();
111 let mut last_event_gc = Instant::now();
112
113 while !shutdown.load(Ordering::Relaxed) {
114 if SIGNALLED.load(Ordering::Relaxed) {
115 break;
116 }
117 if !home.path().is_dir() {
118 let _ = cleanup(&home);
120 return Ok(());
121 }
122
123 if http_activity.swap(false, Ordering::Relaxed) {
124 last_activity = Instant::now();
125 }
126
127 match events_listener.accept() {
128 Ok((stream, _)) => {
129 if let Err(e) = hub.add(stream) {
130 eprintln!("event subscriber error: {e}");
131 }
132 last_activity = Instant::now();
133 }
134 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
135 Err(e) => return Err(e.into()),
136 }
137
138 match tick_listener.accept() {
139 Ok((stream, _)) => {
140 let home = home.clone();
141 let store = Arc::clone(&store);
142 let shutdown = Arc::clone(&shutdown);
143 let hub = Arc::clone(&hub);
144 if let Err(e) = handle_tick_client(stream, &home, &store, &shutdown, &hub) {
145 eprintln!("tick socket client error: {e}");
146 }
147 last_activity = Instant::now();
148 }
149 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
150 Err(e) => return Err(e.into()),
151 }
152
153 match listener.accept() {
154 Ok((stream, _)) => {
155 let home = home.clone();
156 let store = Arc::clone(&store);
157 let shutdown = Arc::clone(&shutdown);
158 let hub = Arc::clone(&hub);
159 if let Err(e) = handle_client(stream, &home, &store, &shutdown, &hub) {
160 eprintln!("daemon client error: {e}");
161 }
162 last_activity = Instant::now();
163 }
164 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
165 let _ = crate::namespace::reap_dead(&home);
166 if last_event_gc.elapsed() >= Duration::from_secs(30) {
167 if let Ok(mut store) = store.lock() {
168 if let Err(e) = store.expire_events(&home) {
169 eprintln!("event gc error: {e}");
170 }
171 }
172 last_event_gc = Instant::now();
173 }
174 maybe_idle_flush(&home, &store, last_activity, idle_after, max_load);
175 if maybe_idle_stop(
176 &home,
177 &store,
178 &hub,
179 &shutdown,
180 last_activity,
181 idle_stop_after,
182 ) {
183 break;
184 }
185 thread::sleep(Duration::from_millis(50));
186 }
187 Err(e) => return Err(e.into()),
188 }
189 }
190
191 flush_on_exit(&home, &store);
192 cleanup(&home)?;
193 Ok(())
194}
195
196fn maybe_idle_flush(
197 home: &UnifierHome,
198 store: &Arc<std::sync::Mutex<HotStore>>,
199 last_activity: Instant,
200 idle_after: Duration,
201 max_load: f64,
202) {
203 if !should_idle_flush(
204 last_activity,
205 Instant::now(),
206 idle_after,
207 system_is_idle(max_load),
208 true,
209 false,
210 ) {
211 return;
212 }
213 let Ok(mut store) = store.lock() else {
214 return;
215 };
216 if store.active_tick.is_some() || !store.is_dirty() {
217 return;
218 }
219 if let Err(e) = store.flush(home) {
220 eprintln!("idle flush error: {e}");
221 }
222}
223
224fn maybe_idle_stop(
226 home: &UnifierHome,
227 store: &Arc<std::sync::Mutex<HotStore>>,
228 hub: &Arc<EventHub>,
229 shutdown: &Arc<AtomicBool>,
230 last_activity: Instant,
231 idle_stop_after: Duration,
232) -> bool {
233 let tick_active = store
234 .lock()
235 .map(|s| s.active_tick.is_some())
236 .unwrap_or(true);
237 if !should_idle_stop(
238 last_activity,
239 Instant::now(),
240 idle_stop_after,
241 tick_active,
242 hub.subscriber_count(),
243 ) {
244 return false;
245 }
246 if let Ok(mut store) = store.lock() {
247 if store.is_dirty() {
248 if let Err(e) = store.flush(home) {
249 eprintln!("idle stop flush error: {e}");
250 }
251 }
252 }
253 shutdown.store(true, Ordering::Relaxed);
254 true
255}
256
257const CLIENT_IDLE_TIMEOUT: Duration = Duration::from_secs(2);
260
261fn read_timed_out(e: &std::io::Error) -> bool {
263 matches!(
264 e.kind(),
265 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
266 )
267}
268
269fn handle_client(
270 mut stream: UnixStream,
271 home: &UnifierHome,
272 store: &Arc<std::sync::Mutex<HotStore>>,
273 shutdown: &Arc<AtomicBool>,
274 hub: &Arc<EventHub>,
275) -> Result<()> {
276 stream.set_read_timeout(Some(CLIENT_IDLE_TIMEOUT))?;
277 let mut reader = BufReader::new(stream.try_clone()?);
278 loop {
281 let mut line = String::new();
282 match reader.read_line(&mut line) {
283 Ok(0) => return Ok(()),
284 Ok(_) => {}
285 Err(e) if read_timed_out(&e) => return Ok(()),
286 Err(e) => return Err(e.into()),
287 }
288 if line.trim().is_empty() {
289 return Ok(());
290 }
291
292 let request =
293 decode_request(&line).map_err(|e| Error::msg(format!("invalid request: {e}")))?;
294 let response = match dispatch(home, store, shutdown, hub, request) {
295 Ok(resp) => resp,
296 Err(e) => Response::Err {
297 error: e.to_string(),
298 },
299 };
300 stream.write_all(encode_response(&response)?.as_bytes())?;
301 stream.flush()?;
302 }
303}
304
305fn handle_tick_client(
307 mut stream: UnixStream,
308 home: &UnifierHome,
309 store: &Arc<std::sync::Mutex<HotStore>>,
310 shutdown: &Arc<AtomicBool>,
311 hub: &Arc<EventHub>,
312) -> Result<()> {
313 stream.set_read_timeout(Some(CLIENT_IDLE_TIMEOUT))?;
314 let mut reader = BufReader::new(stream.try_clone()?);
315 loop {
316 let mut line = String::new();
317 match reader.read_line(&mut line) {
318 Ok(0) => return Ok(()),
319 Ok(_) => {}
320 Err(e) if read_timed_out(&e) => return Ok(()),
321 Err(e) => return Err(e.into()),
322 }
323 if line.trim().is_empty() {
324 return Ok(());
325 }
326
327 let request =
328 decode_request(&line).map_err(|e| Error::msg(format!("invalid request: {e}")))?;
329 let response = if !is_tick_socket_request(&request) {
330 Response::Err {
331 error: "tick.sock accepts only tick start/end/status/lock/unlock/phase and ping"
332 .into(),
333 }
334 } else {
335 match dispatch(home, store, shutdown, hub, request) {
336 Ok(resp) => resp,
337 Err(e) => Response::Err {
338 error: e.to_string(),
339 },
340 }
341 };
342 stream.write_all(encode_response(&response)?.as_bytes())?;
343 stream.flush()?;
344 }
345}
346
347fn dispatch(
348 home: &UnifierHome,
349 store: &Arc<std::sync::Mutex<HotStore>>,
350 shutdown: &Arc<AtomicBool>,
351 hub: &Arc<EventHub>,
352 request: Request,
353) -> Result<Response> {
354 match request {
355 Request::Ping => Ok(ok_empty()),
356 Request::Shutdown => {
357 shutdown.store(true, Ordering::Relaxed);
358 let mut store = store.lock().map_err(lock_err)?;
359 if store.is_dirty() && store.active_tick.is_none() {
360 store.flush(home)?;
361 } else if store.active_tick.is_some() {
362 return Err(Error::msg(
363 "cannot shutdown with active tick; run tick end first",
364 ));
365 }
366 Ok(Response::Ok {
367 value: None,
368 uuid: None,
369 found: None,
370 dirty: Some(false),
371 tick: None,
372 queued: None,
373 phase: None,
374 committed: None,
375 label: None,
376 locked_keys: vec![],
377 messages: vec![],
378 })
379 }
380 Request::Flush => {
381 let mut store = store.lock().map_err(lock_err)?;
382 let was_dirty = store.is_dirty();
383 if store.active_tick.is_some() {
384 return Err(Error::msg("cannot flush while a tick is active"));
385 }
386 if was_dirty {
387 store.flush(home)?;
388 }
389 Ok(Response::Ok {
390 value: None,
391 uuid: None,
392 found: None,
393 dirty: Some(was_dirty),
394 tick: None,
395 queued: None,
396 phase: None,
397 committed: None,
398 label: None,
399 locked_keys: vec![],
400 messages: vec![],
401 })
402 }
403 Request::Put { key, value } => {
404 let mut store = store.lock().map_err(lock_err)?;
405 store.put_key(&key, &value)?;
406 Ok(ok_empty())
407 }
408 Request::Get { key } => {
409 let store = store.lock().map_err(lock_err)?;
410 match store.get_key(&key)? {
411 Some(value) => Ok(Response::Ok {
412 value: Some(value),
413 uuid: None,
414 found: None,
415 dirty: None,
416 tick: None,
417 queued: None,
418 phase: None,
419 committed: None,
420 label: None,
421 locked_keys: vec![],
422 messages: vec![],
423 }),
424 None => Ok(Response::Err {
425 error: format!("key not found: {key}"),
426 }),
427 }
428 }
429 Request::Del { key } => {
430 let mut store = store.lock().map_err(lock_err)?;
431 if store.delete_key(&key)? {
432 Ok(ok_empty())
433 } else {
434 Ok(Response::Err {
435 error: format!("key not found: {key}"),
436 })
437 }
438 }
439 Request::Send {
440 from,
441 recipient,
442 message,
443 } => {
444 let from = from.unwrap_or_else(|| DEFAULT_SENDER.to_string());
445 let mut store = store.lock().map_err(lock_err)?;
446 let id = store.send_from(&from, &recipient, &message)?;
447 hub.broadcast(&Notice::mailbox(id, &from, &recipient));
448 Ok(Response::Ok {
449 value: None,
450 uuid: Some(id),
451 found: None,
452 dirty: None,
453 tick: None,
454 queued: None,
455 phase: None,
456 committed: None,
457 label: None,
458 locked_keys: vec![],
459 messages: vec![],
460 })
461 }
462 Request::Cron { schedule, message } => {
463 let mut store = store.lock().map_err(lock_err)?;
464 let id = store.post_cron(&schedule, &message)?;
465 Ok(Response::Ok {
466 value: None,
467 uuid: Some(id),
468 found: None,
469 dirty: None,
470 tick: None,
471 queued: None,
472 phase: None,
473 committed: None,
474 label: None,
475 locked_keys: vec![],
476 messages: vec![],
477 })
478 }
479 Request::Poll { recipient } => {
480 let store = store.lock().map_err(lock_err)?;
481 let messages = store.poll_mailbox(&recipient)?;
482 Ok(messages_response(messages))
483 }
484 Request::PollCron => {
485 let store = store.lock().map_err(lock_err)?;
486 let messages = store.poll_cron()?;
487 Ok(messages_response(messages))
488 }
489 Request::List { path } => {
490 let store = store.lock().map_err(lock_err)?;
491 let messages = store.list_dir(home, &path)?;
492 Ok(messages_response(messages))
493 }
494 Request::Ack { id_or_path } => {
495 let mut store = store.lock().map_err(lock_err)?;
496 let found = store.ack(home, &id_or_path)?;
497 Ok(Response::Ok {
498 value: None,
499 uuid: None,
500 found: Some(found),
501 dirty: None,
502 tick: None,
503 queued: None,
504 phase: None,
505 committed: None,
506 label: None,
507 locked_keys: vec![],
508 messages: vec![],
509 })
510 }
511 Request::TickStart { label } => {
512 let mut store = store.lock().map_err(lock_err)?;
513 match store.tick_start(&label)? {
514 TickStartOutcome::Started { tick } => {
515 hub.broadcast(&Notice::tick(tick, "start", Some(label.clone())));
516 Ok(Response::Ok {
517 value: None,
518 uuid: None,
519 found: None,
520 dirty: None,
521 tick: Some(tick),
522 queued: None,
523 phase: Some("start".into()),
524 committed: None,
525 label: Some(label),
526 locked_keys: vec![],
527 messages: vec![],
528 })
529 }
530 TickStartOutcome::Queued { position, .. } => {
531 eprintln!("tick queue: queued start {label:?} at position {position}");
532 Ok(Response::Ok {
533 value: None,
534 uuid: None,
535 found: None,
536 dirty: None,
537 tick: None,
538 queued: Some(position),
539 phase: None,
540 committed: None,
541 label: Some(label),
542 locked_keys: vec![],
543 messages: vec![],
544 })
545 }
546 }
547 }
548 Request::TickEnd => {
549 let mut store = store.lock().map_err(lock_err)?;
550 let ended_label = store.active_tick.as_ref().map(|t| t.label.clone());
551 let tick = store.tick_end(home)?;
552 hub.broadcast(&Notice::tick(tick, "end", ended_label));
553 if let Some(active) = store.active_tick.as_ref() {
555 hub.broadcast(&Notice::tick(
556 active.number,
557 "start",
558 Some(active.label.clone()),
559 ));
560 }
561 Ok(Response::Ok {
562 value: None,
563 uuid: None,
564 found: None,
565 dirty: None,
566 tick: Some(tick),
567 queued: None,
568 phase: Some("end".into()),
569 committed: Some(tick),
570 label: None,
571 locked_keys: vec![],
572 messages: vec![],
573 })
574 }
575 Request::TickStatus => {
576 let store = store.lock().map_err(lock_err)?;
577 let status = store.tick_status();
578 Ok(Response::Ok {
579 value: Some(format!(
580 "committed={} active={:?} phase={:?} label={:?} queued={} locks={:?}",
581 status.committed_tick,
582 status.active_tick,
583 status.phase,
584 status.label,
585 status.queued,
586 status.locked_keys
587 )),
588 uuid: None,
589 found: None,
590 dirty: None,
591 tick: status.active_tick,
592 queued: Some(status.queued),
593 phase: status.phase,
594 committed: Some(status.committed_tick),
595 label: status.label,
596 locked_keys: status.locked_keys,
597 messages: vec![],
598 })
599 }
600 Request::TickLock { key } => {
601 let mut store = store.lock().map_err(lock_err)?;
602 store.tick_lock(&key)?;
603 Ok(ok_empty())
604 }
605 Request::TickUnlock { key } => {
606 let mut store = store.lock().map_err(lock_err)?;
607 let found = store.tick_unlock(&key)?;
608 Ok(Response::Ok {
609 value: None,
610 uuid: None,
611 found: Some(found),
612 dirty: None,
613 tick: None,
614 queued: None,
615 phase: None,
616 committed: None,
617 label: None,
618 locked_keys: vec![],
619 messages: vec![],
620 })
621 }
622 Request::TickPhase { phase } => {
623 let mut store = store.lock().map_err(lock_err)?;
624 let (tick, label) = store.tick_phase(&phase)?;
625 hub.broadcast(&Notice::tick(tick, &phase, Some(label.clone())));
626 Ok(Response::Ok {
627 value: Some(phase.clone()),
628 uuid: None,
629 found: None,
630 dirty: None,
631 tick: Some(tick),
632 queued: None,
633 phase: Some(phase),
634 committed: None,
635 label: Some(label),
636 locked_keys: vec![],
637 messages: vec![],
638 })
639 }
640 Request::Event { payload, ttl } => {
641 let mut store = store.lock().map_err(lock_err)?;
642 let id = store.post_event(&payload, ttl)?;
643 hub.broadcast(&Notice::event(id, event_name(&payload)));
644 Ok(Response::Ok {
645 value: None,
646 uuid: Some(id),
647 found: None,
648 dirty: None,
649 tick: None,
650 queued: None,
651 phase: None,
652 committed: None,
653 label: None,
654 locked_keys: vec![],
655 messages: vec![],
656 })
657 }
658 Request::AgentMessage { from, to, payload } => {
659 let mut store = store.lock().map_err(lock_err)?;
660 let id = store.send_agent_message(&from, &to, &payload)?;
661 hub.broadcast(&Notice::mailbox(id, &from, &to));
662 Ok(Response::Ok {
663 value: None,
664 uuid: Some(id),
665 found: None,
666 dirty: None,
667 tick: None,
668 queued: None,
669 phase: None,
670 committed: None,
671 label: None,
672 locked_keys: vec![],
673 messages: vec![],
674 })
675 }
676 Request::WebStatus => {
677 let value = match www::base_url(home) {
678 Some(url) => url,
679 None => return Err(Error::msg("web server is not listening")),
680 };
681 Ok(Response::Ok {
682 value: Some(value),
683 uuid: None,
684 found: None,
685 dirty: None,
686 tick: None,
687 queued: None,
688 phase: None,
689 committed: None,
690 label: None,
691 locked_keys: vec![],
692 messages: vec![],
693 })
694 }
695 Request::WebList => {
696 let entries = www::list(home)?;
697 let lines: Vec<String> = entries
698 .into_iter()
699 .map(|e| {
700 let url = www::entry_url(home, &e.name).unwrap_or_default();
701 format!("{}\t{}\t{}\t{}", e.name, e.content_type, e.bytes, url)
702 })
703 .collect();
704 Ok(Response::Ok {
705 value: Some(lines.join("\n")),
706 uuid: None,
707 found: None,
708 dirty: None,
709 tick: None,
710 queued: None,
711 phase: None,
712 committed: None,
713 label: None,
714 locked_keys: vec![],
715 messages: vec![],
716 })
717 }
718 Request::WebRm { name } => {
719 let found = www::remove(home, &name)?;
720 Ok(Response::Ok {
721 value: None,
722 uuid: None,
723 found: Some(found),
724 dirty: None,
725 tick: None,
726 queued: None,
727 phase: None,
728 committed: None,
729 label: None,
730 locked_keys: vec![],
731 messages: vec![],
732 })
733 }
734 }
735}
736
737fn messages_response(messages: Vec<crate::postbox::Message>) -> Response {
738 Response::Ok {
739 value: None,
740 uuid: None,
741 found: None,
742 dirty: None,
743 tick: None,
744 queued: None,
745 phase: None,
746 committed: None,
747 label: None,
748 locked_keys: vec![],
749 messages: messages.into_iter().map(MessageDto::from).collect(),
750 }
751}
752
753fn write_pid(home: &UnifierHome) -> Result<()> {
754 let pid = std::process::id();
755 fs::write(pid_path(home), format!("{pid}\n"))?;
756 Ok(())
757}
758
759fn cleanup(home: &UnifierHome) -> Result<()> {
760 let _ = fs::remove_file(pid_path(home));
761 let _ = fs::remove_file(socket_path(home));
762 let _ = fs::remove_file(events_socket_path(home));
763 let _ = fs::remove_file(tick_socket_path(home));
764 let _ = fs::remove_file(crate::daemon::paths::http_port_path(home));
765 Ok(())
766}
767
768fn lock_err<E: std::fmt::Display>(e: E) -> Error {
769 Error::msg(format!("daemon store lock poisoned: {e}"))
770}