- src/api/: HTTP router + DTOs + the MCP server (the HTTP/MCP-facing surface): mod.rs (formerly api.rs), dto.rs, mcp_server.rs. - src/cli/: argument parsing and process entry (formerly inline in main.rs) plus session registry: mod.rs, sessions.rs. main.rs is now a thin entry point that just declares modules and calls cli::run(). - src/engine/: the actor + everything it depends on to parse/index/ query log lines: mod.rs (formerly engine.rs), alerts.rs, promql.rs, rate.rs, ingest.rs, logfmt.rs, filter_parser.rs/.pest, filter_datetime.rs, index_key.rs, data_value.rs, metric_primitive.rs, timestamp.rs. - src/config/: persisted configuration: mod.rs (formerly settings.rs), profile.rs. - src/consts.rs stays at the crate root, used from every module above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
159 lines
4.8 KiB
Rust
159 lines
4.8 KiB
Rust
use std::fs;
|
|
use std::io;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SessionInfo {
|
|
pub name: String,
|
|
pub port: u16,
|
|
pub pid: u32,
|
|
}
|
|
|
|
pub fn sessions_dir(settings_path: &Path) -> PathBuf {
|
|
settings_path
|
|
.parent()
|
|
.map(|parent| parent.join("sessions"))
|
|
.unwrap_or_else(|| PathBuf::from("sessions"))
|
|
}
|
|
|
|
fn session_file_path(dir: &Path, name: &str) -> PathBuf {
|
|
dir.join(format!("{name}.json"))
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn is_pid_alive(pid: u32) -> bool {
|
|
process::Command::new("kill")
|
|
.args(["-0", &pid.to_string()])
|
|
.stdout(process::Stdio::null())
|
|
.stderr(process::Stdio::null())
|
|
.status()
|
|
.map(|status| status.success())
|
|
.unwrap_or(true)
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn is_pid_alive(_pid: u32) -> bool {
|
|
true
|
|
}
|
|
|
|
pub fn cleanup_stale(dir: &Path) {
|
|
let Ok(entries) = fs::read_dir(dir) else {
|
|
return;
|
|
};
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
|
|
continue;
|
|
}
|
|
let Ok(contents) = fs::read_to_string(&path) else {
|
|
continue;
|
|
};
|
|
let Ok(info) = serde_json::from_str::<SessionInfo>(&contents) else {
|
|
continue;
|
|
};
|
|
if !is_pid_alive(info.pid) {
|
|
let _ = fs::remove_file(&path);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn generate_name() -> String {
|
|
petname::petname(2, "-").unwrap_or_else(|| format!("session-{}", process::id()))
|
|
}
|
|
|
|
pub fn register_session(settings_path: &Path, name: &str, port: u16) -> io::Result<PathBuf> {
|
|
let dir = sessions_dir(settings_path);
|
|
fs::create_dir_all(&dir)?;
|
|
cleanup_stale(&dir);
|
|
let info = SessionInfo {
|
|
name: name.to_string(),
|
|
port,
|
|
pid: process::id(),
|
|
};
|
|
let path = session_file_path(&dir, name);
|
|
fs::write(&path, serde_json::to_string_pretty(&info)?)?;
|
|
Ok(path)
|
|
}
|
|
|
|
pub fn resolve_session(settings_path: &Path, name: &str) -> Option<SessionInfo> {
|
|
let dir = sessions_dir(settings_path);
|
|
let contents = fs::read_to_string(session_file_path(&dir, name)).ok()?;
|
|
let info: SessionInfo = serde_json::from_str(&contents).ok()?;
|
|
is_pid_alive(info.pid).then_some(info)
|
|
}
|
|
|
|
pub fn list_sessions(dir: &Path) -> Vec<SessionInfo> {
|
|
let Ok(entries) = fs::read_dir(dir) else {
|
|
return Vec::new();
|
|
};
|
|
let mut sessions: Vec<SessionInfo> = entries
|
|
.flatten()
|
|
.filter(|entry| entry.path().extension().and_then(|ext| ext.to_str()) == Some("json"))
|
|
.filter_map(|entry| fs::read_to_string(entry.path()).ok())
|
|
.filter_map(|contents| serde_json::from_str(&contents).ok())
|
|
.collect();
|
|
sessions.sort_by(|a, b| a.name.cmp(&b.name));
|
|
sessions
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn register_session_writes_correct_port() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let settings_path = dir.path().join("settings.json");
|
|
|
|
let path = register_session(&settings_path, "foo", 4242).unwrap();
|
|
assert!(path.exists());
|
|
|
|
let info = resolve_session(&settings_path, "foo").unwrap();
|
|
assert_eq!(info.port, 4242);
|
|
assert_eq!(info.name, "foo");
|
|
assert_eq!(info.pid, process::id());
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_session_returns_none_for_unknown_name() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let settings_path = dir.path().join("settings.json");
|
|
assert!(resolve_session(&settings_path, "does-not-exist").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn stale_session_does_not_block_reregistration_under_same_name() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let settings_path = dir.path().join("settings.json");
|
|
let sessions_dir_path = sessions_dir(&settings_path);
|
|
fs::create_dir_all(&sessions_dir_path).unwrap();
|
|
|
|
let stale = SessionInfo { name: "foo".to_string(), port: 1111, pid: 999_999 };
|
|
fs::write(
|
|
session_file_path(&sessions_dir_path, "foo"),
|
|
serde_json::to_string(&stale).unwrap(),
|
|
)
|
|
.unwrap();
|
|
|
|
register_session(&settings_path, "foo", 5555).unwrap();
|
|
let info = resolve_session(&settings_path, "foo").unwrap();
|
|
assert_eq!(info.port, 5555);
|
|
assert_eq!(info.pid, process::id());
|
|
}
|
|
|
|
#[test]
|
|
fn list_sessions_returns_all_entries_sorted_by_name() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let settings_path = dir.path().join("settings.json");
|
|
register_session(&settings_path, "zeta", 1).unwrap();
|
|
register_session(&settings_path, "alpha", 2).unwrap();
|
|
|
|
let sessions = list_sessions(&sessions_dir(&settings_path));
|
|
let names: Vec<&str> = sessions.iter().map(|s| s.name.as_str()).collect();
|
|
assert_eq!(names, vec!["alpha", "zeta"]);
|
|
}
|
|
}
|