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
23pub fn format_unix_relative(ts: u64) -> String {
26 let now = std::time::SystemTime::now()
27 .duration_since(std::time::UNIX_EPOCH)
28 .map(|d| d.as_secs())
29 .unwrap_or(0);
30 if ts == 0 {
31 return "-".to_string();
32 }
33 let diff = now.saturating_sub(ts);
34 if diff < 60 {
35 format!("{diff}s")
36 } else if diff < 3600 {
37 format!("{}m", diff / 60)
38 } else if diff < 86400 {
39 format!("{}h", diff / 3600)
40 } else {
41 let secs = ts % 86400;
42 let (h, m, s) = (secs / 3600, (secs % 3600) / 60, secs % 60);
43 format!("{h:02}:{m:02}:{s:02}")
44 }
45}
46
47pub fn with_gateway<F, Fut, T>(op: F) -> io::Result<T>
52where
53 F: FnOnce(Arc<muxio_tokio_rpc_ipc_client::RpcIpcClient>) -> Fut,
54 Fut: std::future::Future<Output = T>,
55{
56 let gateway = term_session_muxio_service_definitions::gateway_channel_name();
57 let rt =
58 tokio::runtime::Runtime::new().map_err(|e| io::Error::other(format!("runtime: {e}")))?;
59 rt.block_on(async {
60 let client = muxio_tokio_rpc_ipc_client::RpcIpcClient::new(&gateway.to_string())
61 .await
62 .map_err(|e| {
63 io::Error::new(
64 io::ErrorKind::ConnectionRefused,
65 format!(
66 "No gateway daemon is running on '{gateway}'. Start one with `term-session --channel <name>` or `term-session --daemon` first.\n cause: {e}"
67 ),
68 )
69 })?;
70 Ok(op(client).await)
71 })
72}
73
74pub fn list_channels() -> io::Result<ListChannelsResponse> {
76 with_gateway(|client| async move { ListChannels::call(&*client, ()).await })?
77 .map_err(|e| io::Error::other(format!("list: {e}")))
78}
79
80pub fn kill_channel(channel: &str) -> io::Result<()> {
82 with_gateway(|client| async move { KillChannel::call(&*client, channel.to_string()).await })?
83 .map_err(|e| io::Error::other(format!("kill channel: {e}")))
84}
85
86pub fn kill_client(channel: &str, conn_id: usize) -> io::Result<()> {
88 with_gateway(|client| async move {
89 KillClient::call(&*client, (channel.to_string(), conn_id)).await
90 })?
91 .map_err(|e| io::Error::other(format!("kill client: {e}")))
92}
93
94pub fn stop_gateway(force: bool) -> io::Result<()> {
99 with_gateway(|client| async move { ShutdownGateway::call(&*client, force).await })?
100 .map_err(|e| io::Error::other(format!("shutdown: {e}")))
101}
102
103pub fn run_daemon(selfcheck_marker: Option<std::path::PathBuf>) -> io::Result<()> {
107 tracing_subscriber::fmt::init();
108
109 set_daemon_process_name();
113
114 #[cfg(unix)]
128 unsafe {
129 libc::setsid();
130 }
131 #[cfg(windows)]
132 unsafe {
133 let _ = windows_sys::Win32::System::Console::FreeConsole();
134 }
135
136 let gateway = term_session_muxio_service_definitions::gateway_channel_name();
137
138 if let Some(ref marker) = selfcheck_marker {
141 let gw = gateway.clone();
142 let marker = marker.clone();
143 std::thread::Builder::new()
144 .name("daemon-selfcheck".into())
145 .spawn(move || {
146 for _ in 0..200 {
147 if term_session_muxio_service_definitions::probe_ipc_endpoint(&gw) {
148 write_selfcheck_marker(&marker);
149 return;
150 }
151 std::thread::sleep(std::time::Duration::from_millis(25));
152 }
153 let _ = std::fs::write(&marker, "bound-timeout");
154 })?;
155 }
156
157 let rt =
158 tokio::runtime::Runtime::new().map_err(|e| io::Error::other(format!("runtime: {e}")))?;
159 rt.block_on(term_session_server::run_gateway(gateway.clone()))
160 .map_err(|e| io::Error::other(format!("gateway error: {e}")))?;
161 Ok(())
162}
163
164pub fn set_daemon_process_name() {
182 #[cfg(target_os = "linux")]
183 {
184 use std::ffi::CString;
185 if let Ok(name) = CString::new("term-session-d") {
186 unsafe {
187 libc::prctl(libc::PR_SET_NAME, name.as_ptr() as usize, 0, 0, 0);
188 }
189 }
190 }
191 #[cfg(target_os = "macos")]
192 {
193 use std::ffi::CString;
194 if let Ok(name) = CString::new("term-session-daemon") {
195 unsafe {
196 libc::pthread_setname_np(name.as_ptr());
197 }
198 }
199 }
200 #[cfg(windows)]
201 {
202 use windows_sys::Win32::System::Threading::{GetCurrentThread, SetThreadDescription};
203 let wide: Vec<u16> = "term-session-daemon"
204 .encode_utf16()
205 .chain(std::iter::once(0))
206 .collect();
207 unsafe {
208 SetThreadDescription(GetCurrentThread(), wide.as_ptr());
209 }
210 }
211}
212
213fn write_selfcheck_marker(marker: &std::path::Path) {
215 #[cfg(windows)]
216 let proof = {
217 use windows_sys::Win32::System::Console::{
218 GetConsoleProcessList, GetStdHandle, STD_INPUT_HANDLE,
219 };
220 let mut pids = [0u32; 4];
221 let count = unsafe {
222 let _handle = GetStdHandle(STD_INPUT_HANDLE);
223 GetConsoleProcessList(pids.as_mut_ptr(), pids.len() as u32)
224 };
225 if count == 0 {
226 "windows-no-console"
227 } else {
228 "windows-has-console"
229 }
230 };
231 #[cfg(unix)]
232 let proof = {
233 let sid = unsafe { libc::getsid(0) };
234 let pid = unsafe { libc::getpid() };
235 if sid == pid {
236 "unix-session-leader"
237 } else {
238 "unix-not-leader"
239 }
240 };
241 #[cfg(not(any(unix, windows)))]
242 let proof = "unsupported";
243 let _ = std::fs::write(marker, proof);
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 fn env_lock() -> std::sync::MutexGuard<'static, ()> {
252 static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
253 LOCK.lock().unwrap_or_else(|e| e.into_inner())
254 }
255
256 #[test]
257 fn cli_channel_takes_precedence_over_env() {
258 let _guard = env_lock();
259 unsafe {
260 std::env::set_var(CHANNEL_ENV_VAR, "other/chan");
261 }
262 assert_eq!(resolve_channel(Some("work/dev".to_string())), "work/dev");
263 unsafe {
264 std::env::remove_var(CHANNEL_ENV_VAR);
265 }
266 }
267
268 #[test]
269 fn falls_back_to_env_channel() {
270 let _guard = env_lock();
271 unsafe {
272 std::env::set_var(CHANNEL_ENV_VAR, "work/dev");
273 }
274 assert_eq!(resolve_channel(None), "work/dev");
275 unsafe {
276 std::env::remove_var(CHANNEL_ENV_VAR);
277 }
278 }
279
280 #[test]
281 fn falls_back_to_default_channel() {
282 let _guard = env_lock();
283 unsafe {
284 std::env::remove_var(CHANNEL_ENV_VAR);
285 }
286 assert_eq!(resolve_channel(None), DEFAULT_CHANNEL);
287 }
288}