//! Alacritty socket IPC. use std::ffi::OsStr; use std::io::{BufRead, BufReader, Error as IoError, ErrorKind, Result as IoResult, Write}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::PathBuf; use std::{env, fs, process}; use log::warn; use winit::event_loop::EventLoopProxy; use winit::window::WindowId; use alacritty_terminal::thread; use crate::cli::{Options, SocketMessage}; use crate::event::{Event, EventType}; /// Environment variable name for the IPC socket path. const ALACRITTY_SOCKET_ENV: &str = "ALACRITTY_SOCKET"; /// Create an IPC socket. pub fn spawn_ipc_socket( options: &Options, event_proxy: EventLoopProxy, ) -> IoResult { // Create the IPC socket and export its path as env. let socket_path = options.socket.clone().unwrap_or_else(|| { let mut path = socket_dir(); path.push(format!("{}-{}.sock", socket_prefix(), process::id())); path }); let listener = UnixListener::bind(&socket_path)?; env::set_var(ALACRITTY_SOCKET_ENV, socket_path.as_os_str()); if options.daemon { println!("ALACRITTY_SOCKET={}; export ALACRITTY_SOCKET", socket_path.display()); } // Spawn a thread to listen on the IPC socket. thread::spawn_named("socket listener", move || { let mut data = String::new(); for stream in listener.incoming().filter_map(Result::ok) { data.clear(); let mut stream = BufReader::new(stream); match stream.read_line(&mut data) { Ok(0) | Err(_) => continue, Ok(_) => (), }; // Read pending events on socket. let message: SocketMessage = match serde_json::from_str(&data) { Ok(message) => message, Err(err) => { warn!("Failed to convert data from socket: {}", err); continue; }, }; // Handle IPC events. match message { SocketMessage::CreateWindow(options) => { let event = Event::new(EventType::CreateWindow(options), None); let _ = event_proxy.send_event(event); }, SocketMessage::Config(ipc_config) => { let window_id = ipc_config .window_id .and_then(|id| u64::try_from(id).ok()) .map(WindowId::from); let event = Event::new(EventType::IpcConfig(ipc_config), window_id); let _ = event_proxy.send_event(event); }, } } }); Ok(socket_path) } /// Send a message to the active Alacritty socket. pub fn send_message(socket: Option, message: SocketMessage) -> IoResult<()> { let mut socket = find_socket(socket)?; let message = serde_json::to_string(&message)?; socket.write_all(message[..].as_bytes())?; let _ = socket.flush(); Ok(()) } /// Directory for the IPC socket file. #[cfg(not(target_os = "macos"))] fn socket_dir() -> PathBuf { xdg::BaseDirectories::with_prefix("alacritty") .ok() .and_then(|xdg| xdg.get_runtime_directory().map(ToOwned::to_owned).ok()) .and_then(|path| fs::create_dir_all(&path).map(|_| path).ok()) .unwrap_or_else(env::temp_dir) } /// Directory for the IPC socket file. #[cfg(target_os = "macos")] fn socket_dir() -> PathBuf { env::temp_dir() } /// Find the IPC socket path. fn find_socket(socket_path: Option) -> IoResult { // Handle --socket CLI override. if let Some(socket_path) = socket_path { // Ensure we inform the user about an invalid path. return UnixStream::connect(&socket_path).map_err(|err| { let message = format!("invalid socket path {socket_path:?}"); IoError::new(err.kind(), message) }); } // Handle environment variable. if let Ok(path) = env::var(ALACRITTY_SOCKET_ENV) { let socket_path = PathBuf::from(path); if let Ok(socket) = UnixStream::connect(socket_path) { return Ok(socket); } } // Search for sockets files. for entry in fs::read_dir(socket_dir())?.filter_map(|entry| entry.ok()) { let path = entry.path(); // Skip files that aren't Alacritty sockets. let socket_prefix = socket_prefix(); if path .file_name() .and_then(OsStr::to_str) .filter(|file| file.starts_with(&socket_prefix) && file.ends_with(".sock")) .is_none() { continue; } // Attempt to connect to the socket. match UnixStream::connect(&path) { Ok(socket) => return Ok(socket), // Delete orphan sockets. Err(error) if error.kind() == ErrorKind::ConnectionRefused => { let _ = fs::remove_file(&path); }, // Ignore other errors like permission issues. Err(_) => (), } } Err(IoError::new(ErrorKind::NotFound, "no socket found")) } /// File prefix matching all available sockets. /// /// This prefix will include display server information to allow for environments with multiple /// display servers running for the same user. #[cfg(not(target_os = "macos"))] fn socket_prefix() -> String { let display = env::var("WAYLAND_DISPLAY").or_else(|_| env::var("DISPLAY")).unwrap_or_default(); format!("Alacritty-{}", display.replace('/', "-")) } /// File prefix matching all available sockets. #[cfg(target_os = "macos")] fn socket_prefix() -> String { String::from("Alacritty") }