slipcase_open/endpoint.rs
1//! Where the front door is, and who is allowed through it.
2//
3// Author: David M. Anderson
4// Built with AI assistance (Claude, Anthropic)
5//
6//! Concept 8: where no instance is running, the invocation starts one and hands
7//! over; where one is, it hands over and exits.
8//!
9//! **The endpoint is restricted to its owner by the directory it sits in**,
10//! which is the platform's own mechanism and, §8 says, a requirement rather
11//! than a hardening measure. A socket's own permission bits are not portable —
12//! some kernels ignore them on connect — so the guarantee is the traversal bit
13//! on a directory nobody else can enter, set before the socket is bound.
14//!
15//! **It is runtime state and not saved state**, which is the opposite of the
16//! choice §6.4 made for sessions. A stale socket from a crashed instance is
17//! debris to be cleared, where a stale session directory holds somebody's edit;
18//! so this goes in `$XDG_RUNTIME_DIR` where a platform offers one — cleared at
19//! logout, which is exactly right for this and exactly wrong for a session.
20
21use std::io;
22use std::path::PathBuf;
23
24/// Where this user's endpoint lives.
25///
26/// `$XDG_RUNTIME_DIR` where the platform sets one, which is already private to
27/// its owner and cleared at logout. Otherwise the session state directory's own
28/// parent, which is private for the same reason and is at least on a filesystem
29/// this user can write.
30///
31/// # Errors
32///
33/// Where no per-user directory can be named at all.
34pub fn path() -> io::Result<PathBuf> {
35 #[cfg(windows)]
36 {
37 // Not a filesystem path at all, and `main` never treats it as one: the
38 // door is only ever handed to `bind` and `connect`, and printed once in
39 // the refusal that names it.
40 //
41 // Two things name it. The SID, because the pipe namespace belongs to
42 // the machine rather than to this account, so a fixed name would be one
43 // door for everybody logged in. And the state directory, because the
44 // door belongs to the sessions it serves — which is the same rule the
45 // arm below follows by putting the socket beside them, and is what
46 // gives a redirected world a front door of its own instead of reaching
47 // into whatever this account already has running.
48 let root = crate::session::default_root()?;
49 let mut sum = crc32fast::Hasher::new();
50 sum.update(root.as_os_str().as_encoded_bytes());
51 Ok(PathBuf::from(format!(
52 r"\\.\pipe\slipcase-open.{}.{:08x}",
53 pipe::own_sid()?,
54 sum.finalize()
55 )))
56 }
57 #[cfg(not(windows))]
58 {
59 if let Some(dir) = runtime_dir() {
60 return Ok(dir.join("slipcase-open").join("front-door"));
61 }
62 let sessions = crate::session::default_root()?;
63 let base = sessions.parent().unwrap_or(&sessions).to_path_buf();
64 Ok(base.join("front-door"))
65 }
66}
67
68#[cfg(unix)]
69fn runtime_dir() -> Option<PathBuf> {
70 std::env::var_os("XDG_RUNTIME_DIR")
71 .filter(|v| !v.is_empty())
72 .map(PathBuf::from)
73}
74
75#[cfg(all(not(unix), not(windows)))]
76fn runtime_dir() -> Option<PathBuf> {
77 None
78}
79
80/// Make the endpoint's directory, private to its owner.
81///
82/// Set after creation rather than left to the umask, which is the user's: a
83/// permissive one would leave the front door reachable by every account on the
84/// machine, and §8 puts that among the requirements rather than the
85/// improvements.
86///
87/// # Errors
88///
89/// Where the directory cannot be made or narrowed.
90pub fn prepare(at: &std::path::Path) -> io::Result<()> {
91 let dir = at.parent().unwrap_or(at);
92 std::fs::create_dir_all(dir)?;
93 private(dir)
94}
95
96#[cfg(unix)]
97fn private(dir: &std::path::Path) -> io::Result<()> {
98 use std::os::unix::fs::PermissionsExt as _;
99 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
100}
101
102/// Nothing to narrow. Windows scopes this by the inherited ACL on the directory
103/// above rather than by a mode, so this is the shape of the platform and not a
104/// stub waiting to be filled. `Result` because the Unix arm has one to give.
105#[allow(clippy::unnecessary_wraps)]
106#[cfg(not(unix))]
107fn private(_dir: &std::path::Path) -> io::Result<()> {
108 Ok(())
109}
110
111#[cfg(unix)]
112pub use unix::{bind, connect, Incoming, Listener, Stream};
113
114#[cfg(unix)]
115mod unix {
116 use std::io;
117 use std::os::unix::net::{UnixListener, UnixStream};
118 use std::path::Path;
119
120 /// One connection.
121 pub type Stream = UnixStream;
122
123 /// The bound endpoint. Unlinks the socket when it goes.
124 #[derive(Debug)]
125 pub struct Listener {
126 inner: UnixListener,
127 path: std::path::PathBuf,
128 }
129
130 /// Connections as they arrive.
131 pub type Incoming<'a> = std::os::unix::net::Incoming<'a>;
132
133 impl Listener {
134 /// Connections as they arrive.
135 pub fn incoming(&self) -> Incoming<'_> {
136 self.inner.incoming()
137 }
138 }
139
140 impl Drop for Listener {
141 fn drop(&mut self) {
142 // Best effort. A socket left behind is cleared by the next `bind`,
143 // which is written for that case because a crash cannot run this.
144 let _ = std::fs::remove_file(&self.path);
145 }
146 }
147
148 /// Speak to the instance already running, if there is one.
149 ///
150 /// # Errors
151 ///
152 /// Where there is nothing listening, or the connection fails.
153 pub fn connect(at: &Path) -> io::Result<Stream> {
154 UnixStream::connect(at)
155 }
156
157 /// Become the instance.
158 ///
159 /// **A refused connection means the socket is debris, not a rival.** A
160 /// crashed instance leaves the file behind and nothing listening on it, so
161 /// binding fails with *address in use* forever until somebody removes it.
162 /// Removing it is only safe after a connection has been refused, which is
163 /// what says nobody is on the other end — deleting an endpoint somebody is
164 /// serving would take the running instance's front door away and leave two
165 /// processes holding sessions.
166 ///
167 /// # Errors
168 ///
169 /// Where the endpoint cannot be bound, including where another instance
170 /// bound it first. A caller that loses that race connects instead.
171 pub fn bind(at: &Path) -> io::Result<Listener> {
172 super::prepare(at)?;
173 match UnixListener::bind(at) {
174 Ok(inner) => Ok(Listener {
175 inner,
176 path: at.to_owned(),
177 }),
178 Err(e) if e.kind() == io::ErrorKind::AddrInUse => {
179 // Ask before clearing. Anything but a refusal means somebody is
180 // there, and the caller should be talking to them instead.
181 if UnixStream::connect(at).is_ok() {
182 return Err(io::Error::new(
183 io::ErrorKind::AddrInUse,
184 "another instance is listening",
185 ));
186 }
187 std::fs::remove_file(at)?;
188 Ok(Listener {
189 inner: UnixListener::bind(at)?,
190 path: at.to_owned(),
191 })
192 }
193 Err(e) => Err(e),
194 }
195 }
196}
197
198/// Concept 8's named pipe, and the ACL that makes it this user's front door
199/// rather than the machine's.
200#[cfg(windows)]
201pub use pipe::{bind, connect, Incoming, Listener, Stream};
202
203#[cfg(windows)]
204mod pipe {
205 //! The front door on Windows.
206 //!
207 //! **A pipe leaves no debris, so there is no clearing rule here.** The Unix
208 //! arm has to reason about a socket a crashed instance left behind, because
209 //! the file outlives the process that held it. A named pipe does not exist
210 //! apart from its instances: when the last handle closes, the name is gone.
211 //! Measured 2026-09-01 — after the listener drops, a connect answers
212 //! `NotFound` — and it is why `bind` below is shorter than its counterpart
213 //! rather than for having skipped something it should have done.
214 //!
215 //! **`FILE_FLAG_FIRST_PIPE_INSTANCE` is the exclusion.** It refuses with
216 //! `ERROR_ACCESS_DENIED` where any instance of the name exists already,
217 //! which is precisely *another instance is listening*, and is answered as
218 //! `AddrInUse` so that `main` hands over rather than failing. There is no
219 //! race to lose between asking and binding, where the Unix arm has to
220 //! connect first to tell a rival from debris.
221 //!
222 //! **The name carries the SID and the descriptor enforces it.** The pipe
223 //! prefix is one namespace for the whole machine, so a fixed name would be
224 //! the machine's door and two accounts would collide on it. The SID in the
225 //! name keeps them apart; the ACL is what keeps them out, and concept 8 puts
226 //! that among the requirements rather than the hardening.
227 //!
228 //! **Only the server half reaches past `std`.** A pipe opens as a file, so
229 //! `connect` is `OpenOptions` and nothing else, and `Stream` is
230 //! `std::fs::File` with the `Read` and `Write` that `ipc` asks for.
231
232 use std::cell::Cell;
233 use std::io;
234 use std::os::windows::ffi::OsStrExt as _;
235 use std::os::windows::io::{AsRawHandle as _, FromRawHandle as _, OwnedHandle};
236 use std::path::Path;
237 use std::time::Duration;
238
239 use windows_sys::Win32::Foundation::{
240 LocalFree, ERROR_ACCESS_DENIED, ERROR_PIPE_BUSY, ERROR_PIPE_CONNECTED, INVALID_HANDLE_VALUE,
241 };
242 use windows_sys::Win32::Security::Authorization::{
243 ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
244 SDDL_REVISION_1,
245 };
246 use windows_sys::Win32::Security::{
247 GetTokenInformation, TokenUser, SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER,
248 };
249 use windows_sys::Win32::Storage::FileSystem::{
250 FILE_FLAG_FIRST_PIPE_INSTANCE, PIPE_ACCESS_DUPLEX,
251 };
252 use windows_sys::Win32::System::Pipes::{
253 ConnectNamedPipe, CreateNamedPipeW, PIPE_READMODE_BYTE, PIPE_TYPE_BYTE,
254 PIPE_UNLIMITED_INSTANCES, PIPE_WAIT,
255 };
256 use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
257
258 /// What one instance holds in each direction. A request is a path and a verb
259 /// and a response is a few lines, so this is room to spare rather than a
260 /// budget; the pipe blocks and does not truncate when it is reached.
261 const BUFFER: u32 = 4096;
262
263 /// Every instance busy serving somebody. `connect` waits rather than
264 /// failing: the door is answered in the time it takes to read one request,
265 /// and the caller has nowhere else to go.
266 const BUSY_PAUSE: Duration = Duration::from_millis(20);
267 const BUSY_TRIES: u32 = 50;
268
269 /// One connection. A connected instance is a byte stream and `std` already
270 /// gives `Read` and `Write` over a handle, which is all `ipc` wants.
271 pub type Stream = std::fs::File;
272
273 /// The bound endpoint.
274 pub struct Listener {
275 name: Vec<u16>,
276 security: Vec<u16>,
277 /// The instance waiting for the next client.
278 ///
279 /// One is always outstanding while the listener lives, and that is what
280 /// holds the name: closing every instance would release it to whoever
281 /// asked next. So [`Incoming::next`] makes the replacement *before* it
282 /// hands the connected one over, rather than after.
283 ///
284 /// `Cell` rather than `&mut self`, so that `incoming` takes `&self` and
285 /// reads exactly like the Unix arm — which leaves `resident::run` one
286 /// body for both platforms with nothing to configure out.
287 pending: Cell<Option<OwnedHandle>>,
288 }
289
290 impl std::fmt::Debug for Listener {
291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 f.debug_struct("Listener").finish_non_exhaustive()
293 }
294 }
295
296 /// Connections as they arrive.
297 pub struct Incoming<'a> {
298 listener: &'a Listener,
299 }
300
301 impl Listener {
302 /// Connections as they arrive.
303 #[must_use]
304 pub fn incoming(&self) -> Incoming<'_> {
305 Incoming { listener: self }
306 }
307 }
308
309 impl Iterator for Incoming<'_> {
310 type Item = io::Result<Stream>;
311
312 fn next(&mut self) -> Option<Self::Item> {
313 // No instance outstanding means an earlier turn could not make one,
314 // so the door is shut. `run` reads the end of this iterator as the
315 // listener having gone, and stands the instance down.
316 let pending = self.listener.pending.take()?;
317 if let Err(why) = wait_for_client(&pending) {
318 return Some(Err(why));
319 }
320 // Before this one is handed over, not after: the name is held by an
321 // instance existing, and between the last one closing and the next
322 // being made it would be free for the taking.
323 match instance(&self.listener.name, &self.listener.security, false) {
324 Ok(next) => self.listener.pending.set(Some(next)),
325 Err(why) => return Some(Err(why)),
326 }
327 Some(Ok(Stream::from(pending)))
328 }
329 }
330
331 /// Speak to the instance already running, if there is one.
332 ///
333 /// # Errors
334 ///
335 /// Where there is nothing listening, or the connection fails.
336 pub fn connect(at: &Path) -> io::Result<Stream> {
337 let open = || std::fs::OpenOptions::new().read(true).write(true).open(at);
338 for _ in 0..BUSY_TRIES {
339 match open() {
340 Err(why) if is_error(&why, ERROR_PIPE_BUSY) => {
341 std::thread::sleep(BUSY_PAUSE);
342 }
343 settled => return settled,
344 }
345 }
346 open()
347 }
348
349 /// Become the instance.
350 ///
351 /// # Errors
352 ///
353 /// Where the pipe cannot be created, including where another instance holds
354 /// the name already — which answers `AddrInUse`, so that a caller which lost
355 /// the race connects instead.
356 pub fn bind(at: &Path) -> io::Result<Listener> {
357 let name = wide(at.as_os_str());
358 let security = wide(std::ffi::OsStr::new(&descriptor()?));
359 let first = instance(&name, &security, true).map_err(|why| {
360 if is_error(&why, ERROR_ACCESS_DENIED) {
361 io::Error::new(io::ErrorKind::AddrInUse, "another instance is listening")
362 } else {
363 why
364 }
365 })?;
366 Ok(Listener {
367 name,
368 security,
369 pending: Cell::new(Some(first)),
370 })
371 }
372
373 /// Whether an error is this Win32 code.
374 ///
375 /// `raw_os_error` answers `i32` and the constants are `u32`. A cast between
376 /// them is a lint this crate would have to silence, and the conversion says
377 /// the same thing without one.
378 fn is_error(why: &io::Error, code: u32) -> bool {
379 why.raw_os_error()
380 .and_then(|got| u32::try_from(got).ok())
381 .is_some_and(|got| got == code)
382 }
383
384 /// A wide, null-terminated copy, which is what every `W` entry point wants.
385 fn wide(s: &std::ffi::OsStr) -> Vec<u16> {
386 s.encode_wide().chain(std::iter::once(0)).collect()
387 }
388
389 /// One entry — this account, full control — and nothing inherited into it.
390 ///
391 /// `D:P` is the protected part, and it is not decoration: without it the
392 /// pipe takes whatever inheritable entries the container offers, which is
393 /// not a set this code chose.
394 fn descriptor() -> io::Result<String> {
395 Ok(format!("D:P(A;;GA;;;{})", own_sid()?))
396 }
397
398 /// The SID of the account this process runs as, in string form.
399 ///
400 /// # Errors
401 ///
402 /// Where the process token cannot be opened or read.
403 #[allow(unsafe_code)]
404 pub(super) fn own_sid() -> io::Result<String> {
405 let mut token = std::ptr::null_mut();
406 // SAFETY: `GetCurrentProcess` is a pseudo-handle needing no release, and
407 // `token` is a live pointer the callee writes an owned handle into. The
408 // return is checked before it is read.
409 if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut token) } == 0 {
410 return Err(io::Error::last_os_error());
411 }
412 // SAFETY: the call above succeeded, so this is an owned handle and this
413 // scope is now what closes it.
414 let token = unsafe { OwnedHandle::from_raw_handle(token) };
415
416 let mut wanted = 0u32;
417 // SAFETY: the documented way to ask for the size — a null buffer of
418 // length zero, which fails and writes the length it would have needed.
419 // The failure is expected, so the return is deliberately not read here;
420 // what checks `wanted` is the call below succeeding with it.
421 unsafe {
422 GetTokenInformation(
423 token.as_raw_handle(),
424 TokenUser,
425 std::ptr::null_mut(),
426 0,
427 &raw mut wanted,
428 );
429 }
430 // `u64` rather than `u8`, and this is alignment rather than taste:
431 // `TOKEN_USER` is read back out of these bytes and wants eight, where a
432 // `Vec<u8>` promises one. Enough words to cover `wanted` bytes.
433 let mut buffer = vec![0u64; (wanted as usize).div_ceil(8)];
434 // SAFETY: `buffer` covers `wanted` bytes, which is the size the call
435 // above asked for, and the callee writes no more than it is given.
436 let read = unsafe {
437 GetTokenInformation(
438 token.as_raw_handle(),
439 TokenUser,
440 buffer.as_mut_ptr().cast(),
441 wanted,
442 &raw mut wanted,
443 )
444 };
445 if read == 0 {
446 return Err(io::Error::last_os_error());
447 }
448
449 let mut text: *mut u16 = std::ptr::null_mut();
450 // SAFETY: the call above filled `buffer` with a `TOKEN_USER` whose `Sid`
451 // points inside it, and `buffer` outlives this call.
452 let made = unsafe {
453 ConvertSidToStringSidW(
454 (*buffer.as_ptr().cast::<TOKEN_USER>()).User.Sid,
455 &raw mut text,
456 )
457 };
458 if made == 0 {
459 return Err(io::Error::last_os_error());
460 }
461 // SAFETY: `text` is a null-terminated string the call above allocated,
462 // so it is read to its terminator and released with the matching free.
463 let sid = unsafe {
464 let mut len = 0;
465 while *text.add(len) != 0 {
466 len += 1;
467 }
468 let sid = String::from_utf16_lossy(std::slice::from_raw_parts(text, len));
469 LocalFree(text.cast());
470 sid
471 };
472 Ok(sid)
473 }
474
475 /// One instance of the pipe, waiting for nobody yet.
476 ///
477 /// `first` asks for `FILE_FLAG_FIRST_PIPE_INSTANCE`, which is what makes
478 /// `bind` exclusive. The instances made afterwards to replace a connected
479 /// one must not ask for it, because by then the name is deliberately taken.
480 #[allow(unsafe_code)]
481 fn instance(name: &[u16], security: &[u16], first: bool) -> io::Result<OwnedHandle> {
482 let mut sd = std::ptr::null_mut();
483 // SAFETY: `security` is null-terminated by `wide`, and `sd` is a live
484 // pointer the callee writes an allocated descriptor into. The return is
485 // checked before `sd` is used.
486 let made = unsafe {
487 ConvertStringSecurityDescriptorToSecurityDescriptorW(
488 security.as_ptr(),
489 SDDL_REVISION_1,
490 &raw mut sd,
491 std::ptr::null_mut(),
492 )
493 };
494 if made == 0 {
495 return Err(io::Error::last_os_error());
496 }
497
498 let attributes = SECURITY_ATTRIBUTES {
499 nLength: u32::try_from(std::mem::size_of::<SECURITY_ATTRIBUTES>()).unwrap_or(0),
500 lpSecurityDescriptor: sd,
501 bInheritHandle: 0,
502 };
503 let mut access = PIPE_ACCESS_DUPLEX;
504 if first {
505 access |= FILE_FLAG_FIRST_PIPE_INSTANCE;
506 }
507 // SAFETY: `name` is null-terminated by `wide`, and `attributes` holds
508 // the descriptor made above, which is live until it is released below.
509 let handle = unsafe {
510 CreateNamedPipeW(
511 name.as_ptr(),
512 access,
513 PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
514 PIPE_UNLIMITED_INSTANCES,
515 BUFFER,
516 BUFFER,
517 0,
518 &raw const attributes,
519 )
520 };
521 // Taken before the descriptor is released, because the free sets this
522 // thread's last error too and would then answer for the wrong call.
523 let why = io::Error::last_os_error();
524 // SAFETY: `sd` was allocated by the conversion above and nothing else
525 // holds it — `CreateNamedPipeW` copies what it needs.
526 unsafe {
527 LocalFree(sd.cast());
528 }
529
530 if handle == INVALID_HANDLE_VALUE {
531 return Err(why);
532 }
533 // SAFETY: the call succeeded, so this is an owned handle and nothing
534 // else holds a copy of it.
535 Ok(unsafe { OwnedHandle::from_raw_handle(handle) })
536 }
537
538 /// Block until somebody connects to `pending`.
539 #[allow(unsafe_code)]
540 fn wait_for_client(pending: &OwnedHandle) -> io::Result<()> {
541 // SAFETY: `pending` is a live pipe instance owned by the caller, and a
542 // null overlapped structure is the documented blocking form.
543 let connected = unsafe { ConnectNamedPipe(pending.as_raw_handle(), std::ptr::null_mut()) };
544 if connected != 0 {
545 return Ok(());
546 }
547 let why = io::Error::last_os_error();
548 // A client that arrived between the instance being made and this call is
549 // already through the door, which is success wearing an error code.
550 if is_error(&why, ERROR_PIPE_CONNECTED) {
551 return Ok(());
552 }
553 Err(why)
554 }
555}
556
557/// Neither a socket nor a pipe, so there is no front door to offer.
558///
559/// # Errors
560///
561/// Always.
562#[cfg(not(any(unix, windows)))]
563pub fn connect(_at: &std::path::Path) -> io::Result<std::net::TcpStream> {
564 Err(io::Error::new(
565 io::ErrorKind::Unsupported,
566 "the front door is not implemented on this platform yet",
567 ))
568}
569
570// The Unix socket's own. Gated at the module rather than per test, which was
571// written so that the day the pipe landed the gate would be one line to
572// reconsider rather than six. That day was Phase 4 and the answer was that the
573// gate stays: what is below tests a socket — its permission bits, and the
574// clearing rule a file that outlives its process needs — and the pipe answers
575// none of those questions. Its tests are in `windows_tests` below.
576#[cfg(all(test, unix))]
577mod tests {
578 use super::{bind, connect, path, prepare};
579 use crate::ipc::{answer, ask, take, Request, Response};
580
581 #[test]
582 fn the_endpoint_is_under_a_per_user_directory() {
583 let at = path().unwrap();
584 assert!(at.is_absolute());
585 assert_eq!(at.file_name().unwrap(), "front-door");
586 }
587
588 #[cfg(unix)]
589 #[test]
590 fn the_directory_is_owner_only_whatever_the_umask_says() {
591 use std::os::unix::fs::PermissionsExt as _;
592 let tmp = tempfile::tempdir().unwrap();
593 let at = tmp.path().join("run/slipcase-open/front-door");
594 prepare(&at).unwrap();
595 let mode = std::fs::metadata(at.parent().unwrap())
596 .unwrap()
597 .permissions()
598 .mode()
599 & 0o777;
600 assert_eq!(mode, 0o700);
601 }
602
603 #[cfg(unix)]
604 #[test]
605 fn a_request_reaches_the_instance_and_the_answer_comes_back() {
606 let tmp = tempfile::tempdir().unwrap();
607 let at = tmp.path().join("front-door");
608 let listener = bind(&at).unwrap();
609
610 let serving = std::thread::spawn(move || {
611 let mut stream = listener.incoming().next().unwrap().unwrap();
612 let request = take(&mut stream).unwrap();
613 answer(&mut stream, &Response::Ok(vec![format!("{request:?}")])).unwrap();
614 });
615
616 let mut client = connect(&at).unwrap();
617 let got = ask(&mut client, &Request::Ping).unwrap();
618 serving.join().unwrap();
619 assert_eq!(got, Response::Ok(vec!["Ping".to_string()]));
620 }
621
622 #[cfg(unix)]
623 #[test]
624 fn nothing_listening_is_a_connection_that_fails_rather_than_a_hang() {
625 let tmp = tempfile::tempdir().unwrap();
626 assert!(connect(&tmp.path().join("front-door")).is_err());
627 }
628
629 #[cfg(unix)]
630 #[test]
631 fn a_socket_a_crash_left_behind_is_cleared_rather_than_blocking_forever() {
632 // What a crash leaves: the file on disk and no descriptor behind it.
633 // `std::os::unix::net::UnixListener` does not unlink on drop, so
634 // dropping one is exactly that state — where `mem::forget` would keep
635 // the descriptor open in this process and still be listening, which is
636 // the opposite of the case.
637 //
638 // Without the clearing rule, binding fails with *address in use* until
639 // somebody deletes the file by hand: a tool that stops working after
640 // one crash.
641 let tmp = tempfile::tempdir().unwrap();
642 let at = tmp.path().join("front-door");
643 drop(std::os::unix::net::UnixListener::bind(&at).unwrap());
644 assert!(at.exists(), "the debris should still be there");
645 assert!(connect(&at).is_err(), "nothing should be listening on it");
646
647 let listener = bind(&at);
648 assert!(listener.is_ok(), "{:?}", listener.err());
649 assert!(connect(&at).is_ok(), "the new instance should answer");
650 }
651
652 #[cfg(unix)]
653 #[test]
654 fn an_endpoint_somebody_is_serving_is_not_taken_from_them() {
655 // The other half of the rule. Clearing a live socket would take the
656 // running instance's front door away and leave two processes holding
657 // sessions on the same containers.
658 let tmp = tempfile::tempdir().unwrap();
659 let at = tmp.path().join("front-door");
660 let _live = bind(&at).unwrap();
661
662 let second = bind(&at);
663 assert!(
664 second.is_err(),
665 "the endpoint was taken from a live instance"
666 );
667 // And the live one still answers.
668 assert!(connect(&at).is_ok());
669 }
670
671 #[cfg(unix)]
672 #[test]
673 fn dropping_the_listener_takes_the_socket_with_it() {
674 let tmp = tempfile::tempdir().unwrap();
675 let at = tmp.path().join("front-door");
676 {
677 let _listener = bind(&at).unwrap();
678 assert!(at.exists());
679 }
680 assert!(!at.exists());
681 }
682}
683
684// The named pipe's own. Concept 8's front door on Windows, tested against the
685// same questions the Unix module asks, with one asked in reverse: there is no
686// debris to clear here, and that is a property worth a test rather than a
687// paragraph.
688#[cfg(all(test, windows))]
689mod windows_tests {
690 use super::{bind, connect, path};
691 use crate::ipc::{answer, ask, take, Request, Response};
692
693 /// A door of this test's own.
694 ///
695 /// The pipe namespace belongs to the machine, so two tests sharing a name
696 /// would share a door and the suite would turn on which of them bound
697 /// first. The process id keeps concurrent runs apart as well.
698 fn a_door(what: &str) -> std::path::PathBuf {
699 std::path::PathBuf::from(format!(
700 r"\\.\pipe\slipcase-open-test.{}.{}",
701 std::process::id(),
702 what
703 ))
704 }
705
706 #[test]
707 fn the_endpoint_is_this_users_pipe() {
708 let at = path().unwrap();
709 let name = at.to_str().unwrap();
710 assert!(name.starts_with(r"\\.\pipe\slipcase-open."), "{name}");
711 // The SID is what keeps two accounts logged into one machine off a
712 // single door, so its absence would be the whole guarantee missing.
713 assert!(name.contains("S-1-"), "{name}");
714 }
715
716 #[test]
717 fn a_request_reaches_the_instance_and_the_answer_comes_back() {
718 let at = a_door("round-trip");
719 let listener = bind(&at).unwrap();
720
721 let serving = std::thread::spawn(move || {
722 let mut stream = listener.incoming().next().unwrap().unwrap();
723 let request = take(&mut stream).unwrap();
724 answer(&mut stream, &Response::Ok(vec![format!("{request:?}")])).unwrap();
725 });
726
727 let mut client = connect(&at).unwrap();
728 let got = ask(&mut client, &Request::Ping).unwrap();
729 serving.join().unwrap();
730 assert_eq!(got, Response::Ok(vec!["Ping".to_string()]));
731 }
732
733 #[test]
734 fn the_door_stays_open_for_the_next_caller() {
735 // The replacement instance is made before the connected one is handed
736 // over, and this is what that is for: a second caller arriving after
737 // the first has been served finds a door rather than a closed name.
738 let at = a_door("second-caller");
739 let listener = bind(&at).unwrap();
740
741 let serving = std::thread::spawn(move || {
742 for stream in listener.incoming().take(2) {
743 let mut stream = stream.unwrap();
744 let request = take(&mut stream).unwrap();
745 answer(&mut stream, &Response::Ok(vec![format!("{request:?}")])).unwrap();
746 }
747 });
748
749 for _ in 0..2 {
750 let mut client = connect(&at).unwrap();
751 assert_eq!(
752 ask(&mut client, &Request::Ping).unwrap(),
753 Response::Ok(vec!["Ping".to_string()])
754 );
755 }
756 serving.join().unwrap();
757 }
758
759 #[test]
760 fn nothing_listening_is_a_connection_that_fails_rather_than_a_hang() {
761 assert!(connect(&a_door("empty")).is_err());
762 }
763
764 #[test]
765 fn an_endpoint_somebody_is_serving_is_not_taken_from_them() {
766 // `FILE_FLAG_FIRST_PIPE_INSTANCE` is what refuses, and the refusal is
767 // translated so that `main` reads it as somebody to hand over to rather
768 // than as a failure to report.
769 let at = a_door("rival");
770 let _live = bind(&at).unwrap();
771
772 match bind(&at) {
773 Ok(_) => panic!("the endpoint was taken from a live instance"),
774 Err(why) => assert_eq!(why.kind(), std::io::ErrorKind::AddrInUse),
775 }
776 }
777
778 #[test]
779 fn a_pipe_leaves_nothing_behind_to_clear() {
780 // The counterpart of the Unix arm's test for clearing a socket that a
781 // crash left behind, and it asserts the opposite: a name does not
782 // outlive the handles that hold it, so a crashed instance leaves
783 // nothing for the next one to reason about. That is why `bind` here has
784 // no clearing rule, rather than because it was skipped.
785 let at = a_door("debris");
786 {
787 let _listener = bind(&at).unwrap();
788 }
789 assert!(connect(&at).is_err(), "the name outlived its listener");
790 // And so binding again is an ordinary bind rather than a recovery.
791 assert!(bind(&at).is_ok());
792 }
793}