1use serde::{Deserialize, Serialize};
2use std::env;
3use std::fs;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct InstanceInfo {
8 pub pid: u32,
9 pub socket_path: PathBuf,
10 pub name: Option<String>,
11}
12
13pub fn instances_dir() -> PathBuf {
14 let cache_root = env::var_os("XDG_CACHE_HOME")
15 .filter(|value| !value.is_empty())
16 .map(PathBuf::from)
17 .or_else(|| {
18 env::var_os("HOME")
19 .filter(|value| !value.is_empty())
20 .map(|home| PathBuf::from(home).join(".cache"))
21 })
22 .unwrap_or_else(env::temp_dir);
23 cache_root.join("hx-remote").join("instances")
24}
25
26pub fn register_instance(info: &InstanceInfo) -> std::io::Result<()> {
27 let dir = instances_dir();
28 fs::create_dir_all(&dir)?;
29 let path = dir.join(format!("{}.json", info.pid));
30 let json = serde_json::to_string(info)?;
31 fs::write(path, json)
32}
33
34pub fn unregister_instance(pid: u32) {
35 let _ = fs::remove_file(instances_dir().join(format!("{}.json", pid)));
36}
37
38pub fn get_active_instances() -> Vec<InstanceInfo> {
39 let dir = instances_dir();
40 let Ok(entries) = fs::read_dir(dir) else {
41 return vec![];
42 };
43
44 let mut instances = Vec::new();
45 for entry in entries.flatten() {
46 let path = entry.path();
47 if path.extension().is_some_and(|ext| ext == "json")
48 && let Ok(contents) = fs::read_to_string(&path)
49 {
50 if let Ok(info) = serde_json::from_str::<InstanceInfo>(&contents) {
51 if crate::client::socket_is_listening(&info.socket_path).unwrap_or(false) {
53 instances.push(info);
54 } else {
55 let _ = fs::remove_file(&path);
56 }
57 } else {
58 let _ = fs::remove_file(&path);
59 }
60 }
61 }
62
63 instances.sort_by_key(|info| info.pid);
65 instances
66}