blob: a71b5139f575f31ae7d0dc2a770f55d4de20daf3 [file] [log] [blame]
//! 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 glutin::event_loop::EventLoopProxy;
use log::warn;
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>) -> Option<PathBuf> {
// Create the IPC socket and export its path as env variable if necessary.
let socket_path = options.socket.clone().unwrap_or_else(|| {
let mut path = socket_dir();
path.push(format!("{}-{}.sock", socket_prefix(), process::id()));
path
});
env::set_var(ALACRITTY_SOCKET_ENV, socket_path.as_os_str());
let listener = match UnixListener::bind(&socket_path) {
Ok(listener) => listener,
Err(err) => {
warn!("Unable to create socket: {:?}", err);
return None;
},
};
// 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);
},
}
}
});
Some(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)?;
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<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)
}
/// File prefix matching all available sockets.
#[cfg(target_os = "macos")]
fn socket_prefix() -> String {
String::from("Alacritty")
}