main
rs 142 lines 4.88 KB
Raw
1 use crate::process::Process;
2 use anyhow::{Context, Result};
3 use serde::{Deserialize, Serialize};
4 use std::collections::HashMap;
5 use std::io::{BufRead, BufReader, Write};
6 use std::os::unix::net::UnixStream;
7
8 /// Every command the CLI can send to the daemon over the Unix socket.
9 ///
10 /// Serialised as newline-delimited JSON with an internal `"cmd"` tag, e.g.:
11 /// `{"cmd":"start","name":"myapp","command":"node index.js","cwd":"/app","env":{},"max_restarts":10}`
12 #[derive(Debug, Serialize, Deserialize)]
13 #[serde(tag = "cmd", rename_all = "snake_case")]
14 pub enum Request {
15 /// Launch a new managed process.
16 Start {
17 name: String,
18 command: String,
19 cwd: String,
20 env: HashMap<String, String>,
21 max_restarts: u32,
22 },
23 /// Gracefully stop a running process and mark it `Stopped`
24 /// (no automatic restart).
25 Stop { name_or_id: String },
26 /// Stop then re-spawn a process, resetting its PID and start timestamp.
27 Restart { name_or_id: String },
28 /// Stop (if running) and permanently remove a process from the store.
29 Delete { name_or_id: String },
30 /// Return the full list of managed processes.
31 List,
32 /// Tail the last `lines` lines of a process's stdout log.
33 Logs { name_or_id: String, lines: usize },
34 /// Return detailed information about a single process.
35 Show { name_or_id: String },
36 /// Flush the in-memory store to `~/.proses/store.json`.
37 Save,
38 /// Re-spawn all processes that were in the `Running` state at last save.
39 Resurrect,
40 /// Ping the daemon; returns `{"success":true,"message":"ok"}`.
41 Health,
42 /// Gracefully stop all processes and exit the daemon.
43 Shutdown,
44 }
45
46 /// Envelope returned by the daemon for every [`Request`].
47 #[derive(Debug, Serialize, Deserialize)]
48 pub struct Response {
49 pub success: bool,
50 pub message: String,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub processes: Option<Vec<Process>>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub process: Option<Process>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub logs: Option<String>,
57 }
58
59 impl Response {
60 /// Construct a successful response with `message` and no payload.
61 pub fn ok(message: impl Into<String>) -> Self {
62 Self {
63 success: true,
64 message: message.into(),
65 processes: None,
66 process: None,
67 logs: None,
68 }
69 }
70
71 /// Construct a failure response with `message`.
72 pub fn err(message: impl Into<String>) -> Self {
73 Self {
74 success: false,
75 message: message.into(),
76 processes: None,
77 process: None,
78 logs: None,
79 }
80 }
81
82 /// Attach a process list payload (used by `List`).
83 pub fn with_processes(mut self, procs: Vec<Process>) -> Self {
84 self.processes = Some(procs);
85 self
86 }
87
88 /// Attach a single-process payload (used by `Start`, `Show`, etc.).
89 pub fn with_process(mut self, proc: Process) -> Self {
90 self.process = Some(proc);
91 self
92 }
93
94 /// Attach a log-content payload (used by `Logs`).
95 pub fn with_logs(mut self, logs: String) -> Self {
96 self.logs = Some(logs);
97 self
98 }
99 }
100
101 /// Blocking IPC client.
102 ///
103 /// Connects to the daemon's Unix domain socket, writes the JSON-encoded
104 /// `request` followed by a newline, then reads and deserialises the
105 /// single-line JSON response.
106 ///
107 /// This function uses only `std` I/O so it can be called from the
108 /// synchronous CLI without a Tokio runtime.
109 pub fn send(request: &Request) -> Result<Response> {
110 let sock_path = crate::config::get().sock_path.clone();
111
112 let mut stream = UnixStream::connect(&sock_path).map_err(|e| {
113 anyhow::anyhow!(
114 "Cannot connect to daemon socket at {:?}: {}\n\
115 Hint: run `proses daemon start` first.",
116 sock_path,
117 e
118 )
119 })?;
120
121 // ── Write request ──────────────────────────────────────────────────────
122 let mut payload = serde_json::to_string(request)?;
123 payload.push('\n');
124 stream
125 .write_all(payload.as_bytes())
126 .context("Failed to write request to daemon socket")?;
127 stream
128 .flush()
129 .context("Failed to flush request to daemon socket")?;
130
131 // ── Read response ──────────────────────────────────────────────────────
132 // The daemon always writes exactly one JSON line per connection.
133 let mut line = String::new();
134 BufReader::new(&stream)
135 .read_line(&mut line)
136 .context("Failed to read response from daemon socket")?;
137
138 let response: Response =
139 serde_json::from_str(line.trim()).context("Failed to deserialise daemon response")?;
140
141 Ok(response)
142 }