Skip to main content

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}