use crate::maps::{HashLookup, MapsCache};
use crate::shared::{load_config, serve_xvuid};
use crate::dtob_ffi;
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixListener;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{fs, process};
fn handle_command(
cmd_line: &str,
cache: &MapsCache,
repo_path: &Path,
maps_path: &Path,
out: &mut Vec<u8>,
) -> Result<(), String> {
let parts: Vec<&str> = cmd_line.trim().split_whitespace().collect();
if parts.is_empty() {
return Err("empty command".into());
}
let command = parts[0];
match command {
"serve:xvuid" => {
let xvuid = parts.get(1).ok_or("missing xvuid")?;
let requested_offset: Option<usize> = parts.get(2).and_then(|s| s.parse().ok());
let built = serve_xvuid(cache, repo_path, xvuid, requested_offset, false)
.ok_or_else(|| format!("failed to serve xvuid {}", xvuid))?;
let stripped = xanadoc::decode_content(&built.current_content)
.ok_or_else(|| format!("failed to decode trailing content for xvuid {}", xvuid))?;
out.extend_from_slice(stripped);
}
"serve:cid" | "serve:ccid" => {
let hash = parts.get(1).ok_or("missing hash")?;
let map_name = if command == "serve:cid" { "cid_to_xvuid" } else { "ccid_to_xvuid" };
match cache.lookup_hash(map_name, hash) {
HashLookup::Found { xvuid, index } => {
let built = serve_xvuid(cache, repo_path, &xvuid, Some(index), false)
.ok_or_else(|| format!("failed to serve hash {}", hash))?;
let stripped = xanadoc::decode_content(&built.current_content)
.ok_or_else(|| format!("failed to decode trailing content for hash {}", hash))?;
out.extend_from_slice(stripped);
}
HashLookup::Missing => {
return Err(format!("hash {} not found", hash));
}
}
}
"resolve:cid:xvuid" | "resolve:ccid:xvuid" => {
let hash = parts.get(1).ok_or("missing hash")?;
let map_name = if command == "resolve:cid:xvuid" {
"cid_to_xvuid"
} else {
"ccid_to_xvuid"
};
match cache.lookup_hash(map_name, hash) {
HashLookup::Found { xvuid, .. } => {
out.extend_from_slice(xvuid.as_bytes());
out.push(b'\n');
}
HashLookup::Missing => {
return Err(format!("hash {} not found", hash));
}
}
}
"resolve:ccid:cid" => {
let hash = parts.get(1).ok_or("missing ccid")?;
match cache.lookup_hash("ccid_to_xvuid", hash) {
HashLookup::Found { xvuid, index } => {
let built = serve_xvuid(cache, repo_path, &xvuid, Some(index), false)
.ok_or("failed to parse target dif")?;
if let Some(ids) = xanadoc::decode_ids(&built.current_content) {
let hex_str = hex::encode(ids.cid);
out.extend_from_slice(hex_str.as_bytes());
out.push(b'\n');
}
unsafe { dtob_ffi::ffi_dtob_free(built.target_root); }
}
HashLookup::Missing => {
return Err(format!("ccid {} not found", hash));
}
}
}
"resolve:xvuid:cid" | "resolve:xvuid:ccid" => {
let xvuid = parts.get(1).ok_or("missing xvuid")?;
let field = if command == "resolve:xvuid:cid" { 1 } else { 2 };
match cache.lookup_meta_hex(xvuid, field) {
Some(val) => {
out.extend_from_slice(val.as_bytes());
out.push(b'\n');
}
None => {
return Err(format!("xvuid {} not found in meta", xvuid));
}
}
}
"resolve:ccid:title" => {
let hash = parts.get(1).ok_or("missing ccid")?;
match cache.lookup_hash("ccid_to_xvuid", hash) {
HashLookup::Found { xvuid, .. } => match cache.lookup_title(&xvuid) {
Some(title) => {
out.extend_from_slice(title.as_bytes());
out.push(b'\n');
}
None => {
return Err(format!("title not found for xvuid {}", xvuid));
}
},
HashLookup::Missing => {
return Err(format!("ccid {} not found", hash));
}
}
}
"table" => {
crate::table::generate_table_to_buf(maps_path, out);
}
"table:xvuid" | "table:cid" | "table:ccid" => {
let hash = parts.get(1).ok_or("missing hash")?;
let target_xvuid = if command == "table:xvuid" {
hash.to_string()
} else {
let map_name = if command == "table:cid" {
"cid_to_xvuid"
} else {
"ccid_to_xvuid"
};
match cache.lookup_hash(map_name, hash) {
HashLookup::Found { xvuid, .. } => xvuid,
HashLookup::Missing => {
return Err(format!("hash {} not found", hash));
}
}
};
crate::table::generate_table_for_xvuid_to_buf(maps_path, cache, &target_xvuid, out);
}
"titles" => {
if let Some(meta_root) = cache.meta_root() {
unsafe {
let len = dtob_ffi::ffi_dtob_kvset_len(meta_root);
for i in 0..len {
let key_ptr = dtob_ffi::ffi_dtob_kvset_key(meta_root, i);
let val_ptr = dtob_ffi::ffi_dtob_kvset_value_at(meta_root, i);
if key_ptr.is_null() || val_ptr.is_null() {
continue;
}
let key_c = std::ffi::CStr::from_ptr(key_ptr);
let xvuid_str = key_c.to_string_lossy();
let v_title = dtob_ffi::ffi_dtob_array_get(val_ptr, 0);
let mut tlen = 0;
let tptr = dtob_ffi::ffi_dtob_get_raw(v_title, &mut tlen);
let title = if !tptr.is_null() && tlen > 0 {
String::from_utf8_lossy(std::slice::from_raw_parts(tptr, tlen))
.to_string()
} else {
"Untitled".to_string()
};
let line = format!("{}\t{}\n", xvuid_str, title);
out.extend_from_slice(line.as_bytes());
}
}
}
}
_ => {
return Err(format!("unknown command: {}", command));
}
}
Ok(())
}
pub fn run_daemon() {
let repo_path = load_config("REPO_DIR");
let maps_path = load_config("MAPS_DIR");
let log_path = maps_path.join("sculblog-daemon.log");
unsafe {
let pid = libc::fork();
if pid < 0 { std::process::exit(1); }
if pid > 0 { std::process::exit(0); }
libc::setsid();
let pid2 = libc::fork();
if pid2 < 0 { std::process::exit(1); }
if pid2 > 0 { std::process::exit(0); }
let null_fd = libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDWR);
if null_fd >= 0 {
libc::dup2(null_fd, libc::STDIN_FILENO);
libc::dup2(null_fd, libc::STDOUT_FILENO);
if null_fd > libc::STDERR_FILENO {
libc::close(null_fd);
}
}
let log_c = std::ffi::CString::new(log_path.to_str().unwrap()).unwrap();
let log_fd = libc::open(
log_c.as_ptr(),
libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND,
0o644,
);
if log_fd >= 0 {
libc::dup2(log_fd, libc::STDERR_FILENO);
if log_fd > libc::STDERR_FILENO {
libc::close(log_fd);
}
}
}
let socket_path = maps_path.join("sculblog.sock");
let pid_path = maps_path.join("sculblog-daemon.pid");
let _ = fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).unwrap_or_else(|e| {
eprintln!("Failed to bind {}: {}", socket_path.display(), e);
process::exit(1);
});
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&socket_path, fs::Permissions::from_mode(0o777));
}
let _ = fs::write(&pid_path, format!("{}", process::id()));
let mut cache = MapsCache::load(&maps_path);
eprintln!(
"sculblog daemon started on {} (PID {})",
socket_path.display(),
process::id()
);
unsafe {
libc::signal(libc::SIGHUP, {
extern "C" fn handler(_: libc::c_int) {
RELOAD_REQUESTED.store(true, Ordering::SeqCst);
}
handler as libc::sighandler_t
});
libc::signal(libc::SIGTERM, {
extern "C" fn handler(_: libc::c_int) {
SHUTDOWN_REQUESTED.store(true, Ordering::SeqCst);
}
handler as libc::sighandler_t
});
}
for stream in listener.incoming() {
if SHUTDOWN_REQUESTED.load(Ordering::SeqCst) {
eprintln!("SIGTERM received, shutting down.");
break;
}
if RELOAD_REQUESTED.load(Ordering::SeqCst) {
RELOAD_REQUESTED.store(false, Ordering::SeqCst);
eprintln!("SIGHUP received, reloading maps...");
drop(cache);
cache = MapsCache::load(&maps_path);
eprintln!("Maps reloaded.");
}
match stream {
Ok(stream) => {
let mut reader = BufReader::new(&stream);
let mut line = String::new();
if reader.read_line(&mut line).is_ok() && !line.is_empty() {
let mut response = Vec::new();
let mut writer = stream.try_clone().unwrap_or_else(|_| {
panic!("failed to clone stream");
});
match handle_command(&line, &cache, &repo_path, &maps_path, &mut response) {
Ok(()) => {
let len_bytes = (response.len() as u32).to_be_bytes();
let _ = writer.write_all(&len_bytes);
let _ = writer.write_all(&response);
}
Err(msg) => {
let err_bytes = format!("ERR {}", msg);
let len_bytes = (err_bytes.len() as u32).to_be_bytes();
let _ = writer.write_all(&len_bytes);
let _ = writer.write_all(err_bytes.as_bytes());
eprintln!("Command error: {}", msg);
}
}
let _ = writer.flush();
}
}
Err(e) => {
eprintln!("Connection error: {}", e);
}
}
}
let _ = fs::remove_file(&socket_path);
let _ = fs::remove_file(&pid_path);
}
static RELOAD_REQUESTED: AtomicBool = AtomicBool::new(false);
static SHUTDOWN_REQUESTED: AtomicBool = AtomicBool::new(false);