Modernize feresectl and add bounded IPC event streaming
This commit is contained in:
1 parent
cfdd2a148a
commit
97026a0db7
39 files changed
+1671
-254
No files matched your search
+165
-1
@@ -43,6 +43,7 @@ struct IpcCall {
|
||||
owner: u64,
|
||||
request: Request,
|
||||
response: SyncSender<Response>,
|
||||
events: Option<SyncSender<Arc<ferese_ipc::events::Event>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -108,6 +109,7 @@ pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result<Screen
|
||||
let call = match event {
|
||||
IpcEvent::Call(call) => call,
|
||||
IpcEvent::Closed(owner) => {
|
||||
state.ipc_events.remove(owner);
|
||||
for request in state.screenshot.terminate_owner(owner) {
|
||||
state
|
||||
.pending_screencopies
|
||||
@@ -126,7 +128,25 @@ pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result<Screen
|
||||
return;
|
||||
}
|
||||
};
|
||||
if call.request.command.starts_with("input-capture-") && !call.native_portal {
|
||||
if call.request.command == "event-stream" {
|
||||
let result = validate_request(&call.request).and_then(|()| {
|
||||
if call.request.args["version"].as_u64() != Some(u64::from(ferese_ipc::events::VERSION)) {
|
||||
return Err(CommandError::new(
|
||||
"unsupported_event_version",
|
||||
"Unsupported event schema version",
|
||||
));
|
||||
}
|
||||
state
|
||||
.ipc_events
|
||||
.subscribe(call.owner, call.events.expect("event stream sender"))
|
||||
.map_err(|error| CommandError::new("subscriber_limit", error))
|
||||
});
|
||||
let response = match result {
|
||||
Ok(()) => Response::success(call.request.id, json!({"version": ferese_ipc::events::VERSION})),
|
||||
Err(error) => Response::error(call.request.id, error.code, error.message),
|
||||
};
|
||||
let _ = call.response.try_send(response);
|
||||
} else if call.request.command.starts_with("input-capture-") && !call.native_portal {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
"access_denied",
|
||||
@@ -449,6 +469,8 @@ fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcEvent
|
||||
continue;
|
||||
}
|
||||
let (response, receiver) = sync_channel(1);
|
||||
let event_stream_requested = request.command == "event-stream";
|
||||
let (events, event_receiver) = sync_channel(crate::ipc_events::QUEUE_CAPACITY);
|
||||
let exit_requested = request.command == "exit";
|
||||
let screenshot_requested = matches!(request.command.as_str(), "screenshot" | "screenshot-window");
|
||||
let call = IpcEvent::Call(IpcCall {
|
||||
@@ -456,6 +478,7 @@ fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcEvent
|
||||
owner,
|
||||
request,
|
||||
response,
|
||||
events: event_stream_requested.then_some(events),
|
||||
});
|
||||
if sender.try_send(call).is_err() {
|
||||
tracing::warn!("disconnecting IPC client because the request queue is full");
|
||||
@@ -491,6 +514,7 @@ fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcEvent
|
||||
}
|
||||
};
|
||||
let exit_accepted = exit_requested && response.error.is_none();
|
||||
let event_stream_accepted = event_stream_requested && response.error.is_none();
|
||||
if let Err(error) = write_frame(&mut stream, &response) {
|
||||
if screenshot_requested {
|
||||
discard_undelivered_screenshot(&response);
|
||||
@@ -498,6 +522,11 @@ fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcEvent
|
||||
tracing::debug!(%error, "IPC client disconnected before receiving its response");
|
||||
return;
|
||||
}
|
||||
if event_stream_accepted {
|
||||
let _ = stream.set_write_timeout(Some(Duration::from_secs(2)));
|
||||
relay_events(&mut stream, event_receiver);
|
||||
return;
|
||||
}
|
||||
if exit_accepted {
|
||||
// Acknowledge before stopping so feresectl never races process exit.
|
||||
signal.stop();
|
||||
@@ -507,6 +536,41 @@ fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcEvent
|
||||
}
|
||||
}
|
||||
|
||||
// Socket I/O stays on the bounded connection worker, never the compositor loop.
|
||||
fn relay_events(stream: &mut UnixStream, events: std::sync::mpsc::Receiver<Arc<ferese_ipc::events::Event>>) {
|
||||
loop {
|
||||
match events.recv_timeout(Duration::from_millis(250)) {
|
||||
Ok(event) => {
|
||||
if write_frame(stream, event.as_ref()).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return,
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
let mut byte = 0_u8;
|
||||
// SAFETY: the stream owns its descriptor and byte is writable.
|
||||
let result = unsafe {
|
||||
libc::recv(
|
||||
stream.as_raw_fd(),
|
||||
(&raw mut byte).cast(),
|
||||
1,
|
||||
libc::MSG_PEEK | libc::MSG_DONTWAIT,
|
||||
)
|
||||
};
|
||||
if result >= 0 {
|
||||
return;
|
||||
}
|
||||
if !matches!(
|
||||
io::Error::last_os_error().kind(),
|
||||
io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn u32_arg(args: &Value, key: &str) -> Result<u32, CommandError> {
|
||||
args[key]
|
||||
.as_u64()
|
||||
@@ -1111,6 +1175,58 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ipc_event_snapshot(&self) -> ferese_ipc::events::Snapshot {
|
||||
let locked = self.session_lock.active();
|
||||
let mut outputs = self.outputs_json();
|
||||
if locked {
|
||||
for output in outputs.as_array_mut().into_iter().flatten() {
|
||||
if let Some(fields) = output.as_object_mut() {
|
||||
fields.remove("workspace");
|
||||
fields.remove("focused");
|
||||
}
|
||||
}
|
||||
}
|
||||
let windows = if locked {
|
||||
json!([])
|
||||
} else {
|
||||
Value::Array(self.windows.ordered_ids().filter_map(|id| {
|
||||
let record = self.windows.record(id)?;
|
||||
let workspace = self.workspaces.workspace_for_window(id)?;
|
||||
Some(json!({
|
||||
"id": id.0, "app_id": record.app_id, "title": record.title,
|
||||
"focused": self.focused_window == Some(id), "workspace": workspace.0,
|
||||
"floating": matches!(self.workspaces.placement(id), Some(ferese_core::WindowPlacement::Floating { .. })),
|
||||
"fullscreen": self.workspaces.workspace(workspace).is_some_and(|workspace| workspace.fullscreen == Some(id)),
|
||||
}))
|
||||
}).collect())
|
||||
};
|
||||
let live = &self.theme_engine.live;
|
||||
let theme = json!({
|
||||
"mode": live.mode, "theme": live.theme, "families": live.families,
|
||||
"warnings": live.warnings, "error": live.error, "fallback_note": live.fallback_note,
|
||||
});
|
||||
ferese_ipc::events::Snapshot {
|
||||
outputs,
|
||||
workspaces: if locked { json!([]) } else { self.workspaces_json() },
|
||||
windows,
|
||||
focus: if locked {
|
||||
Value::Null
|
||||
} else {
|
||||
json!({
|
||||
"window": self.focused_window.map(|id| id.0),
|
||||
"output": self.output_workspaces.focused_output().map(|id| id.0),
|
||||
"workspace": self.output_workspaces.focused_output().and_then(|id| self.output_workspaces.active_workspace(id)).map(|id| id.0),
|
||||
})
|
||||
},
|
||||
config: ferese_ipc::events::ConfigState {
|
||||
revision: self.ipc_events.config_revision,
|
||||
error: self.theme_engine.live.error.clone(),
|
||||
},
|
||||
theme,
|
||||
lock: self.session_lock.event_state(),
|
||||
}
|
||||
}
|
||||
|
||||
fn focused_window_json(&self) -> Value {
|
||||
self.focused_window
|
||||
.map(|window| json!({ "id": window.0 }))
|
||||
@@ -1743,6 +1859,54 @@ mod tests {
|
||||
assert!(try_acquire_connection(&active).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_event_stream_disconnect_is_detected_without_an_event() {
|
||||
let (client, mut server) = UnixStream::pair().unwrap();
|
||||
let (_sender, receiver) = sync_channel(1);
|
||||
let (finished, completion) = sync_channel(1);
|
||||
let worker = thread::spawn(move || {
|
||||
relay_events(&mut server, receiver);
|
||||
finished.send(()).unwrap();
|
||||
});
|
||||
drop(client);
|
||||
completion.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
worker.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stalled_event_socket_write_expires_on_the_worker() {
|
||||
let (_client, mut server) = UnixStream::pair().unwrap();
|
||||
server.set_write_timeout(Some(Duration::from_millis(50))).unwrap();
|
||||
let (sender, receiver) = sync_channel(1);
|
||||
let (finished, completion) = sync_channel(1);
|
||||
sender
|
||||
.send(Arc::new(ferese_ipc::events::Event {
|
||||
version: ferese_ipc::events::VERSION,
|
||||
generation: 1,
|
||||
last: true,
|
||||
change: ferese_ipc::events::Change::WindowsChanged {
|
||||
windows: json!([{"title": "x".repeat(900_000)}]),
|
||||
},
|
||||
}))
|
||||
.unwrap();
|
||||
let worker = thread::spawn(move || {
|
||||
relay_events(&mut server, receiver);
|
||||
finished.send(()).unwrap();
|
||||
});
|
||||
completion.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
worker.join().unwrap();
|
||||
assert!(
|
||||
sender
|
||||
.try_send(Arc::new(ferese_ipc::events::Event {
|
||||
version: ferese_ipc::events::VERSION,
|
||||
generation: 2,
|
||||
last: true,
|
||||
change: ferese_ipc::events::Change::FocusChanged { focus: json!(null) },
|
||||
}))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
fn unique_test_directory(label: &str) -> PathBuf {
|
||||
let nonce = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::SyncSender;
|
||||
|
||||
use ferese_ipc::events::{Change, Event, Snapshot, VERSION};
|
||||
|
||||
pub(crate) const QUEUE_CAPACITY: usize = 32;
|
||||
const MAX_SUBSCRIBERS: usize = 16;
|
||||
|
||||
struct Subscriber {
|
||||
sender: SyncSender<Arc<Event>>,
|
||||
initial: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Subscribers {
|
||||
subscribers: HashMap<u64, Subscriber>,
|
||||
previous: Option<Snapshot>,
|
||||
generation: u64,
|
||||
pub(crate) config_revision: u64,
|
||||
}
|
||||
|
||||
impl Subscribers {
|
||||
pub(crate) fn subscribe(&mut self, owner: u64, sender: SyncSender<Arc<Event>>) -> Result<(), &'static str> {
|
||||
if self.subscribers.len() >= MAX_SUBSCRIBERS {
|
||||
return Err("Event subscriber limit reached");
|
||||
}
|
||||
self.subscribers.insert(owner, Subscriber { sender, initial: true });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, owner: u64) {
|
||||
self.subscribers.remove(&owner);
|
||||
if self.subscribers.is_empty() {
|
||||
self.previous = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn active(&self) -> bool {
|
||||
!self.subscribers.is_empty()
|
||||
}
|
||||
|
||||
/// Called only after logical mutations for a dispatch have completed.
|
||||
pub(crate) fn publish(&mut self, snapshot: Snapshot) {
|
||||
let changes = self
|
||||
.previous
|
||||
.as_ref()
|
||||
.map(|previous| snapshot.changes_since(previous))
|
||||
.unwrap_or_default();
|
||||
if changes.is_empty() && !self.subscribers.values().any(|subscriber| subscriber.initial) {
|
||||
return;
|
||||
}
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
let mut events = changes
|
||||
.into_iter()
|
||||
.map(|change| {
|
||||
Arc::new(Event {
|
||||
version: VERSION,
|
||||
generation: self.generation,
|
||||
last: false,
|
||||
change,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(last) = events.last_mut() {
|
||||
Arc::get_mut(last).unwrap().last = true;
|
||||
}
|
||||
let initial = self.subscribers.values().any(|subscriber| subscriber.initial).then(|| {
|
||||
Arc::new(Event {
|
||||
version: VERSION,
|
||||
generation: self.generation,
|
||||
last: true,
|
||||
change: Change::Snapshot {
|
||||
desktop: snapshot.clone(),
|
||||
},
|
||||
})
|
||||
});
|
||||
self.subscribers.retain(|_, subscriber| {
|
||||
let delivered = if subscriber.initial {
|
||||
subscriber.sender.try_send(initial.as_ref().unwrap().clone()).is_ok()
|
||||
} else {
|
||||
events
|
||||
.iter()
|
||||
.all(|event| subscriber.sender.try_send(event.clone()).is_ok())
|
||||
};
|
||||
subscriber.initial = false;
|
||||
// A partial generation is never followed by another one: an
|
||||
// overflowing stream ends, and reconnecting starts with a snapshot.
|
||||
delivered
|
||||
});
|
||||
self.previous = self.active().then_some(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Ferese {
|
||||
pub(crate) fn publish_ipc_events(&mut self) {
|
||||
if !self.ipc_events.active()
|
||||
|| self.desktop_transition.is_some()
|
||||
|| self
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.reconciling())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let snapshot = self.ipc_event_snapshot();
|
||||
self.ipc_events.publish(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ferese_ipc::events::{ConfigState, LockState};
|
||||
use serde_json::json;
|
||||
use std::sync::mpsc::{TryRecvError, sync_channel};
|
||||
|
||||
fn snapshot() -> Snapshot {
|
||||
Snapshot {
|
||||
outputs: json!([]),
|
||||
workspaces: json!([]),
|
||||
windows: json!([]),
|
||||
focus: json!(null),
|
||||
config: ConfigState::default(),
|
||||
theme: json!({}),
|
||||
lock: LockState {
|
||||
phase: "unlocked".into(),
|
||||
sleeping: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_snapshot_and_changes_have_complete_generations_without_duplicates() {
|
||||
let mut hub = Subscribers::default();
|
||||
let (sender, receive) = sync_channel(QUEUE_CAPACITY);
|
||||
hub.subscribe(1, sender).unwrap();
|
||||
let mut state = snapshot();
|
||||
hub.publish(state.clone());
|
||||
assert!(matches!(receive.try_recv().unwrap().change, Change::Snapshot { .. }));
|
||||
hub.publish(state.clone());
|
||||
assert!(matches!(receive.try_recv(), Err(TryRecvError::Empty)));
|
||||
state.outputs = json!([{"id": 2}]);
|
||||
state.workspaces = json!([{"output": 2}]);
|
||||
state.focus = json!({"output": 2});
|
||||
hub.publish(state);
|
||||
let events = receive.try_iter().collect::<Vec<_>>();
|
||||
assert_eq!(events.len(), 3);
|
||||
assert!(events.iter().all(|event| event.generation == events[0].generation));
|
||||
assert!(!events[0].last && !events[1].last && events[2].last);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_and_disconnected_subscribers_do_not_block_healthy_readers() {
|
||||
let mut hub = Subscribers::default();
|
||||
let (slow, slow_receive) = sync_channel(1);
|
||||
let (fast, fast_receive) = sync_channel(QUEUE_CAPACITY);
|
||||
let (closed, closed_receive) = sync_channel(1);
|
||||
drop(closed_receive);
|
||||
hub.subscribe(1, slow).unwrap();
|
||||
hub.subscribe(2, fast).unwrap();
|
||||
hub.subscribe(3, closed).unwrap();
|
||||
let mut state = snapshot();
|
||||
hub.publish(state.clone());
|
||||
fast_receive.try_recv().unwrap();
|
||||
state.focus = json!({"window": 7});
|
||||
hub.publish(state);
|
||||
assert!(matches!(
|
||||
fast_receive.try_recv().unwrap().change,
|
||||
Change::FocusChanged { .. }
|
||||
));
|
||||
assert_eq!(hub.subscribers.len(), 1);
|
||||
slow_receive.try_recv().unwrap();
|
||||
assert!(matches!(slow_receive.try_recv(), Err(TryRecvError::Disconnected)));
|
||||
hub.remove(2);
|
||||
assert!(!hub.active());
|
||||
assert!(hub.previous.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subscribers_are_bounded_independently_of_command_workers() {
|
||||
let mut hub = Subscribers::default();
|
||||
let mut receivers = Vec::new();
|
||||
for owner in 0..MAX_SUBSCRIBERS as u64 {
|
||||
let (sender, receiver) = sync_channel(QUEUE_CAPACITY);
|
||||
hub.subscribe(owner, sender).unwrap();
|
||||
receivers.push(receiver);
|
||||
}
|
||||
assert!(hub.subscribe(99, sync_channel(1).0).is_err());
|
||||
hub.remove(0);
|
||||
assert!(hub.subscribe(99, sync_channel(1).0).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_changed_domains_share_one_generation() {
|
||||
let mut hub = Subscribers::default();
|
||||
let (sender, receive) = sync_channel(QUEUE_CAPACITY);
|
||||
hub.subscribe(1, sender).unwrap();
|
||||
let mut state = snapshot();
|
||||
hub.publish(state.clone());
|
||||
receive.try_recv().unwrap();
|
||||
state.outputs = json!([{"id": 1}]);
|
||||
state.workspaces = json!([{"output": 1}]);
|
||||
state.windows = json!([{"id": 7, "title": "Terminal"}]);
|
||||
state.focus = json!({"window": 7});
|
||||
state.config.revision = 1;
|
||||
state.theme = json!({"mode": "light"});
|
||||
state.lock.phase = "locked".into();
|
||||
hub.publish(state);
|
||||
let events = receive.try_iter().collect::<Vec<_>>();
|
||||
assert_eq!(events.len(), 7);
|
||||
assert!(events.iter().all(|event| event.generation == events[0].generation));
|
||||
assert!(events[..6].iter().all(|event| !event.last));
|
||||
assert!(events[6].last);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn topology_events_wait_for_publication_and_report_config_commits() {
|
||||
use crate::state::DesktopOutput;
|
||||
use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
|
||||
use smithay::reexports::{calloop::EventLoop, wayland_server::Display};
|
||||
use smithay::utils::Transform;
|
||||
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"ipc_events::tests::topology_events_wait_for_publication_and_report_config_commits",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let mut event_loop = EventLoop::try_new().unwrap();
|
||||
let mut config = crate::config::Config::default().runtime_config().unwrap();
|
||||
config.wallpaper.path = None;
|
||||
let mut state = crate::Ferese::new(&mut event_loop, Display::new().unwrap(), config).unwrap();
|
||||
let output = |name: &str, x| DesktopOutput {
|
||||
output: Output::new(
|
||||
name.into(),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
),
|
||||
identity: name.into(),
|
||||
mode: Mode {
|
||||
size: (800, 600).into(),
|
||||
refresh: 60_000,
|
||||
},
|
||||
transform: Transform::Normal,
|
||||
scale: Scale::Fractional(1.0),
|
||||
position: (x, 0).into(),
|
||||
};
|
||||
let (sender, receive) = sync_channel(QUEUE_CAPACITY);
|
||||
state.ipc_events.subscribe(900, sender).unwrap();
|
||||
state.begin_desktop_transition();
|
||||
let changes = state.publish_desktop(vec![output("a", 0)]).unwrap();
|
||||
state.publish_ipc_events();
|
||||
assert!(matches!(receive.try_recv(), Err(TryRecvError::Empty)));
|
||||
state.finish_desktop_transition(changes);
|
||||
state.publish_ipc_events();
|
||||
let initial = receive.try_recv().unwrap();
|
||||
let Change::Snapshot { desktop } = &initial.change else {
|
||||
panic!("initial snapshot")
|
||||
};
|
||||
assert_eq!(desktop.outputs.as_array().unwrap().len(), 1);
|
||||
assert_eq!(desktop.workspaces[0]["output"], desktop.outputs[0]["id"]);
|
||||
state.begin_desktop_transition();
|
||||
let a = state.current_desktop_outputs().remove(0);
|
||||
let changes = state.publish_desktop(vec![a, output("b", 800)]).unwrap();
|
||||
state.publish_ipc_events();
|
||||
assert!(matches!(receive.try_recv(), Err(TryRecvError::Empty)));
|
||||
state.finish_desktop_transition(changes);
|
||||
state.publish_ipc_events();
|
||||
let events = receive.try_iter().collect::<Vec<_>>();
|
||||
assert!(events.last().unwrap().last);
|
||||
assert!(events.iter().all(|event| event.generation == events[0].generation));
|
||||
let final_state = state.ipc_event_snapshot();
|
||||
assert_eq!(final_state.outputs.as_array().unwrap().len(), 2);
|
||||
for workspace in final_state.workspaces.as_array().unwrap() {
|
||||
assert!(
|
||||
final_state
|
||||
.outputs
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|output| output["id"] == workspace["output"])
|
||||
);
|
||||
}
|
||||
let revision = state.ipc_events.config_revision;
|
||||
state
|
||||
.reload_config_source("appearance { focus-effect { inactive-opacity 0.8; }; }".into())
|
||||
.unwrap();
|
||||
assert_eq!(state.ipc_events.config_revision, revision + 1);
|
||||
state.publish_ipc_events();
|
||||
assert!(
|
||||
receive
|
||||
.try_iter()
|
||||
.any(|event| matches!(event.change, Change::ConfigChanged { .. }))
|
||||
);
|
||||
|
||||
// Negotiate with the real listener and connection worker, then verify
|
||||
// that the initial snapshot reaches the wire after dispatch publication.
|
||||
for version in [VERSION + 1, VERSION] {
|
||||
let socket = ferese_ipc::socket::resolve(None).unwrap();
|
||||
let (completed, completion) = sync_channel(1);
|
||||
let worker = std::thread::spawn(move || {
|
||||
let mut stream = std::os::unix::net::UnixStream::connect(socket).unwrap();
|
||||
stream
|
||||
.set_read_timeout(Some(std::time::Duration::from_secs(2)))
|
||||
.unwrap();
|
||||
ferese_ipc::write_frame(
|
||||
&mut stream,
|
||||
&ferese_ipc::Request {
|
||||
version: ferese_ipc::VERSION,
|
||||
id: 42,
|
||||
kind: "command".into(),
|
||||
command: "event-stream".into(),
|
||||
args: json!({"version": version}),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let response: ferese_ipc::Response = ferese_ipc::read_frame(&mut stream).unwrap();
|
||||
assert_eq!(response.id, 42);
|
||||
if version != VERSION {
|
||||
assert_eq!(response.error.unwrap().code, "unsupported_event_version");
|
||||
} else {
|
||||
assert!(response.error.is_none());
|
||||
let initial: Event = ferese_ipc::read_frame(&mut stream).unwrap();
|
||||
assert!(initial.last);
|
||||
let Change::Snapshot { desktop } = initial.change else {
|
||||
panic!("initial wire snapshot")
|
||||
};
|
||||
assert_eq!(desktop.outputs.as_array().unwrap().len(), 2);
|
||||
}
|
||||
completed.send(()).unwrap();
|
||||
});
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
|
||||
while completion.try_recv().is_err() {
|
||||
assert!(std::time::Instant::now() < deadline, "subscription worker timed out");
|
||||
event_loop
|
||||
.dispatch(std::time::Duration::from_millis(10), &mut state)
|
||||
.unwrap();
|
||||
state.publish_ipc_events();
|
||||
}
|
||||
worker.join().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ mod idle_inhibition;
|
||||
mod input;
|
||||
mod input_capture;
|
||||
mod ipc;
|
||||
mod ipc_events;
|
||||
mod metrics;
|
||||
mod monitor_identity;
|
||||
mod output_policy;
|
||||
@@ -208,6 +209,8 @@ fn after_dispatch_with_redraw(state: &mut Ferese, mut redraw: impl FnMut(&mut Fe
|
||||
warn!(%error, "could not schedule wallpaper upload retry");
|
||||
}
|
||||
|
||||
state.publish_ipc_events();
|
||||
|
||||
// Clients need registry/sync/configure replies even when no frame is drawn.
|
||||
if let Err(error) = state.display_handle.flush_clients() {
|
||||
warn!(%error, "failed to flush Wayland clients");
|
||||
|
||||
@@ -347,6 +347,7 @@ impl crate::Ferese {
|
||||
false
|
||||
}
|
||||
});
|
||||
self.ipc_events.config_revision = self.ipc_events.config_revision.wrapping_add(1);
|
||||
tracing::info!("configuration reloaded live");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -115,6 +115,19 @@ pub(crate) struct Lock {
|
||||
}
|
||||
|
||||
impl Lock {
|
||||
pub(crate) fn event_state(&self) -> ferese_ipc::events::LockState {
|
||||
let phase = match self.lifecycle {
|
||||
Lifecycle::Unlocked => "unlocked",
|
||||
Lifecycle::Acquiring(_) => "acquiring",
|
||||
Lifecycle::Locked(_) => "locked",
|
||||
Lifecycle::Orphaned => "orphaned",
|
||||
};
|
||||
ferese_ipc::events::LockState {
|
||||
phase: phase.into(),
|
||||
sleeping: self.sleeping,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn active(&self) -> bool {
|
||||
!matches!(self.lifecycle, Lifecycle::Unlocked)
|
||||
}
|
||||
|
||||
@@ -689,3 +689,39 @@ fn lock_input_preserves_owner_click_grabs_and_rejects_retired_surfaces() {
|
||||
assert_eq!(pointer.current_focus(), None);
|
||||
assert_eq!(fixture.state.seat.get_keyboard().unwrap().current_focus(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipc_events_follow_lock_lifecycle_without_exposing_desktop_objects() {
|
||||
let Some(mut fixture) =
|
||||
Fixture::new("session_lock::tests::ipc_events_follow_lock_lifecycle_without_exposing_desktop_objects")
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let output = fixture.output("lock-events");
|
||||
let (sender, receive) = std::sync::mpsc::sync_channel(crate::ipc_events::QUEUE_CAPACITY);
|
||||
fixture.state.ipc_events.subscribe(900, sender).unwrap();
|
||||
fixture.state.publish_ipc_events();
|
||||
receive.try_recv().unwrap();
|
||||
let mut owner = fixture.acquire();
|
||||
fixture.state.publish_ipc_events();
|
||||
let snapshot = fixture.state.ipc_event_snapshot();
|
||||
assert!(snapshot.workspaces.as_array().unwrap().is_empty());
|
||||
assert!(snapshot.windows.as_array().unwrap().is_empty());
|
||||
assert!(snapshot.focus.is_null());
|
||||
assert_eq!(snapshot.lock.phase, "acquiring");
|
||||
assert!(receive.try_iter().any(
|
||||
|event| matches!(&event.change, ferese_ipc::events::Change::LockChanged { lock } if lock.phase == "acquiring")
|
||||
));
|
||||
owner.surface(&mut fixture, &output);
|
||||
fixture.state.session_lock.presented.insert(output);
|
||||
fixture.state.confirm_lock_if_ready();
|
||||
fixture.state.publish_ipc_events();
|
||||
assert!(receive.try_iter().any(
|
||||
|event| matches!(&event.change, ferese_ipc::events::Change::LockChanged { lock } if lock.phase == "locked")
|
||||
));
|
||||
fixture.state.unlock();
|
||||
fixture.state.publish_ipc_events();
|
||||
assert!(receive.try_iter().any(
|
||||
|event| matches!(&event.change, ferese_ipc::events::Change::LockChanged { lock } if lock.phase == "unlocked")
|
||||
));
|
||||
}
|
||||
@@ -398,6 +398,7 @@ pub struct Ferese {
|
||||
pub(crate) lock_idle: IdleSettings,
|
||||
pub(crate) config_source: Option<String>,
|
||||
pub(crate) config_sections: Option<serde_json::Value>,
|
||||
pub(crate) ipc_events: crate::ipc_events::Subscribers,
|
||||
pub(crate) config_worker: Option<crate::reload::Worker>,
|
||||
pub(crate) theme_engine: crate::theme::Engine,
|
||||
pub start_time: Instant,
|
||||
@@ -646,6 +647,7 @@ impl Ferese {
|
||||
let mut state = Self {
|
||||
session_lock_state,
|
||||
session_lock: crate::session_lock::Lock::default(),
|
||||
ipc_events: crate::ipc_events::Subscribers::default(),
|
||||
lock_idle: config.lock_idle,
|
||||
config_source: None,
|
||||
config_sections: None,
|
||||
|
||||
Reference in new issue
Block a user