Skip to main content

pebble_cms/global/
registry.rs

1use anyhow::{bail, Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::Path;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum SiteStatus {
9    Stopped,
10    Running,
11    Deploying,
12}
13
14impl Default for SiteStatus {
15    fn default() -> Self {
16        Self::Stopped
17    }
18}
19
20impl std::fmt::Display for SiteStatus {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            SiteStatus::Stopped => write!(f, "stopped"),
24            SiteStatus::Running => write!(f, "running"),
25            SiteStatus::Deploying => write!(f, "deploying"),
26        }
27    }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct RegistrySite {
32    pub name: String,
33    pub title: String,
34    pub description: String,
35    pub created_at: String,
36    #[serde(default)]
37    pub status: SiteStatus,
38    pub port: Option<u16>,
39    pub pid: Option<u32>,
40    #[serde(default)]
41    pub last_started: Option<String>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, Default)]
45pub struct Registry {
46    #[serde(default)]
47    pub sites: HashMap<String, RegistrySite>,
48}
49
50impl Registry {
51    pub fn load(path: &Path) -> Result<Self> {
52        if !path.exists() {
53            return Ok(Self::default());
54        }
55
56        let content = std::fs::read_to_string(path)
57            .with_context(|| format!("Failed to read registry from {}", path.display()))?;
58        let registry: Registry = toml::from_str(&content)
59            .with_context(|| format!("Failed to parse registry from {}", path.display()))?;
60        Ok(registry)
61    }
62
63    pub fn save(&self, path: &Path) -> Result<()> {
64        let content = toml::to_string_pretty(self).context("Failed to serialize registry")?;
65        if let Some(parent) = path.parent() {
66            std::fs::create_dir_all(parent)?;
67        }
68        std::fs::write(path, content)
69            .with_context(|| format!("Failed to write registry to {}", path.display()))?;
70        Ok(())
71    }
72
73    pub fn add_site(&mut self, site: RegistrySite) -> Result<()> {
74        if self.sites.contains_key(&site.name) {
75            bail!("Site '{}' already exists in registry", site.name);
76        }
77        self.sites.insert(site.name.clone(), site);
78        Ok(())
79    }
80
81    pub fn get_site(&self, name: &str) -> Option<&RegistrySite> {
82        self.sites.get(name)
83    }
84
85    pub fn get_site_mut(&mut self, name: &str) -> Option<&mut RegistrySite> {
86        self.sites.get_mut(name)
87    }
88
89    pub fn remove_site(&mut self, name: &str) -> Option<RegistrySite> {
90        self.sites.remove(name)
91    }
92
93    pub fn list_sites(&self) -> Vec<&RegistrySite> {
94        let mut sites: Vec<_> = self.sites.values().collect();
95        sites.sort_by(|a, b| a.name.cmp(&b.name));
96        sites
97    }
98
99    pub fn running_sites(&self) -> Vec<&RegistrySite> {
100        self.sites
101            .values()
102            .filter(|s| s.status == SiteStatus::Running)
103            .collect()
104    }
105
106    pub fn find_available_port(&self, start: u16, end: u16) -> Option<u16> {
107        let used_ports: std::collections::HashSet<u16> = self
108            .sites
109            .values()
110            .filter_map(|s| {
111                if s.status == SiteStatus::Running {
112                    s.port
113                } else {
114                    None
115                }
116            })
117            .collect();
118
119        (start..=end).find(|port| !used_ports.contains(port) && is_port_available(*port))
120    }
121
122    pub fn update_site_status(
123        &mut self,
124        name: &str,
125        status: SiteStatus,
126        port: Option<u16>,
127        pid: Option<u32>,
128    ) {
129        if let Some(site) = self.sites.get_mut(name) {
130            site.status = status;
131            site.port = port;
132            site.pid = pid;
133            if status == SiteStatus::Running {
134                site.last_started = Some(chrono::Utc::now().to_rfc3339());
135            }
136        }
137    }
138
139    pub fn cleanup_dead_processes(&mut self) {
140        for site in self.sites.values_mut() {
141            if site.status == SiteStatus::Running {
142                if let Some(pid) = site.pid {
143                    if !is_process_running(pid) {
144                        site.status = SiteStatus::Stopped;
145                        site.port = None;
146                        site.pid = None;
147                    }
148                }
149            }
150        }
151    }
152}
153
154fn is_port_available(port: u16) -> bool {
155    std::net::TcpListener::bind(("127.0.0.1", port)).is_ok()
156}
157
158#[cfg(unix)]
159fn is_process_running(pid: u32) -> bool {
160    unsafe { libc::kill(pid as i32, 0) == 0 }
161}
162
163#[cfg(windows)]
164fn is_process_running(pid: u32) -> bool {
165    use std::process::Command;
166    Command::new("tasklist")
167        .args(["/FI", &format!("PID eq {}", pid), "/NH"])
168        .output()
169        .map(|o| String::from_utf8_lossy(&o.stdout).contains(&pid.to_string()))
170        .unwrap_or(false)
171}