use std::net;
mod types;
pub use types::*;
mod connection;
pub use connection::*;
lazy_static::lazy_static! {
pub static ref DEFAULT_ADDR: net::SocketAddr = {
net::SocketAddr::V4(net::SocketAddrV4::new(net::Ipv4Addr::LOCALHOST, 36330))
};
}
#[derive(Debug)]
pub struct API {
pub conn: Connection,
pub buf: Vec<u8>,
}
impl API {
pub fn connect_timeout(addr: &net::SocketAddr, timeout: core::time::Duration) -> Result<API> {
Ok(API {
conn: Connection::connect_timeout(addr, timeout)?,
buf: Vec::new(),
})
}
pub fn help(&mut self) -> Result<String> {
self.conn.exec("help", &mut self.buf)?;
Ok(std::str::from_utf8(self.buf.as_slice())?.to_string())
}
pub fn log_updates(&mut self, arg: LogUpdatesArg) -> Result<String> {
let command = format!("log-updates {}", arg);
self.conn.exec(command.as_str(), &mut self.buf)?;
self.conn.exec_eval("eval", &mut self.buf)?;
parse_log(std::str::from_utf8(&self.buf)?)
}
pub fn screensaver(&mut self) -> Result<()> {
self.conn.exec("screensaver", &mut self.buf)
}
pub fn always_on(&mut self, slot: i64) -> Result<()> {
let command = format!("always_on {}", slot);
self.conn.exec(command.as_str(), &mut self.buf)
}
pub fn configured(&mut self) -> Result<bool> {
self.conn.exec("configured", &mut self.buf)?;
let s = std::str::from_utf8(&self.buf)?;
Ok(serde_json::from_str(pyon_to_json(s)?.as_str())?)
}
pub fn do_cycle(&mut self) -> Result<()> {
self.conn.exec("do-cycle", &mut self.buf)
}
pub fn finish_slot(&mut self, slot: i64) -> Result<()> {
let command = format!("finish {}", slot);
self.conn.exec(command.as_str(), &mut self.buf)
}
pub fn finish_all(&mut self) -> Result<()> {
self.conn.exec("finish", &mut self.buf)
}
pub fn info(&mut self) -> Result<Vec<Vec<serde_json::Value>>> {
self.conn.exec("info", &mut self.buf)?;
let s = std::str::from_utf8(&self.buf)?;
Ok(serde_json::from_str(pyon_to_json(s)?.as_str())?)
}
pub fn info_struct(&mut self) -> Result<Info> {
Info::new(self.info()?)
}
pub fn num_slots(&mut self) -> Result<i64> {
self.conn.exec("num-slots", &mut self.buf)?;
let s = std::str::from_utf8(&self.buf)?;
Ok(serde_json::from_str(pyon_to_json(s)?.as_str())?)
}
pub fn on_idle(&mut self, slot: i64) -> Result<()> {
let command = format!("on_idle {}", slot);
self.conn.exec(command.as_str(), &mut self.buf)
}
pub fn on_idle_all(&mut self) -> Result<()> {
self.conn.exec("on_idle", &mut self.buf)
}
pub fn options_get(&mut self) -> Result<Options> {
self.conn.exec("options -a", &mut self.buf)?;
let s = std::str::from_utf8(&self.buf)?;
Ok(serde_json::from_str(pyon_to_json(s)?.as_str())?)
}
pub fn options_set<N>(&mut self, key: &str, value: N) -> Result<()>
where
N: std::fmt::Display,
{
let value_str = format!("{}", value);
if key.contains(&['=', ' ', '!'] as &[char]) || value_str.contains(' ') {
return Err(format!("key or value contains bad character: {}={}", key, value).into());
}
let command = format!("options {}={}", key, value_str);
self.conn.exec(command.as_str(), &mut self.buf)
}
pub fn pause_all(&mut self) -> Result<()> {
self.conn.exec("pause", &mut self.buf)
}
pub fn pause_slot(&mut self, slot: i64) -> Result<()> {
let command = format!("pause {}", slot);
self.conn.exec(command.as_str(), &mut self.buf)
}
pub fn ppd(&mut self) -> Result<f64> {
self.conn.exec("ppd", &mut self.buf)?;
let s = std::str::from_utf8(&self.buf)?;
Ok(serde_json::from_str(pyon_to_json(s)?.as_str())?)
}
pub fn queue_info(&mut self) -> Result<Vec<SlotQueueInfo>> {
self.conn.exec("queue-info", &mut self.buf)?;
let s = std::str::from_utf8(&self.buf)?;
Ok(serde_json::from_str(pyon_to_json(s)?.as_str())?)
}
pub fn request_id(&mut self) -> Result<()> {
self.conn.exec("request-id", &mut self.buf)
}
pub fn request_ws(&mut self) -> Result<()> {
self.conn.exec("request-ws", &mut self.buf)
}
pub fn shutdown(&mut self) -> Result<()> {
self.conn.exec("shutdown", &mut self.buf)
}
pub fn simulation_info(&mut self, slot: i64) -> Result<SimulationInfo> {
let command = format!("simulation-info {}", slot);
self.conn.exec(command.as_str(), &mut self.buf)?;
let s = std::str::from_utf8(&self.buf)?;
Ok(serde_json::from_str(pyon_to_json(s)?.as_str())?)
}
pub fn slot_delete(&mut self, slot: i64) -> Result<()> {
let command = format!("slot-delete {}", slot);
self.conn.exec(command.as_str(), &mut self.buf)
}
pub fn slot_info(&mut self) -> Result<Vec<SlotInfo>> {
self.conn.exec("slot-info", &mut self.buf)?;
let s = std::str::from_utf8(&self.buf)?;
Ok(serde_json::from_str(pyon_to_json(s)?.as_str())?)
}
pub fn slot_options_get(&mut self, slot: i64) -> Result<SlotOptions> {
let command = format!("slot-options {} -a", slot);
self.conn.exec(command.as_str(), &mut self.buf)?;
let s = std::str::from_utf8(&self.buf)?;
Ok(serde_json::from_str(pyon_to_json(s)?.as_str())?)
}
pub fn slot_options_set<N>(&mut self, slot: i64, key: &str, value: N) -> Result<()>
where
N: std::fmt::Display,
{
let command = format!("slot-options {} {} {}", slot, key, value);
self.conn.exec(command.as_str(), &mut self.buf)
}
pub fn unpause_all(&mut self) -> Result<()> {
self.conn.exec("unpause", &mut self.buf)
}
pub fn unpause_slot(&mut self, slot: i64) -> Result<()> {
let command = format!("unpause {}", slot);
self.conn.exec(command.as_str(), &mut self.buf)
}
pub fn uptime(&mut self) -> Result<FAHDuration> {
self.conn.exec_eval("uptime", &mut self.buf)?;
let duration = humantime::parse_duration(std::str::from_utf8(&self.buf)?)?;
match chrono::Duration::from_std(duration) {
Ok(d) => Ok(d.into()),
Err(e) => Err(e.to_string().into()),
}
}
pub fn wait_for_units(&mut self) -> Result<()> {
self.conn.exec("wait-for-units", &mut self.buf)
}
}
error_chain::error_chain! {
types {
Error, ErrorKind, ResultExt, Result;
}
foreign_links {
IO(std::io::Error);
}
errors {
EOF {
description("EOF")
}
Parse (msg: String) {
description("parse error")
display("parse error: {}", msg)
}
}
}
impl From<std::str::Utf8Error> for Error {
fn from(e: std::str::Utf8Error) -> Self {
Error::from(ErrorKind::Parse(e.to_string()))
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::from(ErrorKind::Parse(e.to_string()))
}
}
impl From<humantime::DurationError> for Error {
fn from(e: humantime::DurationError) -> Self {
Error::from(ErrorKind::Msg(e.to_string()))
}
}
pub enum LogUpdatesArg {
Start,
Restart,
Stop,
}
impl std::fmt::Display for LogUpdatesArg {
fn fmt(&self, f: &mut std::fmt::Formatter) -> core::fmt::Result {
let s = match self {
LogUpdatesArg::Start => "start",
LogUpdatesArg::Restart => "restart",
LogUpdatesArg::Stop => "stop",
};
write!(f, "{}", s)
}
}
pub fn parse_log(s: &str) -> Result<String> {
const SUFFIX: &str = "\n---\n\n";
let mut removed_suffix = s;
if s.len() > SUFFIX.len() && s[s.len() - SUFFIX.len()..] == *SUFFIX {
removed_suffix = &s[..s.len() - SUFFIX.len()]
}
let start = match removed_suffix.find('\n') {
Some(i) => i + 1,
None => 0,
};
parse_pyon_string(&removed_suffix[start..])
}
pub fn parse_pyon_string(s: &str) -> Result<String> {
if s.len() < 2 || s.bytes().next().unwrap() != b'"' || s.bytes().nth_back(0).unwrap() != b'"' {
return Err(format!("cannot parse {}", s).into());
}
lazy_static::lazy_static! {
static ref MATCH_ESCAPED: regex::Regex = regex::Regex::new(r#"\\x..|\\n|\\r|\\"|\\\\"#).unwrap();
}
let replace_fn: fn(®ex::Captures) -> String = |caps: ®ex::Captures| {
let capture = &caps[0];
if capture.bytes().next().unwrap() == b'\\' {
return match capture.bytes().nth(1).unwrap() {
b'n' => "\n".to_string(),
b'r' => "\r".to_string(),
b'"' => "\"".to_string(),
b'\\' => "\\".to_string(),
b'x' => {
let hex: String = capture.chars().skip(2).collect();
let n = match u32::from_str_radix(hex.as_str(), 16) {
Ok(n) => n,
Err(_) => return capture.to_string(),
};
match std::char::from_u32(n) {
Some(c) => c.to_string(),
None => capture.to_string(),
}
}
_ => capture.to_string(),
};
}
capture.to_string()
};
Ok((*MATCH_ESCAPED.replace_all(&s[1..s.len() - 1], replace_fn)).to_string())
}
pub fn pyon_to_json(s: &str) -> Result<String> {
const PREFIX: &str = "PyON";
const SUFFIX: &str = "\n---";
if s.len() < PREFIX.len()
|| s.bytes().take(PREFIX.len()).partial_cmp(PREFIX.bytes())
!= Some(core::cmp::Ordering::Equal)
|| s.len() < SUFFIX.len()
|| s.bytes()
.skip(s.len() - SUFFIX.len())
.partial_cmp(SUFFIX.bytes())
!= Some(core::cmp::Ordering::Equal)
{
return Err(format!("invalid PyON format: {}", s).into());
}
let mut start = match s.find('\n') {
Some(i) => i + 1,
None => 0,
};
let end = s.len() - SUFFIX.len();
if start > end {
start = end;
}
Ok(match &s[start..end] {
"True" => "true".to_string(),
"False" => "false".to_string(),
_ => s[start..end]
.replace(": None", r#": """#)
.replace(": False", ": false")
.replace(": True", ": true"),
})
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[test]
fn test_parse_log() {
struct Test {
s: &'static str,
expected: &'static str,
expect_error: bool,
}
let tests = vec![
Test {
s: "",
expected: "",
expect_error: true,
},
Test {
s: r#"PyON 1 log-update"#,
expected: "",
expect_error: true,
},
Test {
s: r#""""#,
expected: "",
expect_error: false,
},
Test {
s: r#"\n---\n\n"#,
expected: "",
expect_error: true,
},
Test {
s: "\n\"\"\n---\n\n",
expected: "",
expect_error: false,
},
Test {
s: "PyON 1 log-update\n\n---\n\n",
expected: "",
expect_error: true,
},
Test {
s: "PyON 1 log-update\n\"a\"\n---\n\n",
expected: "a",
expect_error: false,
},
];
for (i, test) in tests.iter().enumerate() {
let result = parse_log(test.s);
assert_eq!(result.is_err(), test.expect_error, "{}", i);
if !test.expect_error {
assert_eq!(result.unwrap(), test.expected, "{}", i);
}
}
}
#[test]
fn test_parse_pyon_string() {
struct Test {
s: &'static str,
expected: &'static str,
expect_error: bool,
}
let tests = vec![
Test {
s: "",
expected: "",
expect_error: true,
},
Test {
s: r#""""#,
expected: "",
expect_error: false,
},
Test {
s: r#""\n\"\\\x01""#,
expected: "\n\"\\\x01",
expect_error: false,
},
Test {
s: r#""a\x01a""#,
expected: "a\x01a",
expect_error: false,
},
];
for (i, test) in tests.iter().enumerate() {
let result = parse_pyon_string(test.s);
assert_eq!(result.is_err(), test.expect_error, "{}", i);
if !test.expect_error {
assert_eq!(result.unwrap(), test.expected, "{}", i);
}
}
}
#[test]
fn test_pyon_to_json() {
struct Test {
s: &'static str,
expected: &'static str,
expect_error: bool,
}
let tests = vec![
Test {
s: "",
expected: "",
expect_error: true,
},
Test {
s: "PyON",
expected: "",
expect_error: true,
},
Test {
s: "PyON\n---",
expected: "",
expect_error: false,
},
Test {
s: "PyON\n\n---",
expected: "",
expect_error: false,
},
Test {
s: "PyON\n1\n---",
expected: "1",
expect_error: false,
},
Test {
s: "PyON\nTrue\n---",
expected: "true",
expect_error: false,
},
Test {
s: "PyON\n{\"\": None}\n---",
expected: "{\"\": \"\"}",
expect_error: false,
},
Test {
s: "\n}÷ ",
expected: "",
expect_error: true,
},
];
for (i, test) in tests.iter().enumerate() {
let result = pyon_to_json(test.s);
assert_eq!(result.is_err(), test.expect_error, "{}", i);
if !test.expect_error {
assert_eq!(result.unwrap(), test.expected, "{}", i);
}
}
}
}
bencher::benchmark_group!(benches, bench_pyon_to_json);
bencher::benchmark_main!(benches);
fn bench_pyon_to_json(b: &mut bencher::Bencher) {
b.iter(|| pyon_to_json("PyON\nFalse\n---"))
}
#[cfg(test)]
mod integration_tests;