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