use crate::protocol::{self, ServerMsg};
use crate::session::{ClientGuard, SessionHandles, SessionManager, DEFAULT_COLS, DEFAULT_ROWS};
use std::sync::{Arc, Mutex as StdMutex};
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex;
use tracing::{debug, warn};
use super::shared::{lock_mutex, store_dims};
pub(super) struct SessionSetup {
pub(super) handles: SessionHandles,
pub(super) is_new_session: bool,
pub(super) evict_rx: tokio::sync::watch::Receiver<bool>,
pub(super) client_guard: ClientGuard,
}
pub(super) struct ConnectRequest {
pub(super) name: String,
pub(super) history: usize,
pub(super) cols: u16,
pub(super) rows: u16,
pub(super) leftover: Vec<u8>,
pub(super) mode: crate::protocol::ConnectMode,
}
pub(super) fn resize_pty(
master: &crate::pty::SharedMasterPty,
screen: &crate::session::SharedScreen,
cols: u16,
rows: u16,
) -> anyhow::Result<()> {
let retach::screen::TerminalSize { cols, rows } =
retach::screen::sanitize_dimensions(cols, rows);
let mut scr = lock_mutex(screen, "screen")?;
let m = lock_mutex(master, "master")?;
m.resize(portable_pty::PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})?;
scr.resize(cols, rows);
Ok(())
}
pub(super) async fn resize_or_sigwinch(
master: &crate::pty::SharedMasterPty,
screen: &crate::session::SharedScreen,
dims: &Arc<StdMutex<retach::screen::TerminalSize>>,
cols: u16,
rows: u16,
current_dims: retach::screen::TerminalSize,
session_name: &str,
) -> anyhow::Result<()> {
let master = master.clone();
let screen = screen.clone();
let dims = dims.clone();
let name = session_name.to_string();
if current_dims.cols != cols || current_dims.rows != rows {
debug!(
session = %session_name,
old_cols = current_dims.cols, old_rows = current_dims.rows,
new_cols = cols, new_rows = rows,
"resizing session"
);
tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
resize_pty(&master, &screen, cols, rows)?;
store_dims(&dims, cols, rows, &name);
Ok(())
})
.await??;
} else {
debug!(session = %session_name, "sending SIGWINCH (same dimensions)");
tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let sanitized = retach::screen::sanitize_dimensions(cols, rows);
let m = lock_mutex(&master, "master")?;
m.resize(portable_pty::PtySize {
rows: sanitized.rows,
cols: sanitized.cols,
pixel_width: 0,
pixel_height: 0,
})
.map_err(|e| anyhow::anyhow!("{}", e))
})
.await??;
}
Ok(())
}
async fn send_error_to_client(stream: &mut tokio::net::UnixStream, msg: String) -> anyhow::Error {
if let Ok(resp) = protocol::encode(&ServerMsg::Error(msg.clone())) {
let _ = stream.write_all(&resp).await;
}
anyhow::anyhow!("{}", msg)
}
pub(super) async fn setup_session(
stream: &mut tokio::net::UnixStream,
manager: &Arc<Mutex<SessionManager>>,
name: &str,
history: usize,
cols: u16,
rows: u16,
mode: crate::protocol::ConnectMode,
) -> anyhow::Result<SessionSetup> {
let mut mgr = manager.lock().await;
use crate::protocol::ConnectMode;
let (session, is_new) = match mode {
ConnectMode::CreateOrAttach => match mgr.get_or_create(name, cols, rows, history) {
Ok(s) => s,
Err(e) => {
return Err(send_error_to_client(stream, format!("{}", e)).await);
}
},
ConnectMode::CreateOnly => {
if mgr.get(name).is_some() {
return Err(send_error_to_client(
stream,
format!("session '{}' already exists", name),
)
.await);
}
if let Err(e) = mgr.create(name.to_string(), cols, rows, history) {
return Err(send_error_to_client(stream, format!("{}", e)).await);
}
match mgr.get_mut(name) {
Some(s) => (s, true),
None => {
return Err(send_error_to_client(
stream,
"session disappeared after creation".into(),
)
.await);
}
}
}
ConnectMode::AttachOnly => match mgr.get_mut(name) {
Some(s) => (s, false),
None => {
return Err(
send_error_to_client(stream, format!("session '{}' not found", name)).await,
);
}
},
};
let (client_guard, handles, evict_rx) = session.connect();
drop(mgr);
if !is_new {
let cur_dims = match handles.dims.lock() {
Ok(d) => *d,
Err(e) => {
warn!(session = %name, error = %e, "dims mutex poisoned during reattach");
retach::screen::TerminalSize {
cols: DEFAULT_COLS,
rows: DEFAULT_ROWS,
}
}
};
if let Err(e) = resize_or_sigwinch(
&handles.master,
&handles.screen,
&handles.dims,
cols,
rows,
cur_dims,
&handles.name,
)
.await
{
warn!(session = %name, error = %e, "failed to resize/SIGWINCH on reattach");
anyhow::bail!("failed to resize/SIGWINCH on reattach to '{}'", name);
}
}
Ok(SessionSetup {
handles,
is_new_session: is_new,
evict_rx,
client_guard,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::SessionManager;
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::test]
async fn setup_creates_new_session() {
let manager = Arc::new(Mutex::new(SessionManager::new()));
let (_client, mut server) = tokio::net::UnixStream::pair().unwrap();
let result = setup_session(
&mut server,
&manager,
"test-new",
100,
80,
24,
crate::protocol::ConnectMode::CreateOrAttach,
)
.await;
assert!(result.is_ok(), "setup_session failed: {:?}", result.err());
let setup = result.unwrap();
assert!(setup.is_new_session);
assert_eq!(setup.handles.name, "test-new");
}
#[tokio::test]
async fn setup_reattaches_existing_session() {
let manager = Arc::new(Mutex::new(SessionManager::new()));
{
let (_client, mut server) = tokio::net::UnixStream::pair().unwrap();
let setup = setup_session(
&mut server,
&manager,
"test-reattach",
100,
80,
24,
crate::protocol::ConnectMode::CreateOrAttach,
)
.await
.unwrap();
assert!(setup.is_new_session);
}
{
let (_client, mut server) = tokio::net::UnixStream::pair().unwrap();
let setup = setup_session(
&mut server,
&manager,
"test-reattach",
100,
80,
24,
crate::protocol::ConnectMode::CreateOrAttach,
)
.await
.unwrap();
assert!(!setup.is_new_session);
}
}
#[tokio::test]
async fn setup_create_only_fails_for_existing() {
let manager = Arc::new(Mutex::new(SessionManager::new()));
{
let (_client, mut server) = tokio::net::UnixStream::pair().unwrap();
setup_session(
&mut server,
&manager,
"test-create-only",
100,
80,
24,
crate::protocol::ConnectMode::CreateOrAttach,
)
.await
.unwrap();
}
let (_client, mut server) = tokio::net::UnixStream::pair().unwrap();
let result = setup_session(
&mut server,
&manager,
"test-create-only",
100,
80,
24,
crate::protocol::ConnectMode::CreateOnly,
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn setup_attach_only_fails_for_missing() {
let manager = Arc::new(Mutex::new(SessionManager::new()));
let (_client, mut server) = tokio::net::UnixStream::pair().unwrap();
let result = setup_session(
&mut server,
&manager,
"nonexistent",
100,
80,
24,
crate::protocol::ConnectMode::AttachOnly,
)
.await;
assert!(result.is_err());
}
}