aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-01-17 10:38:32 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-01-17 10:45:18 +0400
commit40c85da102054caeb86b7905cd27c69e392c8f92 (patch)
treeea0f2547ca2949d43ff5898ebab3a56d7eee6d1e /src
parent768b32602839896012a9ee3c4ed6885360fa5395 (diff)
downloadniri-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.rs9
-rw-r--r--src/backend/tty.rs77
-rw-r--r--src/backend/winit.rs29
-rw-r--r--src/ipc/client.rs106
-rw-r--r--src/ipc/mod.rs2
-rw-r--r--src/ipc/server.rs127
-rw-r--r--src/main.rs26
-rw-r--r--src/niri.rs13
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![],
}