1use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use anyhow::{Context, Result};
9use futures::{SinkExt, StreamExt};
10use serde_json::json;
11use tokio::net::{UnixListener, UnixStream};
12use tokio::task::{JoinError, JoinSet};
13use tokio_util::codec::{Framed, LinesCodec, LinesCodecError};
14use tokio_util::sync::CancellationToken;
15
16use super::accept::{AcceptBackoff, ConnectionSource};
17use super::lifecycle;
18use super::paths;
19use super::protocol::{DaemonEnvelope, DaemonReply, StatusReport, DAEMON_SERVICE, MAX_LINE_BYTES};
20use super::registry::ServiceRegistry;
21use super::service::ServiceStream;
22use super::single_instance;
23
24const DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
29
30const IDLE_TIMEOUT: Duration = Duration::from_secs(60);
39
40const ENV_STREAM_TICK: &str = "OMNI_DEV_DAEMON_STREAM_TICK";
43
44const DEFAULT_STREAM_TICK: Duration = Duration::from_secs(10);
55
56pub(crate) fn stream_tick() -> Duration {
64 duration_secs_from_env(ENV_STREAM_TICK, DEFAULT_STREAM_TICK)
65}
66
67pub(crate) fn duration_secs_from_env(var: &str, default: Duration) -> Duration {
74 duration_secs_from_raw(std::env::var(var).ok(), default)
75}
76
77fn duration_secs_from_raw(raw: Option<String>, default: Duration) -> Duration {
80 raw.and_then(|s| s.trim().parse::<u64>().ok())
81 .filter(|&secs| secs > 0)
82 .map_or(default, Duration::from_secs)
83}
84
85#[derive(Debug, Clone)]
87pub struct DaemonOptions {
88 pub socket_path: PathBuf,
90}
91
92pub async fn run(registry: ServiceRegistry, opts: DaemonOptions) -> Result<()> {
98 run_with_shutdown(Arc::new(registry), opts, CancellationToken::new()).await
99}
100
101pub async fn run_with_shutdown(
107 registry: Arc<ServiceRegistry>,
108 opts: DaemonOptions,
109 shutdown: CancellationToken,
110) -> Result<()> {
111 lifecycle::raise_nofile_limit();
114 if let Some(parent) = opts.socket_path.parent() {
115 paths::ensure_dir_0700(parent)?;
116 }
117 tighten_daemon_log(&opts.socket_path);
124 paths::check_socket_path_len(&opts.socket_path)?;
125
126 let (listener, socket_activated) = acquire_listener(&opts.socket_path).await?;
127 tracing::info!("daemon listening on {}", opts.socket_path.display());
128
129 lifecycle::install_signal_handlers(shutdown.clone());
130
131 let mut conns: JoinSet<()> = JoinSet::new();
134 accept_loop(&listener, ®istry, &shutdown, &mut conns).await;
135
136 drop(listener);
152 if !socket_activated {
153 remove_socket(&opts.socket_path);
154 }
155
156 drain_connections(&mut conns, DRAIN_TIMEOUT).await;
158
159 tracing::info!("daemon shutting down; draining services");
160 registry.shutdown_all().await;
161 Ok(())
162}
163
164async fn accept_loop<S: ConnectionSource>(
174 source: &S,
175 registry: &Arc<ServiceRegistry>,
176 shutdown: &CancellationToken,
177 conns: &mut JoinSet<()>,
178) {
179 let mut backoff = AcceptBackoff::new();
180 loop {
181 tokio::select! {
182 () = shutdown.cancelled() => break,
183 accepted = source.accept() => match accepted {
184 Ok(stream) => {
185 if let Some(recovery) = backoff.on_success(Instant::now()) {
186 registry.credit_accept_outage(recovery.outage);
187 if recovery.announce {
188 tracing::info!(
189 failures = recovery.failures,
190 outage = ?recovery.duration,
191 "daemon accept recovered"
192 );
193 }
194 }
195 conns.spawn(handle_connection(
196 stream,
197 registry.clone(),
198 shutdown.clone(),
199 IDLE_TIMEOUT,
200 ));
201 }
202 Err(e) => {
203 let step = backoff.on_failure(Instant::now());
204 registry.credit_accept_outage(step.outage);
208 if step.log {
209 tracing::warn!(
210 "daemon accept error: {e} (backing off; further errors are not logged for 30s)"
211 );
212 }
213 tokio::select! {
217 () = shutdown.cancelled() => break,
218 () = tokio::time::sleep(step.delay) => {}
219 }
220 }
221 },
222 joined = conns.join_next(), if !conns.is_empty() => {
227 if let Some(result) = joined {
228 note_reaped(result);
229 }
230 }
231 }
232 }
233}
234
235async fn acquire_listener(socket_path: &Path) -> Result<(UnixListener, bool)> {
251 #[cfg(target_os = "macos")]
252 if let Some(listener) = super::launchd::launchd_listener("Listener")? {
253 tracing::info!("daemon adopting launchd-activated control socket");
254 return Ok((listener, true));
255 }
256 #[cfg(target_os = "linux")]
257 if let Some(listener) = super::systemd::systemd_listener()? {
258 tracing::info!("daemon adopting systemd-activated control socket");
259 return Ok((listener, true));
260 }
261 let listener = single_instance::bind_or_reclaim(socket_path).await?;
262 Ok((listener, false))
263}
264
265fn tighten_daemon_log(socket_path: &Path) {
275 let log_path = paths::log_path_for_socket(socket_path);
276 if !log_path.exists() {
277 return;
278 }
279 if let Err(e) = paths::set_file_0600(&log_path) {
280 tracing::warn!("failed to tighten {} to 0600: {e}", log_path.display());
281 }
282}
283
284fn remove_socket(path: &Path) {
287 if let Err(e) = std::fs::remove_file(path) {
288 if e.kind() != std::io::ErrorKind::NotFound {
289 tracing::warn!("failed to remove socket {}: {e}", path.display());
290 }
291 }
292}
293
294fn note_reaped(result: Result<(), JoinError>) {
298 if let Err(e) = result {
299 if e.is_panic() {
300 tracing::warn!("daemon connection task panicked: {e}");
301 }
302 }
303}
304
305async fn drain_connections(conns: &mut JoinSet<()>, timeout: Duration) {
313 let count = conns.len();
314 if count == 0 {
315 return;
316 }
317 tracing::info!("draining {count} in-flight connection(s)");
318 let drain = async {
319 while let Some(result) = conns.join_next().await {
320 note_reaped(result);
321 }
322 };
323 if tokio::time::timeout(timeout, drain).await.is_err() {
324 tracing::warn!(
325 "timed out draining connections after {timeout:?}; aborting {} straggler(s)",
326 conns.len()
327 );
328 conns.abort_all();
329 while conns.join_next().await.is_some() {}
330 }
331}
332
333async fn handle_connection(
348 stream: UnixStream,
349 registry: Arc<ServiceRegistry>,
350 shutdown: CancellationToken,
351 idle_timeout: Duration,
352) {
353 let mut framed = Framed::new(stream, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
354 loop {
355 let line = match tokio::time::timeout(idle_timeout, framed.next()).await {
356 Ok(Some(line)) => line,
357 Ok(None) => break,
358 Err(_elapsed) => {
359 tracing::debug!(?idle_timeout, "closing an idle control-socket connection");
360 break;
361 }
362 };
363 let line = match line {
364 Ok(line) => line,
365 Err(e) => {
366 let msg = match e {
373 LinesCodecError::MaxLineLengthExceeded => {
374 format!("request line exceeds the {MAX_LINE_BYTES}-byte limit")
375 }
376 LinesCodecError::Io(io) => format!("read error: {io}"),
377 };
378 let _ = send_reply(&mut framed, DaemonReply::err(msg)).await;
379 break;
380 }
381 };
382
383 let envelope: DaemonEnvelope = match serde_json::from_str(&line) {
387 Ok(envelope) => envelope,
388 Err(e) => {
389 if !send_reply(
390 &mut framed,
391 DaemonReply::err(format!("invalid envelope: {e}")),
392 )
393 .await
394 {
395 break;
396 }
397 continue;
398 }
399 };
400
401 if let Some(name) = envelope.service.as_deref() {
405 if name != DAEMON_SERVICE {
406 if let Some(stream) = registry.subscribe(name, &envelope.op, &envelope.payload) {
407 run_stream(&mut framed, stream, &shutdown, name, &envelope.op).await;
408 return;
409 }
410 }
411 }
412
413 let reply = dispatch_envelope(envelope, ®istry, &shutdown).await;
414 if !send_reply(&mut framed, reply).await {
415 break;
416 }
417 }
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422enum StreamEnd {
423 ClientCancel,
424 ClientEof,
425 ReadDecodeError,
426 DaemonShutdown,
427 WriteFailure,
428}
429
430fn inbound_end(line: Option<Result<String, LinesCodecError>>) -> StreamEnd {
431 match line {
432 Some(Ok(_)) => StreamEnd::ClientCancel,
433 None => StreamEnd::ClientEof,
434 Some(Err(_)) => StreamEnd::ReadDecodeError,
435 }
436}
437
438async fn run_stream(
449 framed: &mut Framed<UnixStream, LinesCodec>,
450 mut stream: Box<dyn ServiceStream>,
451 shutdown: &CancellationToken,
452 service: &str,
453 op: &str,
454) -> StreamEnd {
455 tracing::debug!(service, op, "subscription_started");
456 let mut pushed = 0_u64;
457 let mut suppressed = 0_u64;
458 let mut last = stream.snapshot().await;
459 let end = if !send_reply(framed, DaemonReply::ok(last.clone())).await {
460 StreamEnd::WriteFailure
461 } else {
462 pushed += 1;
463 tracing::trace!(
464 service,
465 op,
466 trigger = "initial",
467 outcome = "pushed",
468 "subscription_sample"
469 );
470 let mut tick = tokio::time::interval(stream_tick());
471 tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
472 tick.tick().await;
473 loop {
474 let trigger = tokio::select! {
475 () = stream.changed() => "change_notification",
476 _ = tick.tick() => "periodic_tick",
477 line = framed.next() => {
478 if let Some(Err(error)) = &line {
481 tracing::debug!(service, op, %error, "subscription_read_failed");
482 }
483 break inbound_end(line);
484 },
485 () = shutdown.cancelled() => break StreamEnd::DaemonShutdown,
486 };
487 let snap = stream.snapshot().await;
488 if snap != last {
489 if !send_reply(framed, DaemonReply::ok(snap.clone())).await {
490 break StreamEnd::WriteFailure;
491 }
492 pushed = pushed.saturating_add(1);
493 last = snap;
494 tracing::trace!(
495 service,
496 op,
497 trigger,
498 outcome = "pushed",
499 "subscription_sample"
500 );
501 } else {
502 suppressed = suppressed.saturating_add(1);
503 tracing::trace!(
504 service,
505 op,
506 trigger,
507 outcome = "suppressed_identical",
508 "subscription_sample"
509 );
510 }
511 }
512 };
513 tracing::debug!(service, op, outcome = ?end, pushed, suppressed, "subscription_ended");
514 end
515}
516
517async fn send_reply(framed: &mut Framed<UnixStream, LinesCodec>, reply: DaemonReply) -> bool {
520 let encoded = match serde_json::to_string(&reply) {
521 Ok(encoded) => encoded,
522 Err(e) => {
523 tracing::warn!("failed to encode daemon reply: {e}");
524 return false;
525 }
526 };
527 if let Err(e) = framed.send(encoded).await {
528 tracing::warn!(error = %e, "daemon_client_write_failed");
529 return false;
530 }
531 true
532}
533
534async fn dispatch_envelope(
538 envelope: DaemonEnvelope,
539 registry: &ServiceRegistry,
540 shutdown: &CancellationToken,
541) -> DaemonReply {
542 match envelope.service.as_deref() {
543 None | Some(DAEMON_SERVICE) => {
544 let reply = handle_builtin(&envelope.op, registry, shutdown).await;
545 if !reply.ok {
546 tracing::warn!(service = DAEMON_SERVICE, op = %envelope.op, error = ?reply.error, "daemon_dispatch_failed");
547 }
548 reply
549 }
550 Some(name) => {
551 let dispatch = registry.dispatch(name, &envelope.op, envelope.payload);
555 let result = match envelope.origin_invocation_id {
556 Some(origin) => crate::request_log::scope_origin_id(origin, dispatch).await,
557 None => dispatch.await,
558 };
559 match result {
560 Ok(payload) => DaemonReply::ok(payload),
561 Err(e) => {
566 tracing::warn!(service = name, op = %envelope.op, error = %format!("{e:#}"), "daemon_dispatch_failed");
567 DaemonReply::err(format!("{e:#}"))
568 }
569 }
570 }
571 }
572}
573
574async fn handle_builtin(
576 op: &str,
577 registry: &ServiceRegistry,
578 shutdown: &CancellationToken,
579) -> DaemonReply {
580 match op {
581 "ping" => {
586 let mut payload = serde_json::Map::new();
587 payload.insert("pong".to_string(), json!(true));
588 payload.insert("version".to_string(), json!(crate::VERSION));
589 if let Ok(serde_json::Value::Object(prov)) =
590 serde_json::to_value(crate::build_info::provenance())
591 {
592 payload.extend(prov);
593 }
594 DaemonReply::ok(serde_json::Value::Object(payload))
595 }
596 "status" => {
597 let mut report = StatusReport::current(registry.statuses().await);
600 report.github_rate_limit = registry.github_rate_limit();
601 match serde_json::to_value(report) {
602 Ok(payload) => DaemonReply::ok(payload),
603 Err(e) => DaemonReply::err(format!("failed to encode status: {e}")),
604 }
605 }
606 "shutdown" => {
607 shutdown.cancel();
608 DaemonReply::ok(json!({ "stopping": true }))
609 }
610 other => DaemonReply::err(format!("unknown daemon op: {other}")),
611 }
612}
613
614pub fn resolve_socket(socket: Option<PathBuf>) -> Result<PathBuf> {
617 match socket {
618 Some(path) => Ok(path),
619 None => paths::socket_path().context("failed to resolve the default daemon socket path"),
620 }
621}
622
623#[cfg(test)]
629#[allow(clippy::unwrap_used, clippy::expect_used)]
630mod tests {
631
632 #[tokio::test]
633 async fn subscriptions_report_all_terminal_causes() {
634 use tokio::io::AsyncWriteExt;
635 for expected in [
636 StreamEnd::ClientCancel,
637 StreamEnd::ClientEof,
638 StreamEnd::ReadDecodeError,
639 StreamEnd::DaemonShutdown,
640 StreamEnd::WriteFailure,
641 ] {
642 let (mut client, server) = UnixStream::pair().unwrap();
643 let (_tx, rx) = watch::channel(0_u64);
644 let fake = FakeStream {
645 rx,
646 snap: Arc::new(StdMutex::new(json!({"n":0}))),
647 };
648 let token = CancellationToken::new();
649 let shutdown = token.clone();
650 let task = tokio::spawn(async move {
651 let mut framed = Framed::new(server, LinesCodec::new_with_max_length(64));
652 crate::test_support::capture_future_at(
653 tracing::Level::TRACE,
654 run_stream(
655 &mut framed,
656 Box::new(fake),
657 &shutdown,
658 "sessions",
659 "subscribe",
660 ),
661 )
662 .await
663 });
664 if expected == StreamEnd::WriteFailure {
665 drop(client);
666 } else {
667 let mut reader = BufReader::new(&mut client);
668 let _ = read_reply(&mut reader).await;
669 drop(reader);
670 match expected {
671 StreamEnd::ClientCancel => {
672 client.write_all(b"cancel\n").await.unwrap();
673 }
674 StreamEnd::ClientEof => {
675 client.shutdown().await.unwrap();
676 }
677 StreamEnd::ReadDecodeError => {
678 client.write_all(&[b'x'; 65]).await.unwrap();
679 }
680 StreamEnd::DaemonShutdown => token.cancel(),
681 StreamEnd::WriteFailure => unreachable!(),
682 }
683 }
684 let (cause, logs) = tokio::time::timeout(Duration::from_secs(2), task)
685 .await
686 .unwrap()
687 .unwrap();
688 assert_eq!(cause, expected);
689 assert!(logs.contains("subscription_started"));
690 assert!(logs.contains("subscription_ended"));
691 assert!(logs.contains(&format!("outcome={expected:?}")));
692 if expected != StreamEnd::WriteFailure {
693 assert!(logs.contains("pushed=1"));
694 }
695 }
696 }
697
698 #[test]
699 fn inbound_subscription_end_reasons_are_distinct() {
700 assert_eq!(
701 super::inbound_end(Some(Ok("cancel".into()))),
702 super::StreamEnd::ClientCancel
703 );
704 assert_eq!(super::inbound_end(None), super::StreamEnd::ClientEof);
705 assert_eq!(
706 super::inbound_end(Some(Err(LinesCodecError::MaxLineLengthExceeded))),
707 super::StreamEnd::ReadDecodeError
708 );
709 }
710
711 use super::*;
712
713 #[test]
714 fn tighten_daemon_log_sets_0600_and_tolerates_absence() {
715 use std::os::unix::fs::PermissionsExt;
716
717 let dir = tempfile::tempdir().unwrap();
718 let socket = dir.path().join("daemon.sock");
719
720 tighten_daemon_log(&socket);
722
723 let log = paths::log_path_for_socket(&socket);
725 std::fs::write(&log, b"daemon listening on ...\n").unwrap();
726 std::fs::set_permissions(&log, std::fs::Permissions::from_mode(0o644)).unwrap();
727 tighten_daemon_log(&socket);
728 assert_eq!(
729 std::fs::metadata(&log).unwrap().permissions().mode() & 0o777,
730 0o600
731 );
732 }
733
734 #[tokio::test]
735 async fn drain_connections_returns_immediately_when_empty() {
736 let mut conns: JoinSet<()> = JoinSet::new();
737 drain_connections(&mut conns, Duration::from_secs(5)).await;
738 assert!(conns.is_empty());
739 }
740
741 #[tokio::test]
742 async fn drain_connections_awaits_completed_tasks() {
743 let mut conns: JoinSet<()> = JoinSet::new();
744 conns.spawn(async {});
745 drain_connections(&mut conns, Duration::from_secs(5)).await;
746 assert!(conns.is_empty());
748 }
749
750 #[tokio::test]
751 async fn drain_connections_times_out_and_aborts_stragglers() {
752 let mut conns: JoinSet<()> = JoinSet::new();
753 conns.spawn(std::future::pending::<()>());
756 drain_connections(&mut conns, Duration::from_millis(50)).await;
757 assert!(
758 conns.is_empty(),
759 "straggler should have been aborted and joined"
760 );
761 }
762
763 #[tokio::test]
764 async fn note_reaped_ignores_success_and_logs_panic() {
765 note_reaped(Ok(()));
767 let mut js: JoinSet<()> = JoinSet::new();
770 js.spawn(async { panic!("boom") });
771 let result = js.join_next().await.unwrap();
772 assert!(result.is_err());
773 note_reaped(result);
774 }
775
776 #[test]
777 fn duration_secs_from_raw_parses_seconds_and_falls_back_on_junk() {
778 let default = Duration::from_secs(10);
779 assert_eq!(duration_secs_from_raw(None, default), default);
782 assert_eq!(
783 duration_secs_from_raw(Some(String::new()), default),
784 default
785 );
786 assert_eq!(
787 duration_secs_from_raw(Some("garbage".to_string()), default),
788 default
789 );
790 assert_eq!(
791 duration_secs_from_raw(Some(" 0 ".to_string()), default),
792 default
793 );
794 assert_eq!(
796 duration_secs_from_raw(Some("30".to_string()), default),
797 Duration::from_secs(30)
798 );
799 assert_eq!(
800 duration_secs_from_raw(Some(" 5\n".to_string()), default),
801 Duration::from_secs(5)
802 );
803 }
804
805 use std::sync::Mutex as StdMutex;
812 use tokio::io::{AsyncBufReadExt, BufReader};
813 use tokio::sync::watch;
814
815 struct FakeStream {
818 rx: watch::Receiver<u64>,
819 snap: Arc<StdMutex<serde_json::Value>>,
820 }
821
822 #[async_trait::async_trait]
823 impl ServiceStream for FakeStream {
824 async fn changed(&mut self) {
825 if self.rx.changed().await.is_err() {
827 std::future::pending::<()>().await;
828 }
829 }
830 async fn snapshot(&self) -> serde_json::Value {
831 self.snap.lock().unwrap().clone()
832 }
833 }
834
835 async fn read_reply<R: tokio::io::AsyncBufRead + Unpin>(reader: &mut R) -> DaemonReply {
839 let mut line = String::new();
840 let n = reader.read_line(&mut line).await.unwrap();
841 assert!(n > 0, "expected a reply line, got EOF");
842 serde_json::from_str(line.trim_end()).unwrap()
843 }
844
845 #[tokio::test]
846 async fn run_stream_pushes_initial_then_deltas_and_dedupes() {
847 let (client, server) = UnixStream::pair().unwrap();
848 let (tx, rx) = watch::channel(0u64);
849 let snap = Arc::new(StdMutex::new(json!({ "n": 0 })));
850 let fake = FakeStream {
851 rx,
852 snap: snap.clone(),
853 };
854 let shutdown = CancellationToken::new();
855 let server_shutdown = shutdown.clone();
856
857 let server_task = tokio::spawn(async move {
858 let mut framed = Framed::new(server, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
859 run_stream(
860 &mut framed,
861 Box::new(fake),
862 &server_shutdown,
863 "fake",
864 "subscribe",
865 )
866 .await;
867 });
868
869 let mut reader = BufReader::new(client);
870
871 let initial = read_reply(&mut reader).await;
873 assert!(initial.ok);
874 assert_eq!(initial.payload, json!({ "n": 0 }));
875
876 tx.send(1).unwrap(); *snap.lock().unwrap() = json!({ "n": 1 });
881 tx.send(2).unwrap(); let delta = read_reply(&mut reader).await;
883 assert_eq!(delta.payload, json!({ "n": 1 }));
884
885 shutdown.cancel();
887 let mut tail = String::new();
888 let n = reader.read_line(&mut tail).await.unwrap();
889 assert_eq!(n, 0, "stream should close cleanly on shutdown");
890 server_task.await.unwrap();
891 }
892
893 #[tokio::test]
894 async fn run_stream_ends_when_client_sends_a_line() {
895 use tokio::io::AsyncWriteExt;
896
897 let (mut client, server) = UnixStream::pair().unwrap();
898 let (_tx, rx) = watch::channel(0u64);
899 let snap = Arc::new(StdMutex::new(json!({ "n": 0 })));
900 let fake = FakeStream { rx, snap };
901 let shutdown = CancellationToken::new();
902 let server_shutdown = shutdown.clone();
903
904 let server_task = tokio::spawn(async move {
905 let mut framed = Framed::new(server, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
906 run_stream(
907 &mut framed,
908 Box::new(fake),
909 &server_shutdown,
910 "fake",
911 "subscribe",
912 )
913 .await;
914 });
915
916 let mut reader = BufReader::new(&mut client);
917 let _initial = read_reply(&mut reader).await;
918 drop(reader);
920
921 client.write_all(b"cancel\n").await.unwrap();
924 tokio::time::timeout(Duration::from_secs(2), server_task)
925 .await
926 .expect("run_stream should end after a client line")
927 .unwrap();
928 }
929
930 #[tokio::test]
934 async fn handle_connection_rejects_bad_envelope_then_serves_normal_op() {
935 use tokio::io::AsyncWriteExt;
936
937 let (client, server) = UnixStream::pair().unwrap();
938 let mut registry = ServiceRegistry::new();
939 registry.register(Arc::new(
940 crate::daemon::services::worktrees::WorktreesService::new(),
941 ));
942 let shutdown = CancellationToken::new();
943 let task = tokio::spawn(handle_connection(
944 server,
945 Arc::new(registry),
946 shutdown,
947 IDLE_TIMEOUT,
948 ));
949
950 let (read_half, mut write_half) = client.into_split();
951 let mut reader = BufReader::new(read_half);
952
953 write_half.write_all(b"not json\n").await.unwrap();
955 let bad = read_reply(&mut reader).await;
956 assert!(!bad.ok);
957 assert!(bad.error.unwrap().contains("invalid envelope"));
958
959 let env = serde_json::to_string(&DaemonEnvelope::service(
962 "worktrees",
963 "list",
964 serde_json::Value::Null,
965 ))
966 .unwrap();
967 write_half.write_all(env.as_bytes()).await.unwrap();
968 write_half.write_all(b"\n").await.unwrap();
969 let listed = read_reply(&mut reader).await;
970 assert!(listed.ok);
971 assert!(listed.payload.get("windows").is_some());
972
973 drop(write_half);
975 drop(reader);
976 tokio::time::timeout(Duration::from_secs(2), task)
977 .await
978 .expect("handler should end after the client hangs up")
979 .unwrap();
980 }
981
982 #[tokio::test]
986 async fn handle_connection_enters_streaming_for_subscribe() {
987 use tokio::io::AsyncWriteExt;
988
989 let (client, server) = UnixStream::pair().unwrap();
990 let mut registry = ServiceRegistry::new();
991 registry.register(Arc::new(
992 crate::daemon::services::worktrees::WorktreesService::new(),
993 ));
994 let shutdown = CancellationToken::new();
995 let task = tokio::spawn(handle_connection(
996 server,
997 Arc::new(registry),
998 shutdown.clone(),
999 IDLE_TIMEOUT,
1000 ));
1001
1002 let (read_half, mut write_half) = client.into_split();
1003 let mut reader = BufReader::new(read_half);
1004 let env = serde_json::to_string(&DaemonEnvelope::service(
1005 "worktrees",
1006 "subscribe",
1007 serde_json::Value::Null,
1008 ))
1009 .unwrap();
1010 write_half.write_all(env.as_bytes()).await.unwrap();
1011 write_half.write_all(b"\n").await.unwrap();
1012
1013 let initial = read_reply(&mut reader).await;
1016 assert!(initial.ok);
1017 assert_eq!(initial.payload, json!({ "repos": [], "show_closed": true }));
1018
1019 shutdown.cancel();
1021 tokio::time::timeout(Duration::from_secs(2), task)
1022 .await
1023 .expect("shutdown should end the streaming handler")
1024 .unwrap();
1025 }
1026
1027 #[tokio::test]
1032 async fn run_stream_ends_when_a_pushed_delta_cannot_be_written() {
1033 let (mut client, server) = UnixStream::pair().unwrap();
1034 let server = server.into_std().unwrap();
1035 let hangup = server.try_clone().unwrap();
1036 let server = UnixStream::from_std(server).unwrap();
1037 let (tx, rx) = watch::channel(0u64);
1038 let snap = Arc::new(StdMutex::new(json!({ "n": 0 })));
1039 let fake = FakeStream {
1040 rx,
1041 snap: Arc::clone(&snap),
1042 };
1043 let shutdown = CancellationToken::new();
1044 let task = tokio::spawn(async move {
1045 let mut framed = Framed::new(server, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
1046 run_stream(&mut framed, Box::new(fake), &shutdown, "fake", "subscribe").await
1047 });
1048
1049 let mut reader = BufReader::new(&mut client);
1050 read_reply(&mut reader).await;
1051 hangup.shutdown(std::net::Shutdown::Write).unwrap();
1052 *snap.lock().unwrap() = json!({ "n": 1 });
1053 tx.send(1).unwrap();
1054
1055 let end = tokio::time::timeout(Duration::from_secs(2), task)
1056 .await
1057 .expect("run_stream should end when a push cannot be written")
1058 .unwrap();
1059 assert_eq!(end, StreamEnd::WriteFailure);
1060 }
1061
1062 #[tokio::test]
1065 async fn run_stream_returns_when_initial_send_fails() {
1066 let (client, server) = UnixStream::pair().unwrap();
1067 drop(client);
1069 let (_tx, rx) = watch::channel(0u64);
1070 let fake = FakeStream {
1071 rx,
1072 snap: Arc::new(StdMutex::new(json!({ "n": 0 }))),
1073 };
1074 let shutdown = CancellationToken::new();
1075 let mut framed = Framed::new(server, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
1076 tokio::time::timeout(
1077 Duration::from_secs(2),
1078 run_stream(&mut framed, Box::new(fake), &shutdown, "fake", "subscribe"),
1079 )
1080 .await
1081 .expect("run_stream should return promptly when the initial send fails");
1082 }
1083 fn ping_line() -> &'static [u8] {
1086 b"{\"op\":\"ping\"}\n"
1087 }
1088
1089 #[tokio::test]
1092 async fn handle_connection_closes_a_connection_that_never_sends() {
1093 use tokio::io::AsyncReadExt;
1094
1095 let (mut client, server) = UnixStream::pair().unwrap();
1096 let task = tokio::spawn(handle_connection(
1097 server,
1098 worktrees_registry(),
1099 CancellationToken::new(),
1100 Duration::from_millis(50),
1101 ));
1102
1103 tokio::time::timeout(Duration::from_secs(2), task)
1104 .await
1105 .expect("an idle connection should be closed")
1106 .unwrap();
1107 let mut buf = Vec::new();
1109 assert_eq!(client.read_to_end(&mut buf).await.unwrap(), 0);
1110 }
1111
1112 #[tokio::test]
1115 async fn handle_connection_stays_open_while_requests_keep_arriving() {
1116 use tokio::io::AsyncWriteExt;
1117
1118 let (client, server) = UnixStream::pair().unwrap();
1119 let task = tokio::spawn(handle_connection(
1120 server,
1121 worktrees_registry(),
1122 CancellationToken::new(),
1123 Duration::from_millis(600),
1124 ));
1125 let (read_half, mut write_half) = client.into_split();
1126 let mut reader = BufReader::new(read_half);
1127
1128 for _ in 0..3 {
1129 write_half.write_all(ping_line()).await.unwrap();
1130 assert!(read_reply(&mut reader).await.ok);
1131 tokio::time::sleep(Duration::from_millis(250)).await;
1133 }
1134 assert!(
1135 !task.is_finished(),
1136 "the connection was closed while active"
1137 );
1138 }
1139
1140 #[tokio::test]
1142 async fn handle_connection_does_not_time_out_a_subscription() {
1143 use tokio::io::AsyncWriteExt;
1144
1145 let (client, server) = UnixStream::pair().unwrap();
1146 let shutdown = CancellationToken::new();
1147 let task = tokio::spawn(handle_connection(
1148 server,
1149 worktrees_registry(),
1150 shutdown.clone(),
1151 Duration::from_millis(50),
1152 ));
1153 let (read_half, mut write_half) = client.into_split();
1154 let mut reader = BufReader::new(read_half);
1155 let env = serde_json::to_string(&DaemonEnvelope::service(
1156 "worktrees",
1157 "subscribe",
1158 serde_json::Value::Null,
1159 ))
1160 .unwrap();
1161 write_half.write_all(env.as_bytes()).await.unwrap();
1162 write_half.write_all(b"\n").await.unwrap();
1163 assert!(read_reply(&mut reader).await.ok);
1164
1165 tokio::time::sleep(Duration::from_millis(250)).await;
1167 assert!(!task.is_finished(), "the idle timeout cut a subscription");
1168
1169 shutdown.cancel();
1170 tokio::time::timeout(Duration::from_secs(2), task)
1171 .await
1172 .expect("shutdown still ends the stream")
1173 .unwrap();
1174 }
1175
1176 #[tokio::test]
1179 async fn handle_connection_never_interrupts_a_request_in_progress() {
1180 use crate::daemon::testutil::StubService;
1181 use tokio::io::AsyncWriteExt;
1182
1183 let (client, server) = UnixStream::pair().unwrap();
1184 let mut registry = ServiceRegistry::new();
1185 registry.register(Arc::new(StubService::slow(
1187 "slow",
1188 Duration::from_millis(250),
1189 )));
1190 let task = tokio::spawn(handle_connection(
1191 server,
1192 Arc::new(registry),
1193 CancellationToken::new(),
1194 Duration::from_millis(50),
1195 ));
1196 let (read_half, mut write_half) = client.into_split();
1197 let mut reader = BufReader::new(read_half);
1198 let env = serde_json::to_string(&DaemonEnvelope::service(
1199 "slow",
1200 "work",
1201 serde_json::Value::Null,
1202 ))
1203 .unwrap();
1204 write_half.write_all(env.as_bytes()).await.unwrap();
1205 write_half.write_all(b"\n").await.unwrap();
1206
1207 let reply = read_reply(&mut reader).await;
1209 assert!(reply.ok);
1210 assert_eq!(reply.payload, json!({ "done": true }));
1211 drop(task);
1212 }
1213
1214 fn emfile() -> std::io::Error {
1218 std::io::Error::from_raw_os_error(24)
1219 }
1220
1221 struct ScriptedSource {
1224 script: StdMutex<std::collections::VecDeque<std::io::Result<UnixStream>>>,
1225 calls: std::sync::atomic::AtomicUsize,
1226 }
1227
1228 impl ScriptedSource {
1229 fn new(script: Vec<std::io::Result<UnixStream>>) -> Self {
1230 Self {
1231 script: StdMutex::new(script.into()),
1232 calls: std::sync::atomic::AtomicUsize::new(0),
1233 }
1234 }
1235
1236 fn calls(&self) -> usize {
1237 self.calls.load(std::sync::atomic::Ordering::SeqCst)
1238 }
1239 }
1240
1241 #[async_trait::async_trait]
1242 impl ConnectionSource for ScriptedSource {
1243 async fn accept(&self) -> std::io::Result<UnixStream> {
1244 self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1245 let next = self.script.lock().unwrap().pop_front();
1246 match next {
1247 Some(result) => result,
1248 None => std::future::pending().await,
1249 }
1250 }
1251 }
1252
1253 fn worktrees_registry() -> Arc<ServiceRegistry> {
1254 let mut registry = ServiceRegistry::new();
1255 registry.register(Arc::new(
1256 crate::daemon::services::worktrees::WorktreesService::new(),
1257 ));
1258 Arc::new(registry)
1259 }
1260
1261 #[tokio::test]
1265 async fn accept_loop_backs_off_on_errors_then_serves_the_next_connection() {
1266 use tokio::io::AsyncWriteExt;
1267
1268 let (client, server) = UnixStream::pair().unwrap();
1269 let source = ScriptedSource::new(vec![
1270 Err(emfile()),
1271 Err(emfile()),
1272 Err(emfile()),
1273 Ok(server),
1274 ]);
1275 let registry = worktrees_registry();
1276 let shutdown = CancellationToken::new();
1277 let mut conns: JoinSet<()> = JoinSet::new();
1278
1279 let started = Instant::now();
1280 let serving = async {
1281 accept_loop(&source, ®istry, &shutdown, &mut conns).await;
1282 };
1283 let talking = async {
1284 let (read_half, mut write_half) = client.into_split();
1285 let mut reader = BufReader::new(read_half);
1286 write_half.write_all(b"{\"op\":\"ping\"}\n").await.unwrap();
1287 let reply = read_reply(&mut reader).await;
1288 shutdown.cancel();
1289 reply
1290 };
1291 let ((), reply) = tokio::time::timeout(Duration::from_secs(5), async {
1292 tokio::join!(serving, talking)
1293 })
1294 .await
1295 .expect("the loop should recover and serve");
1296
1297 assert!(reply.ok, "{reply:?}");
1298 let elapsed = started.elapsed();
1301 assert!(
1302 elapsed >= Duration::from_millis(35),
1303 "retried too fast: {elapsed:?}"
1304 );
1305 assert!(source.calls() <= 5, "spun: {} accept calls", source.calls());
1308 }
1309
1310 #[tokio::test]
1314 async fn accept_loop_credits_the_outage_to_the_services() {
1315 use crate::daemon::testutil::StubService;
1316 use tokio::io::AsyncWriteExt;
1317
1318 let (client, server) = UnixStream::pair().unwrap();
1319 let source = ScriptedSource::new(vec![
1320 Err(emfile()),
1321 Err(emfile()),
1322 Err(emfile()),
1323 Ok(server),
1324 ]);
1325 let recorder = Arc::new(StubService::new("recorder"));
1326 let mut registry = ServiceRegistry::new();
1327 registry.register(recorder.clone());
1328 let registry = Arc::new(registry);
1329 let shutdown = CancellationToken::new();
1330 let mut conns: JoinSet<()> = JoinSet::new();
1331
1332 let talking = async {
1333 let (read_half, mut write_half) = client.into_split();
1335 let mut reader = BufReader::new(read_half);
1336 write_half.write_all(b"{\"op\":\"ping\"}\n").await.unwrap();
1337 read_reply(&mut reader).await;
1338 shutdown.cancel();
1339 };
1340 tokio::time::timeout(Duration::from_secs(5), async {
1341 tokio::join!(
1342 accept_loop(&source, ®istry, &shutdown, &mut conns),
1343 talking
1344 )
1345 })
1346 .await
1347 .expect("the loop should recover and serve");
1348
1349 let credited = recorder.credited();
1350 assert_eq!(credited.len(), 4, "{credited:?}");
1353 assert_eq!(credited[0], Duration::ZERO);
1354 let total: Duration = credited.iter().sum();
1357 assert!(total >= Duration::from_millis(35), "credited {total:?}");
1358 assert!(total < Duration::from_secs(30), "credited {total:?}");
1361 }
1362
1363 #[tokio::test]
1367 async fn accept_loop_recovers_from_an_unlogged_burst_and_still_credits_it() {
1368 use crate::daemon::testutil::StubService;
1369 use tokio::io::AsyncWriteExt;
1370
1371 let (client_a, server_a) = UnixStream::pair().unwrap();
1372 let (client_b, server_b) = UnixStream::pair().unwrap();
1373 let source = ScriptedSource::new(vec![
1376 Err(emfile()),
1377 Ok(server_a),
1378 Err(emfile()),
1379 Ok(server_b),
1380 ]);
1381 let recorder = Arc::new(StubService::new("recorder"));
1382 let mut registry = ServiceRegistry::new();
1383 registry.register(recorder.clone());
1384 let registry = Arc::new(registry);
1385 let shutdown = CancellationToken::new();
1386 let mut conns: JoinSet<()> = JoinSet::new();
1387
1388 let talking = async {
1389 for client in [client_a, client_b] {
1390 let (read_half, mut write_half) = client.into_split();
1391 let mut reader = BufReader::new(read_half);
1392 write_half.write_all(b"{\"op\":\"ping\"}\n").await.unwrap();
1393 assert!(read_reply(&mut reader).await.ok);
1394 }
1395 shutdown.cancel();
1396 };
1397 tokio::time::timeout(Duration::from_secs(5), async {
1398 tokio::join!(
1399 accept_loop(&source, ®istry, &shutdown, &mut conns),
1400 talking
1401 )
1402 })
1403 .await
1404 .expect("the loop should serve both connections");
1405
1406 assert_eq!(recorder.credited().len(), 4, "{:?}", recorder.credited());
1408 }
1409
1410 #[tokio::test]
1413 async fn accept_loop_stops_promptly_when_shutdown_arrives_mid_backoff() {
1414 let errors = (0..32).map(|_| Err(emfile())).collect();
1415 let source = ScriptedSource::new(errors);
1416 let registry = worktrees_registry();
1417 let shutdown = CancellationToken::new();
1418 let mut conns: JoinSet<()> = JoinSet::new();
1419
1420 let stopper = shutdown.clone();
1421 let stop_later = async move {
1422 tokio::time::sleep(Duration::from_millis(1400)).await;
1426 stopper.cancel();
1427 };
1428 let started = Instant::now();
1429 tokio::time::timeout(Duration::from_secs(5), async {
1430 tokio::join!(
1431 accept_loop(&source, ®istry, &shutdown, &mut conns),
1432 stop_later
1433 )
1434 })
1435 .await
1436 .expect("shutdown must interrupt the backoff sleep");
1437 assert!(
1440 started.elapsed() < Duration::from_millis(1900),
1441 "shutdown waited out a backoff sleep: {:?}",
1442 started.elapsed()
1443 );
1444 }
1445}