mod check;
mod poll;
mod serve;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::atomic::AtomicBool;
use check::Severity;
const USAGE: &str = "\
yodb, an embedded knowledge engine
usage:
yodb check FILE [--quick] [--quiet]
yodb serve [--bind ADDR] [--port PORT] [--unixsocket PATH] [--no-port]
check read a .yo file and report anything wrong with it. Never writes.
--quick skip the records and read only the headers
--quiet print findings and the summary, nothing else
serve speak RESP on a socket, so a Redis client can talk to it.
--bind address to listen on, 127.0.0.1 by default
--port port to listen on, 6379 by default
--unixsocket also listen on a socket file, which skips the
TCP stack and is the faster way in for a client
on the same machine
--no-port no TCP at all, socket file only
exit codes:
0 nothing wrong
1 something wrong
2 the arguments did not make sense, or the file could not be read at all
";
const DEFAULT_PORT: u16 = 6379;
const DEFAULT_BIND: &str = "127.0.0.1";
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut rest: Vec<&str> = args.iter().map(String::as_str).collect();
match rest.first().copied() {
Some("check") => {
rest.remove(0);
check_command(&rest)
}
Some("serve") => {
rest.remove(0);
serve_command(&rest)
}
Some("-h" | "--help") | None => {
print!("{USAGE}");
ExitCode::SUCCESS
}
Some("-V" | "--version") => {
println!("yo {}", env!("CARGO_PKG_VERSION"));
ExitCode::SUCCESS
}
Some(other) => {
eprintln!("yo: no such command: {other}\n");
eprint!("{USAGE}");
ExitCode::from(2)
}
}
}
fn check_command(args: &[&str]) -> ExitCode {
let mut path: Option<PathBuf> = None;
let mut quick = false;
let mut quiet = false;
for a in args {
match *a {
"--quick" => quick = true,
"--quiet" => quiet = true,
"-h" | "--help" => {
print!("{USAGE}");
return ExitCode::SUCCESS;
}
other if other.starts_with('-') => {
eprintln!("yodb check: no such option: {other}");
return ExitCode::from(2);
}
other if path.is_none() => path = Some(PathBuf::from(other)),
other => {
eprintln!("yodb check: takes one file, and was also given {other}");
return ExitCode::from(2);
}
}
}
let Some(path) = path else {
eprintln!("yodb check: which file?\n");
eprint!("{USAGE}");
return ExitCode::from(2);
};
let report = match check::check(&path, !quick) {
Ok(r) => r,
Err(e) => {
eprintln!("yodb check: {}: {e}", path.display());
return ExitCode::from(2);
}
};
if !quiet {
println!("{}", path.display());
}
for f in &report.findings {
println!("{f}");
}
let c = report.counts;
if !quiet {
if quick {
println!("{} segments, records not walked", c.regions);
} else {
println!(
"{} segments, {} records, {} record bytes, {} dead",
c.regions, c.records, c.record_bytes, c.dead_bytes
);
}
}
let errors = report.count(Severity::Error);
let warns = report.count(Severity::Warn);
if report.is_sound() {
println!(
"OK{}",
if warns > 0 {
format!(", with {warns} warning{}", plural(warns))
} else {
String::new()
}
);
ExitCode::SUCCESS
} else {
println!("FAILED: {errors} problem{}", plural(errors));
ExitCode::FAILURE
}
}
fn serve_command(args: &[&str]) -> ExitCode {
let mut bind = DEFAULT_BIND.to_string();
let mut port = DEFAULT_PORT;
let mut unixsocket: Option<std::path::PathBuf> = None;
let mut tcp = true;
let mut at = 0;
while at < args.len() {
let arg = args[at];
at += 1;
match arg {
"-h" | "--help" => {
print!("{USAGE}");
return ExitCode::SUCCESS;
}
"--no-port" => tcp = false,
"--bind" | "--port" | "--unixsocket" => {
let Some(value) = args.get(at) else {
eprintln!("yodb serve: {arg} needs a value");
return ExitCode::from(2);
};
at += 1;
if arg == "--bind" {
bind = (*value).to_string();
} else if arg == "--unixsocket" {
unixsocket = Some(std::path::PathBuf::from(*value));
} else {
match value.parse() {
Ok(p) => port = p,
Err(_) => {
eprintln!("yodb serve: {value} is not a port");
return ExitCode::from(2);
}
}
}
}
other => {
eprintln!("yodb serve: no such option: {other}");
return ExitCode::from(2);
}
}
}
let Ok(addr) = format!("{bind}:{port}").parse::<SocketAddr>() else {
eprintln!("yodb serve: {bind} is not an address to listen on");
return ExitCode::from(2);
};
if !tcp && unixsocket.is_none() {
eprintln!("yodb serve: --no-port with no --unixsocket leaves nothing to connect to");
return ExitCode::from(2);
}
let want = if tcp { Some(addr) } else { None };
let mut server = match serve::Server::open(want, unixsocket.clone()) {
Ok(s) => s,
Err(e) => {
eprintln!("yodb serve: {e}");
return ExitCode::from(2);
}
};
let version = env!("CARGO_PKG_VERSION");
match (tcp, &unixsocket) {
(true, Some(path)) => {
let bound = server.local_addr().unwrap_or(addr);
println!(
"yodb {version} listening on {bound} and on {}",
path.display()
);
}
(true, None) => {
let bound = server.local_addr().unwrap_or(addr);
println!("yodb {version} listening on {bound}");
}
(false, Some(path)) => {
println!("yodb {version} listening on {}", path.display());
}
(false, None) => unreachable!("refused above"),
}
static STOP: AtomicBool = AtomicBool::new(false);
match server.run(&STOP) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("yodb serve: {e}");
ExitCode::FAILURE
}
}
}
fn plural(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}