use serde_json::{json, Value};
use std::net::SocketAddr;
use std::sync::{OnceLock, RwLock};
static HOST: OnceLock<String> = OnceLock::new();
static SOURCE: OnceLock<String> = OnceLock::new();
static CLIENT: RwLock<Option<(String, String)>> = RwLock::new(None);
static PEER: RwLock<Option<(String, Option<u32>)>> = RwLock::new(None);
pub fn set_source(source: &str) {
let _ = SOURCE.set(source.to_string());
}
pub fn record_client(name: &str, version: &str) {
if let Ok(mut slot) = CLIENT.write() {
*slot = Some((name.to_string(), version.to_string()));
}
}
pub fn record_peer(addr: SocketAddr, resolve_pid: bool) {
let ip = addr.ip().to_string();
let pid = if resolve_pid {
resolve_local_pid(&addr)
} else {
None
};
if let Ok(mut slot) = PEER.write() {
let keep_pid = match (&*slot, pid) {
(Some((_, prev)), None) => *prev,
(_, p) => p,
};
*slot = Some((ip, keep_pid));
}
}
fn resolve_local_pid(addr: &SocketAddr) -> Option<u32> {
if !addr.ip().is_loopback() {
return None;
}
let me = std::process::id();
let out = std::process::Command::new("lsof")
.args([
"-nP",
&format!("-iTCP:{}", addr.port()),
"-sTCP:ESTABLISHED",
"-Fp",
])
.output()
.ok()?;
if !out.status.success() {
return None;
}
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|l| {
l.strip_prefix('p')
.and_then(|p| p.trim().parse::<u32>().ok())
})
.find(|&pid| pid != me)
}
pub fn peer_ip() -> Option<String> {
PEER.read()
.ok()
.and_then(|s| s.as_ref().map(|(ip, _)| ip.clone()))
}
pub fn peer_pid() -> Option<u32> {
PEER.read()
.ok()
.and_then(|s| s.as_ref().and_then(|(_, pid)| *pid))
}
pub fn client_version() -> Option<String> {
CLIENT
.read()
.ok()
.and_then(|c| c.as_ref().map(|(_, v)| v.clone()))
.filter(|v| !v.is_empty())
}
pub fn transport() -> Option<String> {
SOURCE.get().cloned()
}
pub fn host_name() -> String {
host().to_string()
}
fn host() -> &'static str {
HOST.get_or_init(|| {
std::env::var("HOSTNAME")
.ok()
.filter(|h| !h.trim().is_empty())
.or_else(|| {
std::process::Command::new("hostname")
.output()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|h| !h.is_empty())
})
.unwrap_or_else(|| "unknown".to_string())
})
}
pub fn writer_json() -> Value {
let client = CLIENT.read().ok().and_then(|c| c.clone());
json!({
"host": host(),
"source": SOURCE.get().map(String::as_str).unwrap_or("unknown"),
"client": client.as_ref().map(|(n, _)| n.clone()),
"client_version": client.as_ref().map(|(_, v)| v.clone()),
})
}
pub fn client_name() -> Option<String> {
CLIENT
.read()
.ok()
.and_then(|c| c.as_ref().map(|(n, _)| n.clone()))
}
pub fn consumer_kind() -> String {
client_name()
.or_else(|| SOURCE.get().cloned())
.unwrap_or_else(|| "unknown".to_string())
}
pub fn stamp_provenance(cm: &mut pensieve_memory::CreateMemory) {
let writer = writer_json();
match &mut cm.provenance {
Some(Value::Object(obj)) => {
obj.insert("writer".to_string(), writer);
}
Some(other) => {
cm.provenance = Some(json!({"value": other.clone(), "writer": writer}));
}
None => cm.provenance = Some(json!({"writer": writer})),
}
}