pub const MAGIC_REQ: &[u8; 4] = b"\0REQ";
pub const MAGIC_RES: &[u8; 4] = b"\0RES";
pub const HEADER_LEN: usize = 12;
pub const MAX_PACKET_BYTES: usize = 1024 * 1024;
pub const MAX_ADMIN_LINE: usize = 1024;
pub const MAX_FUNCTION_NAME: usize = 512;
pub const MAX_UNIQUE: usize = 64;
pub const CAN_DO: u32 = 1;
pub const CANT_DO: u32 = 2;
pub const RESET_ABILITIES: u32 = 3;
pub const PRE_SLEEP: u32 = 4;
pub const SUBMIT_JOB: u32 = 7;
pub const JOB_CREATED: u32 = 8;
pub const GRAB_JOB: u32 = 9;
pub const WORK_STATUS: u32 = 12;
pub const WORK_COMPLETE: u32 = 13;
pub const WORK_FAIL: u32 = 14;
pub const GET_STATUS: u32 = 15;
pub const ECHO_REQ: u32 = 16;
pub const ECHO_RES: u32 = 17;
pub const SUBMIT_JOB_BG: u32 = 18;
pub const ERROR: u32 = 19;
pub const STATUS_RES: u32 = 20;
pub const SUBMIT_JOB_HIGH: u32 = 21;
pub const SET_CLIENT_ID: u32 = 22;
pub const CAN_DO_TIMEOUT: u32 = 23;
pub const ALL_YOURS: u32 = 24;
pub const WORK_EXCEPTION: u32 = 25;
pub const OPTION_REQ: u32 = 26;
pub const OPTION_RES: u32 = 27;
pub const WORK_DATA: u32 = 28;
pub const WORK_WARNING: u32 = 29;
pub const GRAB_JOB_UNIQ: u32 = 30;
pub const SUBMIT_JOB_HIGH_BG: u32 = 32;
pub const SUBMIT_JOB_LOW: u32 = 33;
pub const SUBMIT_JOB_LOW_BG: u32 = 34;
pub const GRAB_JOB_ALL: u32 = 39;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Header {
pub request: bool,
pub packet_type: u32,
pub size: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeaderError {
BadMagic,
TooLarge(u32),
}
pub fn parse_header(bytes: &[u8]) -> Result<Header, HeaderError> {
if bytes.len() < HEADER_LEN {
return Err(HeaderError::BadMagic);
}
let request = match &bytes[..4] {
m if m == MAGIC_REQ => true,
m if m == MAGIC_RES => false,
_ => return Err(HeaderError::BadMagic),
};
let packet_type = u32::from_be_bytes(bytes[4..8].try_into().unwrap());
let size = u32::from_be_bytes(bytes[8..12].try_into().unwrap());
if size as usize > MAX_PACKET_BYTES {
return Err(HeaderError::TooLarge(size));
}
Ok(Header {
request,
packet_type,
size,
})
}
pub fn encode(request: bool, packet_type: u32, args: &[&[u8]]) -> Vec<u8> {
let size: usize = args.iter().map(|a| a.len()).sum::<usize>() + args.len().saturating_sub(1);
let mut out = Vec::with_capacity(HEADER_LEN + size);
out.extend_from_slice(if request { MAGIC_REQ } else { MAGIC_RES });
out.extend_from_slice(&packet_type.to_be_bytes());
out.extend_from_slice(&(size as u32).to_be_bytes());
for (i, arg) in args.iter().enumerate() {
if i > 0 {
out.push(0);
}
out.extend_from_slice(arg);
}
out
}
pub fn response(packet_type: u32, args: &[&[u8]]) -> Vec<u8> {
encode(false, packet_type, args)
}
pub fn split_args(data: &[u8], n: usize) -> Option<Vec<&[u8]>> {
if n == 0 {
return data.is_empty().then(Vec::new);
}
let mut out = Vec::with_capacity(n);
let mut rest = data;
for _ in 0..n - 1 {
let at = rest.iter().position(|b| *b == 0)?;
out.push(&rest[..at]);
rest = &rest[at + 1..];
}
out.push(rest);
Some(out)
}
pub fn request_arg_count(packet_type: u32) -> Option<usize> {
Some(match packet_type {
SUBMIT_JOB | SUBMIT_JOB_BG | SUBMIT_JOB_HIGH | SUBMIT_JOB_HIGH_BG | SUBMIT_JOB_LOW
| SUBMIT_JOB_LOW_BG => 3,
GET_STATUS | ECHO_REQ | OPTION_REQ | SET_CLIENT_ID => 1,
_ => return None,
})
}
pub fn is_worker_packet(packet_type: u32) -> bool {
matches!(
packet_type,
CAN_DO
| CANT_DO
| RESET_ABILITIES
| PRE_SLEEP
| GRAB_JOB
| WORK_STATUS
| WORK_COMPLETE
| WORK_FAIL
| CAN_DO_TIMEOUT
| ALL_YOURS
| WORK_EXCEPTION
| WORK_DATA
| WORK_WARNING
| GRAB_JOB_UNIQ
| GRAB_JOB_ALL
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Priority {
Normal,
High,
Low,
}
impl Priority {
pub fn name(self) -> &'static str {
match self {
Priority::Normal => "normal",
Priority::High => "high",
Priority::Low => "low",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Request {
Submit {
function: String,
unique: String,
workload: Vec<u8>,
priority: Priority,
background: bool,
},
GetStatus(Vec<u8>),
Echo(Vec<u8>),
Option(Vec<u8>),
SetClientId,
Worker(u32),
Unsupported(u32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArgError {
WrongArgCount,
BadName,
}
pub fn parse_request(header: &Header, data: &[u8]) -> Result<Request, ArgError> {
let t = header.packet_type;
if !header.request {
return Ok(Request::Unsupported(t));
}
if is_worker_packet(t) {
return Ok(Request::Worker(t));
}
let Some(n) = request_arg_count(t) else {
return Ok(Request::Unsupported(t));
};
let args = split_args(data, n).ok_or(ArgError::WrongArgCount)?;
Ok(match t {
SUBMIT_JOB | SUBMIT_JOB_BG | SUBMIT_JOB_HIGH | SUBMIT_JOB_HIGH_BG | SUBMIT_JOB_LOW
| SUBMIT_JOB_LOW_BG => {
let (function, unique) = (args[0], args[1]);
if function.is_empty()
|| function.len() > MAX_FUNCTION_NAME
|| unique.len() > MAX_UNIQUE
{
return Err(ArgError::BadName);
}
Request::Submit {
function: String::from_utf8_lossy(function).into_owned(),
unique: String::from_utf8_lossy(unique).into_owned(),
workload: args[2].to_vec(),
priority: match t {
SUBMIT_JOB_HIGH | SUBMIT_JOB_HIGH_BG => Priority::High,
SUBMIT_JOB_LOW | SUBMIT_JOB_LOW_BG => Priority::Low,
_ => Priority::Normal,
},
background: matches!(t, SUBMIT_JOB_BG | SUBMIT_JOB_HIGH_BG | SUBMIT_JOB_LOW_BG),
}
}
GET_STATUS => Request::GetStatus(args[0].to_vec()),
ECHO_REQ => Request::Echo(args[0].to_vec()),
OPTION_REQ => Request::Option(args[0].to_vec()),
_ => Request::SetClientId,
})
}
fn digits(n: u64) -> Vec<u8> {
n.to_string().into_bytes()
}
pub fn job_created(handle: &[u8]) -> Vec<u8> {
response(JOB_CREATED, &[handle])
}
pub fn work_status(handle: &[u8], numerator: u64, denominator: u64) -> Vec<u8> {
response(
WORK_STATUS,
&[handle, &digits(numerator), &digits(denominator)],
)
}
pub fn work_data(handle: &[u8], data: &[u8]) -> Vec<u8> {
response(WORK_DATA, &[handle, data])
}
pub fn work_complete(handle: &[u8], result: &[u8]) -> Vec<u8> {
response(WORK_COMPLETE, &[handle, result])
}
pub fn work_fail(handle: &[u8]) -> Vec<u8> {
response(WORK_FAIL, &[handle])
}
pub fn work_exception(handle: &[u8], text: &[u8]) -> Vec<u8> {
response(WORK_EXCEPTION, &[handle, text])
}
pub fn status_res(handle: &[u8], known: bool, running: bool, num: u64, den: u64) -> Vec<u8> {
response(
STATUS_RES,
&[
handle,
if known { b"1" } else { b"0" },
if running { b"1" } else { b"0" },
&digits(num),
&digits(den),
],
)
}
pub fn error(code: &str, text: &str) -> Result<Vec<u8>, String> {
if code.is_empty()
|| code.len() > 64
|| !code.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
{
return Err(format!(
"error code must be 1-64 letters, digits or '_', got {code:?}"
));
}
if text.len() > 1024 || text.contains('\0') {
return Err("error text must be at most 1024 bytes and contain no NUL".to_string());
}
Ok(response(ERROR, &[code.as_bytes(), text.as_bytes()]))
}
pub fn read_response(packet: &[u8]) -> Option<(u32, Vec<Vec<u8>>)> {
let header = parse_header(packet).ok()?;
if header.request || packet.len() != HEADER_LEN + header.size as usize {
return None;
}
let n = match header.packet_type {
JOB_CREATED | WORK_FAIL | ECHO_RES | OPTION_RES => 1,
WORK_COMPLETE | WORK_DATA | WORK_WARNING | WORK_EXCEPTION | ERROR => 2,
WORK_STATUS => 3,
STATUS_RES => 5,
_ => return None,
};
let args = split_args(&packet[HEADER_LEN..], n)?;
Some((
header.packet_type,
args.into_iter().map(<[u8]>::to_vec).collect(),
))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AdminCommand {
Status,
Workers,
Version,
MaxQueue,
Shutdown,
Unknown,
}
pub fn parse_admin(line: &str) -> AdminCommand {
match line.split_whitespace().next().unwrap_or("") {
"status" => AdminCommand::Status,
"workers" => AdminCommand::Workers,
"version" => AdminCommand::Version,
"maxqueue" => AdminCommand::MaxQueue,
"shutdown" => AdminCommand::Shutdown,
_ => AdminCommand::Unknown,
}
}
pub fn admin_error(code: &str, text: &str) -> String {
format!("ERR {code} {}\n", text.replace(' ', "+"))
}
pub fn admin_status(rows: &[(String, u64, u64, u64)]) -> String {
let mut out = String::new();
for (function, total, running, workers) in rows {
let name = crate::utils::sanitize::line_field(function);
out.push_str(&format!("{name}\t{total}\t{running}\t{workers}\n"));
}
out.push_str(".\n");
out
}