use std::io::{self, ErrorKind};
use std::net::TcpStream;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::thread;
use std::time::{Duration, Instant};
use rich_embed::PtyHost;
use tungstenite::{Message, WebSocket};
use crate::session::{PageMessage, close, deliver, parse_message};
const TURN: Duration = Duration::from_millis(10);
const CHUNK: usize = 32 * 1024;
const END_WAIT: Duration = Duration::from_secs(3);
pub(crate) fn split(max_buffered: usize) -> (usize, usize) {
let window = (max_buffered / 2).max(1);
(window, max_buffered.saturating_sub(window).max(1))
}
struct Running<'a>(&'a AtomicUsize);
impl Drop for Running<'_> {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::SeqCst);
}
}
pub(crate) fn run(
socket: &mut WebSocket<TcpStream>,
mut host: Box<dyn PtyHost>,
(columns, rows): (u16, u16),
stopping: &AtomicBool,
running: &AtomicUsize,
window: usize,
z: crate::session::Compress,
) -> io::Result<()> {
running.fetch_add(1, Ordering::SeqCst);
let counted = Running(running);
let result = if stopping.load(Ordering::SeqCst) {
Ok(Some("The server stopped.".into()))
} else {
match host.start(columns, rows) {
Ok(()) => {
let result = serve(socket, &mut *host, stopping, window, z);
end(&mut *host);
result
}
Err(error) => Ok(Some(format!("The program could not start: {error}."))),
}
};
let _ = host.kill();
drop(host);
drop(counted);
match result {
Ok(reason) => {
close(socket, reason.as_deref());
Ok(())
}
Err(error) => {
close(socket, None);
Err(error)
}
}
}
fn end(host: &mut dyn PtyHost) {
let _ = host.kill();
let deadline = Instant::now() + END_WAIT;
while host.exit_status().is_none() && Instant::now() < deadline {
host.read();
thread::sleep(Duration::from_millis(10));
}
}
fn serve(
socket: &mut WebSocket<TcpStream>,
host: &mut dyn PtyHost,
stopping: &AtomicBool,
window: usize,
z: crate::session::Compress,
) -> io::Result<Option<String>> {
let mut unshown: u64 = 0;
let mut waiting: Option<Vec<u8>> = None;
let window = window as u64;
socket.get_mut().set_read_timeout(Some(TURN))?;
loop {
if stopping.load(Ordering::SeqCst) {
return Ok(Some("The server stopped.".into()));
}
if unshown < window {
let out = host.read();
for chunk in out.chunks(CHUNK) {
deliver(socket, z.bytes(chunk))?;
unshown += chunk.len() as u64;
}
}
if let Some(status) = host.exit_status() {
return Ok(Some(format!("The program {status}.")));
}
if let Some(input) = waiting.take()
&& refused(host.write(&input))
{
waiting = Some(input);
if page_gone(socket) {
return Ok(None);
}
continue;
}
let message = match socket.read() {
Ok(message) => message,
Err(tungstenite::Error::Io(e))
if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) =>
{
continue;
}
Err(tungstenite::Error::ConnectionClosed | tungstenite::Error::AlreadyClosed) => {
return Ok(None);
}
Err(tungstenite::Error::Io(e)) => return Err(e),
Err(e) => return Err(io::Error::other(e)),
};
let text = match &message {
Message::Text(text) => text.as_str(),
Message::Close(_) => return Ok(None),
_ => continue,
};
if let Some(input) = text.strip_prefix('d') {
if refused(host.write(input.as_bytes())) {
waiting = Some(input.as_bytes().to_vec());
}
continue;
}
match parse_message(text) {
PageMessage::Resize(columns, rows) => {
let _ = host.resize(columns, rows);
}
PageMessage::Shown(bytes) => unshown = unshown.saturating_sub(bytes),
_ => {}
}
}
}
fn refused(result: io::Result<()>) -> bool {
matches!(result, Err(error) if error.kind() == ErrorKind::WouldBlock)
}
fn page_gone(socket: &mut WebSocket<TcpStream>) -> bool {
match socket.get_ref().peek(&mut [0u8; 1]) {
Ok(0) => true,
Ok(_) => {
thread::sleep(TURN);
false
}
Err(error) => !matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_buffer_is_split_in_two() {
assert_eq!(split(1 << 20), (1 << 19, 1 << 19));
assert_eq!(split(5), (2, 3));
assert_eq!(split(0), (1, 1));
}
}