Skip to main content

steamroom_cli/daemon/
ipc.rs

1//! Socket name resolution and bind. `interprocess` handles the
2//! platform-specific bits; this module just adds the stale-socket probe.
3
4use std::time::Duration;
5
6use interprocess::local_socket::GenericNamespaced;
7use interprocess::local_socket::ListenerOptions;
8use interprocess::local_socket::Name;
9use interprocess::local_socket::ToNsName;
10use interprocess::local_socket::tokio::Listener;
11use interprocess::local_socket::tokio::Stream;
12use interprocess::local_socket::traits::tokio::Listener as _;
13use interprocess::local_socket::traits::tokio::Stream as _;
14
15use crate::daemon::framing::read_frame;
16use crate::daemon::framing::write_frame;
17use crate::daemon::proto::Frame;
18use crate::daemon::proto::Request;
19use crate::daemon::proto::Response;
20use crate::errors::CliError;
21
22/// Build the platform-appropriate name for the current user's daemon.
23pub fn socket_name() -> Result<Name<'static>, CliError> {
24    let raw = socket_name_string();
25    raw.to_ns_name::<GenericNamespaced>().map_err(CliError::Io)
26}
27
28pub fn socket_name_string() -> String {
29    #[cfg(unix)]
30    {
31        // getuid is infallible on supported targets.
32        let uid = unsafe { libc::getuid() };
33        format!("steamroom-{uid}.sock")
34    }
35    #[cfg(windows)]
36    {
37        let user = std::env::var("USERNAME").unwrap_or_else(|_| "user".into());
38        format!("steamroom-{user}")
39    }
40}
41
42/// Connect-and-probe: send a Status request with a short read timeout.
43/// Returns Ok if a peer answered, Err otherwise. Used to differentiate
44/// "stale socket file" from "daemon already running".
45pub async fn probe_peer() -> Result<(), CliError> {
46    let name = socket_name()?;
47    let mut stream = Stream::connect(name).await.map_err(CliError::Io)?;
48    write_frame(&mut stream, &Frame::Request(Request::Status)).await?;
49    let fut = read_frame(&mut stream);
50    let resp = tokio::time::timeout(Duration::from_millis(200), fut)
51        .await
52        .map_err(|_| {
53            CliError::Io(std::io::Error::new(
54                std::io::ErrorKind::TimedOut,
55                "probe timed out",
56            ))
57        })??;
58    match resp {
59        Frame::Response(Response::Status(_)) => Ok(()),
60        other => Err(CliError::MalformedFrame(format!(
61            "probe expected Status, got {other:?}"
62        ))),
63    }
64}
65
66/// Bind the daemon's listener. Returns `Err(CliError::DaemonAlreadyRunning)`
67/// if a probe shows a live peer; otherwise overwrites stale sockets.
68pub async fn bind_listener() -> Result<Listener, CliError> {
69    if probe_peer().await.is_ok() {
70        return Err(CliError::DaemonAlreadyRunning);
71    }
72    let name = socket_name()?;
73    ListenerOptions::new()
74        .name(name)
75        // The probe above confirmed no live peer, so overwriting a stale
76        // socket file (e.g. from a crashed daemon) is safe.
77        .try_overwrite(true)
78        .create_tokio()
79        .map_err(CliError::Io)
80}
81
82pub async fn accept(listener: &Listener) -> Result<Stream, CliError> {
83    listener.accept().await.map_err(CliError::Io)
84}