use crate::proto::{b64_encode, send_msg, Out};
use crate::store::expand;
use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
use serde_json::{json, Value};
use std::io::{Read, Write};
use std::thread::JoinHandle;
pub struct PtySession {
writer: Box<dyn Write + Send>,
master: Box<dyn MasterPty + Send>,
child: Box<dyn Child + Send + Sync>,
reader: Option<JoinHandle<()>>,
}
fn default_shell() -> String {
if cfg!(windows) {
std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".into())
} else {
std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into())
}
}
fn output_frame(key: &str, bytes: &[u8]) -> Value {
let b64 = b64_encode(bytes);
if key.is_empty() {
json!({ "ev": "output", "b64": b64 })
} else {
json!({ "ev": "output", "pty": key, "b64": b64 })
}
}
fn exit_frame(key: &str) -> Value {
if key.is_empty() {
json!({ "ev": "exit" })
} else {
json!({ "ev": "exit", "pty": key })
}
}
impl PtySession {
pub fn spawn(out: &Out, req: &Value, key: String) -> Option<PtySession> {
let rows = req["rows"].as_u64().unwrap_or(24) as u16;
let cols = req["cols"].as_u64().unwrap_or(80) as u16;
let pair = native_pty_system()
.openpty(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})
.ok()?;
let program = req["shell"]
.as_str()
.map_or_else(default_shell, String::from);
let mut cmd = CommandBuilder::new(&program);
match req["args"].as_array() {
Some(args) => {
for a in args {
if let Some(s) = a.as_str() {
cmd.arg(s);
}
}
}
None => cmd.arg("-l"),
}
if let Some(cwd) = req["cwd"].as_str() {
cmd.cwd(expand(cwd));
}
cmd.env("TERM", "xterm-256color");
if let Some(env) = req["env"].as_object() {
for (k, v) in env {
if let Some(s) = v.as_str() {
cmd.env(k, s);
}
}
}
let child = pair.slave.spawn_command(cmd).ok()?;
drop(pair.slave);
let mut reader = pair.master.try_clone_reader().ok()?;
let writer = pair.master.take_writer().ok()?;
let out2 = out.clone();
let handle = std::thread::spawn(move || {
let mut buf = [0u8; 65536];
loop {
match reader.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
if send_msg(&out2, &output_frame(&key, &buf[..n])).is_err() {
break;
}
}
}
}
let _ = send_msg(&out2, &exit_frame(&key));
});
Some(PtySession {
writer,
master: pair.master,
child,
reader: Some(handle),
})
}
pub fn write(&mut self, req: &Value) {
let bytes = if let Some(s) = req["data"].as_str() {
s.as_bytes().to_vec()
} else if let Some(b) = req["b64"].as_str() {
crate::proto::b64_decode(b).unwrap_or_default()
} else {
return;
};
let _ = self.writer.write_all(&bytes);
let _ = self.writer.flush();
}
pub fn resize(&self, req: &Value) {
let rows = req["rows"].as_u64().unwrap_or(24) as u16;
let cols = req["cols"].as_u64().unwrap_or(80) as u16;
let _ = self.master.resize(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
});
}
}
impl Drop for PtySession {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
if let Some(h) = self.reader.take() {
let _ = h.join();
}
}
}