1use super::memory::{InMemoryControlPlane, InMemoryState};
14use super::*;
15use appcore_core::{Clock, SystemClock};
16use fs2::FileExt;
17use std::fs::{self, File, OpenOptions};
18use std::io::{Read, Write};
19use std::path::{Path, PathBuf};
20use std::sync::atomic::{AtomicU64, Ordering};
21
22const STATE_FORMAT_VERSION: u16 = 1;
23const STATE_FILE: &str = "control-plane-state-v1.json";
24const LOCK_FILE: &str = "control-plane-state.lock";
25const MAX_CONTROL_PLANE_STATE_BYTES: u64 = 16 * 1024 * 1024;
26static TEMP_COUNTER: AtomicU64 = AtomicU64::new(1);
28
29#[derive(Clone)]
36pub struct FileControlPlane {
37 root: PathBuf,
38 state_path: PathBuf,
39 lock_path: PathBuf,
40 retention_ms: u64,
41 clock: Arc<dyn Clock>,
42}
43
44impl std::fmt::Debug for FileControlPlane {
45 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 formatter
47 .debug_struct("FileControlPlane")
48 .field("root", &self.root)
49 .field("retention_ms", &self.retention_ms)
50 .finish_non_exhaustive()
51 }
52}
53
54impl FileControlPlane {
55 pub fn open(root: impl Into<PathBuf>, retention_ms: u64) -> ControlPlaneResult<Self> {
57 Self::with_clock(root, retention_ms, Arc::new(SystemClock::new()))
58 }
59
60 pub fn with_clock(
62 root: impl Into<PathBuf>,
63 retention_ms: u64,
64 clock: Arc<dyn Clock>,
65 ) -> ControlPlaneResult<Self> {
66 if retention_ms == 0 {
67 return Err(ControlPlaneError::Rejected(
68 "control-plane retention must be greater than zero".to_string(),
69 ));
70 }
71 let root = root.into();
72 prepare_root(&root)?;
73 let control = Self {
74 state_path: root.join(STATE_FILE),
75 lock_path: root.join(LOCK_FILE),
76 root,
77 retention_ms,
78 clock,
79 };
80 control.initialize()?;
81 Ok(control)
82 }
83
84 pub fn state_path(&self) -> &Path {
86 &self.state_path
87 }
88
89 pub fn retention_ms(&self) -> u64 {
91 self.retention_ms
92 }
93
94 pub fn backup_to(&self, destination: impl AsRef<Path>) -> ControlPlaneResult<()> {
96 let _lock = self.lock_exclusive()?;
97 let envelope = self.load_envelope()?;
98 let encoded = encode_envelope(&envelope)?;
99 write_atomic(destination.as_ref(), &encoded)
100 }
101
102 pub fn restore_from(&self, source: impl AsRef<Path>) -> ControlPlaneResult<()> {
104 reject_symlink(source.as_ref())?;
105 let encoded = read_bounded(source.as_ref(), "control-plane backup read")?;
106 let _ = decode_envelope(&encoded)?;
107 let _lock = self.lock_exclusive()?;
108 write_atomic(&self.state_path, &encoded)
109 }
110
111 fn initialize(&self) -> ControlPlaneResult<()> {
112 let _lock = self.lock_exclusive()?;
113 if self.state_path.exists() {
114 let _ = self.load_envelope()?;
115 return Ok(());
116 }
117 self.save_control(&InMemoryControlPlane::default())
118 }
119
120 fn load_control(&self) -> ControlPlaneResult<InMemoryControlPlane> {
121 let envelope = self.load_envelope()?;
122 Ok(InMemoryControlPlane::from_state(envelope.state))
123 }
124
125 fn load_envelope(&self) -> ControlPlaneResult<StateEnvelope> {
126 reject_symlink(&self.state_path)?;
127 let encoded = read_bounded(&self.state_path, "control-plane state read")?;
128 decode_envelope(&encoded)
129 }
130
131 fn save_control(&self, control: &InMemoryControlPlane) -> ControlPlaneResult<()> {
132 let envelope = StateEnvelope {
133 format_version: STATE_FORMAT_VERSION,
134 state: control.snapshot()?,
135 };
136 write_atomic(&self.state_path, &encode_envelope(&envelope)?)
137 }
138
139 fn prune(&self, control: &InMemoryControlPlane, now_ms: u64) -> ControlPlaneResult<()> {
140 let cutoff = now_ms.saturating_sub(self.retention_ms);
141 let _ = control.prune_registrations(cutoff)?;
142 Ok(())
143 }
144
145 fn lock_exclusive(&self) -> ControlPlaneResult<File> {
146 reject_symlink(&self.lock_path)?;
147 let mut options = OpenOptions::new();
148 options.create(true).read(true).write(true);
149 #[cfg(unix)]
150 {
151 use std::os::unix::fs::OpenOptionsExt;
152 options.mode(0o600);
153 }
154 let file = options
155 .open(&self.lock_path)
156 .map_err(|error| transport_error("control-plane lock open", error))?;
157 file.lock_exclusive()
158 .map_err(|error| transport_error("control-plane lock acquire", error))?;
159 Ok(file)
160 }
161}
162
163impl ControlPlaneProvider for FileControlPlane {
164 fn register<'a>(
165 &'a self,
166 mut registration: CoreRegistration,
167 ) -> ControlPlaneFuture<'a, CorePresence> {
168 Box::pin(async move {
169 let _lock = self.lock_exclusive()?;
170 let control = self.load_control()?;
171 let now_ms = self.clock.now_ms();
172 registration.registered_at_ms = now_ms;
173 self.prune(&control, now_ms)?;
174 let result = control.register(registration).await?;
175 self.save_control(&control)?;
176 Ok(result)
177 })
178 }
179
180 fn heartbeat<'a>(
181 &'a self,
182 mut request: HeartbeatRequest,
183 ) -> ControlPlaneFuture<'a, HeartbeatResponse> {
184 Box::pin(async move {
185 let _lock = self.lock_exclusive()?;
186 let control = self.load_control()?;
187 let now_ms = self.clock.now_ms();
188 request.sent_at_ms = now_ms;
189 self.prune(&control, now_ms)?;
190 let result = control.heartbeat(request).await?;
191 self.save_control(&control)?;
192 Ok(result)
193 })
194 }
195
196 fn discover_peers<'a>(
197 &'a self,
198 identity: &'a CoreIdentity,
199 ) -> ControlPlaneFuture<'a, PeerDirectory> {
200 Box::pin(async move {
201 let _lock = self.lock_exclusive()?;
202 let control = self.load_control()?;
203 let now_ms = self.clock.now_ms();
204 self.prune(&control, now_ms)?;
205 let mut result = control.discover_peers(identity).await?;
206 result.refreshed_at_ms = now_ms;
207 self.save_control(&control)?;
208 Ok(result)
209 })
210 }
211
212 fn acquire_or_renew_service_lease<'a>(
213 &'a self,
214 identity: &'a CoreIdentity,
215 service_id: &'a ServiceId,
216 ttl_ms: u64,
217 _client_now_ms: u64,
218 ) -> ControlPlaneFuture<'a, ServiceLeaderLease> {
219 Box::pin(async move {
220 let _lock = self.lock_exclusive()?;
221 let control = self.load_control()?;
222 let result = control
223 .acquire_or_renew_service_lease(identity, service_id, ttl_ms, self.clock.now_ms())
224 .await?;
225 self.save_control(&control)?;
226 Ok(result)
227 })
228 }
229
230 fn release_service_lease<'a>(
231 &'a self,
232 lease: ServiceLeaderLease,
233 ) -> ControlPlaneFuture<'a, ()> {
234 Box::pin(async move {
235 let _lock = self.lock_exclusive()?;
236 let control = self.load_control()?;
237 control.release_service_lease(lease).await?;
238 self.save_control(&control)
239 })
240 }
241}
242
243#[derive(Debug, serde::Serialize, serde::Deserialize)]
244#[serde(deny_unknown_fields)]
245struct StateEnvelope {
246 format_version: u16,
247 state: InMemoryState,
248}
249
250fn encode_envelope(envelope: &StateEnvelope) -> ControlPlaneResult<Vec<u8>> {
251 let encoded = serde_json::to_vec(envelope)
252 .map_err(|error| ControlPlaneError::InvalidResponse(error.to_string()))?;
253 if encoded.len() as u64 > MAX_CONTROL_PLANE_STATE_BYTES {
254 return Err(ControlPlaneError::Rejected(
255 "control-plane state exceeds configured limit".to_string(),
256 ));
257 }
258 Ok(encoded)
259}
260
261fn decode_envelope(encoded: &[u8]) -> ControlPlaneResult<StateEnvelope> {
262 let envelope = serde_json::from_slice::<StateEnvelope>(encoded).map_err(|_| {
263 ControlPlaneError::InvalidResponse("NO MORE SUPPORTED PLEASE UPDATE".to_string())
264 })?;
265 if envelope.format_version != STATE_FORMAT_VERSION {
266 return Err(ControlPlaneError::InvalidResponse(
267 "NO MORE SUPPORTED PLEASE UPDATE".to_string(),
268 ));
269 }
270 Ok(envelope)
271}
272
273fn prepare_root(root: &Path) -> ControlPlaneResult<()> {
274 reject_symlink(root)?;
275 fs::create_dir_all(root)
276 .map_err(|error| transport_error("control-plane directory create", error))?;
277 #[cfg(unix)]
278 {
279 use std::os::unix::fs::PermissionsExt;
280 fs::set_permissions(root, fs::Permissions::from_mode(0o700))
281 .map_err(|error| transport_error("control-plane directory permissions", error))?;
282 }
283 Ok(())
284}
285
286fn reject_symlink(path: &Path) -> ControlPlaneResult<()> {
287 match fs::symlink_metadata(path) {
288 Ok(metadata) if metadata.file_type().is_symlink() => Err(ControlPlaneError::Rejected(
289 "control-plane path cannot be a symlink".to_string(),
290 )),
291 Ok(_) => Ok(()),
292 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
293 Err(error) => Err(transport_error("control-plane path inspection", error)),
294 }
295}
296
297fn read_bounded(path: &Path, operation: &str) -> ControlPlaneResult<Vec<u8>> {
298 reject_symlink(path)?;
299 let mut file = File::open(path).map_err(|error| transport_error(operation, error))?;
300 let mut encoded = Vec::new();
301 Read::by_ref(&mut file)
302 .take(MAX_CONTROL_PLANE_STATE_BYTES.saturating_add(1))
303 .read_to_end(&mut encoded)
304 .map_err(|error| transport_error(operation, error))?;
305 if encoded.len() as u64 > MAX_CONTROL_PLANE_STATE_BYTES {
306 return Err(ControlPlaneError::Rejected(
307 "control-plane state exceeds configured limit".to_string(),
308 ));
309 }
310 Ok(encoded)
311}
312
313fn write_atomic(path: &Path, bytes: &[u8]) -> ControlPlaneResult<()> {
314 let parent = path.parent().unwrap_or_else(|| Path::new("."));
315 fs::create_dir_all(parent)
316 .map_err(|error| transport_error("control-plane parent create", error))?;
317 let temp = parent.join(format!(
318 ".control-plane.{}.{}.tmp",
319 std::process::id(),
320 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
321 ));
322 let result = write_and_replace(&temp, path, parent, bytes);
323 if result.is_err() {
324 let _ = fs::remove_file(temp);
325 }
326 result
327}
328
329fn write_and_replace(
330 temp: &Path,
331 path: &Path,
332 _parent: &Path,
333 bytes: &[u8],
334) -> ControlPlaneResult<()> {
335 let mut options = OpenOptions::new();
336 options.create_new(true).write(true);
337 #[cfg(unix)]
338 {
339 use std::os::unix::fs::OpenOptionsExt;
340 options.mode(0o600);
341 }
342 let mut file = options
343 .open(temp)
344 .map_err(|error| transport_error("control-plane temp create", error))?;
345 file.write_all(bytes)
346 .and_then(|_| file.sync_all())
347 .map_err(|error| transport_error("control-plane state write", error))?;
348 fs::rename(temp, path)
349 .map_err(|error| transport_error("control-plane state replace", error))?;
350 #[cfg(unix)]
351 File::open(_parent)
352 .and_then(|directory| directory.sync_all())
353 .map_err(|error| transport_error("control-plane directory sync", error))?;
354 Ok(())
355}
356
357fn transport_error(operation: &str, error: std::io::Error) -> ControlPlaneError {
358 ControlPlaneError::Transport(format!("{operation}: {error}"))
359}