use std::io::{self, BufRead, BufReader, Read, Write};
use std::net::{SocketAddr, TcpListener};
use std::process::{Child, ChildStderr, Command, Stdio};
use std::thread::{self, JoinHandle};
pub(crate) struct Transport {
pub reader: BufReader<Box<dyn Read + Send>>,
pub writer: Box<dyn Write + Send>,
_child: Option<Child>,
_stderr_thread: Option<JoinHandle<()>>,
_socket_path: Option<String>,
transport_name: &'static str,
pub expected_token: Option<String>,
}
pub(crate) enum ListenAddr {
Auto,
Unix(String),
Tcp(SocketAddr),
}
impl ListenAddr {
pub fn parse(arg: Option<&str>) -> Result<Self, String> {
match arg {
None => Ok(ListenAddr::Auto),
Some("") => Ok(ListenAddr::Auto),
Some(s) if s.starts_with(':') => {
let port: u16 = s[1..]
.parse()
.map_err(|_| format!("invalid port in '{s}'"))?;
Ok(ListenAddr::Tcp(SocketAddr::from(([127, 0, 0, 1], port))))
}
Some(s) if s.contains(':') && !s.starts_with('/') => {
let addr: SocketAddr = s
.parse()
.map_err(|e| format!("invalid address '{s}': {e}"))?;
Ok(ListenAddr::Tcp(addr))
}
Some(s) => Ok(ListenAddr::Unix(s.to_string())),
}
}
}
impl Transport {
pub fn stdio() -> Self {
Self {
reader: BufReader::with_capacity(64 * 1024, Box::new(io::stdin())),
writer: Box::new(io::stdout()),
_child: None,
_stderr_thread: None,
_socket_path: None,
transport_name: "stdio",
expected_token: None,
}
}
pub fn exec(command: &str) -> io::Result<Self> {
let shell = if cfg!(windows) { "cmd" } else { "sh" };
let shell_flag = if cfg!(windows) { "/c" } else { "-c" };
let mut child = Command::new(shell)
.args([shell_flag, command])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| io::Error::other(format!("failed to exec '{command}': {e}")))?;
let child_stdout = child.stdout.take().expect("child stdout piped");
let child_stdin = child.stdin.take().expect("child stdin piped");
let child_stderr = child.stderr.take().expect("child stderr piped");
let stderr_thread = spawn_stderr_forwarder(child_stderr);
Ok(Self {
reader: BufReader::with_capacity(64 * 1024, Box::new(child_stdout)),
writer: Box::new(child_stdin),
_child: Some(child),
_stderr_thread: Some(stderr_thread),
_socket_path: None,
transport_name: "exec",
expected_token: None,
})
}
pub fn listen(addr: &ListenAddr, exec_command: Option<&str>) -> io::Result<Self> {
let token = generate_token();
let (listener, display_addr, socket_path) = create_listener(addr)?;
let (mut child, stderr_thread) = if let Some(command) = exec_command {
let mut c = spawn_listen_child(command, &display_addr, &token)?;
let child_stderr = c.stderr.take().expect("child stderr piped");
let stderr_thread = spawn_stderr_forwarder(child_stderr);
log::info!("waiting for host to connect to {display_addr}...");
(Some(c), Some(stderr_thread))
} else {
print_connection_info(&display_addr, &token);
log::info!("waiting for connection on {display_addr}...");
(None, None)
};
let (reader, writer) = if let Some(ref mut c) = child {
accept_with_child_check(&listener, c, &socket_path)?
} else {
accept_connection(&listener)?
};
log::info!("host connected");
Ok(Self {
reader: BufReader::with_capacity(64 * 1024, reader),
writer,
_child: child,
_stderr_thread: stderr_thread,
_socket_path: socket_path,
transport_name: "listen",
expected_token: Some(token),
})
}
pub fn name(&self) -> &'static str {
self.transport_name
}
#[allow(clippy::type_complexity)]
pub fn into_parts(
self,
) -> (
BufReader<Box<dyn Read + Send>>,
Box<dyn Write + Send>,
TransportGuard,
Option<String>,
) {
(
self.reader,
self.writer,
TransportGuard {
_child: self._child,
_stderr_thread: self._stderr_thread,
_socket_path: self._socket_path,
},
self.expected_token,
)
}
}
pub(crate) struct TransportGuard {
_child: Option<Child>,
_stderr_thread: Option<JoinHandle<()>>,
_socket_path: Option<String>,
}
impl Drop for TransportGuard {
fn drop(&mut self) {
if let Some(ref mut child) = self._child {
let _ = child.kill();
let _ = child.wait();
}
if let Some(ref path) = self._socket_path {
let _ = std::fs::remove_file(path);
if let Some(parent) = std::path::Path::new(path).parent() {
let _ = std::fs::remove_dir(parent);
}
}
}
}
enum Listener {
#[cfg(unix)]
Unix(std::os::unix::net::UnixListener),
Tcp(TcpListener),
}
type ReaderWriter = (Box<dyn Read + Send>, Box<dyn Write + Send>);
fn create_listener(addr: &ListenAddr) -> io::Result<(Listener, String, Option<String>)> {
match addr {
ListenAddr::Auto => {
#[cfg(unix)]
{
let path = auto_socket_path();
let listener = bind_unix(&path)?;
Ok((Listener::Unix(listener), path.clone(), Some(path)))
}
#[cfg(not(unix))]
{
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
Ok((Listener::Tcp(listener), addr.to_string(), None))
}
}
ListenAddr::Unix(path) => {
#[cfg(unix)]
{
let listener = bind_unix(path)?;
Ok((Listener::Unix(listener), path.clone(), Some(path.clone())))
}
#[cfg(not(unix))]
{
Err(io::Error::other(
"Unix sockets not supported on this platform",
))
}
}
ListenAddr::Tcp(addr) => {
let listener = TcpListener::bind(addr)?;
let bound = listener.local_addr()?;
Ok((Listener::Tcp(listener), bound.to_string(), None))
}
}
}
#[cfg(unix)]
fn bind_unix(path: &str) -> io::Result<std::os::unix::net::UnixListener> {
let _ = std::fs::remove_file(path);
if let Some(parent) = std::path::Path::new(path).parent() {
std::fs::create_dir_all(parent)?;
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700));
}
std::os::unix::net::UnixListener::bind(path)
}
#[cfg(unix)]
fn auto_socket_path() -> String {
let dir = format!("/tmp/plushie-{}", random_hex(8));
format!("{dir}/plushie.sock")
}
fn accept_connection(listener: &Listener) -> io::Result<ReaderWriter> {
match listener {
#[cfg(unix)]
Listener::Unix(l) => {
let (stream, _) = l.accept()?;
let reader = stream.try_clone()?;
Ok((Box::new(reader), Box::new(stream)))
}
Listener::Tcp(l) => {
let (stream, _) = l.accept()?;
let reader = stream.try_clone()?;
Ok((Box::new(reader), Box::new(stream)))
}
}
}
fn accept_with_child_check(
listener: &Listener,
child: &mut Child,
socket_path: &Option<String>,
) -> io::Result<ReaderWriter> {
set_listener_nonblocking(listener, true)?;
loop {
match accept_connection(listener) {
Ok(rw) => return Ok(rw),
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
if let Some(status) = child.try_wait().ok().flatten() {
if let Some(path) = &socket_path {
let _ = std::fs::remove_file(path);
}
return Err(io::Error::other(format!(
"child exited with {status} before connecting"
)));
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(e) => return Err(e),
}
}
}
fn set_listener_nonblocking(listener: &Listener, nonblocking: bool) -> io::Result<()> {
match listener {
#[cfg(unix)]
Listener::Unix(l) => l.set_nonblocking(nonblocking),
Listener::Tcp(l) => l.set_nonblocking(nonblocking),
}
}
fn spawn_listen_child(command: &str, socket_addr: &str, token: &str) -> io::Result<Child> {
let shell = if cfg!(windows) { "cmd" } else { "sh" };
let shell_flag = if cfg!(windows) { "/c" } else { "-c" };
let negotiation = format!(
"{{\"token\":\"{token}\",\"protocol\":{}}}\n",
plushie_ext::protocol::PROTOCOL_VERSION
);
let mut child = Command::new(shell)
.args([shell_flag, command])
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::piped())
.env("PLUSHIE_SOCKET", socket_addr)
.env("PLUSHIE_TOKEN", token)
.spawn()
.map_err(|e| io::Error::other(format!("failed to exec '{command}': {e}")))?;
if let Some(ref mut stdin) = child.stdin {
let _ = stdin.write_all(negotiation.as_bytes());
let _ = stdin.flush();
}
Ok(child)
}
fn print_connection_info(addr: &str, token: &str) {
println!("Plushie renderer listening.\n");
println!(" Address: {addr}");
println!(" Token: {token}\n");
println!("Connect with:");
println!(" mix plushie.connect MyApp {addr} --token {token}\n");
println!("Via SSH:");
println!(" ssh -T -R {addr}:{addr} server \\");
println!(" 'mix plushie.connect MyApp {addr} --token {token}'\n");
}
fn generate_token() -> String {
random_hex(16)
}
fn random_hex(bytes: usize) -> String {
let mut buf = vec![0u8; bytes];
#[cfg(unix)]
if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
let _ = io::Read::read_exact(&mut f, &mut buf);
return buf.iter().map(|b| format!("{b:02x}")).collect();
}
let seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
for (i, b) in buf.iter_mut().enumerate() {
*b = ((seed >> ((i % 16) * 8))
^ (std::process::id() as u128).wrapping_mul(0x9e3779b97f4a7c15)) as u8;
}
buf.iter().map(|b| format!("{b:02x}")).collect()
}
fn spawn_stderr_forwarder(stderr: ChildStderr) -> JoinHandle<()> {
thread::spawn(move || {
let reader = BufReader::new(stderr);
let mut lines = reader.lines();
while let Some(Ok(line)) = lines.next() {
eprintln!("[remote] {line}");
}
})
}