2023-08-01 18:06:35 +02:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
2023-06-26 00:18:50 +02:00
|
|
|
|
2023-06-22 01:58:49 +02:00
|
|
|
//! Pinnacle's configuration API.
|
|
|
|
//!
|
|
|
|
//! The API works as follows:
|
|
|
|
//!
|
|
|
|
//! - All configuration is done through a Unix socket located at /tmp/pinnacle_socket.
|
|
|
|
//! - Pinnacle is built with the intent of configuration in Lua (and possibly other languages in
|
|
|
|
//! the future). To achieve this, an always running process in the target language needs to be
|
|
|
|
//! spawned, known as the *client*. This allows Pinnacle, the *server*, to call into user-defined
|
|
|
|
//! state through callback functions.
|
|
|
|
//! - The client must:
|
|
|
|
//! - Connect to the socket,
|
|
|
|
//! - send configuration messages through the socket, and finally
|
|
|
|
//! - listen to requests for callbacks,
|
|
|
|
//! in that order.
|
|
|
|
//!
|
|
|
|
//! You may be asking, "what messages am I supposed to send and receive?"
|
|
|
|
//! Great question!
|
|
|
|
//!
|
|
|
|
//! Pinnacle uses [MessagePack](https://msgpack.org/index.html) as the message format.
|
|
|
|
//! Messages should be serialized into MessagePack according to the [defined structs](msg::Msg).
|
|
|
|
//!
|
|
|
|
//! When Pinnacle needs to call a user-defined callback, for example from a keybind setting, it
|
|
|
|
//! sends a [CallCallback](msg::OutgoingMsg::CallCallback) message to the client. This message
|
|
|
|
//! contains a callback_id to identify what callback the client needs to run—but wait, where do you get that?
|
|
|
|
//!
|
|
|
|
//! The callback_id is created by the client to identify one of its callbacks. You will probably
|
|
|
|
//! need to store all callbacks in some central data structure along with a way to associate an id with it.
|
|
|
|
//! This could be an array and its indices or a hashmap and its keys (keep in mind the id needs to
|
|
|
|
//! be an unsigned 32 bit int).
|
|
|
|
//!
|
|
|
|
//! TODO: expand
|
|
|
|
//!
|
|
|
|
//! For an example, look at the Lua implementation in the repository.
|
|
|
|
|
2023-12-15 05:00:04 +01:00
|
|
|
pub mod handlers;
|
2023-06-18 01:55:04 +02:00
|
|
|
pub mod msg;
|
|
|
|
|
|
|
|
use std::{
|
|
|
|
io::{self, Read, Write},
|
2023-06-18 04:02:58 +02:00
|
|
|
os::unix::net::{UnixListener, UnixStream},
|
2023-06-18 01:55:04 +02:00
|
|
|
path::Path,
|
2023-09-22 00:12:16 +02:00
|
|
|
sync::{Arc, Mutex},
|
2023-06-18 01:55:04 +02:00
|
|
|
};
|
|
|
|
|
2023-08-16 18:28:35 +02:00
|
|
|
use anyhow::Context;
|
2023-09-22 00:12:16 +02:00
|
|
|
use calloop::RegistrationToken;
|
2023-06-18 01:55:04 +02:00
|
|
|
use smithay::reexports::calloop::{
|
|
|
|
self, channel::Sender, generic::Generic, EventSource, Interest, Mode, PostAction,
|
|
|
|
};
|
2023-09-11 10:58:43 +02:00
|
|
|
use sysinfo::{ProcessRefreshKind, RefreshKind, SystemExt};
|
2023-06-18 01:55:04 +02:00
|
|
|
|
|
|
|
use self::msg::{Msg, OutgoingMsg};
|
|
|
|
|
2023-09-11 10:22:51 +02:00
|
|
|
pub const SOCKET_NAME: &str = "pinnacle_socket";
|
2023-06-18 01:55:04 +02:00
|
|
|
|
2023-06-30 00:41:08 +02:00
|
|
|
fn handle_client(
|
|
|
|
mut stream: UnixStream,
|
|
|
|
sender: Sender<Msg>,
|
|
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
2023-06-18 01:55:04 +02:00
|
|
|
loop {
|
|
|
|
let mut len_marker_bytes = [0u8; 4];
|
|
|
|
if let Err(err) = stream.read_exact(&mut len_marker_bytes) {
|
|
|
|
if err.kind() == io::ErrorKind::UnexpectedEof {
|
|
|
|
tracing::warn!("stream closed: {}", err);
|
2023-06-30 00:41:08 +02:00
|
|
|
stream.shutdown(std::net::Shutdown::Both)?;
|
|
|
|
break Ok(());
|
2023-06-18 01:55:04 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let len_marker = u32::from_ne_bytes(len_marker_bytes);
|
|
|
|
let mut msg_bytes = vec![0u8; len_marker as usize];
|
|
|
|
|
|
|
|
if let Err(err) = stream.read_exact(msg_bytes.as_mut_slice()) {
|
|
|
|
if err.kind() == io::ErrorKind::UnexpectedEof {
|
|
|
|
tracing::warn!("stream closed: {}", err);
|
2023-06-30 00:41:08 +02:00
|
|
|
stream.shutdown(std::net::Shutdown::Both)?;
|
|
|
|
break Ok(());
|
2023-06-18 01:55:04 +02:00
|
|
|
}
|
|
|
|
};
|
2023-06-30 00:41:08 +02:00
|
|
|
let msg: Msg = rmp_serde::from_slice(msg_bytes.as_slice())?; // TODO: handle error
|
2023-06-18 01:55:04 +02:00
|
|
|
|
2023-06-30 00:41:08 +02:00
|
|
|
sender.send(msg)?;
|
2023-06-18 01:55:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct PinnacleSocketSource {
|
|
|
|
socket: Generic<UnixListener>,
|
|
|
|
sender: Sender<Msg>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PinnacleSocketSource {
|
2023-08-16 17:34:50 +02:00
|
|
|
/// Create a loop source that listens for connections to the provided socket_dir.
|
|
|
|
/// This will also set PINNACLE_SOCKET for use in API implementations.
|
2023-08-16 18:28:35 +02:00
|
|
|
pub fn new(sender: Sender<Msg>, socket_dir: &Path) -> anyhow::Result<Self> {
|
2023-09-21 00:20:48 +02:00
|
|
|
tracing::debug!("Creating socket source for dir {socket_dir:?}");
|
|
|
|
|
2023-09-11 10:58:43 +02:00
|
|
|
let system = sysinfo::System::new_with_specifics(
|
|
|
|
RefreshKind::new().with_processes(ProcessRefreshKind::new()),
|
|
|
|
);
|
|
|
|
|
|
|
|
// Test if you are running multiple instances of Pinnacle
|
|
|
|
let multiple_instances = system.processes_by_exact_name("pinnacle").count() > 1;
|
|
|
|
|
|
|
|
// If you are, append a suffix to the socket name
|
|
|
|
let socket_name = if multiple_instances {
|
|
|
|
let mut suffix: u8 = 1;
|
|
|
|
while let Ok(true) = socket_dir
|
|
|
|
.join(format!("{SOCKET_NAME}_{suffix}"))
|
|
|
|
.try_exists()
|
|
|
|
{
|
|
|
|
suffix += 1;
|
|
|
|
}
|
|
|
|
format!("{SOCKET_NAME}_{suffix}")
|
|
|
|
} else {
|
|
|
|
SOCKET_NAME.to_string()
|
|
|
|
};
|
2023-08-07 02:41:48 +02:00
|
|
|
|
2023-09-11 10:58:43 +02:00
|
|
|
let socket_path = socket_dir.join(socket_name);
|
|
|
|
|
|
|
|
// If there are multiple instances, don't touch other sockets
|
|
|
|
if multiple_instances {
|
|
|
|
if let Ok(exists) = socket_path.try_exists() {
|
|
|
|
if exists {
|
2023-09-21 00:40:36 +02:00
|
|
|
std::fs::remove_file(&socket_path)
|
|
|
|
.context(format!("Failed to remove old socket at {socket_path:?}",))?;
|
2023-09-11 10:58:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// If there are, remove them all
|
2023-09-21 00:20:48 +02:00
|
|
|
for file in std::fs::read_dir(socket_dir)?
|
|
|
|
.filter_map(|entry| entry.ok())
|
|
|
|
.filter(|entry| entry.file_name().to_string_lossy().starts_with(SOCKET_NAME))
|
2023-09-11 10:58:43 +02:00
|
|
|
{
|
2023-09-21 00:40:36 +02:00
|
|
|
tracing::debug!("Removing socket at {:?}", file.path());
|
|
|
|
std::fs::remove_file(file.path())
|
|
|
|
.context(format!("Failed to remove old socket at {:?}", file.path()))?;
|
2023-06-18 01:55:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-16 18:28:35 +02:00
|
|
|
let listener = UnixListener::bind(&socket_path)
|
|
|
|
.with_context(|| format!("Failed to bind to socket at {socket_path:?}"))?;
|
|
|
|
tracing::info!("Bound to socket at {socket_path:?}");
|
|
|
|
|
|
|
|
listener
|
|
|
|
.set_nonblocking(true)
|
|
|
|
.context("Failed to set socket to nonblocking")?;
|
2023-06-18 01:55:04 +02:00
|
|
|
|
|
|
|
let socket = Generic::new(listener, Interest::READ, Mode::Level);
|
|
|
|
|
2023-08-16 17:34:50 +02:00
|
|
|
std::env::set_var("PINNACLE_SOCKET", socket_path);
|
|
|
|
|
2023-06-18 01:55:04 +02:00
|
|
|
Ok(Self { socket, sender })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn send_to_client(
|
|
|
|
stream: &mut UnixStream,
|
|
|
|
msg: &OutgoingMsg,
|
|
|
|
) -> Result<(), rmp_serde::encode::Error> {
|
2023-12-15 05:00:04 +01:00
|
|
|
tracing::trace!("Sending {msg:?}");
|
|
|
|
|
2023-06-18 01:55:04 +02:00
|
|
|
let msg = rmp_serde::to_vec_named(msg)?;
|
|
|
|
let msg_len = msg.len() as u32;
|
|
|
|
let bytes = msg_len.to_ne_bytes();
|
|
|
|
|
|
|
|
if let Err(err) = stream.write_all(&bytes) {
|
|
|
|
if err.kind() == io::ErrorKind::BrokenPipe {
|
|
|
|
// TODO: notify user that config daemon is ded
|
|
|
|
return Ok(()); // TODO:
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if let Err(err) = stream.write_all(msg.as_slice()) {
|
|
|
|
if err.kind() == io::ErrorKind::BrokenPipe {
|
|
|
|
// TODO: something
|
|
|
|
return Ok(()); // TODO:
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
impl EventSource for PinnacleSocketSource {
|
|
|
|
type Event = UnixStream;
|
|
|
|
|
|
|
|
type Metadata = ();
|
|
|
|
|
|
|
|
type Ret = ();
|
|
|
|
|
|
|
|
type Error = io::Error;
|
|
|
|
|
|
|
|
fn process_events<F>(
|
|
|
|
&mut self,
|
|
|
|
readiness: calloop::Readiness,
|
|
|
|
token: calloop::Token,
|
|
|
|
mut callback: F,
|
|
|
|
) -> Result<calloop::PostAction, Self::Error>
|
|
|
|
where
|
|
|
|
F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret,
|
|
|
|
{
|
|
|
|
self.socket
|
|
|
|
.process_events(readiness, token, |_readiness, listener| {
|
|
|
|
while let Ok((stream, _sock_addr)) = listener.accept() {
|
|
|
|
let sender = self.sender.clone();
|
2023-12-15 05:00:04 +01:00
|
|
|
let callback_stream = stream.try_clone()?;
|
|
|
|
|
2023-06-18 01:55:04 +02:00
|
|
|
callback(callback_stream, &mut ());
|
2023-12-15 05:00:04 +01:00
|
|
|
|
|
|
|
// Handle the client in another thread as to not block the main one.
|
|
|
|
//
|
|
|
|
// No idea if this is even needed or if it's premature optimization.
|
2023-06-18 01:55:04 +02:00
|
|
|
std::thread::spawn(move || {
|
2023-06-30 00:41:08 +02:00
|
|
|
if let Err(err) = handle_client(stream, sender) {
|
|
|
|
tracing::error!("handle_client errored: {err}");
|
|
|
|
}
|
2023-06-18 01:55:04 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(PostAction::Continue)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn register(
|
|
|
|
&mut self,
|
|
|
|
poll: &mut calloop::Poll,
|
|
|
|
token_factory: &mut calloop::TokenFactory,
|
|
|
|
) -> calloop::Result<()> {
|
|
|
|
self.socket.register(poll, token_factory)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn reregister(
|
|
|
|
&mut self,
|
|
|
|
poll: &mut calloop::Poll,
|
|
|
|
token_factory: &mut calloop::TokenFactory,
|
|
|
|
) -> calloop::Result<()> {
|
|
|
|
self.socket.reregister(poll, token_factory)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn unregister(&mut self, poll: &mut calloop::Poll) -> calloop::Result<()> {
|
|
|
|
self.socket.unregister(poll)
|
|
|
|
}
|
|
|
|
}
|
2023-09-22 00:12:16 +02:00
|
|
|
|
|
|
|
pub struct ApiState {
|
|
|
|
// TODO: this may not need to be in an arc mutex because of the move to async
|
|
|
|
/// The stream API messages are being sent through.
|
|
|
|
pub stream: Option<Arc<Mutex<UnixStream>>>,
|
|
|
|
/// A token used to remove the socket source from the event loop on config restart.
|
2023-09-22 01:07:56 +02:00
|
|
|
pub socket_token: Option<RegistrationToken>,
|
2023-09-22 00:12:16 +02:00
|
|
|
/// The sending channel used to send API messages received from the socket source to a handler.
|
|
|
|
pub tx_channel: Sender<Msg>,
|
2023-09-22 02:48:52 +02:00
|
|
|
/// A channel used to ping the future in the event loop to drop and kill the child.
|
|
|
|
pub kill_channel: Option<async_channel::Sender<()>>,
|
|
|
|
/// A receiving channel that will cause the config process to be dropped and thus killed.
|
|
|
|
pub future_channel: Option<async_channel::Receiver<()>>,
|
2023-09-22 00:12:16 +02:00
|
|
|
}
|