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, force: bool) -> io::Result<()> {
103 with_gateway(|client| async move {
104 KillChannel::call(&*client, (channel.to_string(), force)).await
105 })?
106 .map_err(|e| io::Error::other(format!("kill channel: {e}")))
107}
108
109pub fn kill_client(channel: &str, conn_id: usize) -> io::Result<()> {
111 with_gateway(|client| async move {
112 KillClient::call(&*client, (channel.to_string(), conn_id)).await
113 })?
114 .map_err(|e| io::Error::other(format!("kill client: {e}")))
115}
116
117pub fn stop_gateway(force: bool) -> io::Result<()> {
122 with_gateway(|client| async move { ShutdownGateway::call(&*client, force).await })?
123 .map_err(|e| io::Error::other(format!("shutdown: {e}")))
124}
125
126pub fn run_daemon(selfcheck_marker: Option<std::path::PathBuf>) -> io::Result<()> {
130 tracing_subscriber::fmt::init();
131
132 set_daemon_process_name();
136
137 #[cfg(unix)]
151 unsafe {
152 libc::setsid();
153 }
154 #[cfg(windows)]
155 unsafe {
156 let _ = windows_sys::Win32::System::Console::FreeConsole();
157 }
158
159 let gateway = term_session_muxio_service_definitions::gateway_channel_name();
160
161 if let Some(ref marker) = selfcheck_marker {
164 let gw = gateway.clone();
165 let marker = marker.clone();
166 std::thread::Builder::new()
167 .name("daemon-selfcheck".into())
168 .spawn(move || {
169 for _ in 0..200 {
170 if term_session_muxio_service_definitions::probe_ipc_endpoint(&gw) {
171 write_selfcheck_marker(&marker);
172 return;
173 }
174 std::thread::sleep(std::time::Duration::from_millis(25));
175 }
176 let _ = std::fs::write(&marker, "bound-timeout");
177 })?;
178 }
179
180 let rt =
181 tokio::runtime::Runtime::new().map_err(|e| io::Error::other(format!("runtime: {e}")))?;
182 rt.block_on(term_session_server::run_gateway(gateway.clone()))
183 .map_err(|e| io::Error::other(format!("gateway error: {e}")))?;
184 Ok(())
185}
186
187pub fn set_daemon_process_name() {
205 #[cfg(target_os = "linux")]
206 {
207 use std::ffi::CString;
208 if let Ok(name) = CString::new("term-session-d") {
209 unsafe {
210 libc::prctl(libc::PR_SET_NAME, name.as_ptr() as usize, 0, 0, 0);
211 }
212 }
213 }
214 #[cfg(target_os = "macos")]
215 {
216 use std::ffi::CString;
217 if let Ok(name) = CString::new("term-session-daemon") {
218 unsafe {
219 libc::pthread_setname_np(name.as_ptr());
220 }
221 }
222 }
223 #[cfg(windows)]
224 {
225 use windows_sys::Win32::System::Threading::{GetCurrentThread, SetThreadDescription};
226 let wide: Vec<u16> = "term-session-daemon"
227 .encode_utf16()
228 .chain(std::iter::once(0))
229 .collect();
230 unsafe {
231 SetThreadDescription(GetCurrentThread(), wide.as_ptr());
232 }
233 }
234}
235
236fn write_selfcheck_marker(marker: &std::path::Path) {
238 #[cfg(windows)]
239 let proof = {
240 use windows_sys::Win32::System::Console::{
241 GetConsoleProcessList, GetStdHandle, STD_INPUT_HANDLE,
242 };
243 let mut pids = [0u32; 4];
244 let count = unsafe {
245 let _handle = GetStdHandle(STD_INPUT_HANDLE);
246 GetConsoleProcessList(pids.as_mut_ptr(), pids.len() as u32)
247 };
248 if count == 0 {
249 "windows-no-console"
250 } else {
251 "windows-has-console"
252 }
253 };
254 #[cfg(unix)]
255 let proof = {
256 let sid = unsafe { libc::getsid(0) };
257 let pid = unsafe { libc::getpid() };
258 if sid == pid {
259 "unix-session-leader"
260 } else {
261 "unix-not-leader"
262 }
263 };
264 #[cfg(not(any(unix, windows)))]
265 let proof = "unsupported";
266 let _ = std::fs::write(marker, proof);
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 fn env_lock() -> std::sync::MutexGuard<'static, ()> {
275 static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
276 LOCK.lock().unwrap_or_else(|e| e.into_inner())
277 }
278
279 #[test]
280 fn cli_channel_takes_precedence_over_env() {
281 let _guard = env_lock();
282 unsafe {
283 std::env::set_var(CHANNEL_ENV_VAR, "other/chan");
284 }
285 assert_eq!(resolve_channel(Some("work/dev".to_string())), "work/dev");
286 unsafe {
287 std::env::remove_var(CHANNEL_ENV_VAR);
288 }
289 }
290
291 #[test]
292 fn falls_back_to_env_channel() {
293 let _guard = env_lock();
294 unsafe {
295 std::env::set_var(CHANNEL_ENV_VAR, "work/dev");
296 }
297 assert_eq!(resolve_channel(None), "work/dev");
298 unsafe {
299 std::env::remove_var(CHANNEL_ENV_VAR);
300 }
301 }
302
303 #[test]
304 fn falls_back_to_default_channel() {
305 let _guard = env_lock();
306 unsafe {
307 std::env::remove_var(CHANNEL_ENV_VAR);
308 }
309 assert_eq!(resolve_channel(None), DEFAULT_CHANNEL);
310 }
311
312 #[test]
313 fn format_zero_timestamp_is_dash() {
314 assert_eq!(format_unix_relative_at(0, SECS_PER_DAY), "-");
315 }
316
317 #[test]
318 fn format_under_a_minute_shows_seconds() {
319 assert_eq!(
320 format_unix_relative_at(SECS_PER_DAY - 42, SECS_PER_DAY),
321 "42s"
322 );
323 }
324
325 #[test]
326 fn format_under_an_hour_shows_minutes() {
327 assert_eq!(
328 format_unix_relative_at(SECS_PER_DAY - 3_300, SECS_PER_DAY),
329 "55m"
330 );
331 }
332
333 #[test]
334 fn format_under_a_day_shows_hours() {
335 assert_eq!(
336 format_unix_relative_at(SECS_PER_DAY - 7_200, SECS_PER_DAY),
337 "2h"
338 );
339 }
340
341 #[test]
342 fn format_older_than_a_day_shows_days_and_hours() {
343 assert_eq!(
344 format_unix_relative_at(10 * SECS_PER_DAY, 11 * SECS_PER_DAY),
345 "1d 0h"
346 );
347 assert_eq!(
348 format_unix_relative_at(10 * SECS_PER_DAY, 11 * SECS_PER_DAY + 3 * SECS_PER_HOUR),
349 "1d 3h"
350 );
351 }
352
353 #[test]
354 fn format_day_boundary_exact() {
355 assert_eq!(
356 format_unix_relative_at(10 * SECS_PER_DAY, 11 * SECS_PER_DAY),
357 "1d 0h"
358 );
359 }
360
361 #[test]
362 fn format_timestamp_newer_than_now_saturates() {
363 assert_eq!(
364 format_unix_relative_at(SECS_PER_DAY + 10, SECS_PER_DAY),
365 "0s"
366 );
367 }
368
369 #[test]
370 fn format_does_not_render_clock_time() {
371 let ts = SECS_PER_DAY * 40 + 18 * SECS_PER_HOUR + 48 * SECS_PER_MIN + 46;
374 let out = format_unix_relative_at(ts, SECS_PER_DAY * 42);
375 assert_eq!(out, "1d 5h");
376 assert!(!out.contains(':'), "clock-time format leaked: {out}");
377 }
378}