From bd04b09171ad2bc1ab21ef4ff80ddbcf71600645 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 6 May 2025 18:12:57 +0200 Subject: [PATCH] cli: add commands to inspect clients --- src/cli.rs | 10 +- src/cli/clients.rs | 243 +++++++++++++++++++++++++ src/ifs.rs | 1 + src/ifs/jay_client_query.rs | 141 ++++++++++++++ src/ifs/jay_compositor.rs | 53 +++++- src/ifs/jay_select_toplevel.rs | 5 +- src/ifs/jay_toplevel.rs | 10 + src/tools/tool_client.rs | 45 ++++- src/wl_usr/usr_ifs/usr_jay_toplevel.rs | 4 + wire/jay_client_query.txt | 49 +++++ wire/jay_compositor.txt | 8 + wire/jay_toplevel.txt | 4 + 12 files changed, 557 insertions(+), 16 deletions(-) create mode 100644 src/cli/clients.rs create mode 100644 src/ifs/jay_client_query.rs create mode 100644 wire/jay_client_query.txt diff --git a/src/cli.rs b/src/cli.rs index d00b7039..2ebdcd47 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,3 +1,4 @@ +mod clients; mod color; mod color_management; mod damage_tracking; @@ -19,9 +20,9 @@ mod xwayland; use { crate::{ cli::{ - color_management::ColorManagementArgs, damage_tracking::DamageTrackingArgs, - idle::IdleCmd, input::InputArgs, randr::RandrArgs, reexec::ReexecArgs, - xwayland::XwaylandArgs, + clients::ClientsArgs, color_management::ColorManagementArgs, + damage_tracking::DamageTrackingArgs, idle::IdleCmd, input::InputArgs, randr::RandrArgs, + reexec::ReexecArgs, xwayland::XwaylandArgs, }, compositor::start_compositor, format::{Format, ref_formats}, @@ -86,6 +87,8 @@ pub enum Cmd { /// Replace the compositor by another process. (Only for development.) #[clap(hide = true)] Reexec(ReexecArgs), + /// Inspect/manipulate the connected clients. + Clients(ClientsArgs), #[cfg(feature = "it")] RunTests, } @@ -244,6 +247,7 @@ pub fn main() { Cmd::DamageTracking(a) => damage_tracking::main(cli.global, a), Cmd::Xwayland(a) => xwayland::main(cli.global, a), Cmd::ColorManagement(a) => color_management::main(cli.global, a), + Cmd::Clients(a) => clients::main(cli.global, a), #[cfg(feature = "it")] Cmd::RunTests => crate::it::run_tests(), Cmd::Reexec(a) => reexec::main(cli.global, a), diff --git a/src/cli/clients.rs b/src/cli/clients.rs new file mode 100644 index 00000000..cd6f268a --- /dev/null +++ b/src/cli/clients.rs @@ -0,0 +1,243 @@ +use { + crate::{ + cli::GlobalArgs, + tools::tool_client::{Handle, ToolClient, with_tool_client}, + wire::{JayClientQueryId, jay_client_query, jay_compositor}, + }, + ahash::AHashMap, + clap::{Args, Subcommand}, + std::{cell::RefCell, mem, rc::Rc}, + uapi::c, +}; + +#[derive(Args, Debug)] +pub struct ClientsArgs { + #[clap(subcommand)] + cmd: Option, +} + +#[derive(Subcommand, Debug)] +enum ClientsCmd { + /// Show information about clients. + Show(ShowArgs), + /// Disconnect a client. + Kill(KillArgs), +} + +#[derive(Args, Debug)] +struct ShowArgs { + #[clap(subcommand)] + cmd: ShowCmd, +} + +#[derive(Subcommand, Debug)] +enum ShowCmd { + /// Show all clients. + All, + /// Show a client with a given ID. + Id(ShowIdArgs), + /// Interactively select a window and show information about its client. + SelectWindow, +} + +#[derive(Args, Debug)] +struct ShowIdArgs { + /// The ID of the client. + id: u64, +} + +#[derive(Args, Debug)] +struct KillArgs { + #[clap(subcommand)] + cmd: KillCmd, +} + +#[derive(Subcommand, Debug)] +enum KillCmd { + /// Kill the client with a given ID. + Id(KillIdArgs), + /// Interactively select a window and kill its client. + SelectWindow, +} + +#[derive(Args, Debug)] +struct KillIdArgs { + /// The ID of the client. + id: u64, +} + +pub fn main(global: GlobalArgs, clients_args: ClientsArgs) { + with_tool_client(global.log_level.into(), |tc| async move { + let clients = Rc::new(Clients { tc: tc.clone() }); + clients.run(clients_args).await; + }); +} + +struct Clients { + tc: Rc, +} + +impl Clients { + async fn run(&self, args: ClientsArgs) { + let tc = &self.tc; + let comp = tc.jay_compositor().await; + let cmd = args + .cmd + .unwrap_or(ClientsCmd::Show(ShowArgs { cmd: ShowCmd::All })); + match cmd { + ClientsCmd::Show(a) => { + let id = tc.id(); + tc.send(jay_compositor::CreateClientQuery { self_id: comp, id }); + match a.cmd { + ShowCmd::All => { + tc.send(jay_client_query::AddAll { self_id: id }); + } + ShowCmd::Id(a) => { + tc.send(jay_client_query::AddId { + self_id: id, + id: a.id, + }); + } + ShowCmd::SelectWindow => { + let client_id = tc.select_toplevel_client().await; + if client_id == 0 { + fatal!("Did not select a window"); + } + tc.send(jay_client_query::AddId { + self_id: id, + id: client_id, + }); + } + } + tc.send(jay_client_query::Execute { self_id: id }); + let clients = handle_client_query(tc, id).await; + let mut clients = clients.values().collect::>(); + clients.sort_by_key(|c| c.id); + let mut prefix = " ".to_string(); + let mut printer = ClientPrinter { + prefix: &mut prefix, + }; + for client in clients { + println!("- client:"); + printer.print_client(client); + } + } + ClientsCmd::Kill(a) => match a.cmd { + KillCmd::Id(id) => { + tc.send(jay_compositor::KillClient { + self_id: comp, + id: id.id, + }); + } + KillCmd::SelectWindow => { + let client_id = tc.select_toplevel_client().await; + if client_id == 0 { + fatal!("Did not select a window"); + } + tc.send(jay_compositor::KillClient { + self_id: comp, + id: client_id, + }); + } + }, + } + tc.round_trip().await; + } +} + +#[derive(Default)] +pub struct Client { + pub id: u64, + pub sandboxed: bool, + pub sandbox_engine: Option, + pub sandbox_app_id: Option, + pub sandbox_instance_id: Option, + pub uid: Option, + pub pid: Option, + pub is_xwayland: bool, + pub comm: Option, + pub exe: Option, +} + +pub async fn handle_client_query( + tl: &Rc, + id: JayClientQueryId, +) -> AHashMap { + use jay_client_query::*; + let c = Rc::new(RefCell::new(Vec::::new())); + macro_rules! last { + ($c:ident) => { + $c.borrow_mut().last_mut().unwrap() + }; + } + Start::handle(tl, id, c.clone(), |c, event| { + c.borrow_mut().push(Client::default()); + last!(c).id = event.id; + }); + Sandboxed::handle(tl, id, c.clone(), |c, _event| { + last!(c).sandboxed = true; + }); + SandboxEngine::handle(tl, id, c.clone(), |c, event| { + last!(c).sandbox_engine = Some(event.engine.to_string()); + }); + SandboxAppId::handle(tl, id, c.clone(), |c, event| { + last!(c).sandbox_app_id = Some(event.app_id.to_string()); + }); + SandboxInstanceId::handle(tl, id, c.clone(), |c, event| { + last!(c).sandbox_instance_id = Some(event.instance_id.to_string()); + }); + Uid::handle(tl, id, c.clone(), |c, event| { + last!(c).uid = Some(event.uid); + }); + Pid::handle(tl, id, c.clone(), |c, event| { + last!(c).pid = Some(event.pid); + }); + IsXwayland::handle(tl, id, c.clone(), |c, _event| { + last!(c).is_xwayland = true; + }); + Comm::handle(tl, id, c.clone(), |c, event| { + last!(c).comm = Some(event.comm.to_string()); + }); + Exe::handle(tl, id, c.clone(), |c, event| { + last!(c).exe = Some(event.exe.to_string()); + }); + tl.round_trip().await; + mem::take(&mut *c.borrow_mut()) + .into_iter() + .map(|c| (c.id, c)) + .collect() +} + +pub struct ClientPrinter<'a> { + pub prefix: &'a mut String, +} + +impl ClientPrinter<'_> { + pub fn print_client(&mut self, c: &Client) { + let p = &self.prefix; + macro_rules! opt { + ($field:ident, $pretty:expr) => { + if let Some(v) = &c.$field { + println!("{p}{}: {}", $pretty, v); + } + }; + } + macro_rules! bol { + ($field:ident, $pretty:expr) => { + if c.$field { + println!("{p}{}", $pretty); + } + }; + } + println!("{p}id: {}", c.id); + bol!(sandboxed, "sandboxed"); + opt!(sandbox_engine, "sandbox engine"); + opt!(sandbox_app_id, "sandbox app id"); + opt!(sandbox_instance_id, "sandbox instance id"); + opt!(uid, "uid"); + opt!(pid, "pid"); + bol!(is_xwayland, "xwayland"); + opt!(comm, "comm"); + opt!(exe, "exe"); + } +} diff --git a/src/ifs.rs b/src/ifs.rs index 9e202ba3..0fb8d916 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -10,6 +10,7 @@ pub mod ext_output_image_capture_source_manager_v1; pub mod ext_session_lock_manager_v1; pub mod ext_session_lock_v1; pub mod ipc; +pub mod jay_client_query; pub mod jay_color_management; pub mod jay_compositor; pub mod jay_damage_tracking; diff --git a/src/ifs/jay_client_query.rs b/src/ifs/jay_client_query.rs new file mode 100644 index 00000000..2a20e109 --- /dev/null +++ b/src/ifs/jay_client_query.rs @@ -0,0 +1,141 @@ +use { + crate::{ + client::{Client, ClientError, ClientId}, + leaks::Tracker, + object::{Object, Version}, + utils::copyhashmap::CopyHashMap, + wire::{ + JayClientQueryId, + jay_client_query::{ + AddAll, AddId, Comm, Destroy, Done, End, Exe, Execute, IsXwayland, + JayClientQueryRequestHandler, Pid, SandboxAppId, SandboxEngine, SandboxInstanceId, + Sandboxed, Start, Uid, + }, + }, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub struct JayClientQuery { + pub id: JayClientQueryId, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, + ids: CopyHashMap, + all: Cell, +} + +impl JayClientQuery { + pub fn new(client: &Rc, id: JayClientQueryId, version: Version) -> Self { + Self { + id, + client: client.clone(), + tracker: Default::default(), + version, + ids: Default::default(), + all: Cell::new(false), + } + } +} + +impl JayClientQueryRequestHandler for JayClientQuery { + type Error = JayClientQueryError; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + Ok(()) + } + + fn execute(&self, _req: Execute, _slf: &Rc) -> Result<(), Self::Error> { + let handle_client = |client: &Rc| { + self.client.event(Start { + self_id: self.id, + id: client.id.raw(), + }); + if !client.is_xwayland { + self.client.event(Uid { + self_id: self.id, + uid: client.pid_info.uid, + }); + self.client.event(Pid { + self_id: self.id, + pid: client.pid_info.pid, + }); + self.client.event(Comm { + self_id: self.id, + comm: &client.pid_info.comm, + }); + self.client.event(Exe { + self_id: self.id, + exe: &client.pid_info.exe, + }); + } + if client.acceptor.sandboxed { + self.client.event(Sandboxed { self_id: self.id }); + } + if client.is_xwayland { + self.client.event(IsXwayland { self_id: self.id }); + } + if let Some(engine) = &client.acceptor.sandbox_engine { + self.client.event(SandboxEngine { + self_id: self.id, + engine, + }); + } + if let Some(app_id) = &client.acceptor.app_id { + self.client.event(SandboxAppId { + self_id: self.id, + app_id, + }); + } + if let Some(instance_id) = &client.acceptor.instance_id { + self.client.event(SandboxInstanceId { + self_id: self.id, + instance_id, + }); + } + self.client.event(End { self_id: self.id }); + }; + if self.all.get() { + for client in self.client.state.clients.clients.borrow().values() { + handle_client(&client.data); + } + } else { + for &id in self.ids.lock().keys() { + let Ok(client) = self.client.state.clients.get(id) else { + continue; + }; + handle_client(&client); + } + } + self.client.event(Done { self_id: self.id }); + Ok(()) + } + + fn add_all(&self, _req: AddAll, _slf: &Rc) -> Result<(), Self::Error> { + self.all.set(true); + Ok(()) + } + + fn add_id(&self, req: AddId, _slf: &Rc) -> Result<(), Self::Error> { + self.ids.set(ClientId::from_raw(req.id), ()); + Ok(()) + } +} + +object_base! { + self = JayClientQuery; + version = self.version; +} + +impl Object for JayClientQuery {} + +simple_add_obj!(JayClientQuery); + +#[derive(Debug, Error)] +pub enum JayClientQueryError { + #[error(transparent)] + ClientError(Box), +} +efrom!(JayClientQueryError, ClientError); diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 703aea33..b8372e89 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -1,9 +1,10 @@ use { crate::{ cli::CliLogLevel, - client::{CAP_JAY_COMPOSITOR, Client, ClientCaps, ClientError}, + client::{CAP_JAY_COMPOSITOR, Client, ClientCaps, ClientError, ClientId}, globals::{Global, GlobalName}, ifs::{ + jay_client_query::JayClientQuery, jay_color_management::JayColorManagement, jay_ei_session_builder::JayEiSessionBuilder, jay_idle::JayIdle, @@ -26,7 +27,10 @@ use { object::{Object, Version}, screenshoter::take_screenshot, utils::{errorfmt::ErrorFmt, toplevel_identifier::ToplevelIdentifier}, - wire::{JayCompositorId, JayScreenshotId, jay_compositor::*}, + wire::{ + JayCompositorId, JayScreenshotId, + jay_compositor::{self, *}, + }, }, bstr::ByteSlice, log::Level, @@ -74,7 +78,7 @@ impl Global for JayCompositorGlobal { } fn version(&self) -> u32 { - 17 + 18 } fn required_caps(&self) -> ClientCaps { @@ -223,7 +227,7 @@ impl JayCompositorRequestHandler for JayCompositor { } fn get_client_id(&self, _req: GetClientId, _slf: &Rc) -> Result<(), Self::Error> { - self.client.event(ClientId { + self.client.event(jay_compositor::ClientId { self_id: self.id, client_id: self.client.id.raw(), }); @@ -367,7 +371,6 @@ impl JayCompositorRequestHandler for JayCompositor { } fn select_toplevel(&self, req: SelectToplevel, _slf: &Rc) -> Result<(), Self::Error> { - let seat = self.client.lookup(req.seat)?; let obj = JaySelectToplevel::new(&self.client, req.id, self.version); track!(self.client, obj); self.client.add_client_obj(&obj)?; @@ -375,12 +378,22 @@ impl JayCompositorRequestHandler for JayCompositor { tl: Default::default(), jst: obj.clone(), }; - seat.global.select_toplevel(selector); + let seat = if req.seat.is_none() { + match self.client.state.seat_queue.last() { + Some(s) => s.deref().clone(), + None => { + obj.done(None); + return Ok(()); + } + } + } else { + self.client.lookup(req.seat)?.global.clone() + }; + seat.select_toplevel(selector); Ok(()) } fn select_workspace(&self, req: SelectWorkspace, _slf: &Rc) -> Result<(), Self::Error> { - let seat = self.client.lookup(req.seat)?; let obj = Rc::new(JaySelectWorkspace { id: req.id, client: self.client.clone(), @@ -393,7 +406,15 @@ impl JayCompositorRequestHandler for JayCompositor { ws: Default::default(), jsw: obj.clone(), }; - seat.global.select_workspace(selector); + let seat = if req.seat.is_none() { + match self.client.state.seat_queue.last() { + Some(s) => s.deref().clone(), + None => return Ok(()), + } + } else { + self.client.lookup(req.seat)?.global.clone() + }; + seat.select_workspace(selector); Ok(()) } @@ -470,6 +491,22 @@ impl JayCompositorRequestHandler for JayCompositor { self.client.add_client_obj(&obj)?; Ok(()) } + + fn create_client_query( + &self, + req: CreateClientQuery, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let obj = Rc::new(JayClientQuery::new(&self.client, req.id, self.version)); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + Ok(()) + } + + fn kill_client(&self, req: KillClient, _slf: &Rc) -> Result<(), Self::Error> { + self.client.state.clients.kill(ClientId::from_raw(req.id)); + Ok(()) + } } object_base! { diff --git a/src/ifs/jay_select_toplevel.rs b/src/ifs/jay_select_toplevel.rs index 5fb9dec2..9a003b69 100644 --- a/src/ifs/jay_select_toplevel.rs +++ b/src/ifs/jay_select_toplevel.rs @@ -2,7 +2,7 @@ use { crate::{ client::{Client, ClientError}, ifs::{ - jay_toplevel::{ID_SINCE, JayToplevel}, + jay_toplevel::{CLIENT_ID_SINCE, ID_SINCE, JayToplevel}, wl_seat::ToplevelSelector, }, leaks::Tracker, @@ -78,6 +78,9 @@ impl JaySelectToplevel { self.send_done(jtl.id); if jtl.version >= ID_SINCE { jtl.send_id(); + if jtl.version >= CLIENT_ID_SINCE { + jtl.send_client_id(); + } jtl.send_done(); } } diff --git a/src/ifs/jay_toplevel.rs b/src/ifs/jay_toplevel.rs index 38048dc0..9cb6f426 100644 --- a/src/ifs/jay_toplevel.rs +++ b/src/ifs/jay_toplevel.rs @@ -11,6 +11,7 @@ use { }; pub const ID_SINCE: Version = Version(12); +pub const CLIENT_ID_SINCE: Version = Version(18); pub struct JayToplevel { pub id: JayToplevelId, @@ -47,6 +48,15 @@ impl JayToplevel { }) } + pub fn send_client_id(&self) { + if let Some(cl) = &self.toplevel.tl_data().client { + self.client.event(ClientId { + self_id: self.id, + id: cl.id.raw(), + }) + } + } + pub fn send_done(&self) { self.client.event(Done { self_id: self.id }) } diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 7c75a8b7..09751475 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -24,7 +24,8 @@ use { wheel::{Wheel, WheelError}, wire::{ JayCompositor, JayCompositorId, JayDamageTracking, JayDamageTrackingId, WlCallbackId, - WlRegistryId, wl_callback, wl_display, wl_registry, + WlRegistryId, WlSeatId, jay_compositor, jay_select_toplevel, jay_toplevel, wl_callback, + wl_display, wl_registry, }, }, ahash::AHashMap, @@ -63,8 +64,8 @@ pub enum ToolClientError { UnalignedMessage, #[error(transparent)] BufFdError(#[from] BufFdError), - #[error("The size of the message is not a multiple of 4")] - Parsing(&'static str, MsgParserError), + #[error("Could not parse a message of type {}", .0)] + Parsing(&'static str, #[source] MsgParserError), #[error("Could not read from the compositor")] Read(#[source] BufFdError), #[error("Could not write to the compositor")] @@ -195,6 +196,7 @@ impl ToolClient { fatal!("The compositor returned a fatal error: {}", val.message); }); wl_display::DeleteId::handle(&slf, WL_DISPLAY_ID, slf.clone(), |tc, val| { + tc.handlers.borrow_mut().remove(&ObjectId::from_raw(val.id)); tc.obj_ids.borrow_mut().release(val.id); }); slf.incoming.set(Some( @@ -332,7 +334,7 @@ impl ToolClient { self_id: s.registry, name: s.jay_compositor.0, interface: JayCompositor.name(), - version: s.jay_compositor.1.min(17), + version: s.jay_compositor.1.min(18), id: id.into(), }); self.jay_compositor.set(Some(id)); @@ -359,6 +361,41 @@ impl ToolClient { self.jay_damage_tracking.set(Some(Some(id))); Some(id) } + + pub async fn select_toplevel_client(self: &Rc) -> u64 { + let id = self.id(); + self.send(jay_compositor::SelectToplevel { + self_id: self.jay_compositor().await, + id, + seat: WlSeatId::NONE, + }); + let ae = Rc::new(AsyncEvent::default()); + let client_id = Rc::new(Cell::new(0)); + jay_select_toplevel::Done::handle( + self, + id, + (self.clone(), ae.clone(), client_id.clone()), + |(tc, ae, client_id), event| { + if event.id.is_some() { + jay_toplevel::ClientId::handle( + tc, + event.id, + client_id.clone(), + |client_id, event| { + client_id.set(event.id); + }, + ); + jay_toplevel::Done::handle(tc, event.id, ae.clone(), |ae, _event| { + ae.trigger(); + }); + } else { + ae.trigger(); + } + }, + ); + ae.triggered().await; + client_id.get() + } } pub struct Singletons { diff --git a/src/wl_usr/usr_ifs/usr_jay_toplevel.rs b/src/wl_usr/usr_ifs/usr_jay_toplevel.rs index 99ad10f6..bb3b172d 100644 --- a/src/wl_usr/usr_ifs/usr_jay_toplevel.rs +++ b/src/wl_usr/usr_ifs/usr_jay_toplevel.rs @@ -36,6 +36,10 @@ impl JayToplevelEventHandler for UsrJayToplevel { Ok(()) } + fn client_id(&self, _ev: ClientId, _slf: &Rc) -> Result<(), Self::Error> { + Ok(()) + } + fn done(&self, _ev: Done, slf: &Rc) -> Result<(), Self::Error> { if let Some(owner) = self.owner.get() { owner.done(slf); diff --git a/wire/jay_client_query.txt b/wire/jay_client_query.txt new file mode 100644 index 00000000..ef841292 --- /dev/null +++ b/wire/jay_client_query.txt @@ -0,0 +1,49 @@ +request destroy { } + +request execute { } + +request add_all { } + +request add_id { + id: pod(u64), +} + +event done { } + +event start { + id: pod(u64), +} + +event end { } + +event sandboxed { } + +event sandbox_engine { + engine: str, +} + +event sandbox_app_id { + app_id: str, +} + +event sandbox_instance_id { + instance_id: str, +} + +event uid { + uid: pod(uapi::c::uid_t), +} + +event pid { + pid: pod(uapi::c::pid_t), +} + +event is_xwayland { } + +event comm { + comm: str, +} + +event exe { + exe: str, +} diff --git a/wire/jay_compositor.txt b/wire/jay_compositor.txt index 24ec05b4..cecbe1f0 100644 --- a/wire/jay_compositor.txt +++ b/wire/jay_compositor.txt @@ -109,6 +109,14 @@ request reexec (since = 17) { id: id(jay_reexec), } +request create_client_query (since = 18) { + id: id(jay_client_query), +} + +request kill_client (since = 18) { + id: pod(u64), +} + # events event client_id { diff --git a/wire/jay_toplevel.txt b/wire/jay_toplevel.txt index 2cb50c06..eda692f1 100644 --- a/wire/jay_toplevel.txt +++ b/wire/jay_toplevel.txt @@ -8,5 +8,9 @@ event id (since = 12) { id: str, } +event client_id (since = 18) { + id: pod(u64), +} + event done (since = 12) { }