Skip to main content

synd_runtime/daemon/
service.rs

1#[cfg(unix)]
2use std::{
3    io::ErrorKind,
4    os::unix::{fs::FileTypeExt, net::UnixStream},
5    path::Path,
6};
7use std::{
8    path::PathBuf,
9    time::{Duration, Instant},
10};
11
12#[cfg(test)]
13use synd_api::session::DaemonSessionLeasePolicy;
14use synd_api::{
15    serve::{self, auth::Authenticator},
16    session::DaemonSessionConfig,
17    shutdown::Shutdown,
18};
19
20#[cfg(unix)]
21use tokio::net::UnixListener;
22use tracing::{debug, info, warn};
23
24#[cfg(unix)]
25use crate::daemon::DaemonClaimOwner;
26use crate::{
27    Error, Result, RuntimeDatabase,
28    api::ApiService,
29    placement::{PlacementEnvironment, PlacementResolver, PlacementSpec},
30};
31
32#[cfg(unix)]
33use crate::uds::UdsEndpoint;
34
35#[derive(Debug, Clone)]
36pub struct Daemon {
37    config: DaemonConfig,
38}
39
40impl Daemon {
41    pub fn new(config: DaemonConfig) -> Self {
42        Self { config }
43    }
44
45    pub fn config(&self) -> &DaemonConfig {
46        &self.config
47    }
48
49    pub async fn serve(self) -> Result<()> {
50        let placement = PlacementResolver::with_environment(self.config.placement_environment())
51            .resolve_database(self.config.database())?;
52
53        self.serve_placement(placement).await
54    }
55
56    async fn serve_placement(self, placement: PlacementSpec) -> Result<()> {
57        #[cfg(unix)]
58        {
59            self.serve_unix(placement).await
60        }
61
62        #[cfg(not(unix))]
63        {
64            Err(crate::Error::UnsupportedTransport {
65                context: "daemon service endpoint",
66            })
67        }
68    }
69
70    #[cfg(unix)]
71    async fn serve_unix(self, placement: PlacementSpec) -> Result<()> {
72        let started_at = Instant::now();
73        let _claim = DaemonClaimOwner::create(&placement)?;
74        let bound_endpoint = DaemonEndpointBinder::new(placement.endpoint()).bind()?;
75        let (listener, endpoint_cleanup) = bound_endpoint.into_parts();
76        let shutdown_endpoint_cleanup = endpoint_cleanup.clone();
77        let shutdown = Shutdown::manual(move || {
78            if let Err(error) = shutdown_endpoint_cleanup.unlink_socket() {
79                warn!(
80                    error = %error,
81                    "Failed to cleanup daemon endpoint during shutdown"
82                );
83            }
84            debug!("Running daemon shutdown callback");
85        });
86        let shutdown_status = shutdown.clone();
87        let serve_options = self.config.serve_options();
88        let daemon_sessions = serve_options.daemon_sessions;
89        let api_service = ApiService::from_database(
90            self.config.database(),
91            Authenticator::trusted_local(),
92            serve_options,
93            &shutdown,
94        )
95        .await?;
96        let (dependency, _event_workers) = api_service.into_parts();
97        info!(
98            pid = std::process::id(),
99            runtime_root = %placement.root().path().display(),
100            runtime_instance_id = %placement.instance().id(),
101            database = %placement.instance().canonical_database_path().display(),
102            endpoint = %placement.endpoint().path().display(),
103            session_lease_ms = daemon_sessions.lease_policy().lease_duration().as_millis(),
104            idle_shutdown_grace_ms = daemon_sessions.idle_shutdown_grace().as_millis(),
105            "Daemon ready"
106        );
107
108        // Keep event workers alive for the entire serve future.
109        serve::serve_unix(listener, dependency, shutdown).await?;
110        info!(
111            reason = shutdown_status
112                .reason()
113                .map_or("unknown", synd_api::shutdown::ShutdownReason::as_str),
114            uptime_ms = started_at.elapsed().as_millis(),
115            "Daemon stopped"
116        );
117
118        Ok(())
119    }
120}
121
122#[derive(Debug, Clone)]
123pub struct DaemonConfig {
124    database: RuntimeDatabase,
125    session: DaemonSessionConfig,
126    placement_environment: PlacementEnvironment,
127    #[cfg(test)]
128    session_lease_policy: Option<DaemonSessionLeasePolicy>,
129}
130
131impl DaemonConfig {
132    pub fn new(database: RuntimeDatabase) -> Self {
133        Self {
134            database,
135            session: DaemonSessionConfig::default(),
136            placement_environment: PlacementEnvironment::capture(),
137            #[cfg(test)]
138            session_lease_policy: None,
139        }
140    }
141
142    pub fn database(&self) -> &RuntimeDatabase {
143        &self.database
144    }
145
146    pub(crate) fn placement_environment(&self) -> PlacementEnvironment {
147        self.placement_environment.clone()
148    }
149
150    #[must_use]
151    pub fn with_session_lease_duration(mut self, lease_duration: Duration) -> Self {
152        self.session = self.session.with_lease_duration(lease_duration);
153        self
154    }
155
156    #[must_use]
157    pub fn with_session_idle_shutdown_grace(mut self, idle_shutdown_grace: Duration) -> Self {
158        self.session = self.session.with_idle_shutdown_grace(idle_shutdown_grace);
159        self
160    }
161
162    #[must_use]
163    pub fn with_runtime_root(mut self, root: impl Into<PathBuf>) -> Self {
164        self.placement_environment = PlacementEnvironment::from_root(root);
165        self
166    }
167
168    fn serve_options(&self) -> serve::ServeOptions {
169        #[cfg(test)]
170        let session = match self.session_lease_policy {
171            Some(lease_policy) => {
172                DaemonSessionConfig::new(lease_policy, self.session.idle_shutdown_grace())
173            }
174            None => self.session,
175        };
176
177        #[cfg(not(test))]
178        let session = self.session;
179
180        serve::ServeOptions::default().with_daemon_sessions(session)
181    }
182
183    #[cfg(test)]
184    fn with_placement_environment(mut self, placement_environment: PlacementEnvironment) -> Self {
185        self.placement_environment = placement_environment;
186        self
187    }
188
189    #[cfg(test)]
190    fn with_session_lease_policy(mut self, lease_policy: DaemonSessionLeasePolicy) -> Self {
191        self.session_lease_policy = Some(lease_policy);
192        self
193    }
194}
195
196/// Binds the Unix domain socket endpoint for a daemon.
197#[cfg(unix)]
198struct DaemonEndpointBinder<'a> {
199    endpoint: &'a UdsEndpoint,
200}
201
202#[cfg(unix)]
203impl<'a> DaemonEndpointBinder<'a> {
204    fn new(endpoint: &'a UdsEndpoint) -> Self {
205        Self { endpoint }
206    }
207
208    fn bind(&self) -> Result<BoundDaemonEndpoint> {
209        if let Some(parent) = self.endpoint.path().parent() {
210            std::fs::create_dir_all(parent)?;
211        }
212
213        Ok(BoundDaemonEndpoint {
214            listener: UnixListener::bind(self.endpoint.path())?,
215            cleanup: DaemonEndpointCleanup::new(self.endpoint.path().to_path_buf()),
216        })
217    }
218}
219
220#[cfg(unix)]
221struct BoundDaemonEndpoint {
222    listener: UnixListener,
223    cleanup: DaemonEndpointCleanup,
224}
225
226#[cfg(unix)]
227impl BoundDaemonEndpoint {
228    fn into_parts(self) -> (UnixListener, DaemonEndpointCleanup) {
229        (self.listener, self.cleanup)
230    }
231}
232
233/// Best-effort cleanup for a daemon endpoint path owned by this runtime.
234#[cfg(unix)]
235#[derive(Clone)]
236struct DaemonEndpointCleanup {
237    path: PathBuf,
238}
239
240#[cfg(unix)]
241impl DaemonEndpointCleanup {
242    fn new(path: PathBuf) -> Self {
243        Self { path }
244    }
245
246    fn unlink_socket(&self) -> Result<()> {
247        match DaemonEndpointFileState::inspect_file(&self.path)? {
248            DaemonEndpointFileState::Missing => {}
249            DaemonEndpointFileState::ConnectedSocket | DaemonEndpointFileState::StaleSocket => {
250                std::fs::remove_file(&self.path)?;
251                debug!(
252                    daemon_endpoint = %self.path.display(),
253                    "Removed daemon endpoint"
254                );
255            }
256            DaemonEndpointFileState::NonSocket => {
257                return Err(Error::NonSocketEndpoint {
258                    path: self.path.clone(),
259                });
260            }
261        }
262
263        Ok(())
264    }
265
266    fn cleanup_stale_socket(&self) -> Result<()> {
267        match DaemonEndpointFileState::inspect(&self.path)? {
268            DaemonEndpointFileState::StaleSocket => {
269                std::fs::remove_file(&self.path)?;
270                debug!(
271                    daemon_endpoint = %self.path.display(),
272                    "Removed stale daemon endpoint"
273                );
274            }
275            DaemonEndpointFileState::Missing | DaemonEndpointFileState::ConnectedSocket => {}
276            DaemonEndpointFileState::NonSocket => {
277                return Err(Error::NonSocketEndpoint {
278                    path: self.path.clone(),
279                });
280            }
281        }
282
283        Ok(())
284    }
285}
286
287#[cfg(unix)]
288impl Drop for DaemonEndpointCleanup {
289    fn drop(&mut self) {
290        if let Err(error) = self.cleanup_stale_socket() {
291            warn!(
292                daemon_endpoint = %self.path.display(),
293                error = %error,
294                "Failed to cleanup daemon endpoint"
295            );
296        }
297    }
298}
299
300#[cfg(unix)]
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302enum DaemonEndpointFileState {
303    Missing,
304    ConnectedSocket,
305    StaleSocket,
306    NonSocket,
307}
308
309#[cfg(unix)]
310impl DaemonEndpointFileState {
311    fn inspect_file(path: &Path) -> Result<Self> {
312        let metadata = match std::fs::symlink_metadata(path) {
313            Ok(metadata) => metadata,
314            Err(error) if error.kind() == ErrorKind::NotFound => return Ok(Self::Missing),
315            Err(error) => return Err(error.into()),
316        };
317
318        if !metadata.file_type().is_socket() {
319            return Ok(Self::NonSocket);
320        }
321
322        Ok(Self::StaleSocket)
323    }
324
325    fn inspect(path: &Path) -> Result<Self> {
326        match Self::inspect_file(path)? {
327            Self::StaleSocket => {}
328            state => return Ok(state),
329        }
330
331        match UnixStream::connect(path) {
332            Ok(_) => Ok(Self::ConnectedSocket),
333            Err(error) if error.kind() == ErrorKind::NotFound => Ok(Self::Missing),
334            Err(error) if error.kind() == ErrorKind::ConnectionRefused => Ok(Self::StaleSocket),
335            Err(error) => Err(error.into()),
336        }
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use std::{
343        path::{Path, PathBuf},
344        time::{Duration, Instant},
345    };
346
347    use synd_api::session::DaemonSessionLeasePolicy;
348    use synd_protocol::session::SessionId;
349    use tokio::sync::mpsc;
350
351    use crate::{
352        DaemonExecutable, DaemonLaunchConfig, DaemonLaunchLog, DaemonState, Runtime, RuntimeConfig,
353        RuntimeDatabase, SessionConfig, SessionRequirements,
354        instance::RuntimeInstance,
355        placement::{PlacementEnvironment, PlacementResolver, PlacementRoot},
356        session::SessionRenewalObserver,
357        uds::UdsEndpoint,
358    };
359
360    use super::{Daemon, DaemonConfig, DaemonEndpointBinder};
361
362    #[cfg(unix)]
363    const DAEMON_READY_TIMEOUT: Duration = Duration::from_secs(30);
364    #[cfg(unix)]
365    const DAEMON_POLL_INTERVAL: Duration = Duration::from_millis(50);
366
367    #[cfg(unix)]
368    #[derive(Debug, Default)]
369    struct StartedDaemonConfig {
370        session_lease_policy: Option<DaemonSessionLeasePolicy>,
371        renewal_observer: Option<SessionRenewalObserver>,
372        session_requirements: Option<SessionRequirements>,
373    }
374
375    #[cfg(unix)]
376    impl StartedDaemonConfig {
377        fn with_session_lease_policy(mut self, lease_policy: DaemonSessionLeasePolicy) -> Self {
378            self.session_lease_policy = Some(lease_policy);
379            self
380        }
381
382        fn with_renewal_observer(mut self, observer: SessionRenewalObserver) -> Self {
383            self.renewal_observer = Some(observer);
384            self
385        }
386
387        fn with_session_requirements(mut self, requirements: SessionRequirements) -> Self {
388            self.session_requirements = Some(requirements);
389            self
390        }
391    }
392
393    #[cfg(unix)]
394    struct SessionRenewalProbe {
395        renewed: mpsc::UnboundedReceiver<SessionId>,
396    }
397
398    #[cfg(unix)]
399    impl SessionRenewalProbe {
400        fn new() -> (SessionRenewalObserver, Self) {
401            let (renewed_tx, renewed_rx) = mpsc::unbounded_channel();
402
403            (
404                SessionRenewalObserver::new(renewed_tx),
405                Self {
406                    renewed: renewed_rx,
407                },
408            )
409        }
410
411        async fn wait_for_renewals(
412            &mut self,
413            expected_renewals: usize,
414            timeout: Duration,
415        ) -> Vec<SessionId> {
416            tokio::time::timeout(timeout, async {
417                let mut renewals = Vec::with_capacity(expected_renewals);
418                while renewals.len() < expected_renewals {
419                    let session_id = self
420                        .renewed
421                        .recv()
422                        .await
423                        .expect("session renewal observer closed before expected renewal");
424                    renewals.push(session_id);
425                }
426                renewals
427            })
428            .await
429            .expect("timed out waiting for session renewals")
430        }
431    }
432
433    #[cfg(unix)]
434    struct StartedDaemon {
435        probe: DaemonLifecycleProbe,
436        daemon_task: tokio::task::JoinHandle<crate::Result<()>>,
437    }
438
439    #[cfg(unix)]
440    impl StartedDaemon {
441        fn spawn(root: &Path) -> crate::Result<Self> {
442            Self::spawn_with_config(root, StartedDaemonConfig::default())
443        }
444
445        fn spawn_with_config(root: &Path, config: StartedDaemonConfig) -> crate::Result<Self> {
446            let database = RuntimeDatabase::sqlite(root.join("synd.db"));
447            let placement_environment =
448                PlacementEnvironment::new(PlacementRoot::from(root.join("runtime")));
449            let placement = PlacementResolver::with_environment(placement_environment.clone())
450                .resolve_database(&database)?;
451            let session_config = {
452                let session_config = SessionConfig::new(Duration::from_secs(2));
453                match config.renewal_observer {
454                    Some(observer) => session_config.with_renewal_observer(observer),
455                    None => session_config,
456                }
457            };
458            let runtime_config = {
459                let runtime_config = RuntimeConfig::new(database.clone())
460                    .with_api_timeout(Duration::from_secs(2), "synd-runtime-test")
461                    .with_session(session_config)
462                    .with_daemon_launch(DaemonLaunchConfig::new(
463                        DaemonExecutable::path("unused"),
464                        DaemonLaunchLog::file(root.join("daemon.log")),
465                    ))
466                    .with_placement_environment(placement_environment.clone());
467                match config.session_requirements {
468                    Some(requirements) => runtime_config.with_requirements(requirements),
469                    None => runtime_config,
470                }
471            };
472            let runtime = Runtime::try_new(runtime_config)?;
473            let mut daemon_config =
474                DaemonConfig::new(database).with_placement_environment(placement_environment);
475            if let Some(lease_policy) = config.session_lease_policy {
476                daemon_config = daemon_config.with_session_lease_policy(lease_policy);
477            }
478            let daemon = Daemon::new(daemon_config);
479            let probe =
480                DaemonLifecycleProbe::new(runtime, placement.endpoint().path().to_path_buf());
481            let daemon_task = tokio::spawn(daemon.serve());
482
483            Ok(Self { probe, daemon_task })
484        }
485
486        async fn wait_until_running(&mut self) {
487            tokio::select! {
488                () = self.probe.wait_until_running() => {}
489                result = &mut self.daemon_task => {
490                    panic!("daemon serve task finished before readiness: {result:?}");
491                }
492            }
493        }
494
495        async fn shutdown(self) {
496            self.probe.shutdown().await;
497            tokio::time::timeout(DAEMON_READY_TIMEOUT, self.daemon_task)
498                .await
499                .unwrap()
500                .unwrap()
501                .unwrap();
502
503            assert!(!self.probe.endpoint.exists());
504        }
505    }
506
507    #[cfg(unix)]
508    struct DaemonLifecycleProbe {
509        runtime: Runtime,
510        endpoint: PathBuf,
511    }
512
513    #[cfg(unix)]
514    impl DaemonLifecycleProbe {
515        fn new(runtime: Runtime, endpoint: PathBuf) -> Self {
516            Self { runtime, endpoint }
517        }
518
519        async fn wait_until_running(&self) {
520            let deadline = Instant::now() + DAEMON_READY_TIMEOUT;
521
522            loop {
523                let last_probe_error = match self.runtime.daemon().inspect().await {
524                    Ok(status) if status.state() == DaemonState::Running => return,
525                    Ok(_) => None,
526                    Err(error) => Some(format!("{error:?}")),
527                };
528
529                let now = Instant::now();
530                assert!(
531                    now < deadline,
532                    "timed out waiting for daemon to run; last probe error: {last_probe_error:?}",
533                );
534
535                tokio::time::sleep(DAEMON_POLL_INTERVAL.min(deadline - now)).await;
536            }
537        }
538
539        async fn shutdown(&self) {
540            let result = self.runtime.daemon().shutdown().await.unwrap();
541
542            assert_eq!(result.status().state(), DaemonState::NotRunning);
543            assert_eq!(
544                result.status().placement().endpoint(),
545                self.endpoint.as_path()
546            );
547        }
548    }
549
550    #[cfg(unix)]
551    mod shutdown {
552        use super::*;
553
554        #[tokio::test]
555        async fn stops_endpoint() -> crate::Result<()> {
556            let tmp = tempfile::tempdir()?;
557            let mut daemon = StartedDaemon::spawn(tmp.path())?;
558
559            daemon.wait_until_running().await;
560            daemon.shutdown().await;
561            Ok(())
562        }
563    }
564
565    #[cfg(unix)]
566    mod session {
567        use super::*;
568
569        mod lifecycle {
570            use super::*;
571
572            #[tokio::test]
573            async fn accepts_and_closes() -> crate::Result<()> {
574                let tmp = tempfile::tempdir()?;
575                let mut daemon = StartedDaemon::spawn(tmp.path())?;
576
577                daemon.wait_until_running().await;
578                let session = daemon.probe.runtime.acquire_session().await?;
579                assert_eq!(
580                    session.available_capabilities(),
581                    &synd_protocol::capability::local_api_capabilities()
582                );
583                session.close().await?;
584
585                daemon.shutdown().await;
586                Ok(())
587            }
588        }
589
590        mod required_capabilities {
591            use super::*;
592
593            #[tokio::test]
594            async fn rejects_missing() -> crate::Result<()> {
595                let tmp = tempfile::tempdir()?;
596                let missing_capabilities = synd_protocol::CapabilitySet::new(["test.missing"]);
597                let mut daemon = StartedDaemon::spawn_with_config(
598                    tmp.path(),
599                    StartedDaemonConfig::default().with_session_requirements(
600                        SessionRequirements::new(missing_capabilities.clone()),
601                    ),
602                )?;
603
604                daemon.wait_until_running().await;
605                let unexpected = match daemon.probe.runtime.acquire_session().await {
606                    Err(crate::Error::MissingSessionCapabilities {
607                        missing_capabilities: actual,
608                        ..
609                    }) => {
610                        assert_eq!(actual, missing_capabilities);
611                        None
612                    }
613                    Err(error) => Some(format!("unexpected acquire_session error: {error:?}")),
614                    Ok(session) => {
615                        session.close().await?;
616                        Some("session unexpectedly opened".to_owned())
617                    }
618                };
619
620                daemon.shutdown().await;
621                if let Some(message) = unexpected {
622                    panic!("{message}");
623                }
624
625                Ok(())
626            }
627        }
628
629        mod lease {
630            use super::*;
631
632            #[tokio::test]
633            async fn renews() -> crate::Result<()> {
634                let tmp = tempfile::tempdir()?;
635                let lease_policy = DaemonSessionLeasePolicy::new(
636                    Duration::from_secs(2),
637                    Duration::from_millis(200),
638                );
639                let (observer, mut renewal_probe) = SessionRenewalProbe::new();
640                let mut daemon = StartedDaemon::spawn_with_config(
641                    tmp.path(),
642                    StartedDaemonConfig::default()
643                        .with_session_lease_policy(lease_policy)
644                        .with_renewal_observer(observer),
645                )?;
646
647                daemon.wait_until_running().await;
648                let session = daemon.probe.runtime.acquire_session().await?;
649                let renewed_session_ids = renewal_probe
650                    .wait_for_renewals(4, Duration::from_secs(8))
651                    .await;
652                let first_session_id = &renewed_session_ids[0];
653
654                assert!(
655                    renewed_session_ids
656                        .iter()
657                        .all(|session_id| session_id == first_session_id)
658                );
659                session.close().await?;
660
661                daemon.shutdown().await;
662                Ok(())
663            }
664        }
665    }
666
667    #[cfg(unix)]
668    mod endpoint_binding {
669        use super::*;
670
671        #[tokio::test]
672        async fn creates_parent_dir() {
673            let tmp = tempfile::tempdir().unwrap();
674            let instance = RuntimeInstance::from_database(&RuntimeDatabase::sqlite(
675                tmp.path().join("synd.db"),
676            ))
677            .unwrap();
678            let endpoint =
679                UdsEndpoint::from_instance_id(&tmp.path().join("runtime"), instance.id());
680
681            let _bound_endpoint = DaemonEndpointBinder::new(&endpoint).bind().unwrap();
682
683            assert!(endpoint.path().exists());
684        }
685    }
686
687    #[cfg(unix)]
688    mod endpoint_cleanup {
689        use super::*;
690
691        mod stale_socket {
692            use super::*;
693
694            #[tokio::test]
695            async fn removes_socket() {
696                let tmp = tempfile::tempdir().unwrap();
697                let instance = RuntimeInstance::from_database(&RuntimeDatabase::sqlite(
698                    tmp.path().join("synd.db"),
699                ))
700                .unwrap();
701                let endpoint = UdsEndpoint::from_instance_id(tmp.path(), instance.id());
702                let bound_endpoint = DaemonEndpointBinder::new(&endpoint).bind().unwrap();
703                let (listener, cleanup) = bound_endpoint.into_parts();
704
705                drop(listener);
706                cleanup.cleanup_stale_socket().unwrap();
707
708                assert!(!endpoint.path().exists());
709            }
710
711            #[tokio::test]
712            async fn keeps_connected_socket() {
713                let tmp = tempfile::tempdir().unwrap();
714                let instance = RuntimeInstance::from_database(&RuntimeDatabase::sqlite(
715                    tmp.path().join("synd.db"),
716                ))
717                .unwrap();
718                let endpoint = UdsEndpoint::from_instance_id(tmp.path(), instance.id());
719                let bound_endpoint = DaemonEndpointBinder::new(&endpoint).bind().unwrap();
720                let (listener, cleanup) = bound_endpoint.into_parts();
721
722                drop(listener);
723                std::fs::remove_file(endpoint.path()).unwrap();
724                let _replacement = std::os::unix::net::UnixListener::bind(endpoint.path()).unwrap();
725                cleanup.cleanup_stale_socket().unwrap();
726
727                assert!(endpoint.path().exists());
728            }
729
730            #[tokio::test]
731            async fn refuses_non_socket_file() {
732                let tmp = tempfile::tempdir().unwrap();
733                let instance = RuntimeInstance::from_database(&RuntimeDatabase::sqlite(
734                    tmp.path().join("synd.db"),
735                ))
736                .unwrap();
737                let endpoint = UdsEndpoint::from_instance_id(tmp.path(), instance.id());
738                let bound_endpoint = DaemonEndpointBinder::new(&endpoint).bind().unwrap();
739                let (listener, cleanup) = bound_endpoint.into_parts();
740
741                drop(listener);
742                std::fs::remove_file(endpoint.path()).unwrap();
743                std::fs::write(endpoint.path(), "").unwrap();
744                let error = cleanup.cleanup_stale_socket().unwrap_err();
745
746                assert!(error.to_string().contains("non-socket runtime endpoint"));
747                assert!(endpoint.path().exists());
748            }
749        }
750
751        mod shutdown {
752            use super::*;
753
754            #[tokio::test]
755            async fn removes_socket() {
756                let tmp = tempfile::tempdir().unwrap();
757                let instance = RuntimeInstance::from_database(&RuntimeDatabase::sqlite(
758                    tmp.path().join("synd.db"),
759                ))
760                .unwrap();
761                let endpoint = UdsEndpoint::from_instance_id(tmp.path(), instance.id());
762                let bound_endpoint = DaemonEndpointBinder::new(&endpoint).bind().unwrap();
763                let (_listener, cleanup) = bound_endpoint.into_parts();
764
765                cleanup.unlink_socket().unwrap();
766
767                assert!(!endpoint.path().exists());
768            }
769        }
770    }
771}