use std::ffi::{OsStr, OsString};
use std::io::{BufRead, ErrorKind, Read, Write};
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::Duration;
pub(crate) const TIMEOUT: Duration = Duration::from_secs(5);
const MAX_REPLY: u64 = 8 << 20;
#[derive(Debug, PartialEq)]
pub(crate) enum Ask {
List,
Call {
target: String,
function: String,
args: Vec<String>,
},
}
pub(crate) fn socket_path(runtime_dir: Option<&OsStr>, display: Option<&OsStr>) -> Option<PathBuf> {
let dir = Path::new(runtime_dir?);
if !dir.is_absolute() {
return None;
}
let display = display
.and_then(|display| Path::new(display).file_name())
.unwrap_or(OsStr::new("wayland-0"));
let mut name = OsString::from("ipc-");
name.push(display);
name.push(".sock");
Some(dir.join("crabshell").join(name))
}
pub(crate) fn request(ask: &Ask) -> String {
let value = match ask {
Ask::List => serde_json::json!({ "list": true }),
Ask::Call {
target,
function,
args,
} => serde_json::json!({ "target": target, "function": function, "args": args }),
};
format!("{value}\n")
}
pub(crate) fn read_reply(reply: &[u8]) -> Result<String, String> {
if reply.is_empty() {
return Err(String::from("the shell hung up without answering"));
}
let line = reply.split(|&byte| byte == b'\n').next().unwrap_or(reply);
let value: serde_json::Value = serde_json::from_slice(line)
.map_err(|error| format!("the shell's reply doesn't parse: {error}"))?;
match (&value["ok"], &value["error"]) {
(serde_json::Value::String(text), serde_json::Value::Null) => Ok(text.clone()),
(serde_json::Value::Null, serde_json::Value::String(text)) => Err(text.clone()),
_ => Err(format!(
"the shell's reply isn't one crabshell knows: {value}"
)),
}
}
pub(crate) fn shown(text: &str) -> String {
let mut shown = text.to_owned();
if !shown.is_empty() && !shown.ends_with('\n') {
shown.push('\n');
}
shown
}
pub(crate) fn ask(path: &Path, line: String, timeout: Duration) -> Result<String, String> {
let (sender, receiver) = std::sync::mpsc::channel();
let at = path.to_owned();
std::thread::spawn(move || sender.send(exchange(&at, &line)));
receiver.recv_timeout(timeout).unwrap_or_else(|_| {
Err(format!(
"the shell at {} didn't answer within {} s",
path.display(),
timeout.as_secs_f64()
))
})
}
fn exchange(path: &Path, line: &str) -> Result<String, String> {
let mut stream = UnixStream::connect(path).map_err(|error| match error.kind() {
ErrorKind::NotFound | ErrorKind::ConnectionRefused => {
format!("no shell is listening at {}", path.display())
}
_ => format!("can't reach the shell at {}: {error}", path.display()),
})?;
let _ = stream.write_all(line.as_bytes());
let mut reply = Vec::new();
std::io::BufReader::new(stream.take(MAX_REPLY))
.read_until(b'\n', &mut reply)
.map_err(|error| format!("can't read the shell's reply: {error}"))?;
read_reply(&reply)
}
pub(crate) fn run(ask_for: &Ask) -> Result<ExitCode, String> {
let path = socket_path(
std::env::var_os("XDG_RUNTIME_DIR").as_deref(),
std::env::var_os("WAYLAND_DISPLAY").as_deref(),
)
.ok_or("XDG_RUNTIME_DIR isn't set, so no shell is listening")?;
let text = ask(&path, request(ask_for), TIMEOUT)?;
let _ = std::io::stdout().write_all(shown(&text).as_bytes());
Ok(ExitCode::SUCCESS)
}