use std::os::unix::fs::FileTypeExt;
use anyhow::Context;
use rustix::fs::Mode as FileMode;
use serde::{Deserialize, Serialize};
use tocat_api::normalize;
use tracing::warn;
use crate::{
config::ByteSize,
endpoint::{
Connection, Direction, EndpointStream, SyncHalves,
parse::{Opt, ParseEndpointError},
sys::{Mode, PathGuard, default_true, size_if_pipe},
},
};
#[derive(Debug, Deserialize, Serialize)]
pub struct Pipe {
pub path: std::path::PathBuf,
#[serde(default = "default_true")]
pub create: bool,
#[serde(default)]
pub mode: Option<Mode>,
#[serde(default)]
pub unlink: bool,
#[serde(default = "default_true")]
pub hold: bool,
#[serde(default)]
pub size: Option<ByteSize>,
#[serde(default)]
pub name: Option<String>,
}
impl Pipe {
const SCHEME: &'static str = "pipe";
pub(super) fn parse<'a>(
body: &str,
opts: impl Iterator<Item = Opt<'a>>,
) -> Result<Self, ParseEndpointError> {
let mut create = true;
let mut hold = true;
let mut mode = None;
let mut unlink = false;
let mut size = None;
let mut name = None;
for opt in opts {
match normalize(opt.key).as_str() {
"create" => create = opt.flag()?,
"hold" => hold = opt.flag()?,
"mode" => mode = Some(opt.mode()?),
"name" => name = Some(opt.string()?),
"size" | "pipesize" => size = Some(opt.size()?),
"unlink" => unlink = opt.flag()?,
_ => return Err(opt.unsupported(Self::SCHEME)),
}
}
Ok(Self {
path: std::path::PathBuf::from(body),
create,
mode,
unlink,
hold,
size,
name,
})
}
pub(super) fn label(&self) -> String {
self.name
.clone()
.unwrap_or_else(|| format!("pipe://{}", self.path.display()))
}
fn access(&self, dir: Direction) -> (bool, bool) {
match (self.hold, dir) {
(true, _) => (true, true),
(false, Direction::Source) => (true, false),
(false, Direction::Sink) => (false, true),
}
}
fn ensure(&self) -> anyhow::Result<()> {
let path: &std::path::Path = &self.path;
match std::fs::metadata(path) {
Ok(meta) => {
anyhow::ensure!(
meta.file_type().is_fifo(),
"{} exists and is not a FIFO",
path.display()
);
return Ok(());
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
anyhow::ensure!(
self.create,
"{} does not exist; pass `create` to make it",
path.display()
);
}
Err(e) => return Err(e).with_context(|| format!("stat {}", path.display())),
}
match rustix::fs::mkfifoat(rustix::fs::CWD, path, FileMode::from_bits_truncate(0o666)) {
Ok(()) => {}
Err(e) if e == rustix::io::Errno::EXIST => {}
Err(e) => return Err(e).with_context(|| format!("mkfifo {}", path.display())),
}
if let Some(mode) = self.mode {
mode.apply(path)?;
}
Ok(())
}
fn guard(&self) -> Option<PathGuard> {
self.unlink.then(|| PathGuard(self.path.clone()))
}
fn resize<F: std::os::fd::AsFd>(&self, fd: &F) {
if let Some(size) = self.size {
size_if_pipe(fd, &self.path.display().to_string(), size.bytes());
}
}
pub(super) async fn connect(&self, dir: Direction) -> anyhow::Result<Connection> {
self.ensure()?;
let (read, write) = self.access(dir);
if !self.hold {
warn!(path = %self.path.display(), "FIFO without `hold`: open blocks until a peer connects");
}
let file = tokio::fs::OpenOptions::new()
.read(read)
.write(write)
.open(&self.path)
.await
.with_context(|| format!("opening {}", self.path.display()))?;
self.resize(&file);
let stream = match dir {
Direction::Source => EndpointStream::read_only(file),
Direction::Sink => EndpointStream::write_only(file),
};
Ok(stream.into_connection_with_guard(self.guard()))
}
pub(super) fn connect_sync(&self, dir: Direction) -> anyhow::Result<SyncHalves> {
self.ensure()?;
let (read, write) = self.access(dir);
let file = std::fs::OpenOptions::new()
.read(read)
.write(write)
.open(&self.path)
.with_context(|| format!("opening {}", self.path.display()))?;
self.resize(&file);
let guard = self.guard();
Ok(match dir {
Direction::Source => SyncHalves {
reader: Some(Box::new(file)),
writer: None,
guard,
},
Direction::Sink => SyncHalves {
reader: None,
writer: Some(Box::new(file)),
guard,
},
})
}
}