//! Milestone 5: systemd socket activation, readiness notification, and //! metered behavior. mod support; use std::os::fd::AsRawFd; use std::os::unix::process::CommandExt; use std::path::Path; use std::process::Command; use std::time::{Duration, Instant}; use varde_proto::{PinPolicy, Request, ResponseData}; extern "C" { fn dup2(oldfd: i32, newfd: i32) -> i32; fn fcntl(fd: i32, cmd: i32, arg: i32) -> i32; } const F_SETFD: i32 = 2; /// Spawn the daemon with an activation socket on fd 3, the LISTEN_FDS /// protocol systemd uses. #[test] fn systemd_socket_activation() { let dir = tempfile::tempdir().unwrap(); let socket_path = dir.path().join("activated.sock"); let listener = std::os::unix::net::UnixListener::bind(&socket_path).unwrap(); let fd = listener.as_raw_fd(); std::fs::write(dir.path().join("no-config.toml"), "").unwrap(); let mut command = Command::new(env!("CARGO_BIN_EXE_varde-daemon")); command .arg("--user") .arg("--store") .arg(dir.path().join("store")) // Deliberately point --socket elsewhere: the activation fd must // win over self-binding. .arg("--socket") .arg(dir.path().join("ignored.sock")) .env("VARDE_CONFIG", dir.path().join("no-config.toml")) .env("VARDE_DISCOVERY", "false") .env("LISTEN_FDS", "1") .env_remove("LISTEN_PID"); // SAFETY: dup2/fcntl are async-signal-safe; we place the listener on // fd 3 in the child. dup2 clears CLOEXEC on the duplicate, but when // the listener already *is* fd 3, dup2(3,3) is a no-op that leaves // CLOEXEC set and exec would close the socket — clear it explicitly. unsafe { command.pre_exec(move || { let rc = if fd == 3 { fcntl(3, F_SETFD, 0) } else { dup2(fd, 3) }; if rc < 0 { return Err(std::io::Error::last_os_error()); } Ok(()) }); } let mut child = command.spawn().expect("spawning daemon"); // Drop our copy of the listener: if the daemon dies, connects must // fail (and this test fail cleanly) rather than queue in the backlog // of a socket nobody accepts on. drop(listener); // The daemon must answer on the activation socket. let deadline = Instant::now() + Duration::from_secs(10); let mut answered = false; while Instant::now() < deadline { if std::os::unix::net::UnixStream::connect(&socket_path).is_ok() { let mut client = support::Client::connect(&socket_path); client.set_read_timeout(Duration::from_secs(5)); let reply = client.request(&Request::Status { hash: None }); if reply.ok { answered = true; break; } } std::thread::sleep(Duration::from_millis(50)); } let _ = child.kill(); let _ = child.wait(); assert!(answered, "daemon did not serve on the activation socket"); assert!( !dir.path().join("ignored.sock").exists(), "daemon bound its own socket despite activation" ); } /// With NOTIFY_SOCKET set, the daemon reports READY=1 once serving. #[test] fn sd_notify_ready_is_sent() { let dir = tempfile::tempdir().unwrap(); let notify_path = dir.path().join("notify.sock"); let notify = std::os::unix::net::UnixDatagram::bind(¬ify_path).unwrap(); notify .set_read_timeout(Some(Duration::from_secs(10))) .unwrap(); std::fs::write(dir.path().join("no-config.toml"), "").unwrap(); let mut child = Command::new(env!("CARGO_BIN_EXE_varde-daemon")) .arg("--user") .arg("--store") .arg(dir.path().join("store")) .arg("--socket") .arg(dir.path().join("varde.sock")) .env("VARDE_CONFIG", dir.path().join("no-config.toml")) .env("VARDE_DISCOVERY", "false") .env("NOTIFY_SOCKET", ¬ify_path) .spawn() .expect("spawning daemon"); let mut buf = [0u8; 64]; let result = notify.recv(&mut buf); let _ = child.kill(); let _ = child.wait(); let n = result.expect("no readiness notification received"); let message = std::str::from_utf8(&buf[..n]).unwrap(); assert!( message.contains("READY=1"), "unexpected notify payload: {message}" ); } fn wait_complete(socket: &Path, hash: &str, timeout: Duration) -> bool { let deadline = Instant::now() + timeout; while Instant::now() < deadline { let mut client = support::Client::connect(socket); let reply = client.request(&Request::Status { hash: Some(hash.to_string()), }); if let Some(ResponseData::HashStatus(s)) = reply.data { if s.complete { return true; } } std::thread::sleep(Duration::from_millis(100)); } false } /// A metered daemon serves nothing, even openly exported content. #[test] fn metered_daemon_stops_serving() { let provider_dir = tempfile::tempdir().unwrap(); let provider = support::spawn_daemon_with_env(provider_dir.path(), &[("VARDE_FORCE_METERED", "true")]); let fetcher = support::spawn_daemon(); let mut provider_client = support::Client::connect(&provider.socket); let mut fetcher_client = support::Client::connect(&fetcher.socket); let dir = tempfile::tempdir().unwrap(); let src = dir.path().join("f.bin"); std::fs::write(&src, vec![4u8; 200_000]).unwrap(); let reply = provider_client.request(&Request::Add { path: src.display().to_string(), recursive: false, }); let hash = match reply.data { Some(ResponseData::Added { hash, .. }) => hash, other => panic!("unexpected add reply: {other:?}"), }; let reply = provider_client.request(&Request::Pin { hash: hash.clone(), policy: PinPolicy::default(), }); assert!(reply.ok); let reply = provider_client.request(&Request::TicketExport { hash: hash.clone() }); let ticket = match reply.data { Some(ResponseData::Ticket { ticket }) => ticket, other => panic!("unexpected ticket reply: {other:?}"), }; let reply = fetcher_client.request(&Request::TicketImport { ticket, pin_policy: PinPolicy::default(), }); assert!(reply.ok, "import should be accepted (pin recorded)"); assert!( !wait_complete(&fetcher.socket, &hash, Duration::from_secs(8)), "metered provider must not serve" ); } /// A metered daemon defers its own fetches too. #[test] fn metered_daemon_defers_fetching() { let provider = support::spawn_daemon(); let fetcher_dir = tempfile::tempdir().unwrap(); let fetcher = support::spawn_daemon_with_env(fetcher_dir.path(), &[("VARDE_FORCE_METERED", "true")]); let mut provider_client = support::Client::connect(&provider.socket); let mut fetcher_client = support::Client::connect(&fetcher.socket); let dir = tempfile::tempdir().unwrap(); let src = dir.path().join("f.bin"); std::fs::write(&src, vec![6u8; 200_000]).unwrap(); let reply = provider_client.request(&Request::Add { path: src.display().to_string(), recursive: false, }); let hash = match reply.data { Some(ResponseData::Added { hash, .. }) => hash, other => panic!("unexpected add reply: {other:?}"), }; assert!( provider_client .request(&Request::Pin { hash: hash.clone(), policy: PinPolicy::default(), }) .ok ); let reply = provider_client.request(&Request::TicketExport { hash: hash.clone() }); let ticket = match reply.data { Some(ResponseData::Ticket { ticket }) => ticket, other => panic!("unexpected ticket reply: {other:?}"), }; let reply = fetcher_client.request(&Request::TicketImport { ticket, pin_policy: PinPolicy::default(), }); assert!(reply.ok, "import records the pin even while metered"); assert!( !wait_complete(&fetcher.socket, &hash, Duration::from_secs(8)), "metered fetcher must not download" ); }