#[cfg(any(target_os = "linux", target_os = "android"))] use std::io::Read; /// Kernel `TASK_COMM_LEN`. #[cfg(any(target_os = "linux", target_os = "android"))] const TASK_COMM_LEN: u64 = 16; /// Returns true if the program was launched as a systemd service. /// /// On Linux, this returns true if the program was launched as a systemd /// service. It will return false if Ghostty was launched any other way. #[cfg(any(target_os = "linux", target_os = "android"))] pub fn launched_by_systemd() -> bool { // On Linux, systemd sets the `INVOCATION_ID` (v232+) and the // `JOURNAL_STREAM` (v231+) environment variables. If these // environment variables are not present we were not launched by // systemd. if std::env::var_os("INVOCATION_ID").is_none() { return false; } if std::env::var_os("JOURNAL_STREAM").is_none() { return false; } // If `INVOCATION_ID` and `JOURNAL_STREAM` are present, check to make sure // that our parent process is actually `systemd`, not some other terminal // emulator that doesn't clean up those environment variables. let ppid = rustix::process::getppid().map_or(0, rustix::process::Pid::as_raw_pid); if ppid == 1 { return true; } // If the parent PID is not 1 we need to check to see if we were launched by // a user systemd daemon. Do that by checking the `/proc//comm` // to see if it ends with `systemd`. let comm_path = format!("/proc/{ppid}/comm"); let Ok(comm_file) = std::fs::File::open(&comm_path) else { log::error!("unable to open '{comm_path}' for reading"); return false; }; // The maximum length of the command name is defined by // `TASK_COMM_LEN` in the Linux kernel. This is usually 16 // bytes at the time of writing (Jun 2025) so its set to that. // Also, since we only care to compare to "systemd", anything // longer can be assumed to not be systemd. let mut comm_data = Vec::new(); if comm_file .take(TASK_COMM_LEN) .read_to_end(&mut comm_data) .is_err() { log::error!("problems reading from '{comm_path}'"); return false; } let end = comm_data .iter() .rposition(|&b| b != b'\n') .map_or(0, |i| i + 1); &comm_data[..end] == b"systemd" } /// For other platforms (macOS, ...), this returns false. #[cfg(not(any(target_os = "linux", target_os = "android")))] pub fn launched_by_systemd() -> bool { false } /// systemd notifications. Used by Ghostty to inform systemd of the state of the /// process. Currently only used to notify systemd that we are ready and that /// configuration reloading has started. /// /// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html /// /// These functions are re-implemented (rather than using `libsystemd`) to /// avoid another external dependency. Linux systems that do not use /// `systemd` should never start Ghostty with the `NOTIFY_SOCKET` environment /// variable set and these functions essentially become a no-op. #[cfg(any(target_os = "linux", target_os = "android"))] pub mod notify { use rustix::net::{ AddressFamily, SendFlags, SocketAddrUnix, SocketFlags, SocketType, connect, send, socket_with, }; /// Send the given message to the UNIX socket specified in the NOTIFY_SOCKET /// environment variable. If there NOTIFY_SOCKET environment variable does /// not exist then no message is sent. pub(super) fn send_message(message: &[u8]) { // Get the socket address that should receive notifications. let Some(socket_path) = std::env::var_os("NOTIFY_SOCKET") else { return; }; let socket_path = socket_path.as_encoded_bytes(); // If the socket address is an empty string return. if socket_path.is_empty() { return; } // The socket address must be a path or an abstract socket. if socket_path[0] != b'/' && socket_path[0] != b'@' { log::warn!( "only AF_UNIX sockets with path or abstract namespace addresses are supported!" ); return; } // sockaddr_un.path is 108 bytes. if 108 < socket_path.len() { log::warn!("NOTIFY_SOCKET path is too long!"); return; } // NOTE: the Zig version copies the path verbatim (including a // leading '@'); the sd_notify protocol says a leading '@' denotes // an abstract socket, which is what we do here. let addr = if socket_path[0] == b'@' { SocketAddrUnix::new_abstract_name(&socket_path[1..]) } else { SocketAddrUnix::new(socket_path) }; let addr = match addr { Ok(addr) => addr, Err(e) => { log::warn!("invalid NOTIFY_SOCKET address: {e}"); return; } }; let sock = match socket_with( AddressFamily::UNIX, SocketType::DGRAM, SocketFlags::CLOEXEC, None, ) { Ok(s) => s, Err(e) => { log::warn!("creating socket failed: {e}"); return; } }; if let Err(e) = connect(&sock, &addr) { log::warn!("unable to connect to notify socket: {e}"); return; } match send(&sock, message, SendFlags::empty()) { Ok(written) if written < message.len() => { log::warn!( "short write to notify socket: {written} < {}", message.len() ); } Ok(_) => {} Err(e) => log::warn!("unable to write to notify socket: {e}"), } } /// Tell systemd that we are ready or that we are finished reloading. /// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#READY=1 pub fn ready() { send_message(b"READY=1"); } /// Tell systemd that we have started reloading our configuration. /// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#RELOADING=1 /// and: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#MONOTONIC_USEC=%E2%80%A6 pub fn reloading() { let ts = rustix::time::clock_gettime(rustix::time::ClockId::Monotonic); let now = ts.tv_sec * 1_000_000 + ts.tv_nsec / 1000; let message = format!("RELOADING=1\nMONOTONIC_USEC={now}"); send_message(message.as_bytes()); } } /// systemd notifications are a no-op on platforms without systemd. #[cfg(not(any(target_os = "linux", target_os = "android")))] pub mod notify { pub fn ready() {} pub fn reloading() {} } #[cfg(test)] mod tests { #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::unix::net::UnixDatagram; use super::*; #[test] fn not_launched_without_env() { temp_env::with_vars( [("INVOCATION_ID", None::<&str>), ("JOURNAL_STREAM", None)], || assert!(!launched_by_systemd()), ); temp_env::with_vars( [("INVOCATION_ID", Some("x")), ("JOURNAL_STREAM", None)], || assert!(!launched_by_systemd()), ); } #[cfg(any(target_os = "linux", target_os = "android"))] fn recv_with_socket(f: impl FnOnce()) -> String { let dir = crate::TempDir::init().unwrap(); let path = dir.path().join("notify.sock"); let sock = UnixDatagram::bind(&path).unwrap(); sock.set_read_timeout(Some(std::time::Duration::from_secs(5))) .unwrap(); temp_env::with_var("NOTIFY_SOCKET", Some(path.to_str().unwrap()), f); let mut buf = [0u8; 128]; let n = sock.recv(&mut buf).unwrap(); String::from_utf8(buf[..n].to_vec()).unwrap() } #[cfg(any(target_os = "linux", target_os = "android"))] #[test] fn notify_ready_sends_message() { assert_eq!(recv_with_socket(notify::ready), "READY=1"); } #[cfg(any(target_os = "linux", target_os = "android"))] #[test] fn notify_reloading_sends_message() { let msg = recv_with_socket(notify::reloading); let rest = msg.strip_prefix("RELOADING=1\nMONOTONIC_USEC=").unwrap(); assert!(rest.parse::().unwrap() > 0); } #[test] fn notify_without_socket_is_noop() { temp_env::with_var("NOTIFY_SOCKET", None::<&str>, notify::ready); temp_env::with_var("NOTIFY_SOCKET", Some(""), notify::ready); temp_env::with_var("NOTIFY_SOCKET", Some("relative"), notify::ready); } /// Upstream bug (`os/systemd.zig:116`): a 108-byte `NOTIFY_SOCKET` path /// passes Zig's length check (`108 < len`) and then writes the /// terminating NUL at `path[108]`, one past the 108-byte array. Rust's /// socket-address constructor builds the address from the 108 bytes /// without a terminator (valid for Linux `AF_UNIX`; the length is part of /// the address), so there is no out-of-bounds write; longer paths are /// rejected. #[test] fn notify_maximum_length_socket_path_is_rejected_safely() { let mut max = vec![b'/'; 108]; max[1..].fill(b'a'); // 108 bytes fit exactly (no terminator needed); 109 do not. assert!(rustix::net::SocketAddrUnix::new(&max[..107]).is_ok()); assert!(rustix::net::SocketAddrUnix::new(&max[..108]).is_ok()); let mut over = max.clone(); over.push(b'a'); assert!(rustix::net::SocketAddrUnix::new(&over[..]).is_err()); for len in [107usize, 108, 109, 4096] { let mut p = vec![b'a'; len]; p[0] = b'/'; let p = String::from_utf8(p).unwrap(); // No panic and no out-of-bounds write (nothing listens on these // paths, so the send just fails or is skipped). temp_env::with_var("NOTIFY_SOCKET", Some(p.as_str()), notify::ready); } // '@' + 107 name bytes = 108 bytes. let mut abs = vec![b'a'; 108]; abs[0] = b'@'; let abs = String::from_utf8(abs).unwrap(); temp_env::with_var("NOTIFY_SOCKET", Some(abs.as_str()), notify::ready); } }