pebble_cms/global/
registry.rs1use 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}