pub mod auto_spawn;
use std::io;
use std::sync::Arc;
use muxio_tokio_rpc_ipc_client::RpcCallPrebuffered;
use term_session_muxio_service_definitions::{
KillChannel, KillClient, ListChannels, ListChannelsResponse, ShutdownGateway,
};
pub const CHANNEL_ENV_VAR: &str = "TERM_WM_CHANNEL";
pub const DEFAULT_CHANNEL: &str = "default/main";
pub fn resolve_channel(cli_channel: Option<String>) -> String {
cli_channel
.or_else(|| std::env::var(CHANNEL_ENV_VAR).ok())
.unwrap_or_else(|| DEFAULT_CHANNEL.to_string())
}
const SECS_PER_MIN: u64 = 60;
const SECS_PER_HOUR: u64 = 3600;
const SECS_PER_DAY: u64 = 86400;
pub fn format_unix_relative(ts: u64) -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
format_unix_relative_at(ts, now)
}
pub fn format_unix_relative_at(ts: u64, now: u64) -> String {
if ts == 0 {
return "-".to_string();
}
let diff = now.saturating_sub(ts);
if diff < SECS_PER_MIN {
format!("{diff}s")
} else if diff < SECS_PER_HOUR {
format!("{}m", diff / SECS_PER_MIN)
} else if diff < SECS_PER_DAY {
format!("{}h", diff / SECS_PER_HOUR)
} else {
format!(
"{}d {}h",
diff / SECS_PER_DAY,
(diff % SECS_PER_DAY) / SECS_PER_HOUR
)
}
}
pub fn with_gateway<F, Fut, T>(op: F) -> io::Result<T>
where
F: FnOnce(Arc<muxio_tokio_rpc_ipc_client::RpcIpcClient>) -> Fut,
Fut: std::future::Future<Output = T>,
{
let gateway = term_session_muxio_service_definitions::gateway_channel_name();
let rt =
tokio::runtime::Runtime::new().map_err(|e| io::Error::other(format!("runtime: {e}")))?;
rt.block_on(async {
let client = muxio_tokio_rpc_ipc_client::RpcIpcClient::new(&gateway.to_string())
.await
.map_err(|e| {
io::Error::new(
io::ErrorKind::ConnectionRefused,
format!(
"No gateway daemon is running on '{gateway}'. Start one with `term-session --channel <name>` or `term-session --daemon` first.\n cause: {e}"
),
)
})?;
Ok(op(client).await)
})
}
pub fn list_channels() -> io::Result<ListChannelsResponse> {
with_gateway(|client| async move { ListChannels::call(&*client, ()).await })?
.map_err(|e| io::Error::other(format!("list: {e}")))
}
pub fn kill_channel(channel: &str, force: bool) -> io::Result<()> {
with_gateway(|client| async move {
KillChannel::call(&*client, (channel.to_string(), force)).await
})?
.map_err(|e| io::Error::other(format!("kill channel: {e}")))
}
pub fn kill_client(channel: &str, conn_id: usize) -> io::Result<()> {
with_gateway(|client| async move {
KillClient::call(&*client, (channel.to_string(), conn_id)).await
})?
.map_err(|e| io::Error::other(format!("kill client: {e}")))
}
pub fn stop_gateway(force: bool) -> io::Result<()> {
with_gateway(|client| async move { ShutdownGateway::call(&*client, force).await })?
.map_err(|e| io::Error::other(format!("shutdown: {e}")))
}
pub fn run_daemon(selfcheck_marker: Option<std::path::PathBuf>) -> io::Result<()> {
tracing_subscriber::fmt::init();
set_daemon_process_name();
#[cfg(unix)]
unsafe {
libc::setsid();
}
#[cfg(windows)]
unsafe {
let _ = windows_sys::Win32::System::Console::FreeConsole();
}
let gateway = term_session_muxio_service_definitions::gateway_channel_name();
if let Some(ref marker) = selfcheck_marker {
let gw = gateway.clone();
let marker = marker.clone();
std::thread::Builder::new()
.name("daemon-selfcheck".into())
.spawn(move || {
for _ in 0..200 {
if term_session_muxio_service_definitions::probe_ipc_endpoint(&gw) {
write_selfcheck_marker(&marker);
return;
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
let _ = std::fs::write(&marker, "bound-timeout");
})?;
}
let rt =
tokio::runtime::Runtime::new().map_err(|e| io::Error::other(format!("runtime: {e}")))?;
rt.block_on(term_session_server::run_gateway(gateway.clone()))
.map_err(|e| io::Error::other(format!("gateway error: {e}")))?;
Ok(())
}
pub fn set_daemon_process_name() {
#[cfg(target_os = "linux")]
{
use std::ffi::CString;
if let Ok(name) = CString::new("term-session-d") {
unsafe {
libc::prctl(libc::PR_SET_NAME, name.as_ptr() as usize, 0, 0, 0);
}
}
}
#[cfg(target_os = "macos")]
{
use std::ffi::CString;
if let Ok(name) = CString::new("term-session-daemon") {
unsafe {
libc::pthread_setname_np(name.as_ptr());
}
}
}
#[cfg(windows)]
{
use windows_sys::Win32::System::Threading::{GetCurrentThread, SetThreadDescription};
let wide: Vec<u16> = "term-session-daemon"
.encode_utf16()
.chain(std::iter::once(0))
.collect();
unsafe {
SetThreadDescription(GetCurrentThread(), wide.as_ptr());
}
}
}
fn write_selfcheck_marker(marker: &std::path::Path) {
#[cfg(windows)]
let proof = {
use windows_sys::Win32::System::Console::{
GetConsoleProcessList, GetStdHandle, STD_INPUT_HANDLE,
};
let mut pids = [0u32; 4];
let count = unsafe {
let _handle = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleProcessList(pids.as_mut_ptr(), pids.len() as u32)
};
if count == 0 {
"windows-no-console"
} else {
"windows-has-console"
}
};
#[cfg(unix)]
let proof = {
let sid = unsafe { libc::getsid(0) };
let pid = unsafe { libc::getpid() };
if sid == pid {
"unix-session-leader"
} else {
"unix-not-leader"
}
};
#[cfg(not(any(unix, windows)))]
let proof = "unsupported";
let _ = std::fs::write(marker, proof);
}
#[cfg(test)]
mod tests {
use super::*;
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn cli_channel_takes_precedence_over_env() {
let _guard = env_lock();
unsafe {
std::env::set_var(CHANNEL_ENV_VAR, "other/chan");
}
assert_eq!(resolve_channel(Some("work/dev".to_string())), "work/dev");
unsafe {
std::env::remove_var(CHANNEL_ENV_VAR);
}
}
#[test]
fn falls_back_to_env_channel() {
let _guard = env_lock();
unsafe {
std::env::set_var(CHANNEL_ENV_VAR, "work/dev");
}
assert_eq!(resolve_channel(None), "work/dev");
unsafe {
std::env::remove_var(CHANNEL_ENV_VAR);
}
}
#[test]
fn falls_back_to_default_channel() {
let _guard = env_lock();
unsafe {
std::env::remove_var(CHANNEL_ENV_VAR);
}
assert_eq!(resolve_channel(None), DEFAULT_CHANNEL);
}
#[test]
fn format_zero_timestamp_is_dash() {
assert_eq!(format_unix_relative_at(0, SECS_PER_DAY), "-");
}
#[test]
fn format_under_a_minute_shows_seconds() {
assert_eq!(
format_unix_relative_at(SECS_PER_DAY - 42, SECS_PER_DAY),
"42s"
);
}
#[test]
fn format_under_an_hour_shows_minutes() {
assert_eq!(
format_unix_relative_at(SECS_PER_DAY - 3_300, SECS_PER_DAY),
"55m"
);
}
#[test]
fn format_under_a_day_shows_hours() {
assert_eq!(
format_unix_relative_at(SECS_PER_DAY - 7_200, SECS_PER_DAY),
"2h"
);
}
#[test]
fn format_older_than_a_day_shows_days_and_hours() {
assert_eq!(
format_unix_relative_at(10 * SECS_PER_DAY, 11 * SECS_PER_DAY),
"1d 0h"
);
assert_eq!(
format_unix_relative_at(10 * SECS_PER_DAY, 11 * SECS_PER_DAY + 3 * SECS_PER_HOUR),
"1d 3h"
);
}
#[test]
fn format_day_boundary_exact() {
assert_eq!(
format_unix_relative_at(10 * SECS_PER_DAY, 11 * SECS_PER_DAY),
"1d 0h"
);
}
#[test]
fn format_timestamp_newer_than_now_saturates() {
assert_eq!(
format_unix_relative_at(SECS_PER_DAY + 10, SECS_PER_DAY),
"0s"
);
}
#[test]
fn format_does_not_render_clock_time() {
let ts = SECS_PER_DAY * 40 + 18 * SECS_PER_HOUR + 48 * SECS_PER_MIN + 46;
let out = format_unix_relative_at(ts, SECS_PER_DAY * 42);
assert_eq!(out, "1d 5h");
assert!(!out.contains(':'), "clock-time format leaked: {out}");
}
}