skiff_cli/session/
spawn.rs1use std::collections::BTreeMap;
8use std::fs::{self, OpenOptions};
9use std::process::{Command, Stdio};
10use std::thread;
11use std::time::{Duration, Instant};
12
13use serde::{Deserialize, Serialize};
14
15use crate::error::{Error, Result};
16use crate::session::paths::chmod_0600;
17use crate::session::paths::{
18 clear_stale_session, ensure_sessions_dir, load_meta, release_session_lock, session_config_path,
19 session_is_alive, session_log_path, session_sock_path, sessions_dir, try_acquire_session_lock,
20 unlink_session_files, validate_session_name,
21};
22
23pub const DEFAULT_IDLE_SECS: u64 = 1800;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct DaemonConfig {
27 pub name: String,
28 pub source: String,
29 pub is_stdio: bool,
30 pub auth_headers: Vec<(String, String)>,
31 pub env_vars: BTreeMap<String, String>,
32 pub transport: String,
33 #[serde(default)]
34 pub clean_env: bool,
35 #[serde(default = "default_idle")]
36 pub idle_secs: u64,
37}
38
39fn default_idle() -> u64 {
40 DEFAULT_IDLE_SECS
41}
42
43pub fn session_start(config: DaemonConfig) -> Result<()> {
45 #[cfg(not(unix))]
46 {
47 return Err(crate::session::sessions_unsupported());
48 }
49 #[cfg(unix)]
50 {
51 session_start_unix(config)
52 }
53}
54
55struct SessionLockGuard<'a> {
58 name: &'a str,
59 released: bool,
60}
61
62impl<'a> SessionLockGuard<'a> {
63 fn release(mut self) {
64 release_session_lock(self.name);
65 self.released = true;
66 }
67}
68
69impl<'a> Drop for SessionLockGuard<'a> {
70 fn drop(&mut self) {
71 if !self.released {
72 release_session_lock(self.name);
73 }
74 }
75}
76
77#[cfg(unix)]
78fn session_start_unix(config: DaemonConfig) -> Result<()> {
79 validate_session_name(&config.name)?;
80 ensure_sessions_dir()?;
81
82 if !try_acquire_session_lock(&config.name)? {
87 return Err(Error::runtime(format!(
88 "session {:?} is already starting (concurrent start in progress)",
89 config.name
90 )));
91 }
92 let lock_guard = SessionLockGuard {
93 name: &config.name,
94 released: false,
95 };
96
97 let _ = clear_stale_session(&config.name)?;
98
99 if let Some(meta) = load_meta(&config.name)? {
100 if session_is_alive(&meta) {
101 return Err(Error::runtime(format!(
102 "session {:?} is already running (pid {})",
103 config.name, meta.pid
104 )));
105 }
106 unlink_session_files(&config.name);
107 }
108
109 let config_path = session_config_path(&config.name);
110 let json = serde_json::to_vec_pretty(&config)?;
111 crate::fsutil::atomic_write_0600(&config_path, &json)?;
112
113 let log_path = session_log_path(&config.name);
114 crate::fsutil::atomic_write_0600(&log_path, b"")?;
116 let log_file = OpenOptions::new()
117 .create(true)
118 .append(true)
119 .open(&log_path)
120 .map_err(|e| Error::runtime(format!("cannot open session log: {e}")))?;
121 chmod_0600(&log_path)?;
122
123 let exe = std::env::current_exe()
124 .map_err(|e| Error::runtime(format!("cannot resolve skiff binary: {e}")))?;
125
126 let mut cmd = Command::new(&exe);
127 cmd.arg("__session_daemon")
128 .arg(&config_path)
129 .stdin(Stdio::null())
130 .stdout(Stdio::null())
131 .stderr(Stdio::from(log_file));
132 use std::os::unix::process::CommandExt;
134 cmd.process_group(0);
135
136 let mut child = cmd
137 .spawn()
138 .map_err(|e| Error::runtime(format!("failed to spawn session daemon: {e}")))?;
139
140 let sock = session_sock_path(&config.name);
141 let deadline = Instant::now() + Duration::from_secs(15);
142 while Instant::now() < deadline {
143 if sock.exists() {
144 lock_guard.release();
147 println!(
148 "Session '{}' started (pid {}). Use --session {} ...",
149 config.name,
150 child.id(),
151 config.name
152 );
153 return Ok(());
154 }
155 if let Ok(Some(status)) = child.try_wait() {
156 unlink_session_files(&config.name);
157 return Err(Error::runtime(format!(
158 "session daemon exited early with {status}. See {}",
159 log_path.display()
160 )));
161 }
162 thread::sleep(Duration::from_millis(50));
163 }
164 let _ = child.kill();
165 let _ = child.wait();
166 unlink_session_files(&config.name);
167 Err(Error::runtime(format!(
168 "session daemon did not become ready within 15s. See {}",
169 log_path.display()
170 )))
171}
172
173pub fn session_stop(name: &str) -> Result<()> {
175 validate_session_name(name)?;
176 let Some(meta) = load_meta(name)? else {
177 unlink_session_files(name);
178 println!("Session '{name}' is not running.");
179 return Ok(());
180 };
181 if session_is_alive(&meta) {
182 let pid = meta.pid as i32;
183 unsafe {
184 libc::kill(pid, libc::SIGTERM);
185 }
186 let deadline = Instant::now() + Duration::from_secs(2);
187 while Instant::now() < deadline {
188 if !crate::session::paths::is_process_alive(meta.pid) {
189 break;
190 }
191 thread::sleep(Duration::from_millis(50));
192 }
193 if crate::session::paths::is_process_alive(meta.pid) {
194 unsafe {
195 libc::kill(pid, libc::SIGKILL);
196 }
197 thread::sleep(Duration::from_millis(100));
198 }
199 }
200 unlink_session_files(name);
201 println!("Session '{name}' stopped.");
202 Ok(())
203}
204
205#[derive(Debug, Clone, Serialize)]
206pub struct SessionListEntry {
207 pub name: String,
208 pub pid: u32,
209 pub alive: bool,
210 pub source: String,
211 pub transport: String,
212 pub idle_secs: u64,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub idle_remaining_secs: Option<u64>,
215}
216
217pub fn session_list() -> Result<Vec<SessionListEntry>> {
219 ensure_sessions_dir()?;
220 let mut out = Vec::new();
221 let dir = sessions_dir();
222 let entries = match fs::read_dir(&dir) {
223 Ok(e) => e,
224 Err(_) => return Ok(out),
225 };
226 for ent in entries.flatten() {
227 let path = ent.path();
228 if path.extension().and_then(|e| e.to_str()) != Some("json") {
229 continue;
230 }
231 if path
233 .file_name()
234 .and_then(|n| n.to_str())
235 .map(|n| n.ends_with(".config.json"))
236 .unwrap_or(false)
237 {
238 continue;
239 }
240 let name = path
241 .file_stem()
242 .and_then(|s| s.to_str())
243 .unwrap_or("")
244 .to_string();
245 if name.is_empty() {
246 continue;
247 }
248 let Ok(Some(meta)) = load_meta(&name) else {
249 continue;
250 };
251 let alive = session_is_alive(&meta);
252 let idle_remaining_secs = if meta.idle_secs == 0 || !alive {
253 None
254 } else {
255 let now = std::time::SystemTime::now()
256 .duration_since(std::time::UNIX_EPOCH)
257 .map(|d| d.as_secs_f64())
258 .unwrap_or(0.0);
259 let last = if meta.last_activity_at > 0.0 {
260 meta.last_activity_at
261 } else {
262 meta.created_at
263 };
264 let elapsed = (now - last).max(0.0) as u64;
265 Some(meta.idle_secs.saturating_sub(elapsed))
266 };
267 out.push(SessionListEntry {
268 name,
269 pid: meta.pid,
270 alive,
271 source: meta.source,
272 transport: meta.transport,
273 idle_secs: meta.idle_secs,
274 idle_remaining_secs,
275 });
276 }
277 out.sort_by(|a, b| a.name.cmp(&b.name));
278 Ok(out)
279}
280
281pub fn resolve_idle_secs(cli: Option<u64>) -> u64 {
282 if let Some(v) = cli {
283 return v;
284 }
285 if let Ok(v) = std::env::var("SKIFF_SESSION_IDLE_SECS") {
286 if let Ok(n) = v.parse::<u64>() {
287 return n;
288 }
289 }
290 DEFAULT_IDLE_SECS
291}