1pub mod auto_spawn;
2
3use std::io;
4use std::sync::Arc;
5
6use muxio_tokio_rpc_ipc_client::RpcCallPrebuffered;
7use term_session_muxio_service_definitions::{
8 KillChannel, KillClient, ListChannels, ListChannelsResponse, ShutdownGateway,
9};
10
11pub const CHANNEL_ENV_VAR: &str = "TERM_WM_CHANNEL";
13pub const DEFAULT_CHANNEL: &str = "default/main";
14
15pub fn resolve_channel(cli_channel: Option<String>) -> String {
18 cli_channel
19 .or_else(|| std::env::var(CHANNEL_ENV_VAR).ok())
20 .unwrap_or_else(|| DEFAULT_CHANNEL.to_string())
21}
22
23const SECS_PER_MIN: u64 = 60;
25const SECS_PER_HOUR: u64 = 3600;
27const SECS_PER_DAY: u64 = 86400;
29
30pub fn format_unix_relative(ts: u64) -> String {
33 let now = std::time::SystemTime::now()
34 .duration_since(std::time::UNIX_EPOCH)
35 .map(|d| d.as_secs())
36 .unwrap_or(0);
37 format_unix_relative_at(ts, now)
38}
39
40pub fn format_unix_relative_at(ts: u64, now: u64) -> String {
46 if ts == 0 {
47 return "-".to_string();
48 }
49 let diff = now.saturating_sub(ts);
50 if diff < SECS_PER_MIN {
51 format!("{diff}s")
52 } else if diff < SECS_PER_HOUR {
53 format!("{}m", diff / SECS_PER_MIN)
54 } else if diff < SECS_PER_DAY {
55 format!("{}h", diff / SECS_PER_HOUR)
56 } else {
57 format!(
58 "{}d {}h",
59 diff / SECS_PER_DAY,
60 (diff % SECS_PER_DAY) / SECS_PER_HOUR
61 )
62 }
63}
64
65pub fn with_gateway<F, Fut, T>(op: F) -> io::Result<T>
70where
71 F: FnOnce(Arc<muxio_tokio_rpc_ipc_client::RpcIpcClient>) -> Fut,
72 Fut: std::future::Future<Output = T>,
73{
74 let gateway = term_session_muxio_service_definitions::gateway_channel_name();
75 let rt =
76 tokio::runtime::Runtime::new().map_err(|e| io::Error::other(format!("runtime: {e}")))?;
77 rt.block_on(async {
78 let client = muxio_tokio_rpc_ipc_client::RpcIpcClient::new(&gateway.to_string())
79 .await
80 .map_err(|e| {
81 io::Error::new(
82 io::ErrorKind::ConnectionRefused,
83 format!(
84 "No gateway daemon is running on '{gateway}'. Start one with `term-session --channel <name>` or `term-session --daemon` first.\n cause: {e}"
85 ),
86 )
87 })?;
88 Ok(op(client).await)
89 })
90}
91
92pub fn list_channels() -> io::Result<ListChannelsResponse> {
94 with_gateway(|client| async move { ListChannels::call(&*client, ()).await })?
95 .map_err(|e| io::Error::other(format!("list: {e}")))
96}
97
98pub fn kill_channel(channel: &str) -> io::Result<()> {
100 with_gateway(|client| async move { KillChannel::call(&*client, channel.to_string()).await })?
101 .map_err(|e| io::Error::other(format!("kill channel: {e}")))
102}
103
104pub fn kill_client(channel: &str, conn_id: usize) -> io::Result<()> {
106 with_gateway(|client| async move {
107 KillClient::call(&*client, (channel.to_string(), conn_id)).await
108 })?
109 .map_err(|e| io::Error::other(format!("kill client: {e}")))
110}
111
112pub fn stop_gateway(force: bool) -> io::Result<()> {
117 with_gateway(|client| async move { ShutdownGateway::call(&*client, force).await })?
118 .map_err(|e| io::Error::other(format!("shutdown: {e}")))
119}
120
121pub fn run_daemon(selfcheck_marker: Option<std::path::PathBuf>) -> io::Result<()> {
125 tracing_subscriber::fmt::init();
126
127 set_daemon_process_name();
131
132 #[cfg(unix)]
146 unsafe {
147 libc::setsid();
148 }
149 #[cfg(windows)]
150 unsafe {
151 let _ = windows_sys::Win32::System::Console::FreeConsole();
152 }
153
154 let gateway = term_session_muxio_service_definitions::gateway_channel_name();
155
156 if let Some(ref marker) = selfcheck_marker {
159 let gw = gateway.clone();
160 let marker = marker.clone();
161 std::thread::Builder::new()
162 .name("daemon-selfcheck".into())
163 .spawn(move || {
164 for _ in 0..200 {
165 if term_session_muxio_service_definitions::probe_ipc_endpoint(&gw) {
166 write_selfcheck_marker(&marker);
167 return;
168 }
169 std::thread::sleep(std::time::Duration::from_millis(25));
170 }
171 let _ = std::fs::write(&marker, "bound-timeout");
172 })?;
173 }
174
175 let rt =
176 tokio::runtime::Runtime::new().map_err(|e| io::Error::other(format!("runtime: {e}")))?;
177 rt.block_on(term_session_server::run_gateway(gateway.clone()))
178 .map_err(|e| io::Error::other(format!("gateway error: {e}")))?;
179 Ok(())
180}
181
182pub fn set_daemon_process_name() {
200 #[cfg(target_os = "linux")]
201 {
202 use std::ffi::CString;
203 if let Ok(name) = CString::new("term-session-d") {
204 unsafe {
205 libc::prctl(libc::PR_SET_NAME, name.as_ptr() as usize, 0, 0, 0);
206 }
207 }
208 }
209 #[cfg(target_os = "macos")]
210 {
211 use std::ffi::CString;
212 if let Ok(name) = CString::new("term-session-daemon") {
213 unsafe {
214 libc::pthread_setname_np(name.as_ptr());
215 }
216 }
217 }
218 #[cfg(windows)]
219 {
220 use windows_sys::Win32::System::Threading::{GetCurrentThread, SetThreadDescription};
221 let wide: Vec<u16> = "term-session-daemon"
222 .encode_utf16()
223 .chain(std::iter::once(0))
224 .collect();
225 unsafe {
226 SetThreadDescription(GetCurrentThread(), wide.as_ptr());
227 }
228 }
229}
230
231fn write_selfcheck_marker(marker: &std::path::Path) {
233 #[cfg(windows)]
234 let proof = {
235 use windows_sys::Win32::System::Console::{
236 GetConsoleProcessList, GetStdHandle, STD_INPUT_HANDLE,
237 };
238 let mut pids = [0u32; 4];
239 let count = unsafe {
240 let _handle = GetStdHandle(STD_INPUT_HANDLE);
241 GetConsoleProcessList(pids.as_mut_ptr(), pids.len() as u32)
242 };
243 if count == 0 {
244 "windows-no-console"
245 } else {
246 "windows-has-console"
247 }
248 };
249 #[cfg(unix)]
250 let proof = {
251 let sid = unsafe { libc::getsid(0) };
252 let pid = unsafe { libc::getpid() };
253 if sid == pid {
254 "unix-session-leader"
255 } else {
256 "unix-not-leader"
257 }
258 };
259 #[cfg(not(any(unix, windows)))]
260 let proof = "unsupported";
261 let _ = std::fs::write(marker, proof);
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 fn env_lock() -> std::sync::MutexGuard<'static, ()> {
270 static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
271 LOCK.lock().unwrap_or_else(|e| e.into_inner())
272 }
273
274 #[test]
275 fn cli_channel_takes_precedence_over_env() {
276 let _guard = env_lock();
277 unsafe {
278 std::env::set_var(CHANNEL_ENV_VAR, "other/chan");
279 }
280 assert_eq!(resolve_channel(Some("work/dev".to_string())), "work/dev");
281 unsafe {
282 std::env::remove_var(CHANNEL_ENV_VAR);
283 }
284 }
285
286 #[test]
287 fn falls_back_to_env_channel() {
288 let _guard = env_lock();
289 unsafe {
290 std::env::set_var(CHANNEL_ENV_VAR, "work/dev");
291 }
292 assert_eq!(resolve_channel(None), "work/dev");
293 unsafe {
294 std::env::remove_var(CHANNEL_ENV_VAR);
295 }
296 }
297
298 #[test]
299 fn falls_back_to_default_channel() {
300 let _guard = env_lock();
301 unsafe {
302 std::env::remove_var(CHANNEL_ENV_VAR);
303 }
304 assert_eq!(resolve_channel(None), DEFAULT_CHANNEL);
305 }
306
307 #[test]
308 fn format_zero_timestamp_is_dash() {
309 assert_eq!(format_unix_relative_at(0, SECS_PER_DAY), "-");
310 }
311
312 #[test]
313 fn format_under_a_minute_shows_seconds() {
314 assert_eq!(
315 format_unix_relative_at(SECS_PER_DAY - 42, SECS_PER_DAY),
316 "42s"
317 );
318 }
319
320 #[test]
321 fn format_under_an_hour_shows_minutes() {
322 assert_eq!(
323 format_unix_relative_at(SECS_PER_DAY - 3_300, SECS_PER_DAY),
324 "55m"
325 );
326 }
327
328 #[test]
329 fn format_under_a_day_shows_hours() {
330 assert_eq!(
331 format_unix_relative_at(SECS_PER_DAY - 7_200, SECS_PER_DAY),
332 "2h"
333 );
334 }
335
336 #[test]
337 fn format_older_than_a_day_shows_days_and_hours() {
338 assert_eq!(
339 format_unix_relative_at(10 * SECS_PER_DAY, 11 * SECS_PER_DAY),
340 "1d 0h"
341 );
342 assert_eq!(
343 format_unix_relative_at(10 * SECS_PER_DAY, 11 * SECS_PER_DAY + 3 * SECS_PER_HOUR),
344 "1d 3h"
345 );
346 }
347
348 #[test]
349 fn format_day_boundary_exact() {
350 assert_eq!(
351 format_unix_relative_at(10 * SECS_PER_DAY, 11 * SECS_PER_DAY),
352 "1d 0h"
353 );
354 }
355
356 #[test]
357 fn format_timestamp_newer_than_now_saturates() {
358 assert_eq!(
359 format_unix_relative_at(SECS_PER_DAY + 10, SECS_PER_DAY),
360 "0s"
361 );
362 }
363
364 #[test]
365 fn format_does_not_render_clock_time() {
366 let ts = SECS_PER_DAY * 40 + 18 * SECS_PER_HOUR + 48 * SECS_PER_MIN + 46;
369 let out = format_unix_relative_at(ts, SECS_PER_DAY * 42);
370 assert_eq!(out, "1d 5h");
371 assert!(!out.contains(':'), "clock-time format leaked: {out}");
372 }
373}