#![allow(
clippy::missing_docs_in_private_items,
clippy::pattern_type_mismatch,
clippy::multiple_crate_versions
)]
use std::any;
use std::env;
use std::fmt;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
const WORKER_ENV_PREFIX: &str = "__TARNISH_WORKER_";
const GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
fn worker_env_name<T: 'static>() -> String {
let type_name = any::type_name::<T>()
.replace("::", "_")
.replace(['<', '>'], "_");
format!("{WORKER_ENV_PREFIX}{type_name}__")
}
#[derive(Debug)]
enum Message {
Request(String),
Response(String),
Error(String),
Shutdown,
Ping,
Pong,
}
impl Message {
fn encode(&self) -> String {
match self {
Self::Request(s) => format!("REQ:{s}"),
Self::Response(s) => format!("RES:{s}"),
Self::Error(s) => format!("ERR:{s}"),
Self::Shutdown => "SHUTDOWN".to_owned(),
Self::Ping => "PING".to_owned(),
Self::Pong => "PONG".to_owned(),
}
}
fn decode(s: &str) -> std::result::Result<Self, String> {
if s == "SHUTDOWN" {
return Ok(Self::Shutdown);
}
if s == "PING" {
return Ok(Self::Ping);
}
if s == "PONG" {
return Ok(Self::Pong);
}
if let Some(payload) = s.strip_prefix("REQ:") {
return Ok(Self::Request(payload.to_owned()));
}
if let Some(payload) = s.strip_prefix("RES:") {
return Ok(Self::Response(payload.to_owned()));
}
if let Some(payload) = s.strip_prefix("ERR:") {
return Ok(Self::Error(payload.to_owned()));
}
Err(format!("Invalid message format: {s}"))
}
}
#[derive(Debug)]
pub enum ProcessError {
SpawnError(io::Error),
ExecutablePathError(io::Error),
CommunicationError(io::Error),
ProcessTerminated,
ProcessPanicked(String),
TaskError(String),
ProtocolError(String),
}
impl fmt::Display for ProcessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SpawnError(e) => write!(f, "Failed to spawn process: {e}"),
Self::ExecutablePathError(e) => {
write!(f, "Failed to get executable path: {e}")
}
Self::CommunicationError(e) => write!(f, "Communication error: {e}"),
Self::ProcessTerminated => write!(f, "Process terminated unexpectedly"),
Self::ProcessPanicked(msg) => write!(f, "Process panicked: {msg}"),
Self::TaskError(msg) => write!(f, "Task error: {msg}"),
Self::ProtocolError(msg) => write!(f, "Protocol error: {msg}"),
}
}
}
impl std::error::Error for ProcessError {}
impl From<io::Error> for ProcessError {
fn from(err: io::Error) -> Self {
Self::CommunicationError(err)
}
}
pub type Result<T> = std::result::Result<T, ProcessError>;
pub trait MessageEncode {
fn encode(&self) -> String;
}
pub trait MessageDecode: Sized {
fn decode(s: &str) -> std::result::Result<Self, String>;
}
#[cfg(feature = "serde")]
mod serde_impl {
use super::{MessageDecode, MessageEncode};
use serde::{Deserialize, Serialize};
impl<T: Serialize> MessageEncode for T {
fn encode(&self) -> String {
#[allow(clippy::expect_used)]
let bytes =
postcard::to_allocvec(self).expect("Serialization should not fail for valid types");
base64_encode(&bytes)
}
}
impl<T: for<'de> Deserialize<'de>> MessageDecode for T {
fn decode(s: &str) -> std::result::Result<Self, String> {
let bytes = base64_decode(s).map_err(|e| format!("Base64 decode error: {e}"))?;
postcard::from_bytes(&bytes).map_err(|e| format!("Deserialization error: {e}"))
}
}
#[allow(
clippy::indexing_slicing,
clippy::arithmetic_side_effects,
clippy::unseparated_literal_suffix
)]
fn base64_encode(bytes: &[u8]) -> String {
const BASE64_CHARS: &[u8] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::new();
for chunk in bytes.chunks(3) {
let mut buf = [0_u8; 3];
for (i, &byte) in chunk.iter().enumerate() {
buf[i] = byte;
}
let b1 = (buf[0] >> 2) as usize;
let b2 = (((buf[0] & 0x03) << 4) | (buf[1] >> 4)) as usize;
let b3 = (((buf[1] & 0x0F) << 2) | (buf[2] >> 6)) as usize;
let b4 = (buf[2] & 0x3F) as usize;
result.push(BASE64_CHARS[b1] as char);
result.push(BASE64_CHARS[b2] as char);
result.push(if chunk.len() > 1 {
BASE64_CHARS[b3] as char
} else {
'='
});
result.push(if chunk.len() > 2 {
BASE64_CHARS[b4] as char
} else {
'='
});
}
result
}
#[allow(clippy::arithmetic_side_effects, clippy::shadow_reuse)]
fn base64_decode(s: &str) -> std::result::Result<Vec<u8>, String> {
let s = s.trim_end_matches('=');
let mut result = Vec::new();
let mut buf = 0_u32;
let mut bits = 0;
for ch in s.chars() {
let val = match ch {
'A'..='Z' => ch as u32 - 'A' as u32,
'a'..='z' => ch as u32 - 'a' as u32 + 26,
'0'..='9' => ch as u32 - '0' as u32 + 52,
'+' => 62,
'/' => 63,
_ => return Err(format!("Invalid base64 character: {ch}")),
};
buf = (buf << 6) | val;
bits += 6;
if bits >= 8 {
bits -= 8;
#[allow(clippy::cast_possible_truncation)]
{
result.push((buf >> bits) as u8);
}
buf &= (1 << bits) - 1;
}
}
Ok(result)
}
}
#[cfg(not(feature = "serde"))]
mod manual_impl {
use super::{MessageDecode, MessageEncode};
impl MessageEncode for String {
fn encode(&self) -> String {
self.clone()
}
}
impl MessageDecode for String {
fn decode(s: &str) -> std::result::Result<Self, String> {
Ok(s.to_string())
}
}
impl MessageEncode for &str {
fn encode(&self) -> String {
self.to_string()
}
}
}
pub trait Task: Default + 'static {
type Input: MessageEncode + MessageDecode;
type Output: MessageEncode + MessageDecode;
type Error: fmt::Display;
fn run(&mut self, input: Self::Input) -> std::result::Result<Self::Output, Self::Error>;
}
pub struct Process<T: Task> {
child: Child,
stdin: BufWriter<std::process::ChildStdin>,
stdout: BufReader<std::process::ChildStdout>,
_phantom: std::marker::PhantomData<T>,
}
impl<T: Task> Process<T> {
pub fn spawn() -> Result<Self> {
Self::spawn_internal()
}
fn spawn_internal() -> Result<Self> {
let exe_path = env::current_exe().map_err(ProcessError::ExecutablePathError)?;
let env_name = worker_env_name::<T>();
let mut child = Command::new(exe_path)
.env(&env_name, "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.map_err(ProcessError::SpawnError)?;
#[allow(clippy::expect_used)]
let stdin = child.stdin.take().expect("Failed to get child stdin");
#[allow(clippy::expect_used)]
let stdout = child.stdout.take().expect("Failed to get child stdout");
Ok(Self {
child,
stdin: BufWriter::new(stdin),
stdout: BufReader::new(stdout),
_phantom: std::marker::PhantomData,
})
}
#[allow(clippy::needless_pass_by_value)] pub fn call(&mut self, input: T::Input) -> Result<T::Output> {
let encoded_input = input.encode();
if let Err(e) = self.send_message(&Message::Request(encoded_input)) {
self.restart()?;
return Err(e);
}
match self.receive_message() {
Ok(Message::Response(encoded_output)) => {
T::Output::decode(&encoded_output).map_err(|e| {
ProcessError::ProtocolError(format!("Failed to decode output: {e}"))
})
}
Ok(Message::Error(err)) => Err(ProcessError::TaskError(err)),
Ok(msg) => {
self.restart()?;
Err(ProcessError::ProtocolError(format!(
"Unexpected message: {msg:?}"
)))
}
Err(e) => {
self.restart()?;
Err(e)
}
}
}
fn send_message(&mut self, msg: &Message) -> Result<()> {
let encoded = msg.encode();
writeln!(self.stdin, "{encoded}")?;
self.stdin.flush()?;
Ok(())
}
#[allow(clippy::shadow_reuse)] fn receive_message(&mut self) -> Result<Message> {
let mut line = String::new();
let bytes_read = self.stdout.read_line(&mut line)?;
if bytes_read == 0 {
return Err(ProcessError::ProcessTerminated);
}
let line = line.trim_end();
Message::decode(line).map_err(ProcessError::ProtocolError)
}
fn restart(&mut self) -> Result<()> {
#[allow(clippy::let_underscore_must_use)]
let _ = self.child.kill();
#[allow(clippy::let_underscore_must_use)]
let _ = self.child.wait();
let new_handle = Self::spawn_internal()?;
*self = new_handle;
Ok(())
}
pub fn is_running(&mut self) -> Result<bool> {
match self.child.try_wait() {
Ok(Some(_)) => Ok(false),
Ok(None) => Ok(true),
Err(e) => Err(ProcessError::CommunicationError(e)),
}
}
}
impl<T: Task> Drop for Process<T> {
fn drop(&mut self) {
if self.send_message(&Message::Shutdown).is_ok() {
let start = Instant::now();
while start.elapsed() < GRACEFUL_SHUTDOWN_TIMEOUT {
if let Ok(Some(_)) = self.child.try_wait() {
return; }
std::thread::sleep(Duration::from_millis(100));
}
}
#[allow(clippy::let_underscore_must_use)]
let _ = self.child.kill();
#[allow(clippy::let_underscore_must_use)]
let _ = self.child.wait();
}
}
pub fn main<T: Task>(parent_main: fn()) {
let env_name = worker_env_name::<T>();
if env::var(&env_name).is_ok() {
let exit_code = run_worker_loop::<T>();
std::process::exit(exit_code);
}
parent_main();
}
#[must_use]
pub fn worker_main<T: Task>() -> Option<i32> {
let env_name = worker_env_name::<T>();
if env::var(&env_name).is_err() {
return None; }
let exit_code = run_worker_loop::<T>();
Some(exit_code)
}
#[allow(clippy::print_stderr)] #[allow(clippy::shadow_reuse)] fn run_worker_loop<T: Task>() -> i32 {
let stdin = io::stdin();
let stdout = io::stdout();
let mut stdin = BufReader::new(stdin);
let mut stdout = BufWriter::new(stdout);
let mut worker = T::default();
loop {
let mut line = String::new();
let bytes_read = match stdin.read_line(&mut line) {
Ok(n) => n,
Err(e) => {
eprintln!("[CHILD] Failed to read from parent: {e}");
return 1;
}
};
if bytes_read == 0 {
return 0;
}
let line = line.trim_end();
let message = match Message::decode(line) {
Ok(msg) => msg,
Err(e) => {
eprintln!("[CHILD] Protocol error: {e}");
return 1;
}
};
match message {
Message::Shutdown => {
return 0;
}
Message::Ping => {
if send_message(&mut stdout, &Message::Pong).is_err() {
return 1;
}
}
Message::Request(encoded_input) => {
let input = match T::Input::decode(&encoded_input) {
Ok(inp) => inp,
Err(e) => {
eprintln!("[WORKER] Failed to decode input: {e}");
let err_msg = Message::Error(format!("Decode error: {e}"));
if send_message(&mut stdout, &err_msg).is_err() {
return 1;
}
continue;
}
};
let response = match worker.run(input) {
Ok(output) => Message::Response(output.encode()),
Err(err) => Message::Error(err.to_string()),
};
if send_message(&mut stdout, &response).is_err() {
return 1;
}
}
Message::Response(_) | Message::Error(_) | Message::Pong => {
eprintln!("[CHILD] Unexpected message type");
return 1;
}
}
}
}
fn send_message(stdout: &mut BufWriter<io::Stdout>, msg: &Message) -> io::Result<()> {
writeln!(stdout, "{}", msg.encode())?;
stdout.flush()?;
Ok(())
}