use std::fs::{self, DirBuilder, Permissions};
use std::io::{self, ErrorKind};
use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
use std::path::{Path, PathBuf};
use tokio::net::unix::{OwnedReadHalf, OwnedWriteHalf};
use tokio::net::{UnixListener, UnixStream};
use crate::connection::Connection;
pub fn socket_path() -> PathBuf {
user_folder().join(SOCKET_FILE)
}
const SOCKET_FILE: &str = "dev.sock";
#[cfg(target_vendor = "apple")]
fn user_folder() -> PathBuf {
home().join("Library/Application Support/WaOS")
}
#[cfg(not(target_vendor = "apple"))]
fn user_folder() -> PathBuf {
match std::env::var_os("XDG_RUNTIME_DIR") {
Some(runtime) if !runtime.is_empty() => PathBuf::from(runtime).join("waos"),
_ => home().join(".local/share/waos"),
}
}
fn home() -> PathBuf {
std::env::home_dir().unwrap_or_else(|| PathBuf::from("/"))
}
impl Connection<UnixStream> {
pub async fn connect(path: &Path) -> io::Result<Connection<UnixStream>> {
Ok(Connection::new(UnixStream::connect(path).await?))
}
pub fn into_split(self) -> (Connection<OwnedReadHalf>, Connection<OwnedWriteHalf>) {
let (reading, writing) = self.stream.into_split();
(Connection::new(reading), Connection::new(writing))
}
}
#[derive(Debug)]
pub struct Listener {
listener: UnixListener,
path: PathBuf,
}
impl Listener {
pub async fn bind(path: &Path) -> io::Result<Listener> {
make_private_folder(path)?;
clear_stale_socket(path).await?;
let listener = UnixListener::bind(path)?;
fs::set_permissions(path, Permissions::from_mode(0o600))?;
Ok(Listener {
listener,
path: path.to_owned(),
})
}
pub async fn accept(&self) -> io::Result<Connection<UnixStream>> {
let (stream, _) = self.listener.accept().await?;
Ok(Connection::new(stream))
}
}
impl Drop for Listener {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
fn make_private_folder(socket: &Path) -> io::Result<()> {
let Some(folder) = socket.parent() else {
return Ok(());
};
DirBuilder::new().recursive(true).mode(0o700).create(folder)
}
async fn clear_stale_socket(path: &Path) -> io::Result<()> {
match UnixStream::connect(path).await {
Ok(_answering) => Err(io::Error::new(
ErrorKind::AddrInUse,
format!("another WaOS answers at {}", path.display()),
)),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(()),
Err(error) if error.kind() == ErrorKind::ConnectionRefused => {
tracing::info!(path = %path.display(), "replacing the socket file a WaOS that died left");
fs::remove_file(path)
}
Err(error) => Err(error),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_socket_is_in_the_user_folder_under_its_name() {
let path = socket_path();
assert!(path.starts_with(home()), "{}", path.display());
assert!(path.ends_with(SOCKET_FILE), "{}", path.display());
}
}