From f92c092acc1cf6bb7b7b131f8e996c57471a77d2 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Mon, 8 Jun 2026 19:56:17 -0400 Subject: [PATCH] add unix socket ipc --- src/cli.rs | 6 +- src/cli/clients.rs | 68 ++- src/cli/dpms.rs | 8 + src/cli/randr.rs | 393 +++++++++++++++++ src/cli/workspaces.rs | 96 +++++ src/compositor.rs | 19 + src/ipc.rs | 959 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/state.rs | 2 + 9 files changed, 1550 insertions(+), 2 deletions(-) create mode 100644 src/cli/workspaces.rs create mode 100644 src/ipc.rs diff --git a/src/cli.rs b/src/cli.rs index f641c157..df3d10ce 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -20,6 +20,7 @@ mod set_log_level; mod tree; mod unlock; mod version; +mod workspaces; mod xwayland; use jay_pr_caps::drop_all_pr_caps; @@ -30,7 +31,7 @@ use { clients::ClientsArgs, color_management::ColorManagementArgs, config::ConfigArgs, damage_tracking::DamageTrackingArgs, idle::IdleCmd, input::InputArgs, json::VERBOSE_JSON, randr::RandrArgs, reexec::ReexecArgs, tree::TreeArgs, - xwayland::XwaylandArgs, + workspaces::WorkspacesArgs, xwayland::XwaylandArgs, }, compositor::start_compositor, @@ -108,6 +109,8 @@ pub enum Cmd { Reexec(ReexecArgs), /// Inspect/manipulate the connected clients. Clients(ClientsArgs), + /// Inspect/manipulate workspaces. + Workspaces(WorkspacesArgs), /// Inspect the surface tree. Tree(TreeArgs), /// Prints the Jay version and exits. @@ -255,6 +258,7 @@ pub fn main() { 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), + Cmd::Workspaces(a) => workspaces::main(cli.global, a), Cmd::Tree(a) => tree::main(cli.global, a), Cmd::Version => version::main(cli.global), Cmd::Pid => pid::main(cli.global), diff --git a/src/cli/clients.rs b/src/cli/clients.rs index 393c2ecd..1c3b04af 100644 --- a/src/cli/clients.rs +++ b/src/cli/clients.rs @@ -4,7 +4,9 @@ use { GlobalArgs, json::{JsonClient, jsonl}, }, + ipc::{self, Client as IpcClient}, tools::tool_client::{Handle, ToolClient, with_tool_client}, + utils::errorfmt::ErrorFmt, wire::{JayClientQueryId, jay_client_query, jay_compositor}, }, ahash::AHashMap, @@ -83,10 +85,13 @@ struct Clients { impl Clients { async fn run(&self, global: &GlobalArgs, args: ClientsArgs) { let tc = &self.tc; - let comp = tc.jay_compositor().await; let cmd = args .cmd .unwrap_or(ClientsCmd::Show(ShowArgs { cmd: ShowCmd::All })); + if self.run_ipc(global, &cmd) { + return; + } + let comp = tc.jay_compositor().await; match cmd { ClientsCmd::Show(a) => { let id = tc.id(); @@ -153,6 +158,50 @@ impl Clients { } tc.round_trip().await; } + + fn run_ipc(&self, global: &GlobalArgs, cmd: &ClientsCmd) -> bool { + match cmd { + ClientsCmd::Show(a) => { + let id = match &a.cmd { + ShowCmd::All => None, + ShowCmd::Id(a) => Some(a.id), + ShowCmd::SelectWindow => return false, + }; + let clients: Vec = match ipc::request(&ipc::Request::ClientsGet { id }) { + Ok(clients) => clients, + Err(e) if e.can_fallback() => return false, + Err(e) => fatal!("Could not query clients over IPC: {}", ErrorFmt(e)), + }; + let mut clients = clients.into_iter().map(Client::from).collect::>(); + clients.sort_by_key(|c| c.id); + if global.json { + for client in &clients { + jsonl(&make_json_client(client)); + } + } else { + let mut prefix = " ".to_string(); + let mut printer = ClientPrinter { + prefix: &mut prefix, + }; + for client in &clients { + println!("- client:"); + printer.print_client(client); + } + } + true + } + ClientsCmd::Kill(a) => match &a.cmd { + KillCmd::Id(id) => { + match ipc::request_unit(&ipc::Request::ClientsKill { id: id.id }) { + Ok(()) => true, + Err(e) if e.can_fallback() => false, + Err(e) => fatal!("Could not kill client over IPC: {}", ErrorFmt(e)), + } + } + KillCmd::SelectWindow => false, + }, + } + } } #[derive(Default)] @@ -169,6 +218,23 @@ pub struct Client { pub exe: Option, } +impl From for Client { + fn from(client: IpcClient) -> Self { + Self { + id: client.client_id, + sandboxed: client.sandboxed, + sandbox_engine: client.sandbox_engine, + sandbox_app_id: client.sandbox_app_id, + sandbox_instance_id: client.sandbox_instance_id, + uid: client.uid, + pid: client.pid, + is_xwayland: client.is_xwayland, + comm: client.comm, + exe: client.exe, + } + } +} + pub async fn handle_client_query( tl: &Rc, id: JayClientQueryId, diff --git a/src/cli/dpms.rs b/src/cli/dpms.rs index ec8fe577..27f87b10 100644 --- a/src/cli/dpms.rs +++ b/src/cli/dpms.rs @@ -1,13 +1,21 @@ use { crate::{ cli::{DpmsArgs, DpmsState, GlobalArgs}, + ipc, tools::tool_client::{ToolClient, with_tool_client}, + utils::errorfmt::ErrorFmt, wire::jay_compositor::SetDpms, }, std::rc::Rc, }; pub fn main(global: GlobalArgs, args: DpmsArgs) { + let active = args.state == DpmsState::On; + match ipc::request_unit(&ipc::Request::DpmsSet { active }) { + Ok(()) => return, + Err(e) if e.can_fallback() => {} + Err(e) => fatal!("Could not set DPMS state over IPC: {}", ErrorFmt(e)), + } with_tool_client(global.log_level, |tc| async move { run(tc, args).await; }); diff --git a/src/cli/randr.rs b/src/cli/randr.rs index ba56ff74..d04ef679 100644 --- a/src/cli/randr.rs +++ b/src/cli/randr.rs @@ -13,6 +13,7 @@ use { cmm::cmm_primaries::Primaries, format::{Format, XRGB8888}, ifs::wl_output::BlendSpace, + ipc, tools::tool_client::{Handle, ToolClient, with_tool_client}, tree::Transform, utils::{errorfmt::ErrorFmt, ordered_float::F64, static_text::StaticText}, @@ -501,12 +502,97 @@ pub struct RemoveVirtualOutputArgs { } pub fn main(global: GlobalArgs, args: RandrArgs) { + if try_ipc(&global, &args) { + return; + } with_tool_client(global.log_level, |tc| async move { let idle = Rc::new(Randr { tc: tc.clone() }); idle.run(&global, args).await; }); } +fn try_ipc(global: &GlobalArgs, args: &RandrArgs) -> bool { + match &args.command { + None => try_ipc_show(global, &ShowArgs::default()), + Some(RandrCmd::Show(args)) => try_ipc_show(global, args), + Some(RandrCmd::Output(args)) => try_ipc_output(args), + Some(RandrCmd::Card(_)) | Some(RandrCmd::VirtualOutput(_)) => false, + } +} + +fn try_ipc_show(global: &GlobalArgs, args: &ShowArgs) -> bool { + let data: ipc::Outputs = match ipc::request(&ipc::Request::OutputsGet) { + Ok(data) => data, + Err(e) if e.can_fallback() => return false, + Err(e) => fatal!("Could not query outputs over IPC: {}", ErrorFmt(e)), + }; + if global.json { + show_ipc_json(&data); + } else { + show_ipc_text(&data, args); + } + true +} + +fn try_ipc_output(args: &OutputArgs) -> bool { + let request = match &args.command { + OutputCommand::Transform(transform) => ipc::Request::OutputsSetTransform { + output: args.output.clone(), + transform: transform_cmd_text(&transform.command).to_string(), + }, + OutputCommand::Scale(scale) => ipc::Request::OutputsSetScale { + output: args.output.clone(), + scale: scale.scale, + round_to_float: scale.round_to_float, + }, + OutputCommand::Mode(mode) => ipc::Request::OutputsSetMode { + output: args.output.clone(), + width: mode.width, + height: mode.height, + refresh_rate: mode.refresh_rate, + }, + OutputCommand::Position(position) => ipc::Request::OutputsSetPosition { + output: args.output.clone(), + x: position.x, + y: position.y, + }, + OutputCommand::Enable => ipc::Request::OutputsSetEnabled { + output: args.output.clone(), + enabled: true, + }, + OutputCommand::Disable => ipc::Request::OutputsSetEnabled { + output: args.output.clone(), + enabled: false, + }, + OutputCommand::NonDesktop(_) + | OutputCommand::Vrr(_) + | OutputCommand::Tearing(_) + | OutputCommand::Format(_) + | OutputCommand::Colors(_) + | OutputCommand::Brightness(_) + | OutputCommand::BlendSpace(_) + | OutputCommand::UseNativeGamut(_) => return false, + }; + match ipc::request_unit(&request) { + Ok(()) => true, + Err(e) if e.can_fallback() => false, + Err(e) => fatal!("Could not modify output over IPC: {}", ErrorFmt(e)), + } +} + +fn transform_cmd_text(cmd: &TransformCmd) -> &'static str { + match cmd { + TransformCmd::None => "none", + TransformCmd::Rotate90 => "rotate-90", + TransformCmd::Rotate180 => "rotate-180", + TransformCmd::Rotate270 => "rotate-270", + TransformCmd::Flip => "flip", + TransformCmd::FlipRotate90 => "flip-rotate-90", + TransformCmd::FlipRotate180 => "flip-rotate-180", + TransformCmd::FlipRotate270 => "flip-rotate-270", + } +} + #[derive(Clone, Debug)] struct Device { pub id: u64, @@ -1474,3 +1560,310 @@ fn make_json_connector(c: &Connector) -> JsonConnector<'_> { output, } } + +fn show_ipc_json(data: &ipc::Outputs) { + let json = JsonRandrData { + drm_devices: data.drm_devices.iter().map(make_ipc_json_device).collect(), + unbound_connectors: data + .unbound_connectors + .iter() + .map(make_ipc_json_connector) + .collect(), + }; + jsonl(&json); +} + +fn make_ipc_json_device(dev: &ipc::DrmDevice) -> JsonDrmDevice<'_> { + JsonDrmDevice { + devnode: &dev.devnode, + syspath: &dev.syspath, + vendor: dev.vendor, + vendor_name: &dev.vendor_name, + model: dev.model, + model_name: &dev.model_name, + gfx_api: &dev.gfx_api, + render_device: dev.render_device, + connectors: dev.connectors.iter().map(make_ipc_json_connector).collect(), + } +} + +fn make_ipc_json_connector(c: &ipc::Connector) -> JsonConnector<'_> { + let output = c.output.as_ref().map(|o| { + let modes = o + .modes + .iter() + .map(|m| JsonMode { + width: m.width, + height: m.height, + refresh_rate_millihz: m.refresh_rate_millihz, + current: m.current, + }) + .collect(); + let formats = o.formats.iter().map(|f| f.as_str()).collect(); + JsonOutput { + product: &o.product, + manufacturer: &o.manufacturer, + serial_number: &o.serial_number, + width_mm: o.width_mm, + height_mm: o.height_mm, + non_desktop: o.non_desktop, + scale: o.scale, + x: o.x, + y: o.y, + width: o.width, + height: o.height, + transform: ipc_transform(&o.transform).text(), + mode: o.mode.map(|m| JsonMode { + width: m.width, + height: m.height, + refresh_rate_millihz: m.refresh_rate_millihz, + current: m.current, + }), + format: o.format.as_deref(), + vrr_capable: o.vrr_capable, + vrr_enabled: o.vrr_enabled, + vrr_mode: JsonVrrMode(VrrMode(o.vrr_mode)), + vrr_cursor_hz: o.vrr_cursor_hz, + tearing_mode: JsonTearingMode(TearingMode(o.tearing_mode)), + flip_margin_ns: o.flip_margin_ns, + supported_color_spaces: o + .supported_color_spaces + .iter() + .map(|s| s.as_str()) + .collect(), + current_color_space: o.current_color_space.as_deref(), + supported_eotfs: o.supported_eotfs.iter().map(|s| s.as_str()).collect(), + current_eotf: o.current_eotf.as_deref(), + min_brightness: o.min_brightness, + max_brightness: o.max_brightness, + brightness: o.brightness, + blend_space: o.blend_space.as_deref(), + native_gamut: o.native_gamut.as_ref().map(|p| JsonPrimaries { + r_x: p.r_x, + r_y: p.r_y, + g_x: p.g_x, + g_y: p.g_y, + b_x: p.b_x, + b_y: p.b_y, + w_x: p.w_x, + w_y: p.w_y, + }), + use_native_gamut: o.use_native_gamut, + arbitrary_modes: o.arbitrary_modes, + modes, + formats, + } + }); + JsonConnector { + name: &c.name, + enabled: c.enabled, + output, + } +} + +fn show_ipc_text(data: &ipc::Outputs, args: &ShowArgs) { + if data.drm_devices.is_not_empty() { + println!("drm devices:"); + } + for dev in &data.drm_devices { + print_ipc_drm_device(dev); + println!(" connectors:"); + for connector in &dev.connectors { + print_ipc_connector(connector, args.modes, args.formats); + } + } + if data.unbound_connectors.is_not_empty() { + println!("unbound connectors:"); + for connector in &data.unbound_connectors { + print_ipc_connector(connector, args.modes, args.formats); + } + } +} + +fn print_ipc_drm_device(dev: &ipc::DrmDevice) { + println!(" {}:", dev.devnode); + println!(" model: {} {}", dev.vendor_name, dev.model_name); + println!(" pci-id: {:x}:{:x}", dev.vendor, dev.model); + println!(" syspath: {}", dev.syspath); + println!(" api: {}", dev.gfx_api); + if dev.render_device { + println!(" primary device"); + } +} + +fn print_ipc_connector(connector: &ipc::Connector, modes: bool, formats: bool) { + println!(" {}:", connector.name); + if !connector.enabled { + println!(" disabled"); + } + let Some(o) = &connector.output else { + if connector.enabled { + println!(" disconnected"); + } + return; + }; + println!(" product: {}", o.product); + println!(" manufacturer: {}", o.manufacturer); + println!(" serial number: {}", o.serial_number); + println!( + " physical size: {}mm x {}mm", + o.width_mm, o.height_mm + ); + if o.non_desktop { + if connector.enabled { + println!(" non-desktop"); + } + return; + } + println!(" VRR capable: {}", o.vrr_capable); + if o.vrr_capable { + println!(" VRR enabled: {}", o.vrr_enabled); + println!(" VRR mode: {}", vrr_mode_text(o.vrr_mode)); + if let Some(hz) = o.vrr_cursor_hz { + println!(" VRR cursor hz: {}", hz); + } + } + println!( + " Tearing mode: {}", + tearing_mode_text(o.tearing_mode) + ); + println!(" position: {} x {}", o.x, o.y); + println!(" logical size: {} x {}", o.width, o.height); + if let Some(mode) = &o.mode { + println!(" mode: {}", mode_text(mode)); + } + if let Some(format) = &o.format + && format != XRGB8888.name + { + println!(" format: {format}"); + } + if o.scale != 1.0 { + println!(" scale: {}", o.scale); + } + if o.transform != "none" { + println!(" transform: {}", o.transform); + } + if let Some(flip_margin_ns) = o.flip_margin_ns { + println!( + " flip margin: {:?}", + Duration::from_nanos(flip_margin_ns) + ); + } + if o.supported_color_spaces.is_not_empty() { + println!(" color spaces:"); + print_current_list("default", o.current_color_space.as_deref()); + for cs in &o.supported_color_spaces { + print_current_list(cs, o.current_color_space.as_deref()); + } + } + if o.supported_eotfs.is_not_empty() { + println!(" eotfs:"); + print_current_list("default", o.current_eotf.as_deref()); + for eotf in &o.supported_eotfs { + print_current_list(eotf, o.current_eotf.as_deref()); + } + } + match (o.min_brightness, o.max_brightness) { + (Some(min), Some(max)) => { + println!(" min brightness: {:>10.4} cd/m^2", min); + println!(" max brightness: {:>10.4} cd/m^2", max); + } + _ => println!(" max brightness: {:>10.4} cd/m^2 (implied)", 80.0), + } + if let Some(lux) = o.brightness { + println!(" brightness: {:>10.4} cd/m^2", lux); + } + if let Some(bs) = &o.blend_space { + println!(" blend space: {bs}"); + } + if let Some(p) = &o.native_gamut { + println!( + " native gamut:{}", + fmt::from_fn(|f| { + if o.use_native_gamut { + f.write_str(" (used for default color space)")?; + } + Ok(()) + }), + ); + println!( + " red: {:.6} {:.6} green: {:.6} {:.6}", + p.r_x, p.r_y, p.g_x, p.g_y + ); + println!( + " blue: {:.6} {:.6} white: {:.6} {:.6}", + p.b_x, p.b_y, p.w_x, p.w_y + ); + } + if o.arbitrary_modes { + println!(" supports arbitrary modes"); + } + if o.modes.is_not_empty() && modes { + println!(" modes:"); + for mode in &o.modes { + print!(" {}", mode_text(mode)); + if mode.current { + print!(" (current)"); + } + println!(); + } + } + if o.formats.is_not_empty() && formats { + println!(" formats:"); + for format in &o.formats { + println!(" {format}"); + } + } +} + +fn print_current_list(value: &str, current: Option<&str>) { + let current = match current == Some(value) { + true => " (current)", + false => "", + }; + println!(" {value}{current}"); +} + +fn mode_text(mode: &ipc::Mode) -> String { + format!( + "{} x {} @ {}", + mode.width, + mode.height, + mode.refresh_rate_millihz as f64 / 1000.0 + ) +} + +fn vrr_mode_text(mode: u32) -> String { + match VrrMode(mode) { + VrrMode::NEVER => "never".to_string(), + VrrMode::ALWAYS => "always".to_string(), + VrrMode::VARIANT_1 => "variant1".to_string(), + VrrMode::VARIANT_2 => "variant2".to_string(), + VrrMode::VARIANT_3 => "variant3".to_string(), + _ => format!("unknown ({mode})"), + } +} + +fn tearing_mode_text(mode: u32) -> String { + match TearingMode(mode) { + TearingMode::NEVER => "never".to_string(), + TearingMode::ALWAYS => "always".to_string(), + TearingMode::VARIANT_1 => "variant1".to_string(), + TearingMode::VARIANT_2 => "variant2".to_string(), + TearingMode::VARIANT_3 => "variant3".to_string(), + _ => format!("unknown ({mode})"), + } +} + +fn ipc_transform(transform: &str) -> Transform { + match transform { + "rotate-90" => Transform::Rotate90, + "rotate-180" => Transform::Rotate180, + "rotate-270" => Transform::Rotate270, + "flip" => Transform::Flip, + "flip-rotate-90" => Transform::FlipRotate90, + "flip-rotate-180" => Transform::FlipRotate180, + "flip-rotate-270" => Transform::FlipRotate270, + _ => Transform::None, + } +} diff --git a/src/cli/workspaces.rs b/src/cli/workspaces.rs new file mode 100644 index 00000000..8527f753 --- /dev/null +++ b/src/cli/workspaces.rs @@ -0,0 +1,96 @@ +use { + crate::{ + cli::{GlobalArgs, json::jsonl}, + ipc::{self, Workspace}, + utils::errorfmt::ErrorFmt, + }, + clap::{Args, Subcommand}, +}; + +#[derive(Args, Debug)] +pub struct WorkspacesArgs { + #[clap(subcommand)] + command: Option, +} + +#[derive(Subcommand, Debug)] +enum WorkspacesCmd { + /// Show known workspaces. + List, + /// Show a workspace on an output. + Show(ShowArgs), + /// Create a workspace on an output. + Create(CreateArgs), + /// Move a workspace to an output. + MoveToOutput(MoveToOutputArgs), +} + +#[derive(Args, Debug)] +struct ShowArgs { + /// The workspace name. + name: String, + /// The output to show the workspace on. + #[arg(long)] + output: Option, +} + +#[derive(Args, Debug)] +struct CreateArgs { + /// The workspace name. + name: String, + /// The output to create the workspace on. + output: String, +} + +#[derive(Args, Debug)] +struct MoveToOutputArgs { + /// The workspace name. + name: String, + /// The output to move the workspace to. + output: String, +} + +pub fn main(global: GlobalArgs, args: WorkspacesArgs) { + let cmd = args.command.unwrap_or(WorkspacesCmd::List); + match cmd { + WorkspacesCmd::List => list(global), + WorkspacesCmd::Show(args) => request_unit(ipc::Request::WorkspacesShow { + name: args.name, + output: args.output, + }), + WorkspacesCmd::Create(args) => request_unit(ipc::Request::WorkspacesCreate { + name: args.name, + output: args.output, + }), + WorkspacesCmd::MoveToOutput(args) => request_unit(ipc::Request::WorkspacesMoveToOutput { + name: args.name, + output: args.output, + }), + } +} + +fn list(global: GlobalArgs) { + let workspaces: Vec = match ipc::request(&ipc::Request::WorkspacesGet) { + Ok(workspaces) => workspaces, + Err(e) => fatal!("Could not query workspaces over IPC: {}", ErrorFmt(e)), + }; + if global.json { + jsonl(&workspaces); + return; + } + for workspace in workspaces { + println!("- workspace:"); + println!(" id: {}", workspace.id); + println!(" name: {}", workspace.name); + println!(" output: {}", workspace.output); + if workspace.visible { + println!(" visible"); + } + } +} + +fn request_unit(request: ipc::Request) { + if let Err(e) = ipc::request_unit(&request) { + fatal!("Could not execute workspace command over IPC: {}", ErrorFmt(e)); + } +} diff --git a/src/compositor.rs b/src/compositor.rs index b2979886..47b4feb3 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -55,6 +55,7 @@ use { wlr_output_manager::wlr_output_manager_done, workspace_manager::workspace_manager_done, }, + ipc::{IpcAcceptor, JAY_IPC_SOCKET}, leaks, @@ -310,6 +311,7 @@ fn start_compositor2( display: Default::default(), }, acceptor: Default::default(), + ipc_acceptor: Default::default(), tagged_acceptors: Default::default(), serial: Default::default(), idle_inhibitor_ids: Default::default(), @@ -417,12 +419,29 @@ fn start_compositor2( state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); let (acceptor, _acceptor_future) = Acceptor::install(&state)?; + let mut _ipc_acceptor_future = None; + let ipc_acceptor = match IpcAcceptor::install(&state) { + Ok((acceptor, futures)) => { + _ipc_acceptor_future = Some(futures); + Some(acceptor) + } + Err(e) => { + log::error!("Could not create IPC socket: {}", ErrorFmt(e)); + None + } + }; if let Some(forker) = forker { forker.install(&state); forker.setenv( WAYLAND_DISPLAY.as_bytes(), acceptor.socket_name().as_bytes(), ); + if let Some(ipc_acceptor) = ipc_acceptor { + forker.setenv( + JAY_IPC_SOCKET.as_bytes(), + ipc_acceptor.socket_path().as_bytes(), + ); + } for (key, val) in STATIC_VARS { forker.setenv(key.as_bytes(), val.as_bytes()); } diff --git a/src/ipc.rs b/src/ipc.rs new file mode 100644 index 00000000..04b426ef --- /dev/null +++ b/src/ipc.rs @@ -0,0 +1,959 @@ +use { + crate::{ + backend, + client::ClientId, + compositor::MAX_EXTENTS, + state::{ConnectorData, State}, + tree::{OutputNode, Transform, WsMoveConfig, move_ws_to_output}, + utils::{ + buf::Buf, + errorfmt::ErrorFmt, + numcell::NumCell, + oserror::{OsError, OsErrorExt, OsErrorExt2}, + pid_info::get_socket_creds, + static_text::StaticText, + xrd::xrd, + }, + }, + ahash::AHashMap, + jay_async_engine::SpawnedFuture, + jay_units::scale::Scale, + serde::{Deserialize, Serialize, de::DeserializeOwned}, + serde_json::Value, + std::{ + cell::RefCell, + collections::VecDeque, + io::{BufRead, BufReader, Write}, + os::unix::net::UnixStream, + path::PathBuf, + rc::Rc, + }, + thiserror::Error, + uapi::{OwnedFd, Ustr, Ustring, c, format_ustr}, +}; + +pub const JAY_IPC_SOCKET: &str = "JAY_IPC_SOCKET"; +const PROTOCOL_VERSION: u32 = 1; +const MAX_LINE_LEN: usize = 1024 * 1024; + +const COMMANDS: &[&str] = &[ + "hello", + "outputs.get", + "outputs.set_enabled", + "outputs.set_mode", + "outputs.set_position", + "outputs.set_scale", + "outputs.set_transform", + "workspaces.get", + "workspaces.show", + "workspaces.create", + "workspaces.move_to_output", + "clients.get", + "clients.kill", + "dpms.set", +]; + +#[derive(Debug, Error)] +pub enum IpcAcceptorError { + #[error("XDG_RUNTIME_DIR is not set")] + XrdNotSet, + #[error("XDG_RUNTIME_DIR ({0:?}) is too long to form a unix socket address")] + XrdTooLong(String), + #[error("Could not create an IPC socket")] + SocketFailed(#[source] OsError), + #[error("Could not stat the existing IPC socket")] + SocketStat(#[source] OsError), + #[error("Could not start listening for incoming IPC connections")] + ListenFailed(#[source] OsError), + #[error("Could not open the IPC lock file")] + OpenLockFile(#[source] OsError), + #[error("Could not lock the IPC lock file")] + LockLockFile(#[source] OsError), + #[error("Could not bind the IPC socket to an address")] + BindFailed(#[source] OsError), +} + +pub struct IpcAcceptor { + socket: AllocatedSocket, + next_client_id: NumCell, + clients: RefCell>>, +} + +struct AllocatedSocket { + path: Ustring, + socket: Rc, + lock_path: Ustring, + _lock_fd: OwnedFd, +} + +impl Drop for AllocatedSocket { + fn drop(&mut self) { + let _ = uapi::unlink(&self.path); + let _ = uapi::unlink(&self.lock_path); + } +} + +impl IpcAcceptor { + pub fn install( + state: &Rc, + ) -> Result<(Rc, Vec>), IpcAcceptorError> { + let socket = allocate_socket()?; + log::info!("bound IPC socket {}", socket.path.display()); + uapi::listen(socket.socket.raw(), 128).map_os_err(IpcAcceptorError::ListenFailed)?; + let acc = Rc::new(IpcAcceptor { + socket, + next_client_id: NumCell::new(1), + clients: Default::default(), + }); + let futures = vec![ + state + .eng + .spawn("IPC acceptor", accept(acc.clone(), state.clone())), + ]; + state.ipc_acceptor.set(Some(acc.clone())); + Ok((acc, futures)) + } + + pub fn socket_path(&self) -> &Ustr { + self.socket.path.as_ustr() + } +} + +fn allocate_socket() -> Result { + let xrd = match xrd() { + Some(d) => d, + _ => return Err(IpcAcceptorError::XrdNotSet), + }; + let socket = uapi::socket(c::AF_UNIX, c::SOCK_STREAM | c::SOCK_CLOEXEC, 0) + .map(Rc::new) + .map_os_err(IpcAcceptorError::SocketFailed)?; + let mut addr: c::sockaddr_un = uapi::pod_zeroed(); + addr.sun_family = c::AF_UNIX as _; + let path = format_ustr!("{}/jay-ipc-{}.sock", xrd, uapi::geteuid()); + let lock_path = format_ustr!("{}.lock", path.display()); + if path.len() + 1 > addr.sun_path.len() { + return Err(IpcAcceptorError::XrdTooLong(xrd.to_string())); + } + let lock_fd = uapi::open(&*lock_path, c::O_CREAT | c::O_CLOEXEC | c::O_RDWR, 0o600) + .map_os_err(IpcAcceptorError::OpenLockFile)?; + uapi::flock(lock_fd.raw(), c::LOCK_EX | c::LOCK_NB) + .map_os_err(IpcAcceptorError::LockLockFile)?; + match uapi::lstat(&path).to_os_error() { + Ok(_) => { + log::info!("Unlinking {}", path.display()); + let _ = uapi::unlink(&path); + } + Err(OsError(c::ENOENT)) => {} + Err(e) => return Err(IpcAcceptorError::SocketStat(e)), + } + let sun_path = uapi::as_bytes_mut(&mut addr.sun_path[..]); + sun_path[..path.len()].copy_from_slice(path.as_bytes()); + sun_path[path.len()] = 0; + uapi::bind(socket.raw(), &addr).map_os_err(IpcAcceptorError::BindFailed)?; + Ok(AllocatedSocket { + path, + socket, + lock_path, + _lock_fd: lock_fd, + }) +} + +async fn accept(acc: Rc, state: Rc) { + loop { + let fd = match state.ring.accept(&acc.socket.socket, c::SOCK_CLOEXEC).await { + Ok(fd) => fd, + Err(e) => { + log::error!("Could not accept IPC client: {}", ErrorFmt(e)); + break; + } + }; + if !peer_is_allowed(&fd) { + continue; + } + let id = acc.next_client_id.fetch_add(1); + let future = state + .eng + .spawn("IPC client", handle_client(id, fd, state.clone(), acc.clone())); + acc.clients.borrow_mut().insert(id, future); + } +} + +fn peer_is_allowed(fd: &Rc) -> bool { + match get_socket_creds(fd) { + Some((uid, _)) if uid == uapi::geteuid() => true, + Some((uid, pid)) => { + log::warn!("Rejecting IPC client pid {pid} with uid {uid}"); + false + } + _ => false, + } +} + +async fn handle_client(id: u64, fd: Rc, state: Rc, acc: Rc) { + handle_client_(fd, state).await; + acc.clients.borrow_mut().remove(&id); +} + +async fn handle_client_(fd: Rc, state: Rc) { + let mut pending = Vec::new(); + loop { + let mut bufs = [Buf::new(4096)]; + let mut fds = VecDeque::new(); + let n = match state.ring.recvmsg(&fd, &mut bufs, &mut fds).await { + Ok(0) => return, + Ok(n) => n, + Err(e) => { + log::debug!("Could not read IPC request: {}", ErrorFmt(e)); + return; + } + }; + pending.extend_from_slice(&bufs[0][..n]); + if pending.len() > MAX_LINE_LEN { + let _ = write_response( + &state, + &fd, + &Response::Error { + message: "IPC request is too large".to_string(), + }, + ) + .await; + return; + } + while let Some(pos) = pending.iter().position(|&b| b == b'\n') { + let mut line = pending.drain(..=pos).collect::>(); + while line.last().is_some_and(|b| *b == b'\n' || *b == b'\r') { + line.pop(); + } + if line.is_empty() { + continue; + } + let response = match serde_json::from_slice::(&line) { + Ok(req) => handle_request(&state, req), + Err(e) => Response::Error { + message: format!("Could not parse request: {e}"), + }, + }; + if let Err(e) = write_response(&state, &fd, &response).await { + log::debug!("Could not write IPC response: {}", ErrorFmt(e)); + return; + } + } + } +} + +async fn write_response( + state: &State, + fd: &Rc, + response: &Response, +) -> Result<(), jay_io_uring::IoUringError> { + let mut bytes = serde_json::to_vec(response).unwrap(); + bytes.push(b'\n'); + let mut offset = 0; + while offset < bytes.len() { + let buf = Buf::from_slice(&bytes[offset..]); + let n = state.ring.write(fd, buf, None).await?; + if n == 0 { + return Ok(()); + } + offset += n; + } + Ok(()) +} + +fn handle_request(state: &Rc, req: Request) -> Response { + match handle_request_(state, req) { + Ok(value) => Response::Ok { result: value }, + Err(message) => Response::Error { message }, + } +} + +fn handle_request_(state: &Rc, req: Request) -> Result { + match req { + Request::Hello => to_value(Hello { + protocol_version: PROTOCOL_VERSION, + pid: state.pid, + commands: COMMANDS, + }), + Request::OutputsGet => to_value(outputs(state)), + Request::OutputsSetEnabled { output, enabled } => { + set_output_enabled(state, &output, enabled)?; + Ok(Value::Null) + } + Request::OutputsSetMode { + output, + width, + height, + refresh_rate, + } => { + set_output_mode(state, &output, width, height, refresh_rate)?; + Ok(Value::Null) + } + Request::OutputsSetPosition { output, x, y } => { + set_output_position(state, &output, x, y)?; + Ok(Value::Null) + } + Request::OutputsSetScale { + output, + scale, + round_to_float, + } => { + let output = get_output_node(state, &output)?; + let scale = match round_to_float { + true => Scale::from_f64_as_float(scale), + false => Scale::from_f64(scale), + }; + output.set_preferred_scale(scale); + Ok(Value::Null) + } + Request::OutputsSetTransform { output, transform } => { + let output = get_output_node(state, &output)?; + let transform = parse_transform(&transform)?; + output.update_transform(transform); + Ok(Value::Null) + } + Request::WorkspacesGet => to_value(workspaces(state)), + Request::WorkspacesShow { name, output } => { + let output = match output { + Some(output) => get_output_node(state, &output)?, + None => default_output(state)?, + }; + let ws = output + .find_workspace(&name) + .unwrap_or_else(|| output.create_workspace(&name)); + state.show_workspace2(None, &output, &ws); + Ok(Value::Null) + } + Request::WorkspacesCreate { name, output } => { + let output = get_output_node(state, &output)?; + if output.find_workspace(&name).is_none() { + output.create_workspace(&name); + } + Ok(Value::Null) + } + Request::WorkspacesMoveToOutput { name, output } => { + let ws = get_workspace_by_name(state, &name)?; + let output = get_output_node(state, &output)?; + let link = match &*ws.output_link.borrow() { + Some(link) => link.to_ref(), + None => return Err(format!("Workspace `{name}` is not linked to an output")), + }; + let config = WsMoveConfig { + make_visible_always: false, + make_visible_if_empty: true, + source_is_destroyed: false, + before: None, + }; + move_ws_to_output(&link, &output, config); + ws.desired_output.set(output.global.output_id.clone()); + state.tree_changed(); + Ok(Value::Null) + } + Request::ClientsGet { id } => to_value(clients(state, id)), + Request::ClientsKill { id } => { + state.clients.kill(ClientId::from_raw(id)); + Ok(Value::Null) + } + Request::DpmsSet { active } => { + state + .set_dpms_active(active) + .map_err(|e| format!("Could not set DPMS state: {}", ErrorFmt(e)))?; + Ok(Value::Null) + } + } +} + +fn to_value(value: T) -> Result { + serde_json::to_value(value).map_err(|e| format!("Could not serialize response: {e}")) +} + +fn get_connector(state: &State, name: &str) -> Result, String> { + for connector in state.connectors.lock().values() { + if connector.name.as_str() == name { + return Ok(connector.clone()); + } + } + let namelc = name.to_ascii_lowercase(); + for connector in state.connectors.lock().values() { + if connector.name.to_ascii_lowercase() == namelc { + return Ok(connector.clone()); + } + } + Err(format!("Found no connector matching `{name}`")) +} + +fn get_output_node(state: &State, name: &str) -> Result, String> { + let connector = get_connector(state, name)?; + let output = state + .outputs + .get(&connector.connector.id()) + .ok_or_else(|| format!("Connector {} is not connected", connector.name))?; + output.node.clone().ok_or_else(|| { + format!( + "Display connected to {} is not a desktop display", + connector.name + ) + }) +} + +fn default_output(state: &State) -> Result, String> { + if let Some(seat) = state.seat_queue.last() { + let output = seat.get_fallback_output(); + if !output.is_dummy { + return Ok(output); + } + } + if let Some(output) = state.root.outputs.lock().values().next().cloned() { + return Ok(output); + } + if let Some(output) = state.dummy_output.get() { + return Ok(output); + } + Err("No output is available".to_string()) +} + +fn get_workspace_by_name(state: &State, name: &str) -> Result, String> { + let mut result = None; + for ws in state.workspaces.lock().values() { + if !ws.is_dummy && ws.name == name { + if result.is_some() { + return Err(format!("Workspace name `{name}` is ambiguous")); + } + result = Some(ws.clone()); + } + } + result.ok_or_else(|| format!("Workspace `{name}` does not exist")) +} + +fn set_output_enabled(state: &Rc, name: &str, enabled: bool) -> Result<(), String> { + let connector = get_connector(state, name)?; + connector + .modify_state(state, |s| s.enabled = enabled) + .map_err(|e| format!("Could not en/disable connector: {}", ErrorFmt(e))) +} + +fn set_output_mode( + state: &Rc, + name: &str, + width: i32, + height: i32, + refresh_rate: f64, +) -> Result<(), String> { + let connector = get_connector(state, name)?; + let refresh_rate_millihz = (refresh_rate * 1_000.0).round() as u32; + if let Some(output) = state.outputs.get(&connector.connector.id()) + && let Some(node) = &output.node + && node.global.modes.is_some() + && !node.global.modes.as_ref().unwrap().iter().any(|mode| { + mode.width == width + && mode.height == height + && mode.refresh_rate_millihz == refresh_rate_millihz + }) + { + return Err(format!("Output {} does not support this mode", connector.name)); + } + connector + .modify_state(state, |s| { + s.mode = backend::Mode { + width, + height, + refresh_rate_millihz, + }; + }) + .map_err(|e| format!("Could not modify connector mode: {}", ErrorFmt(e))) +} + +fn set_output_position(state: &State, name: &str, x: i32, y: i32) -> Result<(), String> { + if x < 0 || y < 0 { + return Err("x and y cannot be less than 0".to_string()); + } + if x > MAX_EXTENTS || y > MAX_EXTENTS { + return Err(format!("x and y cannot be greater than {MAX_EXTENTS}")); + } + let output = get_output_node(state, name)?; + output.set_position(x, y); + Ok(()) +} + +fn parse_transform(transform: &str) -> Result { + match transform { + "none" => Ok(Transform::None), + "rotate-90" => Ok(Transform::Rotate90), + "rotate-180" => Ok(Transform::Rotate180), + "rotate-270" => Ok(Transform::Rotate270), + "flip" => Ok(Transform::Flip), + "flip-rotate-90" => Ok(Transform::FlipRotate90), + "flip-rotate-180" => Ok(Transform::FlipRotate180), + "flip-rotate-270" => Ok(Transform::FlipRotate270), + _ => Err(format!("Unknown transform `{transform}`")), + } +} + +fn outputs(state: &State) -> Outputs { + let mut drm_devices = vec![]; + for dev in state.drm_devs.lock().values() { + let dev_id = dev.dev.id().raw() as u64; + let mut connectors = vec![]; + for connector in state.connectors.lock().values() { + let connector_dev_id = connector.drm_dev.as_ref().map(|d| d.dev.id().raw() as u64); + if connector_dev_id == Some(dev_id) { + connectors.push(output_connector(state, connector)); + } + } + connectors.sort_by(|l, r| l.name.cmp(&r.name)); + drm_devices.push(DrmDevice { + devnode: dev.devnode.as_deref().unwrap_or_default().to_string(), + syspath: dev.syspath.as_deref().unwrap_or_default().to_string(), + vendor: dev.pci_id.map(|p| p.vendor).unwrap_or_default(), + vendor_name: dev.vendor.as_deref().unwrap_or_default().to_string(), + model: dev.pci_id.map(|p| p.model).unwrap_or_default(), + model_name: dev.model.as_deref().unwrap_or_default().to_string(), + gfx_api: dev.dev.gtx_api().to_str().to_string(), + render_device: dev.dev.is_render_device(), + connectors, + }); + } + drm_devices.sort_by(|l, r| l.devnode.cmp(&r.devnode)); + let mut unbound_connectors = vec![]; + for connector in state.connectors.lock().values() { + if connector.drm_dev.is_none() { + unbound_connectors.push(output_connector(state, connector)); + } + } + unbound_connectors.sort_by(|l, r| l.name.cmp(&r.name)); + Outputs { + drm_devices, + unbound_connectors, + } +} + +fn output_connector(state: &State, connector: &ConnectorData) -> Connector { + let state_enabled = connector.state.borrow().enabled; + let output = state + .outputs + .get(&connector.connector.id()) + .map(|output| match &output.node { + Some(node) => Output::from_node(output.as_ref(), node), + None => Output { + product: output.monitor_info.output_id.model.clone(), + manufacturer: output.monitor_info.output_id.manufacturer.clone(), + serial_number: output.monitor_info.output_id.serial_number.clone(), + width_mm: output.monitor_info.width_mm, + height_mm: output.monitor_info.height_mm, + non_desktop: true, + scale: 1.0, + ..Default::default() + }, + }); + Connector { + name: connector.name.to_string(), + enabled: state_enabled, + output, + } +} + +impl Output { + fn from_node(output: &crate::state::OutputData, node: &Rc) -> Self { + let global = &node.global; + let pos = global.pos.get(); + let current_mode = global.mode.get(); + let modes = global + .modes + .as_deref() + .unwrap_or(std::slice::from_ref(¤t_mode)) + .iter() + .map(|mode| Mode { + width: mode.width, + height: mode.height, + refresh_rate_millihz: mode.refresh_rate_millihz, + current: mode == ¤t_mode, + }) + .collect::>(); + let current_format = node.global.format.get(); + let mut formats = vec![current_format.name.to_string()]; + for &format in &*node.global.formats.get() { + if format != current_format { + formats.push(format.name.to_string()); + } + } + let p = &node.global.primaries; + Output { + product: output.monitor_info.output_id.model.clone(), + manufacturer: output.monitor_info.output_id.manufacturer.clone(), + serial_number: output.monitor_info.output_id.serial_number.clone(), + width_mm: global.width_mm, + height_mm: global.height_mm, + non_desktop: false, + scale: global.persistent.scale.get().to_f64(), + x: pos.x1(), + y: pos.y1(), + width: pos.width(), + height: pos.height(), + transform: global.persistent.transform.get().text().to_string(), + mode: modes.iter().copied().find(|mode| mode.current), + modes, + format: Some(current_format.name.to_string()), + formats, + vrr_capable: output.monitor_info.vrr_capable, + vrr_enabled: node.schedule.vrr_enabled(), + vrr_mode: node.global.persistent.vrr_mode.get().to_config().0, + vrr_cursor_hz: node.global.persistent.vrr_cursor_hz.get(), + tearing_mode: node.global.persistent.tearing_mode.get().to_config().0, + flip_margin_ns: node.flip_margin_ns.get(), + supported_color_spaces: node + .global + .color_spaces + .iter() + .map(|cs| cs.name().to_string()) + .collect(), + current_color_space: Some(node.global.bcs.get().name().to_string()), + supported_eotfs: node + .global + .eotfs + .iter() + .map(|eotf| eotf.name().to_string()) + .collect(), + current_eotf: Some(node.global.btf.get().name().to_string()), + min_brightness: node.global.luminance.map(|lum| lum.min), + max_brightness: node.global.luminance.map(|lum| lum.max), + brightness: node.global.persistent.brightness.get(), + blend_space: Some(node.global.persistent.blend_space.get().name().to_string()), + native_gamut: Some(Primaries { + r_x: p.r.0.0, + r_y: p.r.1.0, + g_x: p.g.0.0, + g_y: p.g.1.0, + b_x: p.b.0.0, + b_y: p.b.1.0, + w_x: p.wp.0.0, + w_y: p.wp.1.0, + }), + use_native_gamut: node.global.persistent.use_native_gamut.get(), + arbitrary_modes: global.modes.is_none(), + } + } +} + +fn workspaces(state: &State) -> Vec { + let mut workspaces = state + .workspaces + .lock() + .values() + .filter(|ws| !ws.is_dummy) + .map(|ws| Workspace { + id: ws.id.raw(), + name: ws.name.clone(), + output: ws.output.get().global.connector.name.to_string(), + visible: ws.visible.get(), + }) + .collect::>(); + workspaces.sort_by(|l, r| (&l.output, &l.name, l.id).cmp(&(&r.output, &r.name, r.id))); + workspaces +} + +fn clients(state: &State, id: Option) -> Vec { + let mut clients = vec![]; + for holder in state.clients.clients.borrow().values() { + let client = &holder.data; + if id.is_some_and(|id| id != client.id.raw()) { + continue; + } + clients.push(Client { + client_id: client.id.raw(), + sandboxed: client.metadata.sandboxed, + sandbox_engine: client.metadata.sandbox_engine.clone(), + sandbox_app_id: client.metadata.app_id.clone(), + sandbox_instance_id: client.metadata.instance_id.clone(), + uid: (!client.is_xwayland).then_some(client.pid_info.uid), + pid: (!client.is_xwayland).then_some(client.pid_info.pid), + is_xwayland: client.is_xwayland, + comm: (!client.is_xwayland).then(|| client.pid_info.comm.clone()), + exe: (!client.is_xwayland).then(|| client.pid_info.exe.clone()), + }); + } + clients.sort_by_key(|client| client.client_id); + clients +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "method", rename_all = "snake_case")] +pub enum Request { + #[serde(rename = "hello")] + Hello, + #[serde(rename = "outputs.get")] + OutputsGet, + #[serde(rename = "outputs.set_enabled")] + OutputsSetEnabled { output: String, enabled: bool }, + #[serde(rename = "outputs.set_mode")] + OutputsSetMode { + output: String, + width: i32, + height: i32, + refresh_rate: f64, + }, + #[serde(rename = "outputs.set_position")] + OutputsSetPosition { output: String, x: i32, y: i32 }, + #[serde(rename = "outputs.set_scale")] + OutputsSetScale { + output: String, + scale: f64, + round_to_float: bool, + }, + #[serde(rename = "outputs.set_transform")] + OutputsSetTransform { output: String, transform: String }, + #[serde(rename = "workspaces.get")] + WorkspacesGet, + #[serde(rename = "workspaces.show")] + WorkspacesShow { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + output: Option, + }, + #[serde(rename = "workspaces.create")] + WorkspacesCreate { name: String, output: String }, + #[serde(rename = "workspaces.move_to_output")] + WorkspacesMoveToOutput { name: String, output: String }, + #[serde(rename = "clients.get")] + ClientsGet { + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, + }, + #[serde(rename = "clients.kill")] + ClientsKill { id: u64 }, + #[serde(rename = "dpms.set")] + DpmsSet { active: bool }, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "status", rename_all = "snake_case")] +pub enum Response { + Ok { result: Value }, + Error { message: String }, +} + +#[derive(Serialize)] +struct Hello<'a> { + protocol_version: u32, + pid: c::pid_t, + commands: &'a [&'a str], +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Outputs { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub drm_devices: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub unbound_connectors: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DrmDevice { + pub devnode: String, + pub syspath: String, + pub vendor: u32, + pub vendor_name: String, + pub model: u32, + pub model_name: String, + pub gfx_api: String, + #[serde(default, skip_serializing_if = "is_false")] + pub render_device: bool, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub connectors: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Connector { + pub name: String, + pub enabled: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub output: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Output { + pub product: String, + pub manufacturer: String, + pub serial_number: String, + #[serde(default, skip_serializing_if = "is_zero_i32")] + pub width_mm: i32, + #[serde(default, skip_serializing_if = "is_zero_i32")] + pub height_mm: i32, + #[serde(default, skip_serializing_if = "is_false")] + pub non_desktop: bool, + pub scale: f64, + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, + pub transform: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mode: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub modes: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub format: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub formats: Vec, + #[serde(default, skip_serializing_if = "is_false")] + pub vrr_capable: bool, + #[serde(default, skip_serializing_if = "is_false")] + pub vrr_enabled: bool, + pub vrr_mode: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub vrr_cursor_hz: Option, + pub tearing_mode: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flip_margin_ns: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub supported_color_spaces: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_color_space: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub supported_eotfs: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_eotf: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min_brightness: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_brightness: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub brightness: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub blend_space: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub native_gamut: Option, + #[serde(default, skip_serializing_if = "is_false")] + pub use_native_gamut: bool, + #[serde(default, skip_serializing_if = "is_false")] + pub arbitrary_modes: bool, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct Mode { + pub width: i32, + pub height: i32, + pub refresh_rate_millihz: u32, + #[serde(default, skip_serializing_if = "is_false")] + pub current: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Primaries { + pub r_x: f64, + pub r_y: f64, + pub g_x: f64, + pub g_y: f64, + pub b_x: f64, + pub b_y: f64, + pub w_x: f64, + pub w_y: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Workspace { + pub id: u32, + pub name: String, + pub output: String, + pub visible: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Client { + pub client_id: u64, + #[serde(default, skip_serializing_if = "is_false")] + pub sandboxed: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sandbox_engine: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sandbox_app_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sandbox_instance_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub uid: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pid: Option, + #[serde(default, skip_serializing_if = "is_false")] + pub is_xwayland: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub comm: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub exe: Option, +} + +fn is_zero_i32(v: &i32) -> bool { + *v == 0 +} + +fn is_false(v: &bool) -> bool { + !*v +} + +#[derive(Debug, Error)] +pub enum IpcClientError { + #[error("XDG_RUNTIME_DIR is not set")] + XrdNotSet, + #[error("Could not connect to the IPC socket {path}")] + Connect { + path: PathBuf, + #[source] + error: std::io::Error, + }, + #[error("Could not write the IPC request")] + Write(#[source] std::io::Error), + #[error("Could not read the IPC response")] + Read(#[source] std::io::Error), + #[error("Could not serialize the IPC request")] + Serialize(#[source] serde_json::Error), + #[error("Could not parse the IPC response")] + Deserialize(#[source] serde_json::Error), + #[error("IPC request failed: {0}")] + Server(String), +} + +impl IpcClientError { + pub fn can_fallback(&self) -> bool { + matches!(self, IpcClientError::XrdNotSet | IpcClientError::Connect { .. }) + } +} + +pub fn request(request: &Request) -> Result +where + T: DeserializeOwned, +{ + match raw_request(request)? { + Response::Ok { result } => { + serde_json::from_value(result).map_err(IpcClientError::Deserialize) + } + Response::Error { message } => Err(IpcClientError::Server(message)), + } +} + +pub fn request_unit(req: &Request) -> Result<(), IpcClientError> { + let _: Value = request(req)?; + Ok(()) +} + +fn raw_request(request: &Request) -> Result { + let path = client_socket_path()?; + let mut stream = UnixStream::connect(&path).map_err(|error| IpcClientError::Connect { + path: path.clone(), + error, + })?; + let mut bytes = serde_json::to_vec(request).map_err(IpcClientError::Serialize)?; + bytes.push(b'\n'); + stream.write_all(&bytes).map_err(IpcClientError::Write)?; + let mut reader = BufReader::new(stream); + let mut line = Vec::new(); + reader.read_until(b'\n', &mut line).map_err(IpcClientError::Read)?; + serde_json::from_slice(&line).map_err(IpcClientError::Deserialize) +} + +fn client_socket_path() -> Result { + if let Some(path) = std::env::var_os(JAY_IPC_SOCKET) { + return Ok(PathBuf::from(path)); + } + let xrd = match std::env::var_os("XDG_RUNTIME_DIR") { + Some(xrd) => xrd, + None => return Err(IpcClientError::XrdNotSet), + }; + Ok(PathBuf::from(xrd).join(format!("jay-ipc-{}.sock", uapi::geteuid()))) +} diff --git a/src/main.rs b/src/main.rs index fe938d39..8170310c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,6 +73,7 @@ mod gfx_apis; mod globals; mod icons; mod ifs; +mod ipc; #[cfg(feature = "it")] mod it; mod kbvm; diff --git a/src/state.rs b/src/state.rs index d46a4b16..48290422 100644 --- a/src/state.rs +++ b/src/state.rs @@ -89,6 +89,7 @@ use { zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1, zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, }, + ipc::IpcAcceptor, leaks::Tracker, @@ -194,6 +195,7 @@ pub struct State { pub run_args: RunArgs, pub xwayland: XWaylandState, pub acceptor: CloneCell>>, + pub ipc_acceptor: CloneCell>>, pub tagged_acceptors: TaggedAcceptors, pub serial: NumCell, pub run_toplevel: Rc,