1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-01 10:41:03 +02:00

feat: ipc server and cli

This commit is contained in:
Jake Stanger 2023-06-22 23:06:45 +01:00
parent 93baf8f568
commit f5bdc5a027
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
10 changed files with 427 additions and 6 deletions

View file

@ -2,7 +2,7 @@ use crate::send;
use tokio::spawn;
use tokio::sync::mpsc;
/// MPSC async -> sync channel.
/// MPSC async -> GTK sync channel.
/// The sender uses `tokio::sync::mpsc`
/// while the receiver uses `glib::MainContext::channel`.
///

19
src/cli/mod.rs Normal file
View file

@ -0,0 +1,19 @@
use crate::ipc::commands::Command;
use crate::ipc::responses::Response;
use clap::Parser;
use serde::{Deserialize, Serialize};
#[derive(Parser, Debug, Serialize, Deserialize)]
#[command(version)]
pub struct Args {
#[command(subcommand)]
pub command: Option<Command>,
}
pub fn handle_response(response: Response) {
match response {
Response::Ok => println!("ok"),
Response::OkValue { value } => println!("ok\n{value}"),
Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()),
}
}

28
src/ipc/client.rs Normal file
View file

@ -0,0 +1,28 @@
use super::Ipc;
use crate::ipc::{Command, Response};
use color_eyre::Result;
use color_eyre::{Help, Report};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream;
impl Ipc {
/// Sends a command to the IPC server.
/// The server response is returned.
pub async fn send(&self, command: Command) -> Result<Response> {
let mut stream = match UnixStream::connect(&self.path).await {
Ok(stream) => Ok(stream),
Err(err) => Err(Report::new(err)
.wrap_err("Failed to connect to Ironbar IPC server")
.suggestion("Is Ironbar running?")),
}?;
let write_buffer = serde_json::to_vec(&command)?;
stream.write_all(&write_buffer).await?;
let mut read_buffer = vec![0; 1024];
let bytes = stream.read(&mut read_buffer).await?;
let response = serde_json::from_slice(&read_buffer[..bytes])?;
Ok(response)
}
}

14
src/ipc/commands.rs Normal file
View file

@ -0,0 +1,14 @@
use clap::Subcommand;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Subcommand, Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Command {
/// Return "ok"
Ping,
/// Open the GTK inspector
Inspect,
}

33
src/ipc/mod.rs Normal file
View file

@ -0,0 +1,33 @@
mod client;
pub mod commands;
pub mod responses;
mod server;
use std::path::PathBuf;
use tracing::warn;
pub use commands::Command;
pub use responses::Response;
#[derive(Debug)]
pub struct Ipc {
path: PathBuf,
}
impl Ipc {
/// Creates a new IPC instance.
/// This can be used as both a server and client.
pub fn new() -> Self {
let ipc_socket_file = std::env::var("XDG_RUNTIME_DIR")
.map_or_else(|_| PathBuf::from("/tmp"), PathBuf::from)
.join("ironbar-ipc.sock");
if format!("{}", ipc_socket_file.display()).len() > 100 {
warn!("The IPC socket file's absolute path exceeds 100 bytes, the socket may fail to create.");
}
Self {
path: ipc_socket_file,
}
}
}

17
src/ipc/responses.rs Normal file
View file

@ -0,0 +1,17 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Response {
Ok,
Err { message: Option<String> },
}
impl Response {
/// Creates a new `Response::Error`.
pub fn error(message: &str) -> Self {
Self::Err {
message: Some(message.to_string()),
}
}
}

119
src/ipc/server.rs Normal file
View file

@ -0,0 +1,119 @@
use super::Ipc;
use crate::bridge_channel::BridgeChannel;
use crate::ipc::{Command, Response};
use crate::ironvar::get_variable_manager;
use crate::{read_lock, send_async, try_send, write_lock};
use color_eyre::{Report, Result};
use glib::Continue;
use std::fs;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UnixListener, UnixStream};
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error, info, warn};
impl Ipc {
/// Starts the IPC server on its socket.
///
/// Once started, the server will begin accepting connections.
pub fn start(&self) {
let bridge = BridgeChannel::<Command>::new();
let cmd_tx = bridge.create_sender();
let (res_tx, mut res_rx) = mpsc::channel(32);
let path = self.path.clone();
if path.exists() {
warn!("Socket already exists. Did Ironbar exit abruptly?");
warn!("Attempting IPC shutdown to allow binding to address");
self.shutdown();
}
spawn(async move {
info!("Starting IPC on {}", path.display());
let listener = match UnixListener::bind(&path) {
Ok(listener) => listener,
Err(err) => {
error!(
"{:?}",
Report::new(err).wrap_err("Unable to start IPC server")
);
return;
}
};
loop {
match listener.accept().await {
Ok((stream, _addr)) => {
if let Err(err) =
Self::handle_connection(stream, &cmd_tx, &mut res_rx).await
{
error!("{err:?}");
}
}
Err(err) => {
error!("{err:?}");
}
}
}
});
bridge.recv(move |command| {
let res = Self::handle_command(command);
try_send!(res_tx, res);
Continue(true)
});
}
/// Takes an incoming connections,
/// reads the command message, and sends the response.
///
/// The connection is closed once the response has been written.
async fn handle_connection(
mut stream: UnixStream,
cmd_tx: &Sender<Command>,
res_rx: &mut Receiver<Response>,
) -> Result<()> {
let (mut stream_read, mut stream_write) = stream.split();
let mut read_buffer = vec![0; 1024];
let bytes = stream_read.read(&mut read_buffer).await?;
let command = serde_json::from_slice::<Command>(&read_buffer[..bytes])?;
debug!("Received command: {command:?}");
send_async!(cmd_tx, command);
let res = res_rx
.recv()
.await
.unwrap_or(Response::Err { message: None });
let res = serde_json::to_vec(&res)?;
stream_write.write_all(&res).await?;
stream_write.shutdown().await?;
Ok(())
}
/// Takes an input command, runs it and returns with the appropriate response.
///
/// This runs on the main thread, allowing commands to interact with GTK.
fn handle_command(command: Command) -> Response {
match command {
Command::Inspect => {
gtk::Window::set_interactive_debugging(true);
Response::Ok
}
Command::Ping => Response::Ok,
}
}
/// Shuts down the IPC server,
/// removing the socket file in the process.
pub fn shutdown(&self) {
fs::remove_file(&self.path).ok();
}
}

View file

@ -2,6 +2,8 @@
mod bar;
mod bridge_channel;
#[cfg(feature = "cli")]
mod cli;
mod clients;
mod config;
mod desktop_file;
@ -9,6 +11,8 @@ mod dynamic_string;
mod error;
mod gtk_helpers;
mod image;
#[cfg(feature = "ipc")]
mod ipc;
mod logging;
mod macros;
mod modules;
@ -20,6 +24,9 @@ mod unique_id;
use crate::bar::create_bar;
use crate::config::{Config, MonitorConfig};
use crate::style::load_css;
use cfg_if::cfg_if;
#[cfg(feature = "cli")]
use clap::Parser;
use color_eyre::eyre::Result;
use color_eyre::Report;
use dirs::config_dir;
@ -32,8 +39,9 @@ use std::future::Future;
use std::path::PathBuf;
use std::process::exit;
use std::rc::Rc;
use std::sync::mpsc;
use tokio::runtime::Handle;
use tokio::task::block_in_place;
use tokio::task::{block_in_place, spawn_blocking};
use crate::error::ExitCode;
use clients::wayland::{self, WaylandClient};
@ -47,6 +55,32 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");
async fn main() {
let _guard = logging::install_logging();
cfg_if! {
if #[cfg(feature = "cli")] {
run_with_args().await
} else {
start_ironbar().await
}
}
}
#[cfg(feature = "cli")]
async fn run_with_args() {
let args = cli::Args::parse();
match args.command {
Some(command) => {
let ipc = ipc::Ipc::new();
match ipc.send(command).await {
Ok(res) => cli::handle_response(res),
Err(err) => error!("{err:?}"),
};
}
None => start_ironbar().await,
}
}
async fn start_ironbar() {
info!("Ironbar version {}", VERSION);
info!("Starting application");
@ -64,6 +98,13 @@ async fn main() {
running.set(true);
cfg_if! {
if #[cfg(feature = "ipc")] {
let ipc = ipc::Ipc::new();
ipc.start();
}
}
let display = Display::default().map_or_else(
|| {
let report = Report::msg("Failed to get default GTK display");
@ -112,14 +153,27 @@ async fn main() {
if style_path.exists() {
load_css(style_path);
}
let (tx, rx) = mpsc::channel();
spawn_blocking(move || {
rx.recv().expect("to receive from channel");
info!("Shutting down");
#[cfg(feature = "ipc")]
ipc.shutdown();
exit(0);
});
ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
.expect("Error setting Ctrl-C handler");
});
// Ignore CLI args
// Some are provided by swaybar_config but not currently supported
app.run_with_args(&Vec::<&str>::new());
info!("Shutting down");
exit(0);
}
/// Creates each of the bars across each of the (configured) outputs.