feature/ci-debian
rs 229 lines 6.28 KB
Raw
1 use std::collections::HashMap;
2
3 use crate::daemon::ipc::{self, Request};
4
5 // ── Output helpers ────────────────────────────────────────────────────────────
6
7 fn ok(msg: &str) {
8 println!(" ✓ {msg}");
9 }
10
11 fn fail(msg: &str) -> ! {
12 eprintln!(" ✗ {msg}");
13 std::process::exit(1);
14 }
15
16 fn send_or_fail(req: &Request) -> crate::daemon::ipc::Response {
17 match ipc::send(req) {
18 Ok(resp) => resp,
19 Err(e) => fail(&e.to_string()),
20 }
21 }
22
23 // ── Commands ──────────────────────────────────────────────────────────────────
24
25 /// `proses start <command> [--name NAME] [--cwd DIR] [--env K=V]`
26 ///
27 /// If a process with the same name already exists in the daemon the daemon
28 /// will stop the old instance and re-launch it with the new parameters,
29 /// effectively acting as a restart-with-new-config.
30 pub fn start(
31 command: String,
32 name: Option<String>,
33 cwd: Option<String>,
34 env_pairs: Vec<String>,
35 max_restarts: u32,
36 ) {
37 // Default name: first token of the command string.
38 let name = name.unwrap_or_else(|| {
39 command
40 .split_whitespace()
41 .next()
42 .unwrap_or("process")
43 .to_string()
44 });
45
46 // Default cwd: current working directory.
47 let cwd = cwd.unwrap_or_else(|| {
48 std::env::current_dir()
49 .map(|p| p.to_string_lossy().into_owned())
50 .unwrap_or_else(|_| "/".to_string())
51 });
52
53 // Parse KEY=VALUE pairs.
54 let env: HashMap<String, String> = env_pairs
55 .into_iter()
56 .filter_map(|pair| {
57 let (k, v) = pair.split_once('=')?;
58 Some((k.to_string(), v.to_string()))
59 })
60 .collect();
61
62 let resp = send_or_fail(&Request::Start {
63 name,
64 command,
65 cwd,
66 env,
67 max_restarts,
68 });
69
70 if resp.success {
71 ok(&resp.message);
72 } else {
73 fail(&resp.message);
74 }
75 }
76
77 /// `proses stop <name|id>`
78 ///
79 /// Sends `SIGTERM` to the process and marks it `Stopped` so the daemon
80 /// will not attempt to restart it automatically.
81 pub fn stop(name_or_id: String) {
82 let resp = send_or_fail(&Request::Stop {
83 name_or_id: name_or_id.clone(),
84 });
85 if resp.success {
86 ok(&resp.message);
87 } else {
88 fail(&resp.message);
89 }
90 }
91
92 /// `proses restart <name|id>`
93 ///
94 /// Stops the current instance (SIGTERM) and immediately re-spawns it.
95 pub fn restart(name_or_id: String) {
96 let resp = send_or_fail(&Request::Restart {
97 name_or_id: name_or_id.clone(),
98 });
99 if resp.success {
100 ok(&resp.message);
101 } else {
102 fail(&resp.message);
103 }
104 }
105
106 /// `proses delete <name|id>`
107 ///
108 /// Stops the process (if running) and removes it from the daemon's store.
109 pub fn delete(name_or_id: String) {
110 let resp = send_or_fail(&Request::Delete {
111 name_or_id: name_or_id.clone(),
112 });
113 if resp.success {
114 ok(&resp.message);
115 } else {
116 fail(&resp.message);
117 }
118 }
119
120 /// `proses save`
121 ///
122 /// Flushes the current in-memory process list to `~/.proses/store.json`
123 /// so it survives a daemon restart.
124 pub fn save() {
125 let resp = send_or_fail(&Request::Save);
126 if resp.success {
127 ok(&resp.message);
128 } else {
129 fail(&resp.message);
130 }
131 }
132
133 /// `proses resurrect`
134 ///
135 /// Re-launches all processes that were in the `Running` state the last
136 /// time `save` was called (or the daemon was stopped cleanly).
137 pub fn resurrect() {
138 let resp = send_or_fail(&Request::Resurrect);
139 if resp.success {
140 ok(&resp.message);
141 } else {
142 fail(&resp.message);
143 }
144 }
145
146 /// `proses show <name|id>`
147 ///
148 /// Prints detailed information about a single process.
149 pub fn show(name_or_id: String) {
150 let resp = send_or_fail(&Request::Show {
151 name_or_id: name_or_id.clone(),
152 });
153
154 if !resp.success {
155 fail(&resp.message);
156 }
157
158 let Some(p) = resp.process else {
159 fail("daemon returned no process data");
160 };
161
162 let status_str = format!("{}", p.status);
163 let uptime = if p.pid > 0 {
164 let secs = (chrono::Utc::now() - p.started_at).num_seconds().max(0) as u64;
165 format_uptime(secs)
166 } else {
167 "–".to_string()
168 };
169
170 println!();
171 println!(" {:<16} {}", "id:", p.id);
172 println!(" {:<16} {}", "name:", p.name);
173 println!(" {:<16} {status_str}", "status:");
174 println!(
175 " {:<16} {}",
176 "pid:",
177 if p.pid > 0 {
178 p.pid.to_string()
179 } else {
180 "–".to_string()
181 }
182 );
183 println!(" {:<16} {}", "restarts:", p.restarts);
184 println!(" {:<16} {}", "max restarts:", p.max_restarts);
185 println!(" {:<16} {uptime}", "uptime:");
186 println!(" {:<16} {}", "command:", p.command);
187 println!(" {:<16} {}", "cwd:", p.cwd.display());
188 println!(" {:<16} {}", "stdout log:", p.log_out.display());
189 println!(" {:<16} {}", "stderr log:", p.log_err.display());
190 if !p.env.is_empty() {
191 println!(" {:<16}", "env:");
192 let mut keys: Vec<&String> = p.env.keys().collect();
193 keys.sort();
194 for k in keys {
195 println!(" {}={}", k, p.env[k]);
196 }
197 }
198 println!();
199 }
200
201 /// `proses logs <name|id> [-n LINES]`
202 ///
203 /// Dumps the last `lines` lines of the process's stdout log to stdout.
204 /// For a live follow view, use `proses list` (the TUI dashboard).
205 pub fn logs(name_or_id: String, lines: usize) {
206 let resp = send_or_fail(&Request::Logs { name_or_id, lines });
207
208 if !resp.success {
209 fail(&resp.message);
210 }
211
212 if let Some(content) = resp.logs {
213 print!("{content}");
214 }
215 }
216
217 // ── Utilities ─────────────────────────────────────────────────────────────────
218
219 fn format_uptime(secs: u64) -> String {
220 if secs < 60 {
221 format!("{secs}s")
222 } else if secs < 3600 {
223 format!("{}m {}s", secs / 60, secs % 60)
224 } else if secs < 86_400 {
225 format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
226 } else {
227 format!("{}d {}h", secs / 86_400, (secs % 86_400) / 3600)
228 }
229 }