use std::sync::OnceLock;
use serde_json::{Value, json};
use crate::port_owner::{self, PortOwner};
type OwnerCacheEntry = ((String, u16), Option<PortOwner>);
static OWNER_CACHE: OnceLock<std::sync::Mutex<Vec<OwnerCacheEntry>>> = OnceLock::new();
static REMEMBERED_VERSION: OnceLock<std::sync::Mutex<Option<u32>>> = OnceLock::new();
pub fn remember_version(version: Option<u32>) {
if version.is_none() {
return;
}
let lock = REMEMBERED_VERSION.get_or_init(|| std::sync::Mutex::new(None));
let mut guard = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard = version;
}
pub fn remembered_version() -> Option<u32> {
let lock = REMEMBERED_VERSION.get_or_init(|| std::sync::Mutex::new(None));
let guard = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard
}
fn cached_owner(host: &str, port: u16) -> Option<PortOwner> {
let lock = OWNER_CACHE.get_or_init(|| std::sync::Mutex::new(Vec::new()));
let mut guard = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let key = (host.to_owned(), port);
if let Some((_, owner)) = guard.iter().find(|(k, _)| k == &key) {
return owner.clone();
}
let owner = port_owner::find_listener(port).ok().flatten();
guard.push((key, owner.clone()));
owner
}
pub fn build(host: &str, port: u16, firefox_version: Option<u32>) -> Value {
let mut obj = serde_json::Map::new();
obj.insert("host".to_string(), Value::String(host.to_owned()));
obj.insert("port".to_string(), json!(port));
let version = firefox_version.or_else(remembered_version);
if let Some(v) = version {
obj.insert("firefox_version".to_string(), json!(v));
}
if is_loopback(host)
&& let Some(owner) = cached_owner(host, port)
{
obj.insert("connected_pid".to_string(), json!(owner.pid));
if !owner.process_name.is_empty() {
obj.insert(
"connected_process".to_string(),
Value::String(owner.process_name),
);
}
if let Some(uptime) = owner.uptime_s {
obj.insert("uptime_s".to_string(), json!(uptime));
}
}
Value::Object(obj)
}
pub(crate) fn is_loopback(host: &str) -> bool {
matches!(host, "localhost" | "127.0.0.1" | "::1")
}
pub fn merge_into(meta: &mut Value, host: &str, port: u16, firefox_version: Option<u32>) {
if let Some(obj) = meta.as_object_mut() {
obj.insert("connection".to_string(), build(host, port, firefox_version));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_includes_host_and_port() {
let meta = build("127.0.0.1", 6000, None);
assert_eq!(meta["host"], "127.0.0.1");
assert_eq!(meta["port"], 6000);
}
#[test]
fn build_includes_firefox_version_when_known() {
let meta = build("127.0.0.1", 6000, Some(149));
assert_eq!(meta["firefox_version"], 149);
}
#[test]
fn build_omits_firefox_version_when_unknown() {
let meta = build("127.0.0.1", 6000, None);
assert!(meta.get("firefox_version").is_none());
}
#[test]
fn merge_into_adds_connection_field() {
let mut meta = json!({"host": "127.0.0.1", "port": 6000});
merge_into(&mut meta, "127.0.0.1", 6000, Some(149));
assert!(meta["connection"].is_object());
assert_eq!(meta["connection"]["firefox_version"], 149);
}
}