feature/ci-debian
rs 495 lines 19.5 KB
Raw
1 //! Async daemon internals.
2 //!
3 //! `run()` is called from the daemonized grandchild process created by
4 //! `daemon::fork::spawn_daemon`. It boots a multi-threaded Tokio runtime,
5 //! writes the PID file, binds the Unix socket, and then services IPC
6 //! connections while a background task monitors managed processes.
7
8 use std::{fs, sync::Arc};
9
10 use anyhow::Result;
11 use chrono::Utc;
12 use tokio::{
13 io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
14 net::{UnixListener, UnixStream},
15 sync::Mutex,
16 time::{sleep, Duration},
17 };
18
19 use crate::{
20 config,
21 daemon::ipc::{Request, Response},
22 process::{runner, store::Store, Process, ProcessStatus},
23 };
24
25 // ─── Public entry point ───────────────────────────────────────────────────────
26
27 /// Entry point called from `daemon/mod.rs` after the double-fork.
28 ///
29 /// Creates a multi-threaded Tokio runtime and drives `async_main` to
30 /// completion. Never returns — the process terminates via
31 /// `std::process::exit`.
32 pub fn run() -> ! {
33 // Initialise config inside the daemon process (safe to call multiple
34 // times; subsequent calls are no-ops via OnceCell).
35 config::init();
36
37 let rt = tokio::runtime::Builder::new_multi_thread()
38 .enable_all()
39 .build()
40 .expect("Failed to build Tokio runtime");
41
42 rt.block_on(async {
43 if let Err(e) = async_main().await {
44 eprintln!("[proses-daemon] fatal: {:#}", e);
45 std::process::exit(1);
46 }
47 });
48
49 std::process::exit(0);
50 }
51
52 // ─── Async entry point ────────────────────────────────────────────────────────
53
54 async fn async_main() -> Result<()> {
55 let cfg = config::get();
56
57 // Write PID file so the CLI can find us.
58 let pid = std::process::id();
59 fs::write(&cfg.pid_path, pid.to_string())?;
60
61 // Remove any stale socket left by a previous crash.
62 if cfg.sock_path.exists() {
63 let _ = fs::remove_file(&cfg.sock_path);
64 }
65
66 // Load (or create) the persistent process store.
67 let store: Arc<Mutex<Store>> = Arc::new(Mutex::new(Store::load(&cfg.store_path)));
68
69 // Background task: periodically check for dead processes and restart them.
70 let monitor_store = store.clone();
71 tokio::spawn(async move {
72 monitor_loop(monitor_store).await;
73 });
74
75 // Bind the Unix domain socket.
76 let listener = UnixListener::bind(&cfg.sock_path)?;
77
78 loop {
79 match listener.accept().await {
80 Ok((stream, _addr)) => {
81 let store = store.clone();
82 tokio::spawn(async move {
83 handle_connection(stream, store).await;
84 });
85 }
86 Err(e) => {
87 eprintln!("[proses-daemon] accept error: {}", e);
88 }
89 }
90 }
91 }
92
93 // ─── Monitor loop ─────────────────────────────────────────────────────────────
94
95 /// Wakes every 2 seconds and restarts any `Running` process whose OS PID has
96 /// died. Processes explicitly stopped by the user (`Stopped` status) are
97 /// never restarted here.
98 async fn monitor_loop(store: Arc<Mutex<Store>>) {
99 loop {
100 sleep(Duration::from_secs(2)).await;
101
102 let cfg = config::get();
103
104 // Snapshot the IDs of currently Running processes (short lock hold).
105 let running_ids: Vec<u32> = {
106 let s = store.lock().await;
107 s.processes
108 .values()
109 .filter(|p| p.status == ProcessStatus::Running)
110 .map(|p| p.id)
111 .collect()
112 };
113
114 for id in running_ids {
115 // Re-check with a fresh lock so we pick up Stop commands that
116 // arrived since we collected the IDs.
117 let (pid, restarts, max_restarts) = {
118 let s = store.lock().await;
119 match s.processes.get(&id) {
120 Some(p) if p.status == ProcessStatus::Running => {
121 (p.pid, p.restarts, p.max_restarts)
122 }
123 _ => continue,
124 }
125 };
126
127 // Fast path: the process is still alive.
128 if runner::is_running(pid) {
129 continue;
130 }
131
132 // Process is dead — decide whether to restart or mark Errored.
133 let mut s = store.lock().await;
134 let proc = match s.processes.get_mut(&id) {
135 Some(p) if p.status == ProcessStatus::Running => p,
136 _ => continue,
137 };
138
139 // max_restarts == 0 means unlimited.
140 if max_restarts == 0 || restarts < max_restarts {
141 let snapshot = proc.clone();
142 match runner::spawn(&snapshot) {
143 Ok(new_pid) => {
144 proc.pid = new_pid;
145 proc.restarts += 1;
146 proc.started_at = Utc::now();
147 proc.status = ProcessStatus::Running;
148 }
149 Err(e) => {
150 eprintln!("[proses-daemon] failed to restart '{}': {}", proc.name, e);
151 proc.status = ProcessStatus::Errored;
152 }
153 }
154 } else {
155 eprintln!(
156 "[proses-daemon] '{}' exceeded max_restarts ({}), marking errored",
157 proc.name, max_restarts
158 );
159 proc.status = ProcessStatus::Errored;
160 }
161
162 let _ = s.save(&cfg.store_path);
163 }
164 }
165 }
166
167 // ─── Connection handler ───────────────────────────────────────────────────────
168
169 /// Read one JSON line from `stream`, dispatch to `handle_request`, and
170 /// write back a single JSON response line.
171 async fn handle_connection(stream: UnixStream, store: Arc<Mutex<Store>>) {
172 let (reader_half, mut writer_half) = stream.into_split();
173 let mut reader = BufReader::new(reader_half);
174 let mut line = String::new();
175
176 match reader.read_line(&mut line).await {
177 Ok(0) => return, // EOF — client disconnected without sending anything.
178 Err(e) => {
179 eprintln!("[proses-daemon] read error: {}", e);
180 return;
181 }
182 Ok(_) => {}
183 }
184
185 let response = match serde_json::from_str::<Request>(line.trim()) {
186 Ok(req) => handle_request(req, store).await,
187 Err(e) => Response::err(format!("Malformed request: {}", e)),
188 };
189
190 let mut json = match serde_json::to_string(&response) {
191 Ok(s) => s,
192 Err(_) => r#"{"success":false,"message":"response serialisation error"}"#.to_string(),
193 };
194 json.push('\n');
195
196 let _ = writer_half.write_all(json.as_bytes()).await;
197 }
198
199 // ─── Request dispatcher ───────────────────────────────────────────────────────
200
201 async fn handle_request(req: Request, store: Arc<Mutex<Store>>) -> Response {
202 match req {
203 // ── Start ──────────────────────────────────────────────────────────
204 Request::Start {
205 name,
206 command,
207 cwd,
208 env,
209 max_restarts,
210 } => {
211 let cfg = config::get();
212 let mut s = store.lock().await;
213
214 if s.find(&name).is_some() {
215 return Response::err(format!(
216 "A process named '{}' already exists. \
217 Use `restart` or `delete` it first.",
218 name
219 ));
220 }
221
222 let id = s.alloc_id();
223 let log_out = cfg.log_dir.join(format!("{}-{}.out", name, id));
224 let log_err = cfg.log_dir.join(format!("{}-{}.err", name, id));
225
226 let mut proc = Process {
227 id,
228 name: name.clone(),
229 pid: 0,
230 command,
231 cwd: std::path::PathBuf::from(cwd),
232 env,
233 status: ProcessStatus::Stopped,
234 restarts: 0,
235 max_restarts,
236 started_at: Utc::now(),
237 log_out,
238 log_err,
239 };
240
241 match runner::spawn(&proc) {
242 Ok(pid) => {
243 proc.pid = pid;
244 proc.status = ProcessStatus::Running;
245 proc.started_at = Utc::now();
246 let resp = Response::ok(format!("Process '{}' started (pid {})", name, pid))
247 .with_process(proc.clone());
248 s.processes.insert(id, proc);
249 let _ = s.save(&cfg.store_path);
250 resp
251 }
252 Err(e) => {
253 proc.status = ProcessStatus::Errored;
254 s.processes.insert(id, proc);
255 let _ = s.save(&cfg.store_path);
256 Response::err(format!("Failed to start '{}': {}", name, e))
257 }
258 }
259 }
260
261 // ── Stop ───────────────────────────────────────────────────────────
262 Request::Stop { name_or_id } => {
263 let cfg = config::get();
264 let mut s = store.lock().await;
265
266 let id = match s.find_id(&name_or_id) {
267 Some(id) => id,
268 None => return Response::err(format!("Process '{}' not found", name_or_id)),
269 };
270
271 let pid = s.processes[&id].pid;
272 let _ = runner::stop(pid);
273
274 if let Some(proc) = s.processes.get_mut(&id) {
275 proc.status = ProcessStatus::Stopped;
276 proc.pid = 0;
277 }
278 let _ = s.save(&cfg.store_path);
279 Response::ok(format!("Process '{}' stopped", name_or_id))
280 }
281
282 // ── Restart ────────────────────────────────────────────────────────
283 //
284 // Per spec: release the lock, stop the old process, sleep 300 ms,
285 // respawn, re-acquire the lock to update pid / started_at.
286 Request::Restart { name_or_id } => {
287 let cfg = config::get();
288
289 // Step 1 — snapshot what we need, then release the lock.
290 let (old_pid, snapshot) = {
291 let s = store.lock().await;
292 match s.find(&name_or_id) {
293 None => return Response::err(format!("Process '{}' not found", name_or_id)),
294 Some(p) => (p.pid, p.clone()),
295 }
296 };
297
298 // Step 2 — stop old process (lock free).
299 let _ = runner::stop(old_pid);
300
301 // Step 3 — give the OS a moment to reap the child.
302 sleep(Duration::from_millis(300)).await;
303
304 // Step 4 — spawn the new instance (lock free).
305 let new_pid = match runner::spawn(&snapshot) {
306 Ok(pid) => pid,
307 Err(e) => {
308 return Response::err(format!("Failed to restart '{}': {}", name_or_id, e))
309 }
310 };
311
312 // Step 5 — re-acquire lock and update the store record.
313 let mut s = store.lock().await;
314 if let Some(id) = s.find_id(&name_or_id) {
315 if let Some(proc) = s.processes.get_mut(&id) {
316 proc.pid = new_pid;
317 proc.started_at = Utc::now();
318 proc.status = ProcessStatus::Running;
319 }
320 let _ = s.save(&cfg.store_path);
321 Response::ok(format!(
322 "Process '{}' restarted (pid {})",
323 name_or_id, new_pid
324 ))
325 } else {
326 Response::err(format!("Process '{}' vanished during restart", name_or_id))
327 }
328 }
329
330 // ── Delete ─────────────────────────────────────────────────────────
331 Request::Delete { name_or_id } => {
332 let cfg = config::get();
333 let mut s = store.lock().await;
334
335 let id = match s.find_id(&name_or_id) {
336 Some(id) => id,
337 None => return Response::err(format!("Process '{}' not found", name_or_id)),
338 };
339
340 // Stop if running before removing.
341 let pid_to_stop = s
342 .processes
343 .get(&id)
344 .filter(|p| p.status == ProcessStatus::Running)
345 .map(|p| p.pid);
346
347 if let Some(pid) = pid_to_stop {
348 let _ = runner::stop(pid);
349 }
350
351 s.processes.remove(&id);
352 let _ = s.save(&cfg.store_path);
353 Response::ok(format!("Process '{}' deleted", name_or_id))
354 }
355
356 // ── List ───────────────────────────────────────────────────────────
357 Request::List => {
358 let s = store.lock().await;
359 let procs: Vec<Process> = s.processes.values().cloned().collect();
360 let count = procs.len();
361 Response::ok(format!("{} process(es)", count)).with_processes(procs)
362 }
363
364 // ── Logs ───────────────────────────────────────────────────────────
365 Request::Logs { name_or_id, lines } => {
366 let log_path = {
367 let s = store.lock().await;
368 match s.find(&name_or_id) {
369 None => return Response::err(format!("Process '{}' not found", name_or_id)),
370 Some(p) => p.log_out.clone(),
371 }
372 };
373
374 match fs::read_to_string(&log_path) {
375 Err(e) => Response::err(format!("Cannot read log {:?}: {}", log_path, e)),
376 Ok(content) => {
377 let all_lines: Vec<&str> = content.lines().collect();
378 let start = all_lines.len().saturating_sub(lines);
379 let tail = all_lines[start..].join("\n");
380 Response::ok(format!("Last {} line(s) of stdout log", lines)).with_logs(tail)
381 }
382 }
383 }
384
385 // ── Show ───────────────────────────────────────────────────────────
386 Request::Show { name_or_id } => {
387 let s = store.lock().await;
388 match s.find(&name_or_id) {
389 None => Response::err(format!("Process '{}' not found", name_or_id)),
390 Some(p) => {
391 Response::ok(format!("Process '{}'", name_or_id)).with_process(p.clone())
392 }
393 }
394 }
395
396 // ── Save ───────────────────────────────────────────────────────────
397 Request::Save => {
398 let cfg = config::get();
399 let s = store.lock().await;
400 match s.save(&cfg.store_path) {
401 Ok(_) => Response::ok("Store saved to disk"),
402 Err(e) => Response::err(format!("Save failed: {}", e)),
403 }
404 }
405
406 // ── Resurrect ──────────────────────────────────────────────────────
407 //
408 // Re-spawn all processes whose persisted status is Running but whose
409 // PID is no longer alive. Useful after a daemon crash/restart.
410 Request::Resurrect => {
411 let cfg = config::get();
412 let mut s = store.lock().await;
413
414 let dead_running_ids: Vec<u32> = s
415 .processes
416 .values()
417 .filter(|p| p.status == ProcessStatus::Running && !runner::is_running(p.pid))
418 .map(|p| p.id)
419 .collect();
420
421 let mut spawned = 0u32;
422 let mut failed = 0u32;
423
424 for id in dead_running_ids {
425 if let Some(proc) = s.processes.get_mut(&id) {
426 let snapshot = proc.clone();
427 match runner::spawn(&snapshot) {
428 Ok(new_pid) => {
429 proc.pid = new_pid;
430 proc.started_at = Utc::now();
431 proc.status = ProcessStatus::Running;
432 spawned += 1;
433 }
434 Err(e) => {
435 eprintln!(
436 "[proses-daemon] resurrect failed for '{}': {}",
437 proc.name, e
438 );
439 proc.status = ProcessStatus::Errored;
440 failed += 1;
441 }
442 }
443 }
444 }
445
446 let _ = s.save(&cfg.store_path);
447
448 if failed == 0 {
449 Response::ok(format!("Resurrected {} process(es)", spawned))
450 } else {
451 Response::err(format!(
452 "Resurrected {}, failed to spawn {}",
453 spawned, failed
454 ))
455 }
456 }
457
458 // ── Health ─────────────────────────────────────────────────────────
459 Request::Health => Response::ok("daemon is healthy"),
460
461 // ── Shutdown ───────────────────────────────────────────────────────
462 Request::Shutdown => {
463 let cfg = config::get();
464
465 // Stop every running process and persist the final state.
466 {
467 let mut s = store.lock().await;
468 let ids: Vec<u32> = s.processes.keys().cloned().collect();
469 for id in ids {
470 if let Some(proc) = s.processes.get_mut(&id) {
471 if proc.status == ProcessStatus::Running {
472 let _ = runner::stop(proc.pid);
473 proc.status = ProcessStatus::Stopped;
474 proc.pid = 0;
475 }
476 }
477 }
478 let _ = s.save(&cfg.store_path);
479 }
480
481 // Schedule the actual exit slightly after this response is flushed
482 // so the client always receives the response before the socket
483 // disappears.
484 tokio::spawn(async {
485 sleep(Duration::from_millis(150)).await;
486 let cfg = config::get();
487 let _ = fs::remove_file(&cfg.pid_path);
488 let _ = fs::remove_file(&cfg.sock_path);
489 std::process::exit(0);
490 });
491
492 Response::ok("Daemon is shutting down")
493 }
494 }
495 }