use std::io::{self, Read, Write};
use std::net::TcpStream;
use std::time::Duration;
pub fn resolve_session(session_name: &str) -> io::Result<(u16, String)> {
let home = std::env::var("USERPROFILE")
.or_else(|_| std::env::var("HOME"))
.unwrap_or_default();
let port_path = format!("{}\\.psmux\\{}.port", home, session_name);
let port: u16 = std::fs::read_to_string(&port_path)
.map_err(|_| io::Error::new(io::ErrorKind::NotFound,
format!("no server for session '{}'", session_name)))?
.trim()
.parse()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bad port file"))?;
let key = crate::session::read_session_key(session_name).unwrap_or_default();
Ok((port, key))
}
fn send_to_session(port: u16, key: &str, cmd: &str) -> io::Result<String> {
let addr = format!("127.0.0.1:{}", port);
let mut stream = TcpStream::connect_timeout(
&addr.parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("{}", e)))?,
Duration::from_millis(2000),
)?;
let _ = stream.set_nodelay(true);
let _ = stream.set_read_timeout(Some(Duration::from_millis(5000)));
write!(stream, "AUTH {}\n{}\n", key, cmd)?;
stream.flush()?;
let mut buf = Vec::new();
let mut tmp = [0u8; 65536];
const MAX_RESPONSE: usize = 4 * 1024 * 1024; loop {
match stream.read(&mut tmp) {
Ok(0) => break,
Ok(n) => {
buf.extend_from_slice(&tmp[..n]);
if buf.len() > MAX_RESPONSE {
return Err(io::Error::new(io::ErrorKind::InvalidData,
"response exceeded 4 MB limit"));
}
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock
|| e.kind() == io::ErrorKind::TimedOut => break,
Err(_) => break,
}
}
let r = String::from_utf8_lossy(&buf).to_string();
Ok(if r.starts_with("OK\n") { r[3..].to_string() } else { r })
}
pub fn orchestrate_cross_session_join(
src_session: &str,
src_window: usize,
src_pane: usize,
tgt_session: &str,
tgt_window: Option<usize>,
tgt_pane: Option<usize>,
horizontal: bool,
) -> io::Result<()> {
let (src_port, src_key) = resolve_session(src_session)?;
let (tgt_port, tgt_key) = resolve_session(tgt_session)?;
let src_addr = format!("127.0.0.1:{}", src_port);
let extract_cmd = format!("pane-forward-extract {}.{}", src_window, src_pane);
let extract_resp = send_to_session(src_port, &src_key, &extract_cmd)?;
let extract_resp = extract_resp.trim();
if !extract_resp.starts_with("FORWARD ") {
return Err(io::Error::new(io::ErrorKind::Other,
format!("extract failed: {}", extract_resp)));
}
let parts: Vec<&str> = extract_resp.splitn(8, ' ').collect();
if parts.len() < 8 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "bad FORWARD response"));
}
let forward_id: u64 = parts[1].parse().unwrap_or(0);
let fwd_port: u16 = parts[2].parse().unwrap_or(0);
let pid: u32 = parts[3].parse().unwrap_or(0);
let title = parts[4].replace('\x01', " "); let rows: u16 = parts[5].parse().unwrap_or(24);
let cols: u16 = parts[6].parse().unwrap_or(80);
let screen_b64_len: usize = parts[7].parse().unwrap_or(0);
let screen_b64 = if screen_b64_len > 0 {
if let Some(nl_pos) = extract_resp.find('\n') {
let data = &extract_resp[nl_pos + 1..];
if data.len() >= screen_b64_len {
Some(data[..screen_b64_len].to_string())
} else {
Some(data.to_string())
}
} else {
None
}
} else {
None
};
let _tgt_spec = match (tgt_window, tgt_pane) {
(Some(w), Some(p)) => format!("{}.{}", w, p),
(Some(w), None) => format!("{}", w),
_ => String::new(),
};
let h_flag = if horizontal { " -h" } else { "" };
let screen_payload = screen_b64.as_deref().unwrap_or("");
let inject_cmd = format!(
"pane-forward-inject {} {} {} {} {} {} {} {} {} {}{}\n{}",
src_session,
src_addr,
src_key,
forward_id,
fwd_port,
pid,
title.replace(' ', "\x01"),
rows,
cols,
screen_payload.len(),
h_flag,
screen_payload,
);
let inject_resp = send_to_session(tgt_port, &tgt_key, &inject_cmd)?;
if inject_resp.trim().starts_with("ERR") {
return Err(io::Error::new(io::ErrorKind::Other,
format!("inject failed: {}", inject_resp.trim())));
}
Ok(())
}