Fix config and IPC lifecycle issues and remove obsolete code
This commit is contained in:
1 parent
8007f7a107
commit
5d9673aad3
14 files changed
+723
-753
No files matched your search
+66
-26
@@ -1,5 +1,7 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -195,27 +197,34 @@ fn values(document: &ferese_config::Document) -> Option<[bool; 7]> {
|
||||
}
|
||||
|
||||
fn load() -> Option<[bool; 7]> {
|
||||
let path = ferese_config::config_path()?;
|
||||
let source = match std::fs::read_to_string(path) {
|
||||
Ok(source) if source.len() <= 1024 * 1024 => source,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
|
||||
_ => return None,
|
||||
};
|
||||
load_path(&ferese_config::config_path()?)
|
||||
}
|
||||
|
||||
fn load_path(path: &Path) -> Option<[bool; 7]> {
|
||||
let mut source = String::new();
|
||||
match std::fs::File::open(path) {
|
||||
Ok(file) => {
|
||||
file.take((ferese_config::file::LIMIT + 1) as u64)
|
||||
.read_to_string(&mut source)
|
||||
.ok()?;
|
||||
if source.len() > ferese_config::file::LIMIT {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(_) => return None,
|
||||
}
|
||||
values(&ferese_config::Document::parse(&source).ok()?)
|
||||
}
|
||||
|
||||
fn store(index: usize, value: bool) -> Result<(), String> {
|
||||
let _transaction = crate::backend::CONFIG_TRANSACTION
|
||||
.lock()
|
||||
.map_err(|_| "Configuration transaction failed")?;
|
||||
let path = ferese_config::config_path().ok_or("Missing config directory")?;
|
||||
let source = match std::fs::read_to_string(&path) {
|
||||
Ok(source) if source.len() <= 1024 * 1024 => source,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
|
||||
Err(error) => return Err(error.to_string()),
|
||||
_ => return Err("Configuration is too large".into()),
|
||||
};
|
||||
let mut document = ferese_config::Document::parse(&source).map_err(|error| error.to_string())?;
|
||||
store_path(&path, index, value)
|
||||
}
|
||||
|
||||
fn store_path(path: &Path, index: usize, value: bool) -> Result<(), String> {
|
||||
let file = ferese_config::file::ConfigFile::open(path)?;
|
||||
let mut document = ferese_config::Document::parse(file.source()).map_err(|error| error.to_string())?;
|
||||
if values(&document).is_none() {
|
||||
return Err("Invalid lockdown policy".into());
|
||||
}
|
||||
@@ -225,22 +234,53 @@ fn store(index: usize, value: bool) -> Result<(), String> {
|
||||
value.into(),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let directory = path.parent().ok_or("Invalid config path")?;
|
||||
std::fs::create_dir_all(directory).map_err(|error| error.to_string())?;
|
||||
let mut file = tempfile::NamedTempFile::new_in(directory).map_err(|error| error.to_string())?;
|
||||
std::io::Write::write_all(&mut file, document.to_string().as_bytes()).map_err(|error| error.to_string())?;
|
||||
file.as_file().sync_all().map_err(|error| error.to_string())?;
|
||||
if std::fs::read_to_string(&path).unwrap_or_default() != source {
|
||||
return Err("Configuration changed; please try again".into());
|
||||
}
|
||||
file.persist(path).map_err(|error| error.to_string())?;
|
||||
Ok(())
|
||||
file.replace(&document.to_string(), None, |_| Ok(()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn policy_save_respects_other_config_writers_and_preserves_their_changes() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("config.kdl");
|
||||
let settings = ferese_config::file::ConfigFile::open(&path).unwrap();
|
||||
assert!(store_path(&path, 4, true).unwrap_err().contains("another app"));
|
||||
assert!(!path.exists());
|
||||
settings.replace("theme \"light\"\n", None, |_| Ok(())).unwrap();
|
||||
|
||||
store_path(&path, 4, true).unwrap();
|
||||
let source = std::fs::read_to_string(&path).unwrap();
|
||||
let document = ferese_config::Document::parse(&source).unwrap();
|
||||
assert_eq!(document.get("theme").and_then(|value| value.as_str()), Some("light"));
|
||||
assert!(load_path(&path).unwrap()[4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_policy_files_and_saves_leave_config_untouched() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("config.kdl");
|
||||
for size in [ferese_config::file::LIMIT, ferese_config::file::LIMIT + 1] {
|
||||
let source = format!("//{}\n", "x".repeat(size - 3));
|
||||
std::fs::write(&path, &source).unwrap();
|
||||
assert_eq!(load_path(&path).is_some(), size == ferese_config::file::LIMIT);
|
||||
assert!(store_path(&path, 4, true).unwrap_err().contains("60 KiB"));
|
||||
assert_eq!(std::fs::read_to_string(&path).unwrap(), source);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_policy_is_not_overwritten() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("config.kdl");
|
||||
let source = "portals { lockdown { disable-camera \"yes\"; }; }";
|
||||
std::fs::write(&path, source).unwrap();
|
||||
assert_eq!(store_path(&path, 4, true).unwrap_err(), "Invalid lockdown policy");
|
||||
assert_eq!(std::fs::read_to_string(&path).unwrap(), source);
|
||||
assert!(ferese_config::file::ConfigFile::open(&path).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_policy_is_unrestricted_and_wrong_types_are_rejected() {
|
||||
let empty = ferese_config::Document::parse("").unwrap();
|
||||
|
||||
+227
-8
@@ -3,6 +3,8 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::UnixStream;
|
||||
use tokio::sync::RwLock;
|
||||
use zbus::Connection;
|
||||
use zbus::message::Header;
|
||||
@@ -10,7 +12,7 @@ use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value};
|
||||
|
||||
use crate::backend::{Options, authorize};
|
||||
use crate::desktop::{Requests, consent, ipc, reply};
|
||||
use crate::desktop::{Requests, consent, reply};
|
||||
|
||||
const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
|
||||
@@ -127,20 +129,117 @@ impl Background {
|
||||
pub(crate) async fn watch(self, connection: Connection) {
|
||||
let emitter = SignalEmitter::new(&connection, PATH).unwrap();
|
||||
loop {
|
||||
if let Ok(windows) = ipc("get-windows", serde_json::json!({})).await {
|
||||
let next = app_states(&windows);
|
||||
let mut previous = self.states.write().await;
|
||||
if *previous != next {
|
||||
*previous = next;
|
||||
drop(previous);
|
||||
let _ = Self::running_applications_changed(&emitter).await;
|
||||
let result = async {
|
||||
let path = ferese_ipc::socket::resolve(None).map_err(|error| error.to_string())?;
|
||||
let stream = UnixStream::connect(path).await.map_err(|error| error.to_string())?;
|
||||
let mut events = BackgroundEvents::subscribe(stream).await?;
|
||||
loop {
|
||||
let next = match events.next().await {
|
||||
Ok(next) => next,
|
||||
Err(error) => break Err::<(), String>(error),
|
||||
};
|
||||
if let Some(next) = next {
|
||||
let mut previous = self.states.write().await;
|
||||
if *previous != next {
|
||||
*previous = next;
|
||||
drop(previous);
|
||||
let _ = Self::running_applications_changed(&emitter).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
eprintln!("ferese portal: background event stream: {error}");
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tokio owns the socket so cancelling the watch task also closes an idle stream.
|
||||
struct BackgroundEvents {
|
||||
stream: UnixStream,
|
||||
windows: serde_json::Value,
|
||||
changed: bool,
|
||||
}
|
||||
|
||||
impl BackgroundEvents {
|
||||
async fn subscribe(mut stream: UnixStream) -> Result<Self, String> {
|
||||
let request = ferese_ipc::Request {
|
||||
version: ferese_ipc::VERSION,
|
||||
id: 1,
|
||||
kind: "command".into(),
|
||||
command: "event-stream".into(),
|
||||
args: serde_json::json!({"version": ferese_ipc::events::VERSION}),
|
||||
};
|
||||
let mut bytes = Vec::new();
|
||||
ferese_ipc::write_frame(&mut bytes, &request).map_err(|error| error.to_string())?;
|
||||
tokio::time::timeout(Duration::from_secs(2), async {
|
||||
stream.write_all(&bytes).await.map_err(|error| error.to_string())?;
|
||||
let response: ferese_ipc::Response = read_event_frame(&mut stream).await?;
|
||||
if response.version != ferese_ipc::VERSION || response.id != request.id {
|
||||
return Err("Unexpected event subscription response".into());
|
||||
}
|
||||
if let Some(error) = response.error {
|
||||
return Err(error.message);
|
||||
}
|
||||
if response.result.as_ref().and_then(|value| value["version"].as_u64())
|
||||
!= Some(u64::from(ferese_ipc::events::VERSION))
|
||||
{
|
||||
return Err("Unsupported event stream version".into());
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "Event subscription timed out".to_owned())??;
|
||||
Ok(Self {
|
||||
stream,
|
||||
windows: serde_json::json!([]),
|
||||
changed: false,
|
||||
})
|
||||
}
|
||||
|
||||
async fn next(&mut self) -> Result<Option<HashMap<String, u32>>, String> {
|
||||
use ferese_ipc::events::{Change, Event};
|
||||
let event: Event = read_event_frame(&mut self.stream).await?;
|
||||
event.validate()?;
|
||||
match event.change {
|
||||
Change::Snapshot { desktop } => {
|
||||
self.windows = desktop.windows;
|
||||
self.changed = true;
|
||||
}
|
||||
Change::WindowsChanged { windows } => {
|
||||
self.windows = windows;
|
||||
self.changed = true;
|
||||
}
|
||||
Change::FocusChanged { focus } => {
|
||||
for window in self.windows.as_array_mut().into_iter().flatten() {
|
||||
window["focused"] =
|
||||
serde_json::json!(focus["window"].is_number() && window["id"] == focus["window"]);
|
||||
}
|
||||
self.changed = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if event.last && std::mem::take(&mut self.changed) {
|
||||
Ok(Some(app_states(&self.windows)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_event_frame<T: serde::de::DeserializeOwned>(stream: &mut UnixStream) -> Result<T, String> {
|
||||
let size = stream.read_u32().await.map_err(|error| error.to_string())? as usize;
|
||||
if size > ferese_ipc::MAX_PAYLOAD_SIZE {
|
||||
return Err("Oversized IPC event frame".into());
|
||||
}
|
||||
let mut bytes = vec![0; size];
|
||||
stream.read_exact(&mut bytes).await.map_err(|error| error.to_string())?;
|
||||
serde_json::from_slice(&bytes).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[zbus::interface(name = "org.freedesktop.impl.portal.Background")]
|
||||
impl Background {
|
||||
async fn get_app_state(
|
||||
@@ -323,6 +422,126 @@ fn desktop_argument(argument: &str) -> Result<String, String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn send_frame(peer: &mut UnixStream, value: &impl serde::Serialize) {
|
||||
let mut bytes = Vec::new();
|
||||
ferese_ipc::write_frame(&mut bytes, value).unwrap();
|
||||
peer.write_all(&bytes).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_events_apply_complete_generations_and_close_on_drop() {
|
||||
use ferese_ipc::events::{Change, Event};
|
||||
let (client, mut peer) = UnixStream::pair().unwrap();
|
||||
let server = tokio::spawn(async move {
|
||||
let request: ferese_ipc::Request = read_event_frame(&mut peer).await.unwrap();
|
||||
assert_eq!(request.command, "event-stream");
|
||||
assert_eq!(
|
||||
request.args,
|
||||
serde_json::json!({"version": ferese_ipc::events::VERSION})
|
||||
);
|
||||
send_frame(&mut peer, &ferese_ipc::Response::success(request.id, request.args)).await;
|
||||
peer
|
||||
});
|
||||
let mut events = BackgroundEvents::subscribe(client).await.unwrap();
|
||||
let mut peer = server.await.unwrap();
|
||||
send_frame(
|
||||
&mut peer,
|
||||
&Event {
|
||||
version: ferese_ipc::events::VERSION,
|
||||
generation: 0,
|
||||
last: true,
|
||||
change: Change::Snapshot {
|
||||
desktop: Box::new(ferese_ipc::events::Snapshot {
|
||||
outputs: serde_json::json!([]),
|
||||
workspaces: serde_json::json!([]),
|
||||
windows: serde_json::json!([{"id":1,"app_id":"one.desktop","focused":true}]),
|
||||
focus: serde_json::json!({"window":1}),
|
||||
config: Default::default(),
|
||||
theme: serde_json::Value::Null,
|
||||
lock: ferese_ipc::events::LockState {
|
||||
phase: "unlocked".into(),
|
||||
sleeping: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
events.next().await.unwrap().unwrap(),
|
||||
HashMap::from([("one".into(), 2)])
|
||||
);
|
||||
let windows = serde_json::json!([
|
||||
{"id":1,"app_id":"one.desktop","focused":true},
|
||||
{"id":2,"app_id":"two","focused":false}
|
||||
]);
|
||||
send_frame(
|
||||
&mut peer,
|
||||
&Event {
|
||||
version: ferese_ipc::events::VERSION,
|
||||
generation: 1,
|
||||
last: false,
|
||||
change: Change::WindowsChanged { windows },
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert!(events.next().await.unwrap().is_none());
|
||||
send_frame(
|
||||
&mut peer,
|
||||
&Event {
|
||||
version: ferese_ipc::events::VERSION,
|
||||
generation: 1,
|
||||
last: true,
|
||||
change: Change::FocusChanged {
|
||||
focus: serde_json::json!({"window":2}),
|
||||
},
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
events.next().await.unwrap().unwrap(),
|
||||
HashMap::from([("one".into(), 1), ("two".into(), 2)])
|
||||
);
|
||||
// No command is sent while waiting for a change on an idle desktop.
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_millis(20), peer.read_u32())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
send_frame(
|
||||
&mut peer,
|
||||
&Event {
|
||||
version: ferese_ipc::events::VERSION,
|
||||
generation: 2,
|
||||
last: true,
|
||||
change: Change::WindowsChanged {
|
||||
windows: serde_json::json!([]),
|
||||
},
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert!(events.next().await.unwrap().unwrap().is_empty());
|
||||
drop(events);
|
||||
assert_eq!(
|
||||
peer.read_u8().await.unwrap_err().kind(),
|
||||
std::io::ErrorKind::UnexpectedEof
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_events_reject_oversized_frames_and_disconnects() {
|
||||
let (stream, mut peer) = UnixStream::pair().unwrap();
|
||||
let mut events = BackgroundEvents {
|
||||
stream,
|
||||
windows: serde_json::json!([]),
|
||||
changed: false,
|
||||
};
|
||||
peer.write_u32((ferese_ipc::MAX_PAYLOAD_SIZE + 1) as u32).await.unwrap();
|
||||
assert!(events.next().await.unwrap_err().contains("Oversized"));
|
||||
drop(peer);
|
||||
assert!(events.next().await.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_state_groups_windows_and_keeps_inactive_workspaces_running() {
|
||||
let windows = serde_json::json!([
|
||||
|
||||
Reference in new issue
Block a user