cubesys: add cube systemd daemon (cube-server) + socket client (cubec)

Implements the requested cube service: a long-lived daemon that holds ONE
CubeStore for its whole lifetime and serves the cube command language over a
Unix-domain socket, plus cubec to talk to it.

- cubesys::commands: factored the single command interpreter (Session::exec)
  so cube REPL, cubec client, and the daemon run identical logic
- cubesys::net: dependency-free length-framed AF_UNIX transport
- cubesys::persist: dependency-free JSON snapshot (atomic tmp+rename) so the
  store -- including sealed/encrypted records -- survives daemon restarts
- cube-server: listens on $XDG_RUNTIME_DIR/cube/cube.sock, snapshots to
  $XDG_STATE_HOME/cube/cube-store.json, replays on startup
- cubec: one-shot + REPL client over the socket
- cube.rs trimmed to a thin REPL/script/demo driver (help text updated)
- /etc/systemd/system/cube.service: runs as luulu, ProtectSystem=strict,
  RestrictAddressFamilies=AF_UNIX, Restart=on-failure; enabled + active
- integration.md documents the daemon + caveat (open rewrites plaintext)

Verified: ./check (fmt+tests+clippy -D warnings) green; ./check mount (27
FUSE e2e) green; socket CLI round-trips; sealed record survived a full
service restart and reopened+r with original value.
This commit is contained in:
CUBELinux-2
2026-08-11 00:10:45 -04:00
parent 35e5183193
commit 06ea3252eb
9 changed files with 807 additions and 227 deletions
+91
View File
@@ -0,0 +1,91 @@
//! `cubec` — the CUBELinux-2 system client.
//!
//! Talks to `cube-server` over its Unix-domain socket. Two modes:
//!
//! cubec <command...> one-shot: send a single command, print the reply
//! cubec REPL: read command lines from stdin, one per
//! connection, printing each reply (like `cube repl`)
//!
//! The socket defaults to /run/cube/cube.sock (or $XDG_RUNTIME_DIR/cube/cube.sock)
//! and can be overridden with `--socket PATH`.
use std::io::{BufRead, Write};
use std::os::unix::net::UnixStream;
use cubesys::net::{read_frame, write_frame};
fn main() {
let args: Vec<String> = std::env::args().collect();
let socket_path = socket_from_args(&args);
// Build the command from everything that isn't `--socket PATH`.
let mut rest: Vec<String> = Vec::new();
let mut i = 1;
while i < args.len() {
if args[i] == "--socket" {
i += 2; // skip the flag and its value
continue;
}
rest.push(args[i].clone());
i += 1;
}
if rest.is_empty() {
repl(&socket_path);
} else {
let cmd = rest.join(" ");
match request(&socket_path, &cmd) {
Ok(reply) => println!("{reply}"),
Err(e) => {
eprintln!("cubec: {e}");
std::process::exit(1);
}
}
}
}
fn repl(socket_path: &str) {
let stdin = std::io::stdin();
for line in stdin.lock().lines() {
let line = match line {
Ok(l) => l,
Err(_) => break,
};
if line.trim().is_empty() || line.starts_with('#') {
continue;
}
match request(socket_path, &line) {
Ok(reply) => println!("{reply}"),
Err(e) => eprintln!("error: {e}"),
}
}
}
/// Open a connection, send one command frame, return the reply frame.
fn request(socket_path: &str, command: &str) -> Result<String, String> {
let mut stream = UnixStream::connect(socket_path)
.map_err(|e| format!("cannot connect to {socket_path}: {e}"))?;
write_frame(&mut stream, command).map_err(|e| format!("write: {e}"))?;
read_frame(&mut stream).map_err(|e| format!("read: {e}"))
}
fn socket_from_args(args: &[String]) -> String {
let mut i = 0;
while i < args.len() {
if args[i] == "--socket" {
if let Some(v) = args.get(i + 1) {
return v.clone();
}
}
i += 1;
}
if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
return format!("{runtime}/cube/cube.sock");
}
"/run/cube/cube.sock".to_string()
}
#[allow(dead_code)]
fn _flush() {
let _ = std::io::stdout().flush();
}