use std::io;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::time::Duration;
pub const EXPECTED_SCHEMA_VERSION: u32 = parse_schema_version();
const SCHEMA_VERSION_JSON: &str = include_str!("../schema/version.json");
const fn parse_schema_version() -> u32 {
let bytes = SCHEMA_VERSION_JSON.as_bytes();
let key = b"\"version\"";
let mut i = 0;
while i + key.len() <= bytes.len() {
let mut matched = true;
let mut k = 0;
while k < key.len() {
if bytes[i + k] != key[k] {
matched = false;
break;
}
k += 1;
}
if matched {
let mut j = i + key.len();
while j < bytes.len() {
let c = bytes[j];
if c == b':' || c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' {
j += 1;
} else {
break;
}
}
let mut value: u32 = 0;
let mut saw_digit = false;
while j < bytes.len() {
let c = bytes[j];
if c.is_ascii_digit() {
value = value * 10 + (c - b'0') as u32;
saw_digit = true;
j += 1;
} else {
break;
}
}
if saw_digit {
return value;
}
}
i += 1;
}
panic!("schema/version.json does not contain an integer \"version\" field");
}
const SOCKET_FILE: &str = "kindling.sock";
const PORT_FILE: &str = "kindling.port";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Transport {
#[cfg(unix)]
#[cfg_attr(unix, default)]
Uds,
#[cfg_attr(not(unix), default)]
Tcp,
}
#[derive(Clone, Default)]
pub enum Spawner {
#[default]
Command,
Custom(Arc<dyn Fn() -> io::Result<()> + Send + Sync>),
}
impl Spawner {
pub fn custom<F>(f: F) -> Self
where
F: Fn() -> io::Result<()> + Send + Sync + 'static,
{
Spawner::Custom(Arc::new(f))
}
pub(crate) fn spawn(&self) -> io::Result<()> {
match self {
Spawner::Command => {
let mut cmd = Command::new("kindling");
cmd.args(["serve", "--daemonize"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
cmd.spawn()?;
Ok(())
}
Spawner::Custom(f) => f(),
}
}
}
impl std::fmt::Debug for Spawner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Spawner::Command => f.write_str("Spawner::Command"),
Spawner::Custom(_) => f.write_str("Spawner::Custom(..)"),
}
}
}
#[derive(Clone, Debug)]
pub struct ClientConfig {
pub socket_path: PathBuf,
pub port_path: PathBuf,
pub project_root: String,
pub expected_schema_version: u32,
pub connect_timeout: Duration,
pub poll_interval: Duration,
pub spawn: Spawner,
pub transport: Transport,
}
impl ClientConfig {
pub fn defaults() -> io::Result<Self> {
let socket_path = default_socket_path().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"could not determine kindling home (no HOME/USERPROFILE)",
)
})?;
let port_path = default_port_path().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"could not determine kindling home (no HOME/USERPROFILE)",
)
})?;
let project_root = std::env::current_dir()?.to_string_lossy().into_owned();
Ok(Self {
socket_path,
port_path,
project_root,
expected_schema_version: EXPECTED_SCHEMA_VERSION,
connect_timeout: Duration::from_secs(1),
poll_interval: Duration::from_millis(10),
spawn: Spawner::default(),
transport: Transport::default(),
})
}
}
pub fn default_socket_path() -> Option<PathBuf> {
let home = std::env::var_os("HOME")
.filter(|v| !v.is_empty())
.or_else(|| std::env::var_os("USERPROFILE").filter(|v| !v.is_empty()))?;
Some(PathBuf::from(home).join(".kindling").join(SOCKET_FILE))
}
pub fn default_port_path() -> Option<PathBuf> {
let home = std::env::var_os("HOME")
.filter(|v| !v.is_empty())
.or_else(|| std::env::var_os("USERPROFILE").filter(|v| !v.is_empty()))?;
Some(PathBuf::from(home).join(".kindling").join(PORT_FILE))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn expected_schema_version_parses_to_five() {
assert_eq!(EXPECTED_SCHEMA_VERSION, 5);
}
}