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 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#[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#[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}