add unix socket ipc
This commit is contained in:
parent
dc62d2240f
commit
f92c092acc
9 changed files with 1550 additions and 2 deletions
|
|
@ -20,6 +20,7 @@ mod set_log_level;
|
||||||
mod tree;
|
mod tree;
|
||||||
mod unlock;
|
mod unlock;
|
||||||
mod version;
|
mod version;
|
||||||
|
mod workspaces;
|
||||||
mod xwayland;
|
mod xwayland;
|
||||||
|
|
||||||
use jay_pr_caps::drop_all_pr_caps;
|
use jay_pr_caps::drop_all_pr_caps;
|
||||||
|
|
@ -30,7 +31,7 @@ use {
|
||||||
clients::ClientsArgs, color_management::ColorManagementArgs, config::ConfigArgs,
|
clients::ClientsArgs, color_management::ColorManagementArgs, config::ConfigArgs,
|
||||||
damage_tracking::DamageTrackingArgs, idle::IdleCmd, input::InputArgs,
|
damage_tracking::DamageTrackingArgs, idle::IdleCmd, input::InputArgs,
|
||||||
json::VERBOSE_JSON, randr::RandrArgs, reexec::ReexecArgs, tree::TreeArgs,
|
json::VERBOSE_JSON, randr::RandrArgs, reexec::ReexecArgs, tree::TreeArgs,
|
||||||
xwayland::XwaylandArgs,
|
workspaces::WorkspacesArgs, xwayland::XwaylandArgs,
|
||||||
},
|
},
|
||||||
compositor::start_compositor,
|
compositor::start_compositor,
|
||||||
|
|
||||||
|
|
@ -108,6 +109,8 @@ pub enum Cmd {
|
||||||
Reexec(ReexecArgs),
|
Reexec(ReexecArgs),
|
||||||
/// Inspect/manipulate the connected clients.
|
/// Inspect/manipulate the connected clients.
|
||||||
Clients(ClientsArgs),
|
Clients(ClientsArgs),
|
||||||
|
/// Inspect/manipulate workspaces.
|
||||||
|
Workspaces(WorkspacesArgs),
|
||||||
/// Inspect the surface tree.
|
/// Inspect the surface tree.
|
||||||
Tree(TreeArgs),
|
Tree(TreeArgs),
|
||||||
/// Prints the Jay version and exits.
|
/// Prints the Jay version and exits.
|
||||||
|
|
@ -255,6 +258,7 @@ pub fn main() {
|
||||||
Cmd::Xwayland(a) => xwayland::main(cli.global, a),
|
Cmd::Xwayland(a) => xwayland::main(cli.global, a),
|
||||||
Cmd::ColorManagement(a) => color_management::main(cli.global, a),
|
Cmd::ColorManagement(a) => color_management::main(cli.global, a),
|
||||||
Cmd::Clients(a) => clients::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::Tree(a) => tree::main(cli.global, a),
|
||||||
Cmd::Version => version::main(cli.global),
|
Cmd::Version => version::main(cli.global),
|
||||||
Cmd::Pid => pid::main(cli.global),
|
Cmd::Pid => pid::main(cli.global),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ use {
|
||||||
GlobalArgs,
|
GlobalArgs,
|
||||||
json::{JsonClient, jsonl},
|
json::{JsonClient, jsonl},
|
||||||
},
|
},
|
||||||
|
ipc::{self, Client as IpcClient},
|
||||||
tools::tool_client::{Handle, ToolClient, with_tool_client},
|
tools::tool_client::{Handle, ToolClient, with_tool_client},
|
||||||
|
utils::errorfmt::ErrorFmt,
|
||||||
wire::{JayClientQueryId, jay_client_query, jay_compositor},
|
wire::{JayClientQueryId, jay_client_query, jay_compositor},
|
||||||
},
|
},
|
||||||
ahash::AHashMap,
|
ahash::AHashMap,
|
||||||
|
|
@ -83,10 +85,13 @@ struct Clients {
|
||||||
impl Clients {
|
impl Clients {
|
||||||
async fn run(&self, global: &GlobalArgs, args: ClientsArgs) {
|
async fn run(&self, global: &GlobalArgs, args: ClientsArgs) {
|
||||||
let tc = &self.tc;
|
let tc = &self.tc;
|
||||||
let comp = tc.jay_compositor().await;
|
|
||||||
let cmd = args
|
let cmd = args
|
||||||
.cmd
|
.cmd
|
||||||
.unwrap_or(ClientsCmd::Show(ShowArgs { cmd: ShowCmd::All }));
|
.unwrap_or(ClientsCmd::Show(ShowArgs { cmd: ShowCmd::All }));
|
||||||
|
if self.run_ipc(global, &cmd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let comp = tc.jay_compositor().await;
|
||||||
match cmd {
|
match cmd {
|
||||||
ClientsCmd::Show(a) => {
|
ClientsCmd::Show(a) => {
|
||||||
let id = tc.id();
|
let id = tc.id();
|
||||||
|
|
@ -153,6 +158,50 @@ impl Clients {
|
||||||
}
|
}
|
||||||
tc.round_trip().await;
|
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<IpcClient> = 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::<Vec<_>>();
|
||||||
|
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)]
|
#[derive(Default)]
|
||||||
|
|
@ -169,6 +218,23 @@ pub struct Client {
|
||||||
pub exe: Option<String>,
|
pub exe: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<IpcClient> 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(
|
pub async fn handle_client_query(
|
||||||
tl: &Rc<ToolClient>,
|
tl: &Rc<ToolClient>,
|
||||||
id: JayClientQueryId,
|
id: JayClientQueryId,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,21 @@
|
||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
cli::{DpmsArgs, DpmsState, GlobalArgs},
|
cli::{DpmsArgs, DpmsState, GlobalArgs},
|
||||||
|
ipc,
|
||||||
tools::tool_client::{ToolClient, with_tool_client},
|
tools::tool_client::{ToolClient, with_tool_client},
|
||||||
|
utils::errorfmt::ErrorFmt,
|
||||||
wire::jay_compositor::SetDpms,
|
wire::jay_compositor::SetDpms,
|
||||||
},
|
},
|
||||||
std::rc::Rc,
|
std::rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn main(global: GlobalArgs, args: DpmsArgs) {
|
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 {
|
with_tool_client(global.log_level, |tc| async move {
|
||||||
run(tc, args).await;
|
run(tc, args).await;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
393
src/cli/randr.rs
393
src/cli/randr.rs
|
|
@ -13,6 +13,7 @@ use {
|
||||||
cmm::cmm_primaries::Primaries,
|
cmm::cmm_primaries::Primaries,
|
||||||
format::{Format, XRGB8888},
|
format::{Format, XRGB8888},
|
||||||
ifs::wl_output::BlendSpace,
|
ifs::wl_output::BlendSpace,
|
||||||
|
ipc,
|
||||||
tools::tool_client::{Handle, ToolClient, with_tool_client},
|
tools::tool_client::{Handle, ToolClient, with_tool_client},
|
||||||
tree::Transform,
|
tree::Transform,
|
||||||
utils::{errorfmt::ErrorFmt, ordered_float::F64, static_text::StaticText},
|
utils::{errorfmt::ErrorFmt, ordered_float::F64, static_text::StaticText},
|
||||||
|
|
@ -501,12 +502,97 @@ pub struct RemoveVirtualOutputArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main(global: GlobalArgs, args: RandrArgs) {
|
pub fn main(global: GlobalArgs, args: RandrArgs) {
|
||||||
|
if try_ipc(&global, &args) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
with_tool_client(global.log_level, |tc| async move {
|
with_tool_client(global.log_level, |tc| async move {
|
||||||
let idle = Rc::new(Randr { tc: tc.clone() });
|
let idle = Rc::new(Randr { tc: tc.clone() });
|
||||||
idle.run(&global, args).await;
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
struct Device {
|
struct Device {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
|
|
@ -1474,3 +1560,310 @@ fn make_json_connector(c: &Connector) -> JsonConnector<'_> {
|
||||||
output,
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
96
src/cli/workspaces.rs
Normal file
96
src/cli/workspaces.rs
Normal file
|
|
@ -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<WorkspacesCmd>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Workspace> = 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -55,6 +55,7 @@ use {
|
||||||
wlr_output_manager::wlr_output_manager_done,
|
wlr_output_manager::wlr_output_manager_done,
|
||||||
workspace_manager::workspace_manager_done,
|
workspace_manager::workspace_manager_done,
|
||||||
},
|
},
|
||||||
|
ipc::{IpcAcceptor, JAY_IPC_SOCKET},
|
||||||
|
|
||||||
leaks,
|
leaks,
|
||||||
|
|
||||||
|
|
@ -310,6 +311,7 @@ fn start_compositor2(
|
||||||
display: Default::default(),
|
display: Default::default(),
|
||||||
},
|
},
|
||||||
acceptor: Default::default(),
|
acceptor: Default::default(),
|
||||||
|
ipc_acceptor: Default::default(),
|
||||||
tagged_acceptors: Default::default(),
|
tagged_acceptors: Default::default(),
|
||||||
serial: Default::default(),
|
serial: Default::default(),
|
||||||
idle_inhibitor_ids: Default::default(),
|
idle_inhibitor_ids: Default::default(),
|
||||||
|
|
@ -417,12 +419,29 @@ fn start_compositor2(
|
||||||
state.tracker.register(ClientId::from_raw(0));
|
state.tracker.register(ClientId::from_raw(0));
|
||||||
create_dummy_output(&state);
|
create_dummy_output(&state);
|
||||||
let (acceptor, _acceptor_future) = Acceptor::install(&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 {
|
if let Some(forker) = forker {
|
||||||
forker.install(&state);
|
forker.install(&state);
|
||||||
forker.setenv(
|
forker.setenv(
|
||||||
WAYLAND_DISPLAY.as_bytes(),
|
WAYLAND_DISPLAY.as_bytes(),
|
||||||
acceptor.socket_name().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 {
|
for (key, val) in STATIC_VARS {
|
||||||
forker.setenv(key.as_bytes(), val.as_bytes());
|
forker.setenv(key.as_bytes(), val.as_bytes());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
959
src/ipc.rs
Normal file
959
src/ipc.rs
Normal file
|
|
@ -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<u64>,
|
||||||
|
clients: RefCell<AHashMap<u64, SpawnedFuture<()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AllocatedSocket {
|
||||||
|
path: Ustring,
|
||||||
|
socket: Rc<OwnedFd>,
|
||||||
|
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<State>,
|
||||||
|
) -> Result<(Rc<IpcAcceptor>, Vec<SpawnedFuture<()>>), 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<AllocatedSocket, IpcAcceptorError> {
|
||||||
|
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<IpcAcceptor>, state: Rc<State>) {
|
||||||
|
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<OwnedFd>) -> 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<OwnedFd>, state: Rc<State>, acc: Rc<IpcAcceptor>) {
|
||||||
|
handle_client_(fd, state).await;
|
||||||
|
acc.clients.borrow_mut().remove(&id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_client_(fd: Rc<OwnedFd>, state: Rc<State>) {
|
||||||
|
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::<Vec<_>>();
|
||||||
|
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::<Request>(&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<OwnedFd>,
|
||||||
|
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<State>, req: Request) -> Response {
|
||||||
|
match handle_request_(state, req) {
|
||||||
|
Ok(value) => Response::Ok { result: value },
|
||||||
|
Err(message) => Response::Error { message },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_request_(state: &Rc<State>, req: Request) -> Result<Value, String> {
|
||||||
|
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<T: Serialize>(value: T) -> Result<Value, String> {
|
||||||
|
serde_json::to_value(value).map_err(|e| format!("Could not serialize response: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_connector(state: &State, name: &str) -> Result<Rc<ConnectorData>, 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<Rc<OutputNode>, 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<Rc<OutputNode>, 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<Rc<crate::tree::WorkspaceNode>, 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<State>, 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<State>,
|
||||||
|
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<Transform, String> {
|
||||||
|
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<OutputNode>) -> 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::<Vec<_>>();
|
||||||
|
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<Workspace> {
|
||||||
|
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::<Vec<_>>();
|
||||||
|
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<u64>) -> Vec<Client> {
|
||||||
|
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<String>,
|
||||||
|
},
|
||||||
|
#[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<u64>,
|
||||||
|
},
|
||||||
|
#[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<DrmDevice>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub unbound_connectors: Vec<Connector>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Connector>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Output>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Mode>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub modes: Vec<Mode>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub format: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub formats: Vec<String>,
|
||||||
|
#[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<f64>,
|
||||||
|
pub tearing_mode: u32,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub flip_margin_ns: Option<u64>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub supported_color_spaces: Vec<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_color_space: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub supported_eotfs: Vec<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_eotf: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub min_brightness: Option<f64>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_brightness: Option<f64>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub brightness: Option<f64>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub blend_space: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub native_gamut: Option<Primaries>,
|
||||||
|
#[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<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub sandbox_app_id: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub sandbox_instance_id: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub uid: Option<c::uid_t>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub pid: Option<c::pid_t>,
|
||||||
|
#[serde(default, skip_serializing_if = "is_false")]
|
||||||
|
pub is_xwayland: bool,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub comm: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub exe: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<T>(request: &Request) -> Result<T, IpcClientError>
|
||||||
|
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<Response, IpcClientError> {
|
||||||
|
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<PathBuf, IpcClientError> {
|
||||||
|
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())))
|
||||||
|
}
|
||||||
|
|
@ -73,6 +73,7 @@ mod gfx_apis;
|
||||||
mod globals;
|
mod globals;
|
||||||
mod icons;
|
mod icons;
|
||||||
mod ifs;
|
mod ifs;
|
||||||
|
mod ipc;
|
||||||
#[cfg(feature = "it")]
|
#[cfg(feature = "it")]
|
||||||
mod it;
|
mod it;
|
||||||
mod kbvm;
|
mod kbvm;
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ use {
|
||||||
zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
|
zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
|
||||||
zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1,
|
zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1,
|
||||||
},
|
},
|
||||||
|
ipc::IpcAcceptor,
|
||||||
|
|
||||||
leaks::Tracker,
|
leaks::Tracker,
|
||||||
|
|
||||||
|
|
@ -194,6 +195,7 @@ pub struct State {
|
||||||
pub run_args: RunArgs,
|
pub run_args: RunArgs,
|
||||||
pub xwayland: XWaylandState,
|
pub xwayland: XWaylandState,
|
||||||
pub acceptor: CloneCell<Option<Rc<Acceptor>>>,
|
pub acceptor: CloneCell<Option<Rc<Acceptor>>>,
|
||||||
|
pub ipc_acceptor: CloneCell<Option<Rc<IpcAcceptor>>>,
|
||||||
pub tagged_acceptors: TaggedAcceptors,
|
pub tagged_acceptors: TaggedAcceptors,
|
||||||
pub serial: NumCell<u64>,
|
pub serial: NumCell<u64>,
|
||||||
pub run_toplevel: Rc<RunToplevel>,
|
pub run_toplevel: Rc<RunToplevel>,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue