use std::collections::VecDeque;
use std::net::SocketAddr;
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use bytes::Bytes;
use tokio::sync::mpsc;
pub const CLOSE_JOIN_TIMEOUT: Duration = Duration::from_secs(5);
pub const CHANNEL_CAPACITY: usize = 64;
#[derive(Debug)]
pub enum DownMsg {
Data(Bytes),
Eof,
}
#[derive(Debug)]
pub enum UpMsg {
Data(Bytes),
Eof,
}
#[derive(Debug)]
pub struct PendingSession {
pub exec_command: Option<String>,
pub username: Option<String>,
pub peer_addr: Option<SocketAddr>,
pub pty_size: SharedPtySize,
pub up_rx: mpsc::Receiver<UpMsg>,
pub down_tx: mpsc::Sender<DownMsg>,
}
#[derive(Debug)]
pub struct QueueState {
sessions: VecDeque<PendingSession>,
shutdown: bool,
}
pub enum Dequeue {
Session(PendingSession),
Shutdown,
Empty,
}
#[derive(Debug)]
pub struct SessionQueue {
state: Mutex<QueueState>,
cvar: Condvar,
}
impl SessionQueue {
pub fn new() -> Self {
Self {
state: Mutex::new(QueueState {
sessions: VecDeque::new(),
shutdown: false,
}),
cvar: Condvar::new(),
}
}
pub fn push(&self, session: PendingSession) {
let mut guard = self
.state
.lock()
.unwrap_or_else(|poison| poison.into_inner());
if guard.shutdown {
return;
}
guard.sessions.push_back(session);
self.cvar.notify_one();
}
pub fn signal_shutdown(&self) {
let mut guard = self
.state
.lock()
.unwrap_or_else(|poison| poison.into_inner());
guard.shutdown = true;
self.cvar.notify_all();
}
pub fn try_pop(&self) -> Dequeue {
let mut guard = self
.state
.lock()
.unwrap_or_else(|poison| poison.into_inner());
if let Some(session) = guard.sessions.pop_front() {
Dequeue::Session(session)
} else if guard.shutdown {
Dequeue::Shutdown
} else {
Dequeue::Empty
}
}
pub fn wait_for_session(&self, timeout: Duration) -> Dequeue {
let guard = self
.state
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let (mut guard, _timeout_result) = self
.cvar
.wait_timeout_while(guard, timeout, |state| {
state.sessions.is_empty() && !state.shutdown
})
.unwrap_or_else(|poison| poison.into_inner());
if let Some(session) = guard.sessions.pop_front() {
Dequeue::Session(session)
} else if guard.shutdown {
Dequeue::Shutdown
} else {
Dequeue::Empty
}
}
}
impl Default for SessionQueue {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub enum ShutdownSignal {
Close,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PtySize {
pub rows: u16,
pub cols: u16,
}
impl PtySize {
pub fn new(rows: u32, cols: u32) -> Self {
Self {
rows: rows.try_into().unwrap_or(u16::MAX),
cols: cols.try_into().unwrap_or(u16::MAX),
}
}
pub fn effective(&self) -> (u16, u16) {
(
if self.rows == 0 { 24 } else { self.rows },
if self.cols == 0 { 80 } else { self.cols },
)
}
}
impl Default for PtySize {
fn default() -> Self {
Self { rows: 24, cols: 80 }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PtySizeStamp {
pub size: PtySize,
pub(crate) seq: u64,
}
impl PtySizeStamp {
fn bump(&mut self, size: PtySize) {
self.size = size;
self.seq = self.seq.wrapping_add(1);
}
}
pub type SharedPtySize = Arc<Mutex<PtySizeStamp>>;
pub fn shared_pty_size() -> SharedPtySize {
Arc::new(Mutex::new(PtySizeStamp {
size: PtySize::default(),
seq: 0,
}))
}
pub fn set_shared_pty_size(cell: &SharedPtySize, size: PtySize) {
if let Ok(mut slot) = cell.lock() {
slot.bump(size);
}
}
pub fn snapshot_pty_size(cell: &SharedPtySize) -> PtySizeStamp {
cell.lock()
.map(|slot| *slot)
.unwrap_or_else(|poison| *poison.into_inner())
}
#[derive(Debug)]
pub struct ServerState {
id: String,
local_addr: SocketAddr,
addr_text: String,
queue: Arc<SessionQueue>,
shutdown_tx: Mutex<Option<std::sync::mpsc::Sender<ShutdownSignal>>>,
thread: Mutex<Option<std::thread::JoinHandle<()>>>,
registry: Arc<oxdock_net_plugin::EndpointRegistry>,
endpoint: oxdock_net_plugin::VirtualEndpoint,
}
pub struct ServerConfig {
pub id: String,
pub local_addr: SocketAddr,
pub addr_text: String,
pub queue: Arc<SessionQueue>,
pub shutdown_tx: std::sync::mpsc::Sender<ShutdownSignal>,
pub thread: Option<std::thread::JoinHandle<()>>,
pub registry: Arc<oxdock_net_plugin::EndpointRegistry>,
pub endpoint: oxdock_net_plugin::VirtualEndpoint,
}
impl ServerState {
pub fn new(config: ServerConfig) -> Self {
Self {
id: config.id,
local_addr: config.local_addr,
addr_text: config.addr_text,
queue: config.queue,
shutdown_tx: Mutex::new(Some(config.shutdown_tx)),
thread: Mutex::new(config.thread),
registry: config.registry,
endpoint: config.endpoint,
}
}
pub fn id(&self) -> &str {
&self.id
}
pub fn local_addr(&self) -> SocketAddr {
self.local_addr
}
pub fn addr_text(&self) -> &str {
&self.addr_text
}
pub fn queue(&self) -> &Arc<SessionQueue> {
&self.queue
}
pub fn request_shutdown(&self) {
self.queue.signal_shutdown();
if let Ok(mut slot) = self.shutdown_tx.lock()
&& let Some(tx) = slot.take()
{
let _ = tx.send(ShutdownSignal::Close);
}
self.registry.release(&self.endpoint);
}
pub fn join_thread(&self, timeout: Duration) -> bool {
let handle = self
.thread
.lock()
.map(|mut slot| slot.take())
.unwrap_or(None);
let Some(handle) = handle else {
return true;
};
let deadline = Instant::now() + timeout;
loop {
if handle.is_finished() {
let _ = handle.join();
return true;
}
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(Duration::from_millis(10));
}
}
}
impl Drop for ServerState {
fn drop(&mut self) {
self.request_shutdown();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pending_pair() -> (PendingSession, mpsc::Sender<DownMsg>) {
let (_up_tx, up_rx) = mpsc::channel(CHANNEL_CAPACITY);
let (down_tx, _down_rx) = mpsc::channel(1);
let session = PendingSession {
exec_command: None,
username: None,
peer_addr: None,
pty_size: shared_pty_size(),
up_rx,
down_tx: down_tx.clone(),
};
(session, down_tx)
}
#[test]
fn shutdown_wakes_empty_waiter() {
let queue = SessionQueue::new();
std::thread::scope(|scope| {
let waiter = scope.spawn(|| queue.wait_for_session(Duration::from_secs(30)));
std::thread::sleep(Duration::from_millis(50));
queue.signal_shutdown();
assert!(matches!(
waiter.join().expect("waiter joins"),
Dequeue::Shutdown
));
});
}
#[test]
fn fifo_ordering() {
let queue = SessionQueue::new();
let (first, _) = pending_pair();
let (second, _) = pending_pair();
queue.push(first);
queue.push(second);
assert!(matches!(queue.try_pop(), Dequeue::Session(_)));
assert!(matches!(queue.try_pop(), Dequeue::Session(_)));
assert!(matches!(queue.try_pop(), Dequeue::Empty));
}
#[test]
fn push_after_shutdown_dropped() {
let queue = SessionQueue::new();
queue.signal_shutdown();
let (session, _) = pending_pair();
queue.push(session);
assert!(matches!(queue.try_pop(), Dequeue::Shutdown));
}
}