Skip to main content

unifier/daemon/
server.rs

1//! Hot daemon server: owns a [`HotStore`] and serves requests over a Unix socket.
2
3use 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
30/// Raised by the termination handler; the main loop turns it into a flushing
31/// exit so hot state survives `kill`, logout, and shutdown.
32static 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
45/// Last-chance flush on the way out. Safe to call after a graceful flush.
46fn 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            // TempDir / store root removed — exit so orphan daemons do not pile up.
119            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
224/// Returns true when the daemon should exit after an idle stop.
225fn 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
257/// A connection is dropped after this long with no further request, so a peer
258/// that stops talking cannot pin the (serially served) accept loop.
259const CLIENT_IDLE_TIMEOUT: Duration = Duration::from_secs(2);
260
261/// True when a read returned because the idle timeout elapsed, not on error.
262fn 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    // Clients reuse one connection for multi-step flows (`poll --ack`), so keep
279    // serving until the peer closes or goes quiet.
280    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
305/// Tick control socket: same JSON protocol, tick ops (+ ping) only.
306fn 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            // A queued tick may have auto-started.
554            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}