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::{Path, PathBuf};
use std::sync::Arc;
use std::{env, fs};
use log::{error, warn};
use std::result::Result;
use winit::event_loop::EventLoopProxy;
use winit::window::WindowId;
use crate::cli::{Options, SocketMessage};
use crate::event::{Event, EventType};
const ALACRITTY_SOCKET_ENV: &str = "ALACRITTY_SOCKET";
pub struct IpcListener {
pub socket: UnixListener,
event_proxy: EventLoopProxy<Event>,
data: String,
}
impl IpcListener {
pub fn new(
options: &Options,
event_proxy: EventLoopProxy<Event>,
path: &Path,
) -> Result<Self, IoError> {
let socket = UnixListener::bind(path)?;
socket.set_nonblocking(true)?;
unsafe { env::set_var(ALACRITTY_SOCKET_ENV, path.as_os_str()) };
if options.daemon {
println!("ALACRITTY_SOCKET={}; export ALACRITTY_SOCKET", path.display());
}
Ok(Self { event_proxy, socket, data: Default::default() })
}
pub fn process_message(&mut self) -> Result<(), IoError> {
let (stream, _) = self.socket.accept()?;
self.data.clear();
let mut reader = BufReader::new(&stream);
match reader.read_line(&mut self.data) {
Ok(0) | Err(_) => return Ok(()),
Ok(_) => (),
};
let message: SocketMessage = match serde_json::from_str(&self.data) {
Ok(message) => message,
Err(err) => {
warn!("Failed to parse IPC message: {err}");
return Ok(());
},
};
match message {
SocketMessage::CreateWindow(options) => {
let event = Event::new(EventType::CreateWindow(options), None);
let _ = self.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 _ = self.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 _ = self.event_proxy.send_event(event);
},
}
Ok(())
}
}
pub fn send_message(socket: Option<PathBuf>, message: SocketMessage) -> IoResult<()> {
let mut socket = find_socket(socket)?;
let message_json = serde_json::to_string(&message)?;
socket.write_all(message_json.as_bytes())?;
let _ = socket.flush();
socket.shutdown(Shutdown::Write)?;
handle_reply(&socket, &message)?;
Ok(())
}
fn handle_reply(stream: &UnixStream, message: &SocketMessage) -> IoResult<()> {
let mut buffer = String::new();
let mut reader = BufReader::new(stream);
if let Ok(0) | Err(_) = reader.read_line(&mut buffer) {
return Ok(());
}
let reply: SocketReply = serde_json::from_str(&buffer)
.map_err(|err| IoError::other(format!("Invalid IPC format: {err}")))?;
match (message, &reply) {
(SocketMessage::GetConfig(..), SocketReply::GetConfig(config)) => {
println!("{config}");
Ok(())
},
_ => Ok(()),
}
}
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}");
}
}
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(())
}
#[cfg(not(target_os = "macos"))]
pub 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)
}
#[cfg(target_os = "macos")]
pub fn socket_dir() -> PathBuf {
env::temp_dir()
}
fn find_socket(socket_path: Option<PathBuf>) -> IoResult<UnixStream> {
if let Some(socket_path) = socket_path {
return UnixStream::connect(&socket_path).map_err(|err| {
let message = format!("invalid socket path {socket_path:?}");
IoError::new(err.kind(), message)
});
}
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);
}
}
for entry in fs::read_dir(socket_dir())?.filter_map(|entry| entry.ok()) {
let path = entry.path();
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;
}
match UnixStream::connect(&path) {
Ok(socket) => return Ok(socket),
Err(error) if error.kind() == ErrorKind::ConnectionRefused => {
let _ = fs::remove_file(&path);
},
Err(_) => (),
}
}
Err(IoError::new(ErrorKind::NotFound, "no socket found"))
}
#[cfg(not(target_os = "macos"))]
pub fn socket_prefix() -> String {
let display = env::var("WAYLAND_DISPLAY").or_else(|_| env::var("DISPLAY")).unwrap_or_default();
format!("Alacritty-{}", display.replace('/', "-"))
}
#[cfg(target_os = "macos")]
pub fn socket_prefix() -> String {
String::from("Alacritty")
}
#[derive(Serialize, Deserialize, Debug)]
pub enum SocketReply {
GetConfig(String),
}