diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-01-17 10:38:32 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-01-17 10:45:18 +0400 |
| commit | 40c85da102054caeb86b7905cd27c69e392c8f92 (patch) | |
| tree | ea0f2547ca2949d43ff5898ebab3a56d7eee6d1e /src | |
| parent | 768b32602839896012a9ee3c4ed6885360fa5395 (diff) | |
| download | niri-40c85da102054caeb86b7905cd27c69e392c8f92.tar.gz niri-40c85da102054caeb86b7905cd27c69e392c8f92.tar.bz2 niri-40c85da102054caeb86b7905cd27c69e392c8f92.zip | |
Add an IPC socket and a niri msg outputs subcommand
Diffstat (limited to 'src')
| -rw-r--r-- | src/backend/mod.rs | 9 | ||||
| -rw-r--r-- | src/backend/tty.rs | 77 | ||||
| -rw-r--r-- | src/backend/winit.rs | 29 | ||||
| -rw-r--r-- | src/ipc/client.rs | 106 | ||||
| -rw-r--r-- | src/ipc/mod.rs | 2 | ||||
| -rw-r--r-- | src/ipc/server.rs | 127 | ||||
| -rw-r--r-- | src/main.rs | 26 | ||||
| -rw-r--r-- | src/niri.rs | 13 |
8 files changed, 380 insertions, 9 deletions
diff --git a/src/backend/mod.rs b/src/backend/mod.rs index a273f769..595b8513 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,4 +1,6 @@ +use std::cell::RefCell; use std::collections::HashMap; +use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -110,6 +112,13 @@ impl Backend { } } + pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> { + match self { + Backend::Tty(tty) => tty.ipc_outputs(), + Backend::Winit(winit) => winit.ipc_outputs(), + } + } + #[cfg_attr(not(feature = "dbus"), allow(unused))] pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> { match self { diff --git a/src/backend/tty.rs b/src/backend/tty.rs index 0dacf9b7..f86851cb 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -74,6 +74,7 @@ pub struct Tty { // The allocator for the primary GPU. It is only `Some()` if we have a device corresponding to // the primary GPU. primary_allocator: Option<DmabufAllocator<GbmAllocator<DrmDeviceFd>>>, + ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>, enabled_outputs: Arc<Mutex<HashMap<String, Output>>>, } @@ -221,6 +222,7 @@ impl Tty { devices: HashMap::new(), dmabuf_global: None, primary_allocator: None, + ipc_outputs: Rc::new(RefCell::new(HashMap::new())), enabled_outputs: Arc::new(Mutex::new(HashMap::new())), } } @@ -367,6 +369,8 @@ impl Tty { warn!("error adding device: {err:?}"); } } + + self.refresh_ipc_outputs(); } } } @@ -512,6 +516,8 @@ impl Tty { _ => (), } } + + self.refresh_ipc_outputs(); } fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) { @@ -576,6 +582,8 @@ impl Tty { self.gpu_manager.as_mut().remove_node(&device.render_node); niri.event_loop.remove(device.token); + + self.refresh_ipc_outputs(); } fn connector_connected( @@ -608,16 +616,7 @@ impl Tty { let device = self.devices.get_mut(&node).context("missing device")?; - // FIXME: print modes here until we have a better way to list all modes. for m in connector.modes() { - let wl_mode = Mode::from(*m); - debug!( - "mode: {}x{}@{:.3}", - m.size().0, - m.size().1, - wl_mode.refresh as f64 / 1000., - ); - trace!("{m:?}"); } @@ -1157,6 +1156,64 @@ impl Tty { } } + fn refresh_ipc_outputs(&self) { + let _span = tracy_client::span!("Tty::refresh_ipc_outputs"); + + let mut ipc_outputs = HashMap::new(); + + for device in self.devices.values() { + for (connector, crtc) in device.drm_scanner.crtcs() { + let connector_name = format!( + "{}-{}", + connector.interface().as_str(), + connector.interface_id(), + ); + + let physical_size = connector.size(); + + let (make, model) = EdidInfo::for_connector(&device.drm, connector.handle()) + .map(|info| (info.manufacturer, info.model)) + .unwrap_or_else(|| ("Unknown".into(), "Unknown".into())); + + let modes = connector + .modes() + .iter() + .map(|m| niri_ipc::Mode { + width: m.size().0, + height: m.size().1, + refresh_rate: Mode::from(*m).refresh as u32, + }) + .collect(); + + let mut output = niri_ipc::Output { + name: connector_name.clone(), + make, + model, + physical_size, + modes, + current_mode: None, + }; + + if let Some(surface) = device.surfaces.get(&crtc) { + let current = surface.compositor.pending_mode(); + if let Some(current) = connector.modes().iter().position(|m| *m == current) { + output.current_mode = Some(current); + } else { + error!("connector mode list missing current mode"); + } + } + + ipc_outputs.insert(connector_name, output); + } + } + + self.ipc_outputs.replace(ipc_outputs); + } + + pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> { + self.ipc_outputs.clone() + } + pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> { self.enabled_outputs.clone() } @@ -1305,6 +1362,8 @@ impl Tty { warn!("error connecting connector: {err:?}"); } } + + self.refresh_ipc_outputs(); } } diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 9b31d2c5..3c4b9ad3 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -28,6 +28,7 @@ pub struct Winit { output: Output, backend: WinitGraphicsBackend<GlesRenderer>, damage_tracker: OutputDamageTracker, + ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>, enabled_outputs: Arc<Mutex<HashMap<String, Output>>>, } @@ -56,6 +57,23 @@ impl Winit { output.change_current_state(Some(mode), Some(Transform::Flipped180), None, None); output.set_preferred(mode); + let physical_properties = output.physical_properties(); + let ipc_outputs = Rc::new(RefCell::new(HashMap::from([( + "winit".to_owned(), + niri_ipc::Output { + name: output.name(), + make: physical_properties.make, + model: physical_properties.model, + physical_size: None, + modes: vec![niri_ipc::Mode { + width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16, + height: backend.window_size().h.clamp(0, u16::MAX as i32) as u16, + refresh_rate: 60_000, + }], + current_mode: Some(0), + }, + )]))); + let enabled_outputs = Arc::new(Mutex::new(HashMap::from([( "winit".to_owned(), output.clone(), @@ -76,6 +94,12 @@ impl Winit { None, None, ); + + let mut ipc_outputs = winit.ipc_outputs.borrow_mut(); + let mode = &mut ipc_outputs.get_mut("winit").unwrap().modes[0]; + mode.width = size.w.clamp(0, u16::MAX as i32) as u16; + mode.height = size.h.clamp(0, u16::MAX as i32) as u16; + state.niri.output_resized(winit.output.clone()); } WinitEvent::Input(event) => state.process_input_event(event), @@ -95,6 +119,7 @@ impl Winit { output, backend, damage_tracker, + ipc_outputs, enabled_outputs, } } @@ -198,6 +223,10 @@ impl Winit { } } + pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> { + self.ipc_outputs.clone() + } + pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> { self.enabled_outputs.clone() } diff --git a/src/ipc/client.rs b/src/ipc/client.rs new file mode 100644 index 00000000..c09ef84e --- /dev/null +++ b/src/ipc/client.rs @@ -0,0 +1,106 @@ +use std::env; +use std::io::{Read, Write}; +use std::net::Shutdown; +use std::os::unix::net::UnixStream; + +use anyhow::{bail, Context}; +use niri_ipc::{Mode, Output, Request, Response}; + +use crate::Msg; + +pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> { + let socket_path = env::var_os(niri_ipc::SOCKET_PATH_ENV).with_context(|| { + format!( + "{} is not set, are you running this within niri?", + niri_ipc::SOCKET_PATH_ENV + ) + })?; + + let mut stream = + UnixStream::connect(socket_path).context("error connecting to {socket_path}")?; + + let request = match msg { + Msg::Outputs => Request::Outputs, + }; + let mut buf = serde_json::to_vec(&request).unwrap(); + stream + .write_all(&buf) + .context("error writing IPC request")?; + stream + .shutdown(Shutdown::Write) + .context("error closing IPC stream for writing")?; + + buf.clear(); + stream + .read_to_end(&mut buf) + .context("error reading IPC response")?; + + let response = serde_json::from_slice(&buf).context("error parsing IPC response")?; + match msg { + Msg::Outputs => { + #[allow(irrefutable_let_patterns)] + let Response::Outputs(outputs) = response + else { + bail!("unexpected response: expected Outputs, got {response:?}"); + }; + + if json { + let output = + serde_json::to_string(&outputs).context("error formatting response")?; + println!("{output}"); + return Ok(()); + } + + let mut outputs = outputs.into_iter().collect::<Vec<_>>(); + outputs.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + + for (connector, output) in outputs.into_iter() { + let Output { + name, + make, + model, + physical_size, + modes, + current_mode, + } = output; + + println!(r#"Output "{connector}" ({make} - {model} - {name})"#); + + if let Some(current) = current_mode { + let mode = *modes + .get(current) + .context("invalid response: current mode does not exist")?; + let Mode { + width, + height, + refresh_rate, + } = mode; + let refresh = refresh_rate as f64 / 1000.; + println!(" Current mode: {width}x{height} @ {refresh:.3} Hz"); + } else { + println!(" Disabled"); + } + + if let Some((width, height)) = physical_size { + println!(" Physical size: {width}x{height} mm"); + } else { + println!(" Physical size: unknown"); + } + + println!(" Available modes:"); + for mode in modes { + let Mode { + width, + height, + refresh_rate, + } = mode; + let refresh = refresh_rate as f64 / 1000.; + println!(" {width}x{height}@{refresh:.3}"); + } + println!(); + } + } + } + + Ok(()) +} diff --git a/src/ipc/mod.rs b/src/ipc/mod.rs new file mode 100644 index 00000000..c07f47e0 --- /dev/null +++ b/src/ipc/mod.rs @@ -0,0 +1,2 @@ +pub mod client; +pub mod server; diff --git a/src/ipc/server.rs b/src/ipc/server.rs new file mode 100644 index 00000000..d493e861 --- /dev/null +++ b/src/ipc/server.rs @@ -0,0 +1,127 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::PathBuf; +use std::rc::Rc; +use std::{env, io, process}; + +use anyhow::Context; +use calloop::io::Async; +use directories::BaseDirs; +use futures_util::io::{AsyncReadExt, BufReader}; +use futures_util::{AsyncBufReadExt, AsyncWriteExt}; +use niri_ipc::{Request, Response}; +use smithay::reexports::calloop::generic::Generic; +use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction}; +use smithay::reexports::rustix::fs::unlink; + +use crate::niri::State; + +pub struct IpcServer { + pub socket_path: PathBuf, +} + +struct ClientCtx { + ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>, +} + +impl IpcServer { + pub fn start( + event_loop: &LoopHandle<'static, State>, + wayland_socket_name: &str, + ) -> anyhow::Result<Self> { + let _span = tracy_client::span!("Ipc::start"); + + let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id()); + let mut socket_path = socket_dir(); + socket_path.push(socket_name); + + let listener = UnixListener::bind(&socket_path).context("error binding socket")?; + listener + .set_nonblocking(true) + .context("error setting socket to non-blocking")?; + + let source = Generic::new(listener, Interest::READ, Mode::Level); + event_loop + .insert_source(source, |_, socket, state| { + match socket.accept() { + Ok((stream, _)) => on_new_ipc_client(state, stream), + Err(e) if e.kind() == io::ErrorKind::WouldBlock => (), + Err(e) => return Err(e), + } + + Ok(PostAction::Continue) + }) + .unwrap(); + + Ok(Self { socket_path }) + } +} + +impl Drop for IpcServer { + fn drop(&mut self) { + let _ = unlink(&self.socket_path); + } +} + +fn socket_dir() -> PathBuf { + BaseDirs::new() + .as_ref() + .and_then(|x| x.runtime_dir()) + .map(|x| x.to_owned()) + .unwrap_or_else(env::temp_dir) +} + +fn on_new_ipc_client(state: &mut State, stream: UnixStream) { + let _span = tracy_client::span!("on_new_ipc_client"); + trace!("new IPC client connected"); + + let stream = match state.niri.event_loop.adapt_io(stream) { + Ok(stream) => stream, + Err(err) => { + warn!("error making IPC stream async: {err:?}"); + return; + } + }; + + let ctx = ClientCtx { + ipc_outputs: state.backend.ipc_outputs(), + }; + + let future = async move { + if let Err(err) = handle_client(ctx, stream).await { + warn!("error handling IPC client: {err:?}"); + } + }; + if let Err(err) = state.niri.scheduler.schedule(future) { + warn!("error scheduling IPC stream future: {err:?}"); + } +} + +async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow::Result<()> { + let (read, mut write) = stream.split(); + let mut buf = String::new(); + + // Read a single line to allow extensibility in the future to keep reading. + BufReader::new(read) + .read_line(&mut buf) + .await + .context("error reading request")?; + + let request: Request = serde_json::from_str(&buf).context("error parsing request")?; + + let response = match request { + Request::Outputs => { + let ipc_outputs = ctx.ipc_outputs.borrow().clone(); + Response::Outputs(ipc_outputs) + } + }; + + let buf = serde_json::to_vec(&response).context("error formatting response")?; + write + .write_all(&buf) + .await + .context("error writing response")?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 427e75b2..b4de4656 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod dbus; mod frame_clock; mod handlers; mod input; +mod ipc; mod layout; mod niri; mod render_helpers; @@ -40,6 +41,7 @@ use tracing_subscriber::EnvFilter; use utils::spawn; use watcher::Watcher; +use crate::ipc::client::handle_msg; use crate::utils::{cause_panic, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE}; #[derive(Parser)] @@ -67,10 +69,24 @@ enum Sub { #[arg(short, long)] config: Option<PathBuf>, }, + /// Communicate with the running niri instance. + Msg { + #[command(subcommand)] + msg: Msg, + /// Format output as JSON. + #[arg(short, long)] + json: bool, + }, /// Cause a panic to check if the backtraces are good. Panic, } +#[derive(Subcommand)] +enum Msg { + /// List connected outputs. + Outputs, +} + fn main() -> Result<(), Box<dyn std::error::Error>> { // Set backtrace defaults if not set. if env::var_os("RUST_BACKTRACE").is_none() { @@ -120,6 +136,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { info!("config is valid"); return Ok(()); } + Sub::Msg { msg, json } => { + handle_msg(msg, json)?; + return Ok(()); + } Sub::Panic => cause_panic(), } } @@ -159,6 +179,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { socket_name.to_string_lossy() ); + // Set NIRI_SOCKET for children. + if let Some(ipc) = &state.niri.ipc_server { + env::set_var(niri_ipc::SOCKET_PATH_ENV, &ipc.socket_path); + info!("IPC listening on: {}", ipc.socket_path.to_string_lossy()); + } + if is_systemd_service { // We're starting as a systemd service. Export our variables. import_env_to_systemd(); diff --git a/src/niri.rs b/src/niri.rs index f22612ed..d0a7490f 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -98,6 +98,7 @@ use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri}; use crate::frame_clock::FrameClock; use crate::handlers::configure_lock_surface; use crate::input::{apply_libinput_settings, TabletData}; +use crate::ipc::server::IpcServer; use crate::layout::{Layout, MonitorRenderElement}; use crate::pw_utils::{Cast, PipeWire}; use crate::render_helpers::{NiriRenderer, PrimaryGpuTextureRenderElement}; @@ -190,6 +191,8 @@ pub struct Niri { #[cfg(feature = "dbus")] pub inhibit_power_key_fd: Option<zbus::zvariant::OwnedFd>, + pub ipc_server: Option<IpcServer>, + // Casts are dropped before PipeWire to prevent a double-free (yay). pub casts: Vec<Cast>, pub pipewire: Option<PipeWire>, @@ -865,6 +868,14 @@ impl Niri { }) .unwrap(); + let ipc_server = match IpcServer::start(&event_loop, &socket_name.to_string_lossy()) { + Ok(server) => Some(server), + Err(err) => { + warn!("error starting IPC server: {err:?}"); + None + } + }; + let pipewire = match PipeWire::new(&event_loop) { Ok(pipewire) => Some(pipewire), Err(err) => { @@ -949,6 +960,8 @@ impl Niri { #[cfg(feature = "dbus")] inhibit_power_key_fd: None, + ipc_server, + pipewire, casts: vec![], } |
