blob: 64d96f91b77965e327f1c8ef06e9d4d41cbb7c56 [file] [log] [blame]
//! Alacritty socket IPC.
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::io::{BufRead, BufReader, Error as IoError, ErrorKind, Result as IoResult, Write};
use std::net::Shutdown;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::sync::Arc;
use std::{env, fs, process};
use log::{error, warn};
use std::result::Result;
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<Event>,
) -> IoResult<PathBuf> {
// 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)?;
unsafe { 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 reader = BufReader::new(&stream);
match reader.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);
},
SocketMessage::GetConfig(config) => {
let window_id =
config.window_id.and_then(|id| u64::try_from(id).ok()).map(WindowId::from);
let event = Event::new(EventType::IpcGetConfig(Arc::new(stream)), 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<PathBuf>, message: SocketMessage) -> IoResult<()> {
let mut socket = find_socket(socket)?;
// Write message to socket.
let message_json = serde_json::to_string(&message)?;
socket.write_all(message_json.as_bytes())?;
let _ = socket.flush();
// Shutdown write end, to allow reading.
socket.shutdown(Shutdown::Write)?;
// Get matching IPC reply.
handle_reply(&socket, &message)?;
Ok(())
}
/// Process IPC responses.
fn handle_reply(stream: &UnixStream, message: &SocketMessage) -> IoResult<()> {
// Read reply, returning early if there is none.
let mut buffer = String::new();
let mut reader = BufReader::new(stream);
if let Ok(0) | Err(_) = reader.read_line(&mut buffer) {
return Ok(());
}
// Parse IPC reply.
let reply: SocketReply = serde_json::from_str(&buffer)
.map_err(|err| IoError::other(format!("Invalid IPC format: {err}")))?;
// Ensure reply matches request.
match (message, &reply) {
// Write requested config to STDOUT.
(SocketMessage::GetConfig(..), SocketReply::GetConfig(config)) => {
println!("{config}");
Ok(())
},
// Ignore requests without reply.
_ => Ok(()),
}
}
/// Send IPC message reply.
pub fn send_reply(stream: &mut UnixStream, message: SocketReply) {
if let Err(err) = send_reply_fallible(stream, message) {
error!("Failed to send IPC reply: {err}");
}
}
/// Send IPC message reply, returning possible errors.
fn send_reply_fallible(stream: &mut UnixStream, message: SocketReply) -> IoResult<()> {
let json = serde_json::to_string(&message).map_err(IoError::other)?;
stream.write_all(json.as_bytes())?;
stream.flush()?;
Ok(())
}
/// Directory for the IPC socket file.
#[cfg(not(target_os = "macos"))]
fn socket_dir() -> PathBuf {
xdg::BaseDirectories::with_prefix("alacritty")
.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<PathBuf>) -> IoResult<UnixStream> {
// 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")
}
/// IPC socket replies.
#[derive(Serialize, Deserialize, Debug)]
pub enum SocketReply {
GetConfig(String),
}