use std::collections::HashMap;
use std::io::{self, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{self, Sender};
use std::sync::Arc;
use std::time::{Duration, Instant};
use alacritty_terminal::event::Event as TermEvent;
use alacritty_terminal::sync::FairMutex;
use alacritty_terminal::vte::ansi::{Processor, StdSyncHandler};
use alacritty_terminal::Term;
use parking_lot::Mutex;
use super::protocol::{ClientMsg, Frame, PaneInfo, ServerMsg, Size, VERSION};
use super::server::socket_path;
use crate::session::{EventProxy, EventSink, GridSize, PaneId, UserEvent};
use crate::shell::ShellState;
const REPLY_TIMEOUT: Duration = Duration::from_secs(5);
pub struct Replica {
pub term: Arc<FairMutex<Term<EventProxy>>>,
pub shell: Arc<Mutex<ShellState>>,
pub pid: u32,
info: PaneInfo,
parser: Processor<StdSyncHandler>,
rewind: Arc<Mutex<crate::rewind::Recorder>>,
images: Arc<Mutex<crate::graphics::Images>>,
graphics: crate::graphics::GraphicsTap,
}
type Replicas = Arc<Mutex<HashMap<PaneId, Replica>>>;
pub type ReplicaHandle = (
Arc<FairMutex<Term<EventProxy>>>,
Arc<Mutex<ShellState>>,
u32,
Arc<Mutex<crate::rewind::Recorder>>,
Arc<Mutex<crate::graphics::Images>>,
);
pub struct DaemonClient {
writer: Arc<Mutex<UnixStream>>,
next_req: AtomicU64,
pending: Arc<Mutex<HashMap<u64, Sender<ServerMsg>>>>,
ready: Arc<Mutex<Option<Sender<ServerMsg>>>>,
replicas: Replicas,
}
fn grid(size: Size) -> GridSize {
GridSize {
cols: size.cols.max(2) as usize,
rows: size.rows.max(1) as usize,
}
}
fn shell_state(info: &super::protocol::ShellInfo) -> ShellState {
ShellState {
cwd: info.cwd.clone(),
remote_cwd: info.remote_cwd.clone(),
last_exit: info.last_exit,
command_running: info.command_running,
prompts: info.prompts,
blocks: info.blocks.iter().cloned().collect(),
notice: info.notice.clone(),
}
}
impl DaemonClient {
pub fn connect_or_start(sink: EventSink) -> io::Result<Arc<Self>> {
let path = socket_path();
let stream = match UnixStream::connect(&path) {
Ok(s) => s,
Err(_) => {
start_daemon()?;
let deadline = Instant::now() + Duration::from_secs(3);
loop {
match UnixStream::connect(&path) {
Ok(s) => break s,
Err(e) if Instant::now() >= deadline => return Err(e),
Err(_) => std::thread::sleep(Duration::from_millis(50)),
}
}
}
};
Self::start(stream, Some(sink))
}
pub fn connect_existing() -> io::Result<Arc<Self>> {
Self::start(UnixStream::connect(socket_path())?, None)
}
fn start(stream: UnixStream, sink: Option<EventSink>) -> io::Result<Arc<Self>> {
let read_half = stream.try_clone()?;
let client = Arc::new(Self {
writer: Arc::new(Mutex::new(stream)),
next_req: AtomicU64::new(1),
pending: Arc::new(Mutex::new(HashMap::new())),
ready: Arc::new(Mutex::new(None)),
replicas: Arc::new(Mutex::new(HashMap::new())),
});
let (pending, ready, replicas, writer) = (
client.pending.clone(),
client.ready.clone(),
client.replicas.clone(),
client.writer.clone(),
);
std::thread::Builder::new()
.name("daemon reader".into())
.spawn(move || read_loop(read_half, sink, pending, ready, replicas, writer))?;
client.send(&ClientMsg::Hello {
version: VERSION.into(),
})?;
Ok(client)
}
pub fn send(&self, msg: &ClientMsg) -> io::Result<()> {
let mut w = self.writer.lock();
Frame::json(msg).write_to(&mut *w)?;
w.flush()
}
pub fn send_input(&self, pane: PaneId, bytes: Vec<u8>) {
let mut w = self.writer.lock();
let _ = Frame::Input(pane, bytes).write_to(&mut *w);
let _ = w.flush();
}
fn request(&self, make: impl FnOnce(u64) -> ClientMsg) -> io::Result<ServerMsg> {
let req = self.next_req.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = mpsc::channel();
self.pending.lock().insert(req, tx);
self.send(&make(req))?;
rx.recv_timeout(REPLY_TIMEOUT)
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "the daemon didn't answer"))
}
fn session(&self, msg: ClientMsg) -> io::Result<(String, serde_json::Value, Vec<PaneInfo>)> {
let (tx, rx) = mpsc::channel();
*self.ready.lock() = Some(tx);
self.send(&msg)?;
let reply = rx
.recv_timeout(REPLY_TIMEOUT)
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "the daemon didn't answer"))?;
*self.ready.lock() = None;
match reply {
ServerMsg::SessionReady {
name,
layout,
panes,
} => Ok((name, layout, panes)),
ServerMsg::Error { message } => Err(io::Error::other(message)),
other => Err(io::Error::other(format!("unexpected reply {other:?}"))),
}
}
pub fn new_session(
&self,
name: Option<String>,
) -> io::Result<(String, serde_json::Value, Vec<PaneInfo>)> {
self.session(ClientMsg::NewSession { name })
}
pub fn attach(
&self,
name: Option<String>,
force: bool,
) -> io::Result<(String, serde_json::Value, Vec<PaneInfo>)> {
self.session(ClientMsg::Attach { name, force })
}
pub fn spawn(&self, build: impl FnOnce(u64) -> ClientMsg) -> io::Result<PaneInfo> {
match self.request(build)? {
ServerMsg::Spawned { pane, .. } => Ok(pane),
ServerMsg::Ack { message, .. } => Err(io::Error::other(message)),
other => Err(io::Error::other(format!("unexpected reply {other:?}"))),
}
}
pub fn list_sessions(&self) -> io::Result<Vec<super::protocol::SessionSummary>> {
match self.request(|req| ClientMsg::ListSessions { req })? {
ServerMsg::Sessions { sessions, .. } => Ok(sessions),
other => Err(io::Error::other(format!("unexpected reply {other:?}"))),
}
}
pub fn kill_session(&self, name: &str) -> io::Result<()> {
match self.request(|req| ClientMsg::KillSession {
req,
name: name.to_string(),
})? {
ServerMsg::Ack { ok: true, .. } => Ok(()),
ServerMsg::Ack { message, .. } => Err(io::Error::other(message)),
other => Err(io::Error::other(format!("unexpected reply {other:?}"))),
}
}
pub fn replica(&self, pane: PaneId) -> Option<ReplicaHandle> {
self.replicas.lock().get(&pane).map(|r| {
(
r.term.clone(),
r.shell.clone(),
r.pid,
r.rewind.clone(),
r.images.clone(),
)
})
}
pub fn forget(&self, pane: PaneId) {
self.replicas.lock().remove(&pane);
}
pub fn close(&self) {
let _ = self.writer.lock().shutdown(std::net::Shutdown::Both);
}
}
fn start_daemon() -> io::Result<()> {
use std::os::unix::process::CommandExt;
let exe: PathBuf = std::env::current_exe()?;
let mut child = Command::new(exe)
.arg("+daemon")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.process_group(0)
.spawn()?;
std::thread::spawn(move || {
let _ = child.wait();
});
Ok(())
}
fn new_replica(info: &PaneInfo, sink: &EventSink, writer: &Arc<Mutex<UnixStream>>) -> Replica {
let config = super::term_config(&info.settings);
let images = Arc::new(Mutex::new(crate::graphics::Images::default()));
let (pane, writer) = (info.id, writer.clone());
let respond: crate::graphics::Responder = Box::new(move |bytes: &[u8]| {
let mut w = writer.lock();
let _ = Frame::Input(pane, bytes.to_vec()).write_to(&mut *w);
});
let listener = EventProxy::new(info.id, sink.clone());
Replica {
term: Arc::new(FairMutex::new(Term::new(
config,
&grid(info.size),
listener,
))),
shell: Arc::new(Mutex::new(shell_state(&info.shell))),
pid: info.pid,
info: info.clone(),
parser: Processor::new(),
rewind: Arc::new(Mutex::new(crate::rewind::Recorder::new(grid(info.size)))),
graphics: crate::graphics::GraphicsTap::new(Some(images.clone()), false, Some(respond)),
images,
}
}
fn read_loop(
stream: UnixStream,
sink: Option<EventSink>,
pending: Arc<Mutex<HashMap<u64, Sender<ServerMsg>>>>,
ready: Arc<Mutex<Option<Sender<ServerMsg>>>>,
replicas: Replicas,
writer: Arc<Mutex<UnixStream>>,
) {
let mut reader = BufReader::new(stream);
let wake = |pane: PaneId, event: TermEvent| {
if let Some(sink) = &sink {
sink(UserEvent::Term(pane, event));
}
};
while let Ok(Some(frame)) = Frame::read_from(&mut reader) {
match frame {
Frame::Output(pane, bytes) => {
if let Some(r) = replicas.lock().get_mut(&pane) {
let mut shown = Vec::with_capacity(bytes.len());
r.graphics.feed(&bytes, &mut shown);
let mut term = r.term.lock();
r.parser.advance(&mut *term, &shown);
r.rewind.lock().feed(&shown);
}
wake(pane, TermEvent::Wakeup);
}
Frame::Snapshot(pane, bytes) => {
if let Some(r) = replicas.lock().get_mut(&pane) {
let mut term = r.term.lock();
let size = grid(r.info.size);
let listener =
EventProxy::new(pane, sink.clone().expect("replicas need an event sink"));
*term = Term::new(super::term_config(&r.info.settings), &size, listener);
r.parser = Processor::new();
r.parser.advance(&mut *term, &bytes);
r.rewind.lock().reset(size, &bytes);
}
wake(pane, TermEvent::Wakeup);
}
Frame::Json(value) => {
let Ok(msg) = serde_json::from_value::<ServerMsg>(value) else {
continue;
};
match &msg {
ServerMsg::SessionReady { panes, .. } => {
if let Some(sink) = &sink {
let mut map = replicas.lock();
for info in panes {
map.insert(info.id, new_replica(info, sink, &writer));
}
}
}
ServerMsg::Spawned { pane, .. } => {
if let Some(sink) = &sink {
replicas
.lock()
.insert(pane.id, new_replica(pane, sink, &writer));
}
}
ServerMsg::ShellState { pane, shell } => {
if let Some(r) = replicas.lock().get(pane) {
*r.shell.lock() = shell_state(shell);
}
wake(*pane, TermEvent::Wakeup);
continue;
}
ServerMsg::Detached { reason } => {
if let Some(sink) = &sink {
sink(UserEvent::DaemonLost(reason.clone()));
}
continue;
}
ServerMsg::PaneExited { pane } => {
replicas.lock().remove(pane);
wake(*pane, TermEvent::Exit);
continue;
}
_ => {}
}
if let Some(req) = msg.reply_to() {
if let Some(tx) = pending.lock().remove(&req) {
let _ = tx.send(msg);
}
} else if matches!(
msg,
ServerMsg::SessionReady { .. } | ServerMsg::Error { .. }
) {
if let Some(tx) = ready.lock().as_ref() {
let _ = tx.send(msg);
} else if let ServerMsg::Error { message } = msg {
eprintln!("cyberterm: daemon: {message}");
}
}
}
Frame::Input(..) => {}
}
}
if let Some(sink) = &sink {
sink(UserEvent::DaemonLost(
"lost the session daemon; these shells are gone".into(),
));
}
}