use std::{
env,
net::{Shutdown, TcpStream, ToSocketAddrs},
path::PathBuf,
process::{Child, ChildStdin, ChildStdout, Command, ExitStatus, Stdio},
sync::{
Arc, Mutex, MutexGuard, Once, PoisonError,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
use monty_proto::{FrameError, FrameReader, MAX_FRAME_LEN, decode_frame, encode_to_capped_vec, pb, write_frame};
use rustls::crypto::aws_lc_rs::default_provider;
use tungstenite::{
Error as WsError, Message, WebSocket,
client::{IntoClientRequest, uri_mode},
client_tls_with_config,
protocol::WebSocketConfig,
stream::{MaybeTlsStream, Mode},
};
use crate::{MontyTransport, PoolConfig, PoolError};
type WsSocket = WebSocket<MaybeTlsStream<TcpStream>>;
pub(crate) struct Worker {
kind: WorkerKind,
interrupt: Arc<Interrupt>,
pub(crate) checkouts_served: u32,
}
enum WorkerKind {
Subprocess(SubprocessWorker),
WebSocket(Box<WebSocketWorker>),
}
struct SubprocessWorker {
child: Arc<Mutex<Child>>,
writer: ChildStdin,
reader: FrameReader<ChildStdout>,
}
struct WebSocketWorker {
socket: WsSocket,
closed: Arc<AtomicBool>,
}
impl WebSocketWorker {
fn send(&mut self, request: &pb::ParentRequest) -> Result<(), FrameError> {
let body = encode_to_capped_vec(request)?;
self.socket
.write(Message::Binary(body.into()))
.map_err(ws_to_frame_error)?;
self.socket.flush().map_err(ws_to_frame_error)?;
Ok(())
}
fn recv(&mut self) -> Result<pb::ChildEvent, FrameError> {
loop {
match self.socket.read() {
Ok(Message::Binary(data)) => return decode_frame::<pb::ChildEvent>(data.as_ref()),
Ok(Message::Ping(_)) => {
let _ = self.socket.flush();
}
Ok(Message::Pong(_)) => {}
Ok(Message::Close(_) | Message::Text(_) | Message::Frame(_)) => return Err(FrameError::Truncated),
Err(WsError::Io(err)) => return Err(FrameError::Io(err)),
Err(_) => return Err(FrameError::Truncated),
}
}
}
}
impl Worker {
pub(crate) fn new(config: &PoolConfig) -> Result<Self, PoolError> {
match &config.transport {
MontyTransport::Subprocess(binary_path) => Self::subprocess(binary_path),
MontyTransport::Websocket(url) => {
Self::websocket(url, config.request_timeout.unwrap_or(DEFAULT_DIAL_TIMEOUT))
}
}
}
fn subprocess(binary_path: &PathBuf) -> Result<Self, PoolError> {
let mut command = Command::new(binary_path);
command
.arg("subprocess")
.env_clear()
.stdin(Stdio::piped())
.stdout(Stdio::piped());
if cfg!(windows)
&& let Ok(system_root) = env::var("SystemRoot")
{
command.env("SystemRoot", system_root);
}
let mut child = command
.spawn()
.map_err(|err| PoolError::Spawn(format!("{}: {err}", binary_path.display())))?;
let writer = child.stdin.take().expect("piped stdin");
let reader = FrameReader::new(child.stdout.take().expect("piped stdout"));
let child = Arc::new(Mutex::new(child));
let interrupt = Interrupt::new(InterruptKind::Subprocess(Arc::clone(&child)));
Ok(Self::with_kind(
WorkerKind::Subprocess(SubprocessWorker { child, writer, reader }),
interrupt,
))
}
fn websocket(url: &str, timeout: Duration) -> Result<Self, PoolError> {
install_crypto_provider();
let socket = dial_ws(url, timeout)?;
let tcp = underlying_tcp(socket.get_ref())
.and_then(|tcp| tcp.try_clone().ok())
.ok_or_else(|| {
PoolError::Spawn(format!(
"{url}: could not clone the connection socket for timeout enforcement"
))
})?;
let interrupt = Interrupt::new(InterruptKind::WebSocket(tcp));
Ok(Self::with_kind(
WorkerKind::WebSocket(Box::new(WebSocketWorker {
socket,
closed: Arc::new(AtomicBool::new(false)),
})),
interrupt,
))
}
fn with_kind(kind: WorkerKind, interrupt: Arc<Interrupt>) -> Self {
Self {
kind,
interrupt,
checkouts_served: 0,
}
}
pub(crate) fn send(&mut self, request: &pb::ParentRequest) -> Result<(), FrameError> {
match &mut self.kind {
WorkerKind::Subprocess(w) => write_frame(&mut w.writer, request),
WorkerKind::WebSocket(w) => w.send(request),
}
}
pub(crate) fn recv(&mut self) -> Result<pb::ChildEvent, FrameError> {
match &mut self.kind {
WorkerKind::Subprocess(w) => w.reader.read::<pb::ChildEvent>()?.ok_or(FrameError::Truncated),
WorkerKind::WebSocket(w) => w.recv(),
}
}
pub(crate) fn pid(&self) -> Option<u32> {
match &self.kind {
WorkerKind::Subprocess(w) => Some(lock_ignore_poison(&w.child).id()),
WorkerKind::WebSocket(_) => None,
}
}
pub(crate) fn interrupt(&self) -> &Arc<Interrupt> {
&self.interrupt
}
pub(crate) fn was_killed_for_timeout(&self) -> bool {
self.interrupt().was_killed_for_timeout()
}
pub(crate) fn reset_killed_for_timeout(&self) {
self.interrupt().reset_killed_for_timeout();
}
pub(crate) fn is_dead(&self) -> bool {
match &self.kind {
WorkerKind::Subprocess(w) => lock_ignore_poison(&w.child).try_wait().is_ok_and(|s| s.is_some()),
WorkerKind::WebSocket(w) => w.closed.load(Ordering::SeqCst),
}
}
pub(crate) fn kill_and_reap(&mut self) -> Option<ExitStatus> {
match &mut self.kind {
WorkerKind::Subprocess(w) => {
let mut child = lock_ignore_poison(&w.child);
let _ = child.kill();
child.wait().ok()
}
WorkerKind::WebSocket(w) => {
w.closed.store(true, Ordering::SeqCst);
self.interrupt.kill();
None
}
}
}
}
impl Drop for Worker {
fn drop(&mut self) {
self.kill_and_reap();
}
}
fn ws_to_frame_error(err: WsError) -> FrameError {
match err {
WsError::Io(err) => FrameError::Io(err),
_ => FrameError::Truncated,
}
}
const DEFAULT_DIAL_TIMEOUT: Duration = Duration::from_secs(30);
fn dial_ws(url: &str, timeout: Duration) -> Result<WsSocket, PoolError> {
let spawn_err = |msg: String| PoolError::Spawn(format!("{url}: {msg}"));
let request = url
.into_client_request()
.map_err(|err| spawn_err(format!("invalid WebSocket URL: {err}")))?;
let uri = request.uri();
let mode = uri_mode(uri).map_err(|err| spawn_err(err.to_string()))?;
let host = uri.host().ok_or_else(|| spawn_err("URL has no host".to_owned()))?;
let host = host.strip_prefix('[').and_then(|h| h.strip_suffix(']')).unwrap_or(host);
let port = uri.port_u16().unwrap_or(match mode {
Mode::Plain => 80,
Mode::Tls => 443,
});
let addrs = (host, port)
.to_socket_addrs()
.map_err(|err| spawn_err(format!("could not resolve {host}:{port}: {err}")))?;
let deadline = Instant::now() + timeout;
let mut stream = None;
let mut last_err = None;
for addr in addrs {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
break;
}
match TcpStream::connect_timeout(&addr, remaining) {
Ok(tcp) => {
stream = Some(tcp);
break;
}
Err(err) => last_err = Some(err),
}
}
let stream = stream.ok_or_else(|| {
spawn_err(match last_err {
Some(err) => format!("connect failed: {err}"),
None => "connect timed out".to_owned(),
})
})?;
let _ = stream.set_nodelay(true);
let _ = stream.set_read_timeout(Some(timeout));
let _ = stream.set_write_timeout(Some(timeout));
let ws_config = WebSocketConfig::default()
.max_frame_size(Some(MAX_FRAME_LEN as usize))
.max_message_size(Some(MAX_FRAME_LEN as usize));
let (socket, _response) = client_tls_with_config(request, stream, Some(ws_config), None)
.map_err(|err| spawn_err(format!("handshake failed: {err}")))?;
if let Some(tcp) = underlying_tcp(socket.get_ref()) {
let _ = tcp.set_read_timeout(None);
let _ = tcp.set_write_timeout(None);
}
Ok(socket)
}
fn underlying_tcp(stream: &MaybeTlsStream<TcpStream>) -> Option<&TcpStream> {
match stream {
MaybeTlsStream::Plain(tcp) => Some(tcp),
MaybeTlsStream::Rustls(tls) => Some(tls.get_ref()),
_ => None,
}
}
fn install_crypto_provider() {
static INSTALL: Once = Once::new();
INSTALL.call_once(|| {
let _ = default_provider().install_default();
});
}
pub(crate) fn lock_ignore_poison<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) struct Interrupt {
kind: InterruptKind,
killed_for_timeout: AtomicBool,
}
enum InterruptKind {
Subprocess(Arc<Mutex<Child>>),
WebSocket(TcpStream),
}
impl Interrupt {
fn new(kind: InterruptKind) -> Arc<Self> {
Arc::new(Self {
kind,
killed_for_timeout: AtomicBool::new(false),
})
}
pub(crate) fn kill(&self) {
match &self.kind {
InterruptKind::Subprocess(child) => {
let _ = lock_ignore_poison(child).kill();
}
InterruptKind::WebSocket(tcp) => {
let _ = tcp.shutdown(Shutdown::Both);
}
}
}
pub(crate) fn flag_timeout(&self) {
self.killed_for_timeout.store(true, Ordering::SeqCst);
}
fn was_killed_for_timeout(&self) -> bool {
self.killed_for_timeout.load(Ordering::SeqCst)
}
fn reset_killed_for_timeout(&self) {
self.killed_for_timeout.store(false, Ordering::SeqCst);
}
}