Isolate nested IPC sockets and define session ownership policy
This commit is contained in:
1 parent
97026a0db7
commit
ce024d4794
42 files changed
+635
-147
No files matched your search
@@ -7,6 +7,7 @@ use smithay::reexports::calloop::EventLoop;
|
|||||||
|
|
||||||
use crate::Ferese;
|
use crate::Ferese;
|
||||||
use crate::private_client::ClientCapabilities;
|
use crate::private_client::ClientCapabilities;
|
||||||
|
use crate::session_environment::SessionPolicy;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum BackendKind {
|
pub enum BackendKind {
|
||||||
@@ -17,6 +18,7 @@ pub enum BackendKind {
|
|||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub struct LaunchConfig {
|
pub struct LaunchConfig {
|
||||||
pub backend: BackendKind,
|
pub backend: BackendKind,
|
||||||
|
pub(crate) session_policy: SessionPolicy,
|
||||||
pub client: Vec<OsString>,
|
pub client: Vec<OsString>,
|
||||||
pub client_capabilities: ClientCapabilities,
|
pub client_capabilities: ClientCapabilities,
|
||||||
}
|
}
|
||||||
@@ -75,6 +77,10 @@ impl LaunchConfig {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
backend,
|
backend,
|
||||||
|
session_policy: match backend {
|
||||||
|
BackendKind::Drm => SessionPolicy::Desktop,
|
||||||
|
BackendKind::Nested => SessionPolicy::Embedded,
|
||||||
|
},
|
||||||
client,
|
client,
|
||||||
client_capabilities,
|
client_capabilities,
|
||||||
})
|
})
|
||||||
@@ -110,6 +116,7 @@ mod tests {
|
|||||||
let config = LaunchConfig::parse([OsString::from("foot")], true).unwrap();
|
let config = LaunchConfig::parse([OsString::from("foot")], true).unwrap();
|
||||||
|
|
||||||
assert_eq!(config.backend, BackendKind::Nested);
|
assert_eq!(config.backend, BackendKind::Nested);
|
||||||
|
assert_eq!(config.session_policy, SessionPolicy::Embedded);
|
||||||
assert_eq!(config.client, [OsString::from("foot")]);
|
assert_eq!(config.client, [OsString::from("foot")]);
|
||||||
assert!(config.client_capabilities.is_empty());
|
assert!(config.client_capabilities.is_empty());
|
||||||
}
|
}
|
||||||
@@ -119,6 +126,7 @@ mod tests {
|
|||||||
let config = LaunchConfig::parse(Vec::<OsString>::new(), false).unwrap();
|
let config = LaunchConfig::parse(Vec::<OsString>::new(), false).unwrap();
|
||||||
|
|
||||||
assert_eq!(config.backend, BackendKind::Drm);
|
assert_eq!(config.backend, BackendKind::Drm);
|
||||||
|
assert_eq!(config.session_policy, SessionPolicy::Desktop);
|
||||||
assert!(config.client.is_empty());
|
assert!(config.client.is_empty());
|
||||||
assert!(config.client_capabilities.is_empty());
|
assert!(config.client_capabilities.is_empty());
|
||||||
}
|
}
|
||||||
@@ -178,4 +186,15 @@ mod tests {
|
|||||||
|
|
||||||
assert!(error.contains("unknown backend"));
|
assert!(error.contains("unknown backend"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_backend_selects_the_default_session_policy() {
|
||||||
|
for (backend, graphical, policy) in [
|
||||||
|
("--backend=drm", true, SessionPolicy::Desktop),
|
||||||
|
("--backend=nested", false, SessionPolicy::Embedded),
|
||||||
|
] {
|
||||||
|
let config = LaunchConfig::parse([OsString::from(backend)], graphical).unwrap();
|
||||||
|
assert_eq!(config.session_policy, policy);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+69
-16
@@ -9,10 +9,10 @@ use smithay::reexports::calloop::generic::Generic;
|
|||||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||||
use smithay::reexports::calloop::{Interest, Mode, PostAction, RegistrationToken};
|
use smithay::reexports::calloop::{Interest, Mode, PostAction, RegistrationToken};
|
||||||
|
|
||||||
use crate::Ferese;
|
|
||||||
use crate::config::DaemonConfig;
|
use crate::config::DaemonConfig;
|
||||||
use crate::private_client::ClientCapabilities;
|
use crate::private_client::ClientCapabilities;
|
||||||
use crate::process::{pidfd, spawn_client, terminate_child};
|
use crate::process::{pidfd, spawn_client, terminate_child};
|
||||||
|
use crate::{Ferese, SessionPolicy};
|
||||||
|
|
||||||
struct Service {
|
struct Service {
|
||||||
config: DaemonConfig,
|
config: DaemonConfig,
|
||||||
@@ -31,7 +31,7 @@ impl Runner {
|
|||||||
pub fn start(state: &mut Ferese) -> Rc<RefCell<Self>> {
|
pub fn start(state: &mut Ferese) -> Rc<RefCell<Self>> {
|
||||||
let runner = Rc::new(RefCell::new(Self::new(
|
let runner = Rc::new(RefCell::new(Self::new(
|
||||||
&state.autostart,
|
&state.autostart,
|
||||||
state.direct_backend.is_none(),
|
state.session_environment.policy,
|
||||||
)));
|
)));
|
||||||
Self::refresh(&runner, state);
|
Self::refresh(&runner, state);
|
||||||
runner
|
runner
|
||||||
@@ -114,9 +114,12 @@ impl Runner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reconcile(&mut self, configs: &[DaemonConfig], nested: bool) {
|
pub fn reconcile(&mut self, configs: &[DaemonConfig], policy: SessionPolicy) {
|
||||||
let mut previous = std::mem::take(&mut self.0);
|
let mut previous = std::mem::take(&mut self.0);
|
||||||
for config in configs.iter().filter(|c| c.enabled && (!nested || c.nested)) {
|
for config in configs
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.enabled && policy.allows_autostart(c.nested))
|
||||||
|
{
|
||||||
if self.0.iter().any(|service| service.config.command == config.command) {
|
if self.0.iter().any(|service| service.config.command == config.command) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -148,11 +151,11 @@ impl Runner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(configs: &[DaemonConfig], nested: bool) -> Self {
|
pub fn new(configs: &[DaemonConfig], policy: SessionPolicy) -> Self {
|
||||||
Self(
|
Self(
|
||||||
configs
|
configs
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|config| config.enabled && (!nested || config.nested))
|
.filter(|config| config.enabled && policy.allows_autostart(config.nested))
|
||||||
.map(|config| Service {
|
.map(|config| Service {
|
||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
child: None,
|
child: None,
|
||||||
@@ -166,7 +169,7 @@ impl Runner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn tick(&mut self, state: &mut Ferese) {
|
pub fn tick(&mut self, state: &mut Ferese) {
|
||||||
self.reconcile(&state.autostart, state.direct_backend.is_none());
|
self.reconcile(&state.autostart, state.session_environment.policy);
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
for service in &mut self.0 {
|
for service in &mut self.0 {
|
||||||
if let Some(child) = &mut service.child {
|
if let Some(child) = &mut service.child {
|
||||||
@@ -244,17 +247,17 @@ mod tests {
|
|||||||
restart: false,
|
restart: false,
|
||||||
nested: false,
|
nested: false,
|
||||||
};
|
};
|
||||||
let mut runner = Runner::new(&[config.clone()], false);
|
let mut runner = Runner::new(&[config.clone()], SessionPolicy::Desktop);
|
||||||
runner.0[0].child = Some(std::process::Command::new("/bin/sleep").arg("30").spawn().unwrap());
|
runner.0[0].child = Some(std::process::Command::new("/bin/sleep").arg("30").spawn().unwrap());
|
||||||
let pid = runner.0[0].child.as_ref().unwrap().id();
|
let pid = runner.0[0].child.as_ref().unwrap().id();
|
||||||
config.restart = true;
|
config.restart = true;
|
||||||
runner.reconcile(&[config.clone()], false);
|
runner.reconcile(&[config.clone()], SessionPolicy::Desktop);
|
||||||
assert_eq!(runner.0[0].child.as_ref().unwrap().id(), pid);
|
assert_eq!(runner.0[0].child.as_ref().unwrap().id(), pid);
|
||||||
assert!(runner.0[0].config.restart);
|
assert!(runner.0[0].config.restart);
|
||||||
runner.reconcile(&[config.clone(), config.clone()], false);
|
runner.reconcile(&[config.clone(), config.clone()], SessionPolicy::Desktop);
|
||||||
assert_eq!(runner.0.len(), 1);
|
assert_eq!(runner.0.len(), 1);
|
||||||
config.enabled = false;
|
config.enabled = false;
|
||||||
runner.reconcile(&[config], false);
|
runner.reconcile(&[config], SessionPolicy::Desktop);
|
||||||
assert!(runner.0.is_empty());
|
assert!(runner.0.is_empty());
|
||||||
let deadline = Instant::now() + Duration::from_secs(2);
|
let deadline = Instant::now() + Duration::from_secs(2);
|
||||||
while unsafe { libc::kill(pid as i32, 0) } == 0 {
|
while unsafe { libc::kill(pid as i32, 0) } == 0 {
|
||||||
@@ -271,22 +274,72 @@ mod tests {
|
|||||||
restart: true,
|
restart: true,
|
||||||
nested: false,
|
nested: false,
|
||||||
}];
|
}];
|
||||||
assert!(Runner::new(&configs, true).0.is_empty());
|
assert!(Runner::new(&configs, SessionPolicy::Embedded).0.is_empty());
|
||||||
let mut direct = Runner::new(&configs, false);
|
let mut direct = Runner::new(&configs, SessionPolicy::Desktop);
|
||||||
assert_eq!(direct.0.len(), 1);
|
assert_eq!(direct.0.len(), 1);
|
||||||
direct.stop();
|
direct.stop();
|
||||||
assert!(direct.0[0].finished);
|
assert!(direct.0[0].finished);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn disabled_login_items_never_start_on_either_backend() {
|
fn disabled_login_items_never_start_under_either_session_policy() {
|
||||||
let configs = vec![DaemonConfig {
|
let configs = vec![DaemonConfig {
|
||||||
command: vec!["not-executed".into()],
|
command: vec!["not-executed".into()],
|
||||||
enabled: false,
|
enabled: false,
|
||||||
restart: true,
|
restart: true,
|
||||||
nested: true,
|
nested: true,
|
||||||
}];
|
}];
|
||||||
assert!(Runner::new(&configs, true).0.is_empty());
|
assert!(Runner::new(&configs, SessionPolicy::Embedded).0.is_empty());
|
||||||
assert!(Runner::new(&configs, false).0.is_empty());
|
assert!(Runner::new(&configs, SessionPolicy::Desktop).0.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embedded_autostart_requires_opt_in_on_start_and_reload() {
|
||||||
|
let regular = DaemonConfig {
|
||||||
|
command: vec!["regular".into()],
|
||||||
|
enabled: true,
|
||||||
|
restart: false,
|
||||||
|
nested: false,
|
||||||
|
};
|
||||||
|
let embedded = DaemonConfig {
|
||||||
|
command: vec!["embedded".into()],
|
||||||
|
nested: true,
|
||||||
|
..regular.clone()
|
||||||
|
};
|
||||||
|
let configs = [regular, embedded.clone()];
|
||||||
|
let mut runner = Runner::new(&configs, SessionPolicy::Embedded);
|
||||||
|
assert_eq!(runner.0.len(), 1);
|
||||||
|
assert_eq!(runner.0[0].config, embedded);
|
||||||
|
runner.reconcile(&configs, SessionPolicy::Desktop);
|
||||||
|
assert_eq!(runner.0.len(), 2);
|
||||||
|
runner.reconcile(&configs, SessionPolicy::Embedded);
|
||||||
|
assert_eq!(runner.0.len(), 1);
|
||||||
|
assert_eq!(runner.0[0].config, embedded);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn desktop_autostart_does_not_depend_on_a_live_drm_backend() {
|
||||||
|
if !crate::startup_tests::private_runtime(
|
||||||
|
"daemon::tests::desktop_autostart_does_not_depend_on_a_live_drm_backend",
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
||||||
|
let mut state = crate::startup_tests::state(&mut events);
|
||||||
|
assert!(state.direct_backend.is_none());
|
||||||
|
assert_eq!(state.session_environment.policy, SessionPolicy::Desktop);
|
||||||
|
state.autostart = vec![DaemonConfig {
|
||||||
|
command: vec!["/bin/sleep".into(), "30".into()],
|
||||||
|
enabled: true,
|
||||||
|
restart: false,
|
||||||
|
nested: false,
|
||||||
|
}];
|
||||||
|
let runner = Runner::start(&mut state);
|
||||||
|
assert_eq!(runner.borrow().0.len(), 1);
|
||||||
|
assert!(runner.borrow().0[0].child.is_some());
|
||||||
|
runner.borrow_mut().stop();
|
||||||
|
state.session_environment.policy = SessionPolicy::Embedded;
|
||||||
|
Runner::refresh(&runner, &mut state);
|
||||||
|
assert!(runner.borrow().0.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+98
-12
@@ -8,7 +8,7 @@ use std::sync::Arc;
|
|||||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||||
use std::sync::mpsc::{SyncSender, sync_channel};
|
use std::sync::mpsc::{SyncSender, sync_channel};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{env, fs, io, thread};
|
use std::{fs, io, thread};
|
||||||
|
|
||||||
use ferese_core::LayoutMode;
|
use ferese_core::LayoutMode;
|
||||||
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
|
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
|
||||||
@@ -78,6 +78,41 @@ pub(crate) struct IpcSocketGuard {
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
device: u64,
|
device: u64,
|
||||||
inode: u64,
|
inode: u64,
|
||||||
|
_instance: Option<tempfile::TempDir>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Endpoint {
|
||||||
|
pub(crate) path: PathBuf,
|
||||||
|
instance: Option<tempfile::TempDir>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Endpoint {
|
||||||
|
pub(crate) fn desktop() -> io::Result<Self> {
|
||||||
|
Self::desktop_at(ferese_ipc::socket::runtime_default()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desktop_at(path: PathBuf) -> io::Result<Self> {
|
||||||
|
prepare_parent(&path)?;
|
||||||
|
Ok(Self { path, instance: None })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn instance() -> io::Result<Self> {
|
||||||
|
Self::instance_at(ferese_ipc::socket::runtime_default()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instance_at(default: PathBuf) -> io::Result<Self> {
|
||||||
|
let root = default.parent().expect("runtime control socket has a parent");
|
||||||
|
prepare_directory(root)?;
|
||||||
|
let instances = root.join("instances");
|
||||||
|
prepare_directory(&instances)?;
|
||||||
|
let instance = tempfile::Builder::new()
|
||||||
|
.prefix(&format!("{}-", std::process::id()))
|
||||||
|
.tempdir_in(instances)?;
|
||||||
|
Ok(Self {
|
||||||
|
path: instance.path().join("control.sock"),
|
||||||
|
instance: Some(instance),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for IpcSocketGuard {
|
impl Drop for IpcSocketGuard {
|
||||||
@@ -90,8 +125,11 @@ impl Drop for IpcSocketGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result<ScreenshotInit, Box<dyn std::error::Error>> {
|
pub(crate) fn init(
|
||||||
let path = socket_path()?;
|
event_loop: &mut EventLoop<'static, Ferese>,
|
||||||
|
endpoint: Endpoint,
|
||||||
|
) -> Result<ScreenshotInit, Box<dyn std::error::Error>> {
|
||||||
|
let Endpoint { path, instance } = endpoint;
|
||||||
prepare_parent(&path)?;
|
prepare_parent(&path)?;
|
||||||
let listener = bind_listener(&path)?;
|
let listener = bind_listener(&path)?;
|
||||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
|
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
|
||||||
@@ -100,6 +138,7 @@ pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result<Screen
|
|||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
device: metadata.dev(),
|
device: metadata.dev(),
|
||||||
inode: metadata.ino(),
|
inode: metadata.ino(),
|
||||||
|
_instance: instance,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (sender, receiver): (channel::SyncSender<IpcEvent>, channel::Channel<IpcEvent>) =
|
let (sender, receiver): (channel::SyncSender<IpcEvent>, channel::Channel<IpcEvent>) =
|
||||||
@@ -1531,16 +1570,17 @@ fn workspace_arg(args: &Value) -> Result<u32, CommandError> {
|
|||||||
.ok_or_else(|| CommandError::new("invalid_argument", "index must be a positive 32-bit integer"))
|
.ok_or_else(|| CommandError::new("invalid_argument", "index must be a positive 32-bit integer"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn socket_path() -> Result<PathBuf, io::Error> {
|
|
||||||
env::var_os("XDG_RUNTIME_DIR")
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.map(|directory| directory.join("ferese/control.sock"))
|
|
||||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "XDG_RUNTIME_DIR is not set"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_parent(socket: &Path) -> Result<(), io::Error> {
|
fn prepare_parent(socket: &Path) -> Result<(), io::Error> {
|
||||||
let parent = socket.parent().expect("the Ferese control socket always has a parent");
|
let parent = socket.parent().expect("the Ferese control socket always has a parent");
|
||||||
|
prepare_directory(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_directory(parent: &Path) -> Result<(), io::Error> {
|
||||||
|
match fs::create_dir(parent) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
match fs::symlink_metadata(parent) {
|
match fs::symlink_metadata(parent) {
|
||||||
Ok(metadata) => {
|
Ok(metadata) => {
|
||||||
if !metadata.is_dir() || metadata.file_type().is_symlink() {
|
if !metadata.is_dir() || metadata.file_type().is_symlink() {
|
||||||
@@ -1556,7 +1596,6 @@ fn prepare_parent(socket: &Path) -> Result<(), io::Error> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(error) if error.kind() == io::ErrorKind::NotFound => fs::create_dir(parent)?,
|
|
||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1613,6 +1652,7 @@ fn effective_uid() -> u32 {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::env;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::Shutdown;
|
use std::net::Shutdown;
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
@@ -1818,6 +1858,52 @@ mod tests {
|
|||||||
fs::remove_dir_all(root).unwrap();
|
fs::remove_dir_all(root).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_endpoints_reserve_private_unique_directories_and_preserve_the_default() {
|
||||||
|
let runtime = tempfile::tempdir().unwrap();
|
||||||
|
let default = runtime.path().join("ferese/control.sock");
|
||||||
|
let drm = Endpoint::desktop_at(default.clone()).unwrap();
|
||||||
|
let host = bind_listener(&drm.path).unwrap();
|
||||||
|
let first = Endpoint::instance_at(default.clone()).unwrap();
|
||||||
|
let second = Endpoint::instance_at(default.clone()).unwrap();
|
||||||
|
assert_eq!(drm.path, default);
|
||||||
|
assert_ne!(first.path, second.path);
|
||||||
|
assert_eq!(
|
||||||
|
first.path.parent().unwrap().parent().unwrap(),
|
||||||
|
default.parent().unwrap().join("instances")
|
||||||
|
);
|
||||||
|
for endpoint in [&first, &second] {
|
||||||
|
assert_eq!(
|
||||||
|
fs::metadata(endpoint.path.parent().unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.permissions()
|
||||||
|
.mode()
|
||||||
|
& 0o777,
|
||||||
|
0o700
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let first_directory = first.path.parent().unwrap().to_owned();
|
||||||
|
drop(first);
|
||||||
|
assert!(!first_directory.exists());
|
||||||
|
assert!(second.path.parent().unwrap().is_dir());
|
||||||
|
assert!(default.symlink_metadata().unwrap().file_type().is_socket());
|
||||||
|
assert_eq!(bind_listener(&default).unwrap_err().kind(), io::ErrorKind::AddrInUse);
|
||||||
|
drop(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_endpoint_rejects_an_instances_symlink() {
|
||||||
|
let runtime = tempfile::tempdir().unwrap();
|
||||||
|
let default = runtime.path().join("ferese/control.sock");
|
||||||
|
let _drm = Endpoint::desktop_at(default.clone()).unwrap();
|
||||||
|
let outside = runtime.path().join("outside");
|
||||||
|
fs::create_dir(&outside).unwrap();
|
||||||
|
fs::set_permissions(&outside, fs::Permissions::from_mode(0o755)).unwrap();
|
||||||
|
std::os::unix::fs::symlink(&outside, default.parent().unwrap().join("instances")).unwrap();
|
||||||
|
assert!(Endpoint::instance_at(default).is_err());
|
||||||
|
assert_eq!(fs::metadata(outside).unwrap().permissions().mode() & 0o777, 0o755);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn listener_reclaims_only_a_stale_owned_socket() {
|
fn listener_reclaims_only_a_stale_owned_socket() {
|
||||||
let root = unique_test_directory("stale-socket");
|
let root = unique_test_directory("stale-socket");
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ mod window_rules;
|
|||||||
mod winit;
|
mod winit;
|
||||||
mod xwayland;
|
mod xwayland;
|
||||||
|
|
||||||
pub(crate) use session_environment::{SessionEnvironment, X11Environment};
|
pub(crate) use session_environment::{SessionEnvironment, SessionPolicy, X11Environment};
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -95,7 +95,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
state.loop_signal.stop();
|
state.loop_signal.stop();
|
||||||
})?;
|
})?;
|
||||||
let display = Display::new()?;
|
let display = Display::new()?;
|
||||||
let mut state = Ferese::new(&mut event_loop, display, runtime)?;
|
let endpoint = match launch.backend {
|
||||||
|
backends::BackendKind::Drm => ipc::Endpoint::desktop()?,
|
||||||
|
backends::BackendKind::Nested => ipc::Endpoint::instance()?,
|
||||||
|
};
|
||||||
|
let mut state = Ferese::new_with_session(&mut event_loop, display, runtime, endpoint, launch.session_policy)?;
|
||||||
state.config_source = initial_source.filter(|source| source.len() <= 60 * 1024);
|
state.config_source = initial_source.filter(|source| source.len() <= 60 * 1024);
|
||||||
state.config_sections = state
|
state.config_sections = state
|
||||||
.config_source
|
.config_source
|
||||||
|
|||||||
@@ -8,10 +8,31 @@ pub(crate) struct X11Environment {
|
|||||||
pub authority: PathBuf,
|
pub authority: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum SessionPolicy {
|
||||||
|
Desktop,
|
||||||
|
Embedded,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionPolicy {
|
||||||
|
fn environment_value(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Desktop => "desktop",
|
||||||
|
Self::Embedded => "embedded",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn allows_autostart(self, embedded_opt_in: bool) -> bool {
|
||||||
|
self == Self::Desktop || embedded_opt_in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Child endpoints are separate from the host endpoints used by the nested backend.
|
// Child endpoints are separate from the host endpoints used by the nested backend.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub(crate) struct SessionEnvironment {
|
pub(crate) struct SessionEnvironment {
|
||||||
pub wayland_display: OsString,
|
pub wayland_display: OsString,
|
||||||
|
pub control_socket: PathBuf,
|
||||||
|
pub policy: SessionPolicy,
|
||||||
pub x11: Option<X11Environment>,
|
pub x11: Option<X11Environment>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +42,8 @@ impl SessionEnvironment {
|
|||||||
command
|
command
|
||||||
.env("WAYLAND_DISPLAY", &self.wayland_display)
|
.env("WAYLAND_DISPLAY", &self.wayland_display)
|
||||||
.env("FERESE_PUBLIC_WAYLAND_DISPLAY", &self.wayland_display)
|
.env("FERESE_PUBLIC_WAYLAND_DISPLAY", &self.wayland_display)
|
||||||
|
.env("FERESE_SOCKET", &self.control_socket)
|
||||||
|
.env("FERESE_SESSION_MODE", self.policy.environment_value())
|
||||||
.env_remove("WAYLAND_SOCKET")
|
.env_remove("WAYLAND_SOCKET")
|
||||||
.env_remove("FERESE_SHELL_CONTROL_SOCKET")
|
.env_remove("FERESE_SHELL_CONTROL_SOCKET")
|
||||||
.env_remove("DISPLAY")
|
.env_remove("DISPLAY")
|
||||||
@@ -29,6 +52,9 @@ impl SessionEnvironment {
|
|||||||
if let Some(x11) = &self.x11 {
|
if let Some(x11) = &self.x11 {
|
||||||
command.env("DISPLAY", &x11.display).env("XAUTHORITY", &x11.authority);
|
command.env("DISPLAY", &x11.display).env("XAUTHORITY", &x11.authority);
|
||||||
}
|
}
|
||||||
|
if self.policy == SessionPolicy::Embedded {
|
||||||
|
command.env("FERESE_SESSION_IMPORT_ENV", "0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +65,8 @@ mod tests {
|
|||||||
fn environment(x11: Option<X11Environment>) -> SessionEnvironment {
|
fn environment(x11: Option<X11Environment>) -> SessionEnvironment {
|
||||||
SessionEnvironment {
|
SessionEnvironment {
|
||||||
wayland_display: OsString::from("wayland-7"),
|
wayland_display: OsString::from("wayland-7"),
|
||||||
|
control_socket: PathBuf::from("/run/user/1000/ferese/instances/preview/control.sock"),
|
||||||
|
policy: SessionPolicy::Desktop,
|
||||||
x11,
|
x11,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,4 +163,42 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(value_of(&command, "DISPLAY"), Some(Some(":7".to_owned())));
|
assert_eq!(value_of(&command, "DISPLAY"), Some(Some(":7".to_owned())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_host_control_socket_is_replaced_by_the_instance_socket() {
|
||||||
|
let mut command = Command::new("true");
|
||||||
|
command.env("FERESE_SOCKET", "/run/user/1000/ferese/control.sock");
|
||||||
|
environment(None).apply_public(&mut command);
|
||||||
|
assert_eq!(
|
||||||
|
value_of(&command, "FERESE_SOCKET"),
|
||||||
|
Some(Some("/run/user/1000/ferese/instances/preview/control.sock".into()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embedded_clients_cannot_import_their_environment_into_the_host_session() {
|
||||||
|
let mut command = Command::new("true");
|
||||||
|
command.env("FERESE_SESSION_IMPORT_ENV", "1");
|
||||||
|
command.env("FERESE_SESSION_MODE", "desktop");
|
||||||
|
let mut embedded = environment(None);
|
||||||
|
embedded.policy = SessionPolicy::Embedded;
|
||||||
|
embedded.apply_public(&mut command);
|
||||||
|
assert_eq!(value_of(&command, "FERESE_SESSION_IMPORT_ENV"), Some(Some("0".into())));
|
||||||
|
assert_eq!(value_of(&command, "FERESE_SESSION_MODE"), Some(Some("embedded".into())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn desktop_clients_preserve_the_launchers_activation_import_choice() {
|
||||||
|
for choice in ["0", "1"] {
|
||||||
|
let mut command = Command::new("true");
|
||||||
|
command.env("FERESE_SESSION_IMPORT_ENV", choice);
|
||||||
|
command.env("FERESE_SESSION_MODE", "embedded");
|
||||||
|
environment(None).apply_public(&mut command);
|
||||||
|
assert_eq!(
|
||||||
|
value_of(&command, "FERESE_SESSION_IMPORT_ENV"),
|
||||||
|
Some(Some(choice.into()))
|
||||||
|
);
|
||||||
|
assert_eq!(value_of(&command, "FERESE_SESSION_MODE"), Some(Some("desktop".into())));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
use std::os::unix::fs::FileTypeExt;
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
@@ -64,6 +65,127 @@ pub(crate) fn state(events: &mut EventLoop<'static, Ferese>) -> Ferese {
|
|||||||
Ferese::new(events, Display::new().unwrap(), config).unwrap()
|
Ferese::new(events, Display::new().unwrap(), config).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_instances_share_a_runtime_and_route_launched_clients_to_their_own_ipc() {
|
||||||
|
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||||
|
if !private_runtime("startup_tests::nested_instances_share_a_runtime_and_route_launched_clients_to_their_own_ipc") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut host_events = EventLoop::try_new().unwrap();
|
||||||
|
let host = state(&mut host_events);
|
||||||
|
let host_socket = host.session_environment.control_socket.clone();
|
||||||
|
let mut first_events = EventLoop::try_new().unwrap();
|
||||||
|
let mut second_events = EventLoop::try_new().unwrap();
|
||||||
|
let config = || {
|
||||||
|
let mut config = crate::config::Config::default().runtime_config().unwrap();
|
||||||
|
config.wallpaper.path = None;
|
||||||
|
config
|
||||||
|
};
|
||||||
|
let mut first = Ferese::new_with_session(
|
||||||
|
&mut first_events,
|
||||||
|
Display::new().unwrap(),
|
||||||
|
config(),
|
||||||
|
crate::ipc::Endpoint::instance().unwrap(),
|
||||||
|
crate::SessionPolicy::Embedded,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut second = Ferese::new_with_session(
|
||||||
|
&mut second_events,
|
||||||
|
Display::new().unwrap(),
|
||||||
|
config(),
|
||||||
|
crate::ipc::Endpoint::instance().unwrap(),
|
||||||
|
crate::SessionPolicy::Embedded,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let first_socket = first.session_environment.control_socket.clone();
|
||||||
|
let second_socket = second.session_environment.control_socket.clone();
|
||||||
|
assert_ne!(first_socket, host_socket);
|
||||||
|
assert_ne!(second_socket, host_socket);
|
||||||
|
assert_ne!(first_socket, second_socket);
|
||||||
|
assert_ne!(first.socket_name, second.socket_name);
|
||||||
|
let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap());
|
||||||
|
assert_eq!(host_socket, runtime.join("ferese/control.sock"));
|
||||||
|
for (events, state, name) in [
|
||||||
|
(&mut first_events, &mut first, "first"),
|
||||||
|
(&mut second_events, &mut second, "second"),
|
||||||
|
] {
|
||||||
|
let output = Output::new(
|
||||||
|
name.into(),
|
||||||
|
PhysicalProperties {
|
||||||
|
size: (0, 0).into(),
|
||||||
|
subpixel: Subpixel::Unknown,
|
||||||
|
make: "test".into(),
|
||||||
|
model: "test".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
output.change_current_state(
|
||||||
|
Some(Mode {
|
||||||
|
size: (800, 600).into(),
|
||||||
|
refresh: 60_000,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
state.space.map_output(&output, (0, 0));
|
||||||
|
state.register_output(&output, name.into());
|
||||||
|
let report = runtime.join(format!("{name}.json"));
|
||||||
|
let script = r#"import json, os, socket, struct, sys
|
||||||
|
s = socket.socket(socket.AF_UNIX)
|
||||||
|
s.connect(os.environ['FERESE_SOCKET'])
|
||||||
|
s.settimeout(2)
|
||||||
|
payload = json.dumps({'version':1, 'id':1, 'type':'command', 'command':'outputs', 'args':{}}).encode()
|
||||||
|
s.sendall(struct.pack('>I', len(payload)) + payload)
|
||||||
|
def read_exact(n):
|
||||||
|
data = b''
|
||||||
|
while len(data) < n:
|
||||||
|
part = s.recv(n - len(data))
|
||||||
|
if not part: raise RuntimeError('IPC disconnected')
|
||||||
|
data += part
|
||||||
|
return data
|
||||||
|
response = json.loads(read_exact(struct.unpack('>I', read_exact(4))[0]))
|
||||||
|
with open(sys.argv[1], 'w') as report:
|
||||||
|
json.dump({'socket':os.environ['FERESE_SOCKET'], 'response':response}, report)
|
||||||
|
"#;
|
||||||
|
let mut child = crate::process::spawn_client(
|
||||||
|
state,
|
||||||
|
[
|
||||||
|
std::ffi::OsStr::new("python3"),
|
||||||
|
std::ffi::OsStr::new("-c"),
|
||||||
|
std::ffi::OsStr::new(script),
|
||||||
|
report.as_os_str(),
|
||||||
|
],
|
||||||
|
crate::private_client::ClientCapabilities::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(3);
|
||||||
|
loop {
|
||||||
|
if let Some(status) = child.try_wait().unwrap() {
|
||||||
|
assert!(status.success());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
assert!(Instant::now() < deadline, "nested IPC client timed out");
|
||||||
|
events.dispatch(Duration::from_millis(10), state).unwrap();
|
||||||
|
}
|
||||||
|
let result: serde_json::Value = serde_json::from_slice(&std::fs::read(report).unwrap()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result["socket"],
|
||||||
|
state.session_environment.control_socket.to_str().unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(result["response"]["result"][0]["name"], name);
|
||||||
|
assert!(result["response"]["error"].is_null());
|
||||||
|
}
|
||||||
|
let first_directory = first_socket.parent().unwrap().to_owned();
|
||||||
|
drop(first);
|
||||||
|
assert!(!first_directory.exists());
|
||||||
|
assert!(host_socket.symlink_metadata().unwrap().file_type().is_socket());
|
||||||
|
assert!(second_socket.symlink_metadata().unwrap().file_type().is_socket());
|
||||||
|
let second_directory = second_socket.parent().unwrap().to_owned();
|
||||||
|
drop(second);
|
||||||
|
assert!(!second_directory.exists());
|
||||||
|
assert!(host_socket.symlink_metadata().unwrap().file_type().is_socket());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn launched_clients_unblock_termination_signals_and_keep_private_sockets() {
|
fn launched_clients_unblock_termination_signals_and_keep_private_sockets() {
|
||||||
if !private_runtime("startup_tests::launched_clients_unblock_termination_signals_and_keep_private_sockets") {
|
if !private_runtime("startup_tests::launched_clients_unblock_termination_signals_and_keep_private_sockets") {
|
||||||
|
|||||||
+20
-1
@@ -565,10 +565,27 @@ pub struct RuntimeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Ferese {
|
impl Ferese {
|
||||||
|
#[cfg(test)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
event_loop: &mut EventLoop<'static, Self>,
|
event_loop: &mut EventLoop<'static, Self>,
|
||||||
display: Display<Self>,
|
display: Display<Self>,
|
||||||
config: RuntimeConfig,
|
config: RuntimeConfig,
|
||||||
|
) -> Result<Self, Box<dyn Error>> {
|
||||||
|
Self::new_with_session(
|
||||||
|
event_loop,
|
||||||
|
display,
|
||||||
|
config,
|
||||||
|
crate::ipc::Endpoint::desktop()?,
|
||||||
|
crate::SessionPolicy::Desktop,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new_with_session(
|
||||||
|
event_loop: &mut EventLoop<'static, Self>,
|
||||||
|
display: Display<Self>,
|
||||||
|
config: RuntimeConfig,
|
||||||
|
ipc_endpoint: crate::ipc::Endpoint,
|
||||||
|
session_policy: crate::SessionPolicy,
|
||||||
) -> Result<Self, Box<dyn Error>> {
|
) -> Result<Self, Box<dyn Error>> {
|
||||||
let display_handle = display.handle();
|
let display_handle = display.handle();
|
||||||
|
|
||||||
@@ -657,6 +674,8 @@ impl Ferese {
|
|||||||
|
|
||||||
session_environment: crate::SessionEnvironment {
|
session_environment: crate::SessionEnvironment {
|
||||||
wayland_display: socket_name.clone(),
|
wayland_display: socket_name.clone(),
|
||||||
|
control_socket: ipc_endpoint.path.clone(),
|
||||||
|
policy: session_policy,
|
||||||
x11: None,
|
x11: None,
|
||||||
},
|
},
|
||||||
socket_name,
|
socket_name,
|
||||||
@@ -791,7 +810,7 @@ impl Ferese {
|
|||||||
xdg_toplevel_icon_manager,
|
xdg_toplevel_icon_manager,
|
||||||
};
|
};
|
||||||
state.config_worker = Some(crate::reload::Worker::new(event_loop)?);
|
state.config_worker = Some(crate::reload::Worker::new(event_loop)?);
|
||||||
let screenshot = crate::ipc::init(event_loop)?;
|
let screenshot = crate::ipc::init(event_loop, ipc_endpoint)?;
|
||||||
state
|
state
|
||||||
.screenshot
|
.screenshot
|
||||||
.set_deadline_observer(move |deadline| screenshot.deadline_timer.update(deadline));
|
.set_deadline_observer(move |deadline| screenshot.deadline_timer.update(deadline));
|
||||||
|
|||||||
@@ -349,6 +349,8 @@ mod tests {
|
|||||||
|
|
||||||
fn environment() -> SessionEnvironment {
|
fn environment() -> SessionEnvironment {
|
||||||
SessionEnvironment {
|
SessionEnvironment {
|
||||||
|
control_socket: std::path::PathBuf::from("/run/user/1000/ferese/control.sock"),
|
||||||
|
policy: crate::SessionPolicy::Desktop,
|
||||||
wayland_display: OsString::from("wayland-7"),
|
wayland_display: OsString::from("wayland-7"),
|
||||||
x11: None,
|
x11: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ impl Fixture {
|
|||||||
|
|
||||||
fn environment(&self) -> SessionEnvironment {
|
fn environment(&self) -> SessionEnvironment {
|
||||||
SessionEnvironment {
|
SessionEnvironment {
|
||||||
|
control_socket: PathBuf::from("/run/user/1000/ferese/control.sock"),
|
||||||
|
policy: crate::SessionPolicy::Desktop,
|
||||||
wayland_display: std::ffi::OsString::from("wayland-it"),
|
wayland_display: std::ffi::OsString::from("wayland-it"),
|
||||||
x11: Some(X11Environment {
|
x11: Some(X11Environment {
|
||||||
display: std::ffi::OsString::from(self.reservation.display_name()),
|
display: std::ffi::OsString::from(self.reservation.display_name()),
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ pub fn resolve(explicit: Option<&Path>) -> io::Result<PathBuf> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The DRM session's listening address, independent of a client's IPC target.
|
||||||
|
pub fn runtime_default() -> io::Result<PathBuf> {
|
||||||
|
resolve_with(None, None, std::env::var_os("XDG_RUNTIME_DIR").as_deref())
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_with(explicit: Option<&Path>, socket: Option<&OsStr>, runtime: Option<&OsStr>) -> io::Result<PathBuf> {
|
fn resolve_with(explicit: Option<&Path>, socket: Option<&OsStr>, runtime: Option<&OsStr>) -> io::Result<PathBuf> {
|
||||||
if let Some(path) = explicit {
|
if let Some(path) = explicit {
|
||||||
if path.as_os_str().is_empty() {
|
if path.as_os_str().is_empty() {
|
||||||
|
|||||||
@@ -242,12 +242,20 @@ authentication, idle locking and limits.
|
|||||||
|
|
||||||
## Preview and logs
|
## Preview and logs
|
||||||
|
|
||||||
|
`ferese` selects a nested window when `WAYLAND_DISPLAY` or `DISPLAY` is set,
|
||||||
|
and DRM otherwise. Nested instances have separate control sockets. Clients
|
||||||
|
launched by Ferese inherit that instance’s `FERESE_SOCKET`. Use
|
||||||
|
`--backend=nested` or `--backend=drm` to override detection.
|
||||||
|
|
||||||
|
Nested previews leave the host activation environment and session target alone.
|
||||||
|
They skip XDG autostart and run configured autostart entries only with `nested true`.
|
||||||
|
|
||||||
To preview a source build before installing, run these commands from the checkout
|
To preview a source build before installing, run these commands from the checkout
|
||||||
inside an existing Wayland desktop:
|
inside an existing Wayland desktop:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo build --release --locked -p ferese -p ferese-shell -p ferese-settings -p feresectl -p ferese-lock -p xdg-desktop-portal-ferese
|
cargo build --release --locked -p ferese -p ferese-shell -p ferese-settings -p feresectl -p ferese-lock -p xdg-desktop-portal-ferese
|
||||||
FERESE_ENABLE_SCREENCOPY=1 target/release/ferese --backend nested \
|
FERESE_ENABLE_SCREENCOPY=1 target/release/ferese \
|
||||||
--grant-effects --grant-shell-control -- \
|
--grant-effects --grant-shell-control -- \
|
||||||
target/release/ferese-shell
|
target/release/ferese-shell
|
||||||
```
|
```
|
||||||
|
|||||||
+11
-4
@@ -45,19 +45,26 @@ its target in this order:
|
|||||||
3. `$XDG_RUNTIME_DIR/ferese/control.sock`
|
3. `$XDG_RUNTIME_DIR/ferese/control.sock`
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
feresectl --socket /tmp/ferese-preview/ferese/control.sock outputs
|
feresectl --socket "$XDG_RUNTIME_DIR/ferese/instances/12345-abcdef/control.sock" outputs
|
||||||
FERESE_SOCKET=/tmp/ferese-preview/ferese/control.sock feresectl theme get -j
|
FERESE_SOCKET="$XDG_RUNTIME_DIR/ferese/instances/12345-abcdef/control.sock" feresectl theme get -j
|
||||||
```
|
```
|
||||||
|
|
||||||
`--socket` selects an existing server; it does not create one or change where the
|
`--socket` selects an existing server; it does not create one or change where the
|
||||||
compositor listens. A nested compositor uses its own runtime directory.
|
compositor listens. DRM uses `$XDG_RUNTIME_DIR/ferese/control.sock`. Nested
|
||||||
|
instances use `$XDG_RUNTIME_DIR/ferese/instances/<id>/control.sock`; copy the
|
||||||
|
actual path from the compositor's startup log. Each instance removes its own
|
||||||
|
socket and directory on normal shutdown. A forced kill can leave the directory
|
||||||
|
behind; later instances allocate a different one.
|
||||||
|
|
||||||
|
Clients launched by a nested compositor inherit its `FERESE_SOCKET`, so
|
||||||
|
`feresectl outputs` inside that session connects to that instance.
|
||||||
`autostart` launches desktop entries locally and does not use IPC.
|
`autostart` launches desktop entries locally and does not use IPC.
|
||||||
|
|
||||||
## Follow changes
|
## Follow changes
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
feresectl event-stream
|
feresectl event-stream
|
||||||
feresectl --socket /tmp/ferese-preview/ferese/control.sock event-stream
|
feresectl --socket "$XDG_RUNTIME_DIR/ferese/instances/12345-abcdef/control.sock" event-stream
|
||||||
```
|
```
|
||||||
|
|
||||||
`event-stream` writes one JSON value per line and flushes each line. It always
|
`event-stream` writes one JSON value per line and flushes each line. It always
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ session_target_started=0
|
|||||||
shell_pid=
|
shell_pid=
|
||||||
agent_pid=
|
agent_pid=
|
||||||
autostart_pid=
|
autostart_pid=
|
||||||
|
manage_session=0
|
||||||
|
if [[ ${FERESE_SESSION_MODE:-desktop} == desktop && ${FERESE_SESSION_IMPORT_ENV:-0} == 1 ]]; then
|
||||||
|
manage_session=1
|
||||||
|
fi
|
||||||
cleanup() {
|
cleanup() {
|
||||||
trap - EXIT INT TERM
|
trap - EXIT INT TERM
|
||||||
if [[ -n $autostart_pid ]]; then
|
if [[ -n $autostart_pid ]]; then
|
||||||
@@ -25,7 +29,7 @@ trap 'exit 143' TERM
|
|||||||
|
|
||||||
# The compositor supplies its new Wayland socket before this helper starts.
|
# The compositor supplies its new Wayland socket before this helper starts.
|
||||||
# Nested previews must never replace the host session's activation environment.
|
# Nested previews must never replace the host session's activation environment.
|
||||||
if [[ ${FERESE_SESSION_IMPORT_ENV:-0} == 1 ]]; then
|
if ((manage_session)); then
|
||||||
# Shell control uses a private socket FD. Activation must use the public
|
# Shell control uses a private socket FD. Activation must use the public
|
||||||
# socket so portal and other desktop services connect as ordinary clients.
|
# socket so portal and other desktop services connect as ordinary clients.
|
||||||
public_display=${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}}
|
public_display=${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}}
|
||||||
@@ -34,6 +38,7 @@ if [[ ${FERESE_SESSION_IMPORT_ENV:-0} == 1 ]]; then
|
|||||||
"WAYLAND_DISPLAY=$public_display"
|
"WAYLAND_DISPLAY=$public_display"
|
||||||
"DISPLAY=${DISPLAY:-}"
|
"DISPLAY=${DISPLAY:-}"
|
||||||
"XAUTHORITY=${XAUTHORITY:-}"
|
"XAUTHORITY=${XAUTHORITY:-}"
|
||||||
|
"FERESE_SOCKET=${FERESE_SOCKET:-}"
|
||||||
XDG_CURRENT_DESKTOP
|
XDG_CURRENT_DESKTOP
|
||||||
XDG_SESSION_DESKTOP
|
XDG_SESSION_DESKTOP
|
||||||
XDG_SESSION_TYPE
|
XDG_SESSION_TYPE
|
||||||
@@ -70,7 +75,7 @@ env -u WAYLAND_SOCKET WAYLAND_DISPLAY="${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAN
|
|||||||
agent_pid=$!
|
agent_pid=$!
|
||||||
"$release_dir/ferese-shell" &
|
"$release_dir/ferese-shell" &
|
||||||
shell_pid=$!
|
shell_pid=$!
|
||||||
if [[ ${FERESE_SESSION_IMPORT_ENV:-0} == 1 && -x $release_dir/feresectl ]]; then
|
if ((manage_session)) && [[ -x $release_dir/feresectl ]]; then
|
||||||
env WAYLAND_DISPLAY="${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}}" \
|
env WAYLAND_DISPLAY="${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}}" \
|
||||||
"$release_dir/feresectl" autostart &
|
"$release_dir/feresectl" autostart &
|
||||||
autostart_pid=$!
|
autostart_pid=$!
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ struct BridgeInner {
|
|||||||
|
|
||||||
impl Bridge {
|
impl Bridge {
|
||||||
pub(crate) fn connect() -> Result<Self, String> {
|
pub(crate) fn connect() -> Result<Self, String> {
|
||||||
let path = std::env::var_os("XDG_RUNTIME_DIR")
|
let path = ferese_ipc::socket::resolve(None).map_err(|error| error.to_string())?;
|
||||||
.map(std::path::PathBuf::from)
|
|
||||||
.ok_or("Missing runtime directory")?
|
|
||||||
.join("ferese/control.sock");
|
|
||||||
let stream = UnixStream::connect(path).map_err(|error| error.to_string())?;
|
let stream = UnixStream::connect(path).map_err(|error| error.to_string())?;
|
||||||
Self::from_stream(stream, Duration::from_secs(2))
|
Self::from_stream(stream, Duration::from_secs(2))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,10 +236,7 @@ pub(crate) async fn ipc(command: &'static str, args: serde_json::Value) -> Resul
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn ipc_sync(command: &'static str, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
pub(crate) fn ipc_sync(command: &'static str, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||||
let path = std::env::var_os("XDG_RUNTIME_DIR")
|
let path = ferese_ipc::socket::resolve(None).map_err(|error| error.to_string())?;
|
||||||
.map(PathBuf::from)
|
|
||||||
.ok_or("Missing runtime directory")?
|
|
||||||
.join("ferese/control.sock");
|
|
||||||
let mut stream = UnixStream::connect(path).map_err(|error| error.to_string())?;
|
let mut stream = UnixStream::connect(path).map_err(|error| error.to_string())?;
|
||||||
stream
|
stream
|
||||||
.set_read_timeout(Some(Duration::from_secs(15)))
|
.set_read_timeout(Some(Duration::from_secs(15)))
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"""Find the compositor endpoint in an isolated test runtime."""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def ipc_socket(runtime):
|
||||||
|
runtime = Path(runtime)
|
||||||
|
default = runtime / "ferese/control.sock"
|
||||||
|
instances = sorted(path for path in runtime.glob("ferese/instances/*/control.sock") if path.is_socket())
|
||||||
|
if default.is_socket():
|
||||||
|
instances.append(default)
|
||||||
|
if len(instances) > 1:
|
||||||
|
raise RuntimeError("Isolated test runtime contains multiple compositor instances")
|
||||||
|
return instances[0] if instances else default
|
||||||
|
|
||||||
|
|
||||||
|
def ipc_environment(environment):
|
||||||
|
return dict(environment, FERESE_SOCKET=str(ipc_socket(environment["XDG_RUNTIME_DIR"])))
|
||||||
@@ -6,6 +6,7 @@ Requires a Wayland host, wayland-scanner and a C compiler.
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
@@ -56,7 +57,7 @@ class BlurredPopupTest(unittest.TestCase):
|
|||||||
with log_path.open("w") as log:
|
with log_path.open("w") as log:
|
||||||
compositor = subprocess.Popen(
|
compositor = subprocess.Popen(
|
||||||
[str(binary), "--backend", "nested", "--grant-effects", "--", str(client)],
|
[str(binary), "--backend", "nested", "--grant-effects", "--", str(client)],
|
||||||
env=env, stdout=log, stderr=log,
|
env=ipc_environment(env), stdout=log, stderr=log,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
deadline = time.monotonic() + 30
|
deadline = time.monotonic() + 30
|
||||||
@@ -66,7 +67,7 @@ class BlurredPopupTest(unittest.TestCase):
|
|||||||
self.fail("Blurred popup stopped presenting frames:\n" + log_path.read_text())
|
self.fail("Blurred popup stopped presenting frames:\n" + log_path.read_text())
|
||||||
time.sleep(.025)
|
time.sleep(.025)
|
||||||
control = repo / os.environ.get("FERESE_TEST_CTL", "target/debug/feresectl")
|
control = repo / os.environ.get("FERESE_TEST_CTL", "target/debug/feresectl")
|
||||||
subprocess.run([str(control), "-j", "outputs"], env=env, check=True,
|
subprocess.run([str(control), "-j", "outputs"], env=ipc_environment(env), check=True,
|
||||||
stdout=subprocess.DEVNULL, timeout=5)
|
stdout=subprocess.DEVNULL, timeout=5)
|
||||||
phases = log_path.read_text().splitlines()
|
phases = log_path.read_text().splitlines()
|
||||||
for name, count in [("parent", 1), ("open", 3), ("update", 9), ("close", 3)]:
|
for name, count in [("parent", 1), ("open", 3), ("update", 9), ("close", 3)]:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ the compositor and FERESE_TEST_CTL at feresectl when they are not siblings.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
@@ -50,7 +51,7 @@ class FloatingSizeTest(unittest.TestCase):
|
|||||||
self.fail(f"missing {ctl}; build it with cargo build -p feresectl")
|
self.fail(f"missing {ctl}; build it with cargo build -p feresectl")
|
||||||
with (root / "compositor.log").open("w") as log:
|
with (root / "compositor.log").open("w") as log:
|
||||||
process = subprocess.Popen([str(binary), "--backend", "nested", "--", str(root / "client")],
|
process = subprocess.Popen([str(binary), "--backend", "nested", "--", str(root / "client")],
|
||||||
env=env, stdout=log, stderr=subprocess.STDOUT)
|
env=ipc_environment(env), stdout=log, stderr=subprocess.STDOUT)
|
||||||
try:
|
try:
|
||||||
for _ in range(100):
|
for _ in range(100):
|
||||||
if process.poll() is not None:
|
if process.poll() is not None:
|
||||||
@@ -62,7 +63,7 @@ class FloatingSizeTest(unittest.TestCase):
|
|||||||
self.assertTrue(sockets, "nested Wayland socket did not appear")
|
self.assertTrue(sockets, "nested Wayland socket did not appear")
|
||||||
childenv = dict(env, WAYLAND_DISPLAY=str(sockets[0]))
|
childenv = dict(env, WAYLAND_DISPLAY=str(sockets[0]))
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
listed = subprocess.run([str(ctl), "-j", "outputs"], env=childenv,
|
listed = subprocess.run([str(ctl), "-j", "outputs"], env=ipc_environment(childenv),
|
||||||
check=True, capture_output=True, timeout=10, text=True)
|
check=True, capture_output=True, timeout=10, text=True)
|
||||||
enabled = [o for o in json.loads(listed.stdout) if o.get("enabled")]
|
enabled = [o for o in json.loads(listed.stdout) if o.get("enabled")]
|
||||||
if not enabled:
|
if not enabled:
|
||||||
@@ -74,7 +75,7 @@ class FloatingSizeTest(unittest.TestCase):
|
|||||||
screenshot = root / "frame.png"
|
screenshot = root / "frame.png"
|
||||||
with screenshot.open("wb") as png:
|
with screenshot.open("wb") as png:
|
||||||
captured = subprocess.run([str(ctl), "screenshot", "-g", geometry],
|
captured = subprocess.run([str(ctl), "screenshot", "-g", geometry],
|
||||||
env=childenv, stdout=png, stderr=subprocess.PIPE, timeout=15)
|
env=ipc_environment(childenv), stdout=png, stderr=subprocess.PIPE, timeout=15)
|
||||||
if captured.returncode != 0:
|
if captured.returncode != 0:
|
||||||
self.fail(f"capture of {geometry} failed: {captured.stderr.decode()}")
|
self.fail(f"capture of {geometry} failed: {captured.stderr.decode()}")
|
||||||
if screenshot.stat().st_size == 0:
|
if screenshot.stat().st_size == 0:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Requires built debug ferese/feresectl, cc, wayland-scanner and a Wayland host.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -63,12 +64,12 @@ class FloatingPlacementTest(unittest.TestCase):
|
|||||||
children = []
|
children = []
|
||||||
|
|
||||||
def launch(command, **kwargs):
|
def launch(command, **kwargs):
|
||||||
child = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
child = subprocess.Popen(command, env=ipc_environment(env), start_new_session=True, **kwargs)
|
||||||
children.append(child)
|
children.append(child)
|
||||||
return child
|
return child
|
||||||
|
|
||||||
def command(*args):
|
def command(*args):
|
||||||
return json.loads(subprocess.check_output([str(repo / "target/debug/feresectl"), "-j", *args], env=env, timeout=5))
|
return json.loads(subprocess.check_output([str(repo / "target/debug/feresectl"), "-j", *args], env=ipc_environment(env), timeout=5))
|
||||||
|
|
||||||
with (root / "compositor.log").open("w") as log:
|
with (root / "compositor.log").open("w") as log:
|
||||||
try:
|
try:
|
||||||
@@ -76,7 +77,7 @@ class FloatingPlacementTest(unittest.TestCase):
|
|||||||
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
||||||
compositor = launch([str(repo / "target/debug/ferese"), "--backend", "nested"], stdout=log, stderr=log)
|
compositor = launch([str(repo / "target/debug/ferese"), "--backend", "nested"], stdout=log, stderr=log)
|
||||||
deadline = time.monotonic() + 10
|
deadline = time.monotonic() + 10
|
||||||
while not (runtime / "ferese/control.sock").is_socket():
|
while not (ipc_socket(runtime)).is_socket():
|
||||||
if compositor.poll() is not None or time.monotonic() > deadline:
|
if compositor.poll() is not None or time.monotonic() > deadline:
|
||||||
self.fail((root / "compositor.log").read_text())
|
self.fail((root / "compositor.log").read_text())
|
||||||
time.sleep(.025)
|
time.sleep(.025)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Requires built debug ferese/feresectl, foot, dbus-daemon, and a Wayland host.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -34,13 +35,13 @@ class FocusHistoryTest(unittest.TestCase):
|
|||||||
children = []
|
children = []
|
||||||
|
|
||||||
def launch(command, **kwargs):
|
def launch(command, **kwargs):
|
||||||
child = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
child = subprocess.Popen(command, env=ipc_environment(env), start_new_session=True, **kwargs)
|
||||||
children.append(child)
|
children.append(child)
|
||||||
return child
|
return child
|
||||||
|
|
||||||
def command(*args):
|
def command(*args):
|
||||||
return json.loads(subprocess.check_output(
|
return json.loads(subprocess.check_output(
|
||||||
[str(repo / "target/debug/feresectl"), "-j", *args], env=env, timeout=5
|
[str(repo / "target/debug/feresectl"), "-j", *args], env=ipc_environment(env), timeout=5
|
||||||
))
|
))
|
||||||
|
|
||||||
def wait_for(predicate):
|
def wait_for(predicate):
|
||||||
@@ -62,7 +63,7 @@ class FocusHistoryTest(unittest.TestCase):
|
|||||||
stdout=subprocess.PIPE, text=True)
|
stdout=subprocess.PIPE, text=True)
|
||||||
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
||||||
launch([str(repo / "target/debug/ferese"), "--backend", "nested"], stdout=log, stderr=log)
|
launch([str(repo / "target/debug/ferese"), "--backend", "nested"], stdout=log, stderr=log)
|
||||||
wait_for(lambda: (runtime / "ferese/control.sock").is_socket())
|
wait_for(lambda: (ipc_socket(runtime)).is_socket())
|
||||||
command("focus-last-window")
|
command("focus-last-window")
|
||||||
self.assertIsNone(focused())
|
self.assertIsNone(focused())
|
||||||
env["WAYLAND_DISPLAY"] = str(next(
|
env["WAYLAND_DISPLAY"] = str(next(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ FERESE_TEST_BINARY and FERESE_TEST_CTL can select release binaries.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -99,7 +100,7 @@ def private_checks():
|
|||||||
|
|
||||||
with (root / "compositor.log").open("w") as log:
|
with (root / "compositor.log").open("w") as log:
|
||||||
try:
|
try:
|
||||||
compositor = subprocess.Popen([str(BINARY), "--backend=nested"], env=env, stdout=log, stderr=log, start_new_session=True)
|
compositor = subprocess.Popen([str(BINARY), "--backend=nested"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
processes.append(compositor)
|
processes.append(compositor)
|
||||||
|
|
||||||
def wait(check, message):
|
def wait(check, message):
|
||||||
@@ -112,11 +113,11 @@ def private_checks():
|
|||||||
time.sleep(0.02)
|
time.sleep(0.02)
|
||||||
raise AssertionError(message + "\n" + (root / "compositor.log").read_text())
|
raise AssertionError(message + "\n" + (root / "compositor.log").read_text())
|
||||||
|
|
||||||
wait(lambda: (runtime / "ferese/control.sock").exists(), "compositor startup")
|
wait(lambda: (ipc_socket(runtime)).exists(), "compositor startup")
|
||||||
env["WAYLAND_DISPLAY"] = str(next(path for path in runtime.glob("wayland-*") if not path.name.endswith(".lock")))
|
env["WAYLAND_DISPLAY"] = str(next(path for path in runtime.glob("wayland-*") if not path.name.endswith(".lock")))
|
||||||
|
|
||||||
def call(*args):
|
def call(*args):
|
||||||
return json.loads(subprocess.check_output([str(CTL), "-j", *args], env=env, timeout=5))
|
return json.loads(subprocess.check_output([str(CTL), "-j", *args], env=ipc_environment(env), timeout=5))
|
||||||
|
|
||||||
def inhibited(expected):
|
def inhibited(expected):
|
||||||
def matches():
|
def matches():
|
||||||
@@ -126,7 +127,7 @@ def private_checks():
|
|||||||
wait(matches, f"idle inhibition != {expected}")
|
wait(matches, f"idle inhibition != {expected}")
|
||||||
|
|
||||||
wait(lambda: len(call("get-idle-inhibition")["players"]) == 1, "MPRIS discovery")
|
wait(lambda: len(call("get-idle-inhibition")["players"]) == 1, "MPRIS discovery")
|
||||||
client = subprocess.Popen([str(root / "client")], env=env, stdout=log, stderr=log, start_new_session=True)
|
client = subprocess.Popen([str(root / "client")], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
processes.append(client)
|
processes.append(client)
|
||||||
wait(lambda: len(call("get-windows")) == 1, "window map")
|
wait(lambda: len(call("get-windows")) == 1, "window map")
|
||||||
call("toggle-fullscreen")
|
call("toggle-fullscreen")
|
||||||
@@ -153,7 +154,7 @@ def private_checks():
|
|||||||
inhibited(False)
|
inhibited(False)
|
||||||
player.set("Playing")
|
player.set("Playing")
|
||||||
inhibited(True)
|
inhibited(True)
|
||||||
ambiguous = subprocess.Popen([str(root / "client"), "cover"], env=env,
|
ambiguous = subprocess.Popen([str(root / "client"), "cover"], env=ipc_environment(env),
|
||||||
stdout=log, stderr=log, start_new_session=True)
|
stdout=log, stderr=log, start_new_session=True)
|
||||||
processes.append(ambiguous)
|
processes.append(ambiguous)
|
||||||
wait(lambda: len(call("get-windows")) == 2, "second player window")
|
wait(lambda: len(call("get-windows")) == 2, "second player window")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Uses a nested compositor and mock logind; never suspends or ends the host sessio
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
@@ -141,13 +142,13 @@ def private_test():
|
|||||||
env.pop("WAYLAND_SOCKET", None)
|
env.pop("WAYLAND_SOCKET", None)
|
||||||
log = (root / "test.log").open("w")
|
log = (root / "test.log").open("w")
|
||||||
try:
|
try:
|
||||||
compositor = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=env, stdout=log, stderr=log, start_new_session=True)
|
compositor = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
processes.append(compositor)
|
processes.append(compositor)
|
||||||
deadline = time.monotonic() + 10
|
deadline = time.monotonic() + 10
|
||||||
while not (runtime / "ferese/control.sock").exists():
|
while not (ipc_socket(runtime)).exists():
|
||||||
assert compositor.poll() is None and time.monotonic() < deadline, (root / "test.log").read_text()
|
assert compositor.poll() is None and time.monotonic() < deadline, (root / "test.log").read_text()
|
||||||
time.sleep(0.02)
|
time.sleep(0.02)
|
||||||
backend = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese")], env=env, stdout=log, stderr=log, start_new_session=True)
|
backend = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese")], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
processes.append(backend)
|
processes.append(backend)
|
||||||
deadline = time.monotonic() + 10
|
deadline = time.monotonic() + 10
|
||||||
while True:
|
while True:
|
||||||
@@ -164,7 +165,7 @@ def private_test():
|
|||||||
except GLib.Error as error:
|
except GLib.Error as error:
|
||||||
assert "AccessDenied" in str(error)
|
assert "AccessDenied" in str(error)
|
||||||
bus.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "RequestName", GLib.Variant("(su)", ("org.freedesktop.portal.Desktop", 0)), None, Gio.DBusCallFlags.NONE, 2000, None)
|
bus.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "RequestName", GLib.Variant("(su)", ("org.freedesktop.portal.Desktop", 0)), None, Gio.DBusCallFlags.NONE, 2000, None)
|
||||||
admin = IPC(runtime / "ferese/control.sock")
|
admin = IPC(ipc_socket(runtime))
|
||||||
def inhibit(path, flags):
|
def inhibit(path, flags):
|
||||||
call(INTERFACE, "Inhibit", GLib.Variant("(ossua{sv})", (path, "test.media", "", flags, {"reason": GLib.Variant("s", "Playing media")})))
|
call(INTERFACE, "Inhibit", GLib.Variant("(ossua{sv})", (path, "test.media", "", flags, {"reason": GLib.Variant("s", "Playing media")})))
|
||||||
inhibit(request, 8)
|
inhibit(request, 8)
|
||||||
@@ -180,7 +181,7 @@ def private_test():
|
|||||||
assert call(INTERFACE, "CreateMonitor", GLib.Variant("(ooss)", (PATH + "/request/test/monitor", session, "test.media", "")), "(u)") == (0,)
|
assert call(INTERFACE, "CreateMonitor", GLib.Variant("(ooss)", (PATH + "/request/test/monitor", session, "test.media", "")), "(u)") == (0,)
|
||||||
wait_for(lambda: any(value["session-state"] == 1 for value in changes), "Missing initial Running")
|
wait_for(lambda: any(value["session-state"] == 1 for value in changes), "Missing initial Running")
|
||||||
assert call("org.freedesktop.DBus.Properties", "Get", GLib.Variant("(ss)", ("org.freedesktop.impl.portal.Session", "version")), "(v)", session) == (1,)
|
assert call("org.freedesktop.DBus.Properties", "Get", GLib.Variant("(ss)", ("org.freedesktop.impl.portal.Session", "version")), "(v)", session) == (1,)
|
||||||
query = IPC(runtime / "ferese/control.sock")
|
query = IPC(ipc_socket(runtime))
|
||||||
state = query.call("begin-session-end")
|
state = query.call("begin-session-end")
|
||||||
assert state["query-ready"] is False
|
assert state["query-ready"] is False
|
||||||
wait_for(lambda: any(value["session-state"] == 2 for value in changes), "Missing QueryEnd")
|
wait_for(lambda: any(value["session-state"] == 2 for value in changes), "Missing QueryEnd")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Requires built binaries and bwrap. FERESE_TEST_BIN_DIR selects debug/release. Ho
|
|||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
|
|
||||||
@unittest.skipUnless(os.environ.get("FERESE_TEST_LOCK") == "1", "visible nested lock test is opt-in")
|
@unittest.skipUnless(os.environ.get("FERESE_TEST_LOCK") == "1", "visible nested lock test is opt-in")
|
||||||
class NativeLockTest(unittest.TestCase):
|
class NativeLockTest(unittest.TestCase):
|
||||||
@@ -23,7 +24,7 @@ class NativeLockTest(unittest.TestCase):
|
|||||||
env=dict(os.environ,XDG_RUNTIME_DIR=str(runtime),XDG_CONFIG_HOME=str(root/'config'),WAYLAND_DISPLAY=host_display,FERESE_ENABLE_SCREENCOPY='1')
|
env=dict(os.environ,XDG_RUNTIME_DIR=str(runtime),XDG_CONFIG_HOME=str(root/'config'),WAYLAND_DISPLAY=host_display,FERESE_ENABLE_SCREENCOPY='1')
|
||||||
binary_dir = Path(os.environ.get('FERESE_TEST_BIN_DIR', 'target/release'))
|
binary_dir = Path(os.environ.get('FERESE_TEST_BIN_DIR', 'target/release'))
|
||||||
log=(root/'compositor.log').open('w')
|
log=(root/'compositor.log').open('w')
|
||||||
compositor=subprocess.Popen([str(binary_dir / 'ferese'),'--backend','nested'],env=env,stdout=log,stderr=subprocess.STDOUT)
|
compositor=subprocess.Popen([str(binary_dir / 'ferese'),'--backend','nested'],env=ipc_environment(env),stdout=log,stderr=subprocess.STDOUT)
|
||||||
ctl=binary_dir / 'feresectl'
|
ctl=binary_dir / 'feresectl'
|
||||||
assert ctl.is_file(), f'missing {ctl}; build it with cargo build --release -p feresectl'
|
assert ctl.is_file(), f'missing {ctl}; build it with cargo build --release -p feresectl'
|
||||||
locker=None
|
locker=None
|
||||||
@@ -37,13 +38,13 @@ class NativeLockTest(unittest.TestCase):
|
|||||||
childenv=dict(env,WAYLAND_DISPLAY=str(sockets[0]),FERESE_LOCK_READY='1')
|
childenv=dict(env,WAYLAND_DISPLAY=str(sockets[0]),FERESE_LOCK_READY='1')
|
||||||
def capture(destination, timeout=10):
|
def capture(destination, timeout=10):
|
||||||
with open(destination,'wb') as png:
|
with open(destination,'wb') as png:
|
||||||
return subprocess.run([str(ctl),'screenshot'],env=childenv,stdout=png,stderr=subprocess.PIPE,timeout=timeout)
|
return subprocess.run([str(ctl),'screenshot'],env=ipc_environment(childenv),stdout=png,stderr=subprocess.PIPE,timeout=timeout)
|
||||||
time.sleep(.6)
|
time.sleep(.6)
|
||||||
result=capture(root/'unlocked.png')
|
result=capture(root/'unlocked.png')
|
||||||
assert result.returncode == 0, f'Capture failed while unlocked: {result.stderr.decode()}'
|
assert result.returncode == 0, f'Capture failed while unlocked: {result.stderr.decode()}'
|
||||||
assert (root/'unlocked.png').stat().st_size > 0, 'Capture produced an empty file'
|
assert (root/'unlocked.png').stat().st_size > 0, 'Capture produced an empty file'
|
||||||
print('Native capture succeeded while unlocked',flush=True)
|
print('Native capture succeeded while unlocked',flush=True)
|
||||||
locker=subprocess.Popen(['bwrap','--bind','/','/','--dev-bind','/dev','/dev','--ro-bind',str(pam),'/etc/pam.d','--unshare-user','--',str(binary_dir / 'ferese-lock')],env=childenv,stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=True)
|
locker=subprocess.Popen(['bwrap','--bind','/','/','--dev-bind','/dev','/dev','--ro-bind',str(pam),'/etc/pam.d','--unshare-user','--',str(binary_dir / 'ferese-lock')],env=ipc_environment(childenv),stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=True)
|
||||||
assert locker.wait(timeout=20) == 0, (root/'locker.log').read_text()
|
assert locker.wait(timeout=20) == 0, (root/'locker.log').read_text()
|
||||||
print('Native locker received compositor confirmation',flush=True)
|
print('Native locker received compositor confirmation',flush=True)
|
||||||
result=capture(root/'locked.png')
|
result=capture(root/'locked.png')
|
||||||
@@ -54,7 +55,7 @@ class NativeLockTest(unittest.TestCase):
|
|||||||
result=capture(root/'after-crash.png')
|
result=capture(root/'after-crash.png')
|
||||||
assert result.returncode != 0, 'Capture unexpectedly succeeded after locker crash'
|
assert result.returncode != 0, 'Capture unexpectedly succeeded after locker crash'
|
||||||
print('Capture remains denied after locker crash',flush=True)
|
print('Capture remains denied after locker crash',flush=True)
|
||||||
locker=subprocess.Popen(['bwrap','--bind','/','/','--dev-bind','/dev','/dev','--ro-bind',str(pam),'/etc/pam.d','--unshare-user','--',str(binary_dir / 'ferese-lock')],env=childenv,stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=True)
|
locker=subprocess.Popen(['bwrap','--bind','/','/','--dev-bind','/dev','/dev','--ro-bind',str(pam),'/etc/pam.d','--unshare-user','--',str(binary_dir / 'ferese-lock')],env=ipc_environment(childenv),stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=True)
|
||||||
assert locker.wait(timeout=20) == 0, (root/'locker.log').read_text()
|
assert locker.wait(timeout=20) == 0, (root/'locker.log').read_text()
|
||||||
result=capture(root/'replacement.png')
|
result=capture(root/'replacement.png')
|
||||||
assert result.returncode != 0, 'Capture unexpectedly succeeded after locker takeover'
|
assert result.returncode != 0, 'Capture unexpectedly succeeded after locker takeover'
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Requires built debug compositor binaries, a Wayland host and dbus-daemon.
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -34,17 +35,17 @@ class MaterialLifetime(unittest.TestCase):
|
|||||||
with (root / "compositor.log").open("w") as log:
|
with (root / "compositor.log").open("w") as log:
|
||||||
try:
|
try:
|
||||||
bus = subprocess.Popen(["dbus-daemon", "--session", "--nofork", "--print-address=1"],
|
bus = subprocess.Popen(["dbus-daemon", "--session", "--nofork", "--print-address=1"],
|
||||||
env=env, stdout=subprocess.PIPE, text=True, start_new_session=True)
|
env=ipc_environment(env), stdout=subprocess.PIPE, text=True, start_new_session=True)
|
||||||
children.append(bus)
|
children.append(bus)
|
||||||
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
||||||
compositor = subprocess.Popen([str(ROOT / "target/debug/ferese"), "--backend", "nested"],
|
compositor = subprocess.Popen([str(ROOT / "target/debug/ferese"), "--backend", "nested"],
|
||||||
env=env, stdout=log, stderr=log, start_new_session=True)
|
env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
children.append(compositor)
|
children.append(compositor)
|
||||||
deadline = time.monotonic() + 10
|
deadline = time.monotonic() + 10
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
self.assertIsNone(compositor.poll(), (root / "compositor.log").read_text())
|
self.assertIsNone(compositor.poll(), (root / "compositor.log").read_text())
|
||||||
sockets = [p for p in runtime.glob("wayland-*") if p.is_socket()]
|
sockets = [p for p in runtime.glob("wayland-*") if p.is_socket()]
|
||||||
if sockets and (runtime / "ferese/control.sock").is_socket():
|
if sockets and (ipc_socket(runtime)).is_socket():
|
||||||
break
|
break
|
||||||
time.sleep(.05)
|
time.sleep(.05)
|
||||||
else:
|
else:
|
||||||
@@ -54,7 +55,7 @@ class MaterialLifetime(unittest.TestCase):
|
|||||||
"cargo", "test", "--workspace", "--locked",
|
"cargo", "test", "--workspace", "--locked",
|
||||||
"material::tests::attachment_acknowledgement_and_last_clone_release",
|
"material::tests::attachment_acknowledgement_and_last_clone_release",
|
||||||
"--", "--ignored", "--exact",
|
"--", "--ignored", "--exact",
|
||||||
], cwd=ROOT, env=env, capture_output=True, text=True, timeout=120)
|
], cwd=ROOT, env=ipc_environment(env), capture_output=True, text=True, timeout=120)
|
||||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
self.assertIn("1 passed", result.stdout)
|
self.assertIn("1 passed", result.stdout)
|
||||||
self.assertIsNone(compositor.poll())
|
self.assertIsNone(compositor.poll())
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Requires a Wayland host, PyGObject and dbus-run-session.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
@@ -135,7 +136,7 @@ def private_checks():
|
|||||||
binary = REPO / os.environ.get('FERESE_TEST_BINARY', 'target/debug/ferese')
|
binary = REPO / os.environ.get('FERESE_TEST_BINARY', 'target/debug/ferese')
|
||||||
ctl = REPO / os.environ.get('FERESE_TEST_CTL', 'target/debug/feresectl')
|
ctl = REPO / os.environ.get('FERESE_TEST_CTL', 'target/debug/feresectl')
|
||||||
with (root / 'compositor.log').open('w') as log:
|
with (root / 'compositor.log').open('w') as log:
|
||||||
child = subprocess.Popen([str(binary), '--backend=nested'], env=env,
|
child = subprocess.Popen([str(binary), '--backend=nested'], env=ipc_environment(env),
|
||||||
stdout=log, stderr=log, start_new_session=True)
|
stdout=log, stderr=log, start_new_session=True)
|
||||||
|
|
||||||
def wait(check):
|
def wait(check):
|
||||||
@@ -148,17 +149,17 @@ def private_checks():
|
|||||||
time.sleep(.02)
|
time.sleep(.02)
|
||||||
raise AssertionError('Timed out\n' + (root / 'compositor.log').read_text())
|
raise AssertionError('Timed out\n' + (root / 'compositor.log').read_text())
|
||||||
|
|
||||||
wait(lambda: (runtime / 'ferese/control.sock').exists())
|
wait(lambda: (ipc_socket(runtime)).exists())
|
||||||
|
|
||||||
def call(*args):
|
def call(*args):
|
||||||
return json.loads(subprocess.check_output([str(ctl), "-j", *args], env=env, timeout=5))
|
return json.loads(subprocess.check_output([str(ctl), "-j", *args], env=ipc_environment(env), timeout=5))
|
||||||
|
|
||||||
def request(command, args):
|
def request(command, args):
|
||||||
payload = json.dumps({'version': 1, 'id': 17, 'type': 'command',
|
payload = json.dumps({'version': 1, 'id': 17, 'type': 'command',
|
||||||
'command': command, 'args': args}).encode()
|
'command': command, 'args': args}).encode()
|
||||||
connection = socket.socket(socket.AF_UNIX)
|
connection = socket.socket(socket.AF_UNIX)
|
||||||
connection.settimeout(3)
|
connection.settimeout(3)
|
||||||
connection.connect(str(runtime / 'ferese/control.sock'))
|
connection.connect(str(ipc_socket(runtime)))
|
||||||
connection.sendall(struct.pack('!I', len(payload)) + payload)
|
connection.sendall(struct.pack('!I', len(payload)) + payload)
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
@@ -181,7 +182,7 @@ def private_checks():
|
|||||||
env['WAYLAND_DISPLAY'] = str(next(path for path in runtime.glob('wayland-*') if not path.name.endswith('.lock')))
|
env['WAYLAND_DISPLAY'] = str(next(path for path in runtime.glob('wayland-*') if not path.name.endswith('.lock')))
|
||||||
shell_binary = os.environ.get('FERESE_TEST_SHELL')
|
shell_binary = os.environ.get('FERESE_TEST_SHELL')
|
||||||
if shell_binary:
|
if shell_binary:
|
||||||
shell = subprocess.Popen([str(REPO / shell_binary)], env=env, stdout=log, stderr=log, start_new_session=True)
|
shell = subprocess.Popen([str(REPO / shell_binary)], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
first = players[0]
|
first = players[0]
|
||||||
selected(first.name)
|
selected(first.name)
|
||||||
initial = call('media')
|
initial = call('media')
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import errno
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -46,12 +47,12 @@ class OverviewDamageTest(unittest.TestCase):
|
|||||||
log_path = root / "compositor.log"
|
log_path = root / "compositor.log"
|
||||||
|
|
||||||
def launch(command, **kwargs):
|
def launch(command, **kwargs):
|
||||||
child = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
child = subprocess.Popen(command, env=ipc_environment(env), start_new_session=True, **kwargs)
|
||||||
children.append(child)
|
children.append(child)
|
||||||
return child
|
return child
|
||||||
|
|
||||||
def command(*args):
|
def command(*args):
|
||||||
return json.loads(subprocess.check_output([str(control), "-j", *args], env=env, timeout=5))
|
return json.loads(subprocess.check_output([str(control), "-j", *args], env=ipc_environment(env), timeout=5))
|
||||||
|
|
||||||
def wait_for(predicate, seconds=12):
|
def wait_for(predicate, seconds=12):
|
||||||
deadline = time.monotonic() + seconds
|
deadline = time.monotonic() + seconds
|
||||||
@@ -98,7 +99,7 @@ class OverviewDamageTest(unittest.TestCase):
|
|||||||
stdout=subprocess.PIPE, text=True)
|
stdout=subprocess.PIPE, text=True)
|
||||||
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
||||||
launch([str(binary), "--backend", "nested"], stdout=log, stderr=log)
|
launch([str(binary), "--backend", "nested"], stdout=log, stderr=log)
|
||||||
wait_for(lambda: (runtime / "ferese/control.sock").is_socket())
|
wait_for(lambda: (ipc_socket(runtime)).is_socket())
|
||||||
env["WAYLAND_DISPLAY"] = str(next(
|
env["WAYLAND_DISPLAY"] = str(next(
|
||||||
path for path in runtime.glob("wayland-*") if not path.name.endswith(".lock")
|
path for path in runtime.glob("wayland-*") if not path.name.endswith(".lock")
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import io
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -55,7 +56,7 @@ class OverviewPreviews(unittest.TestCase):
|
|||||||
log_path = root / "compositor.log"
|
log_path = root / "compositor.log"
|
||||||
|
|
||||||
def call(*args):
|
def call(*args):
|
||||||
return subprocess.check_output([str(ctl), "-j", *args], env=env, timeout=10)
|
return subprocess.check_output([str(ctl), "-j", *args], env=ipc_environment(env), timeout=10)
|
||||||
|
|
||||||
def wait_for(predicate):
|
def wait_for(predicate):
|
||||||
deadline = time.monotonic() + 10
|
deadline = time.monotonic() + 10
|
||||||
@@ -69,12 +70,12 @@ class OverviewPreviews(unittest.TestCase):
|
|||||||
|
|
||||||
with log_path.open("w") as log:
|
with log_path.open("w") as log:
|
||||||
def launch(command):
|
def launch(command):
|
||||||
child = subprocess.Popen(command, env=env, stdout=log, stderr=log, start_new_session=True)
|
child = subprocess.Popen(command, env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
children.append(child)
|
children.append(child)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
launch([str(binary), "--backend", "nested"])
|
launch([str(binary), "--backend", "nested"])
|
||||||
wait_for(lambda: (runtime / "ferese/control.sock").is_socket())
|
wait_for(lambda: (ipc_socket(runtime)).is_socket())
|
||||||
env["WAYLAND_DISPLAY"] = str(next(path for path in runtime.glob("wayland-*") if path.is_socket()))
|
env["WAYLAND_DISPLAY"] = str(next(path for path in runtime.glob("wayland-*") if path.is_socket()))
|
||||||
launch([str(root / "client")])
|
launch([str(root / "client")])
|
||||||
wait_for(lambda: len(json.loads(call("get-windows"))) == 1)
|
wait_for(lambda: len(json.loads(call("get-windows"))) == 1)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Requires built debug compositor/control binaries, a Wayland host, cc and wayland
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -57,10 +58,10 @@ class Presentation(unittest.TestCase):
|
|||||||
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
||||||
log = (root / "compositor.log").open("w")
|
log = (root / "compositor.log").open("w")
|
||||||
try:
|
try:
|
||||||
compositor = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=env, stdout=log, stderr=log, start_new_session=True)
|
compositor = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
processes.append(compositor)
|
processes.append(compositor)
|
||||||
deadline = time.monotonic() + 10
|
deadline = time.monotonic() + 10
|
||||||
while not (runtime / "ferese/control.sock").exists():
|
while not (ipc_socket(runtime)).exists():
|
||||||
if compositor.poll() is not None or time.monotonic() > deadline:
|
if compositor.poll() is not None or time.monotonic() > deadline:
|
||||||
self.fail((root / "compositor.log").read_text())
|
self.fail((root / "compositor.log").read_text())
|
||||||
time.sleep(0.02)
|
time.sleep(0.02)
|
||||||
@@ -68,7 +69,7 @@ class Presentation(unittest.TestCase):
|
|||||||
env["WAYLAND_DISPLAY"] = str(next(path for path in sockets if not path.name.endswith(".lock")))
|
env["WAYLAND_DISPLAY"] = str(next(path for path in sockets if not path.name.endswith(".lock")))
|
||||||
|
|
||||||
def call(*args):
|
def call(*args):
|
||||||
return subprocess.check_output([str(REPO / "target/debug/feresectl"), "-j", *args], env=env, timeout=10)
|
return subprocess.check_output([str(REPO / "target/debug/feresectl"), "-j", *args], env=ipc_environment(env), timeout=10)
|
||||||
|
|
||||||
def windows(count):
|
def windows(count):
|
||||||
deadline = time.monotonic() + 10
|
deadline = time.monotonic() + 10
|
||||||
@@ -80,7 +81,7 @@ class Presentation(unittest.TestCase):
|
|||||||
self.fail((root / "compositor.log").read_text())
|
self.fail((root / "compositor.log").read_text())
|
||||||
|
|
||||||
events = (root / "events").open("w")
|
events = (root / "events").open("w")
|
||||||
target = subprocess.Popen([str(root / "client")], env=env, stdin=subprocess.PIPE,
|
target = subprocess.Popen([str(root / "client")], env=ipc_environment(env), stdin=subprocess.PIPE,
|
||||||
stdout=events, stderr=log, start_new_session=True)
|
stdout=events, stderr=log, start_new_session=True)
|
||||||
processes.append(target)
|
processes.append(target)
|
||||||
windows(1)
|
windows(1)
|
||||||
@@ -95,7 +96,7 @@ class Presentation(unittest.TestCase):
|
|||||||
self.assertGreater(count("frame"), before, "visible empty commits must get callbacks")
|
self.assertGreater(count("frame"), before, "visible empty commits must get callbacks")
|
||||||
self.assertEqual(count("idle"), idle, "visible inhibitor must keep the notifier active")
|
self.assertEqual(count("idle"), idle, "visible inhibitor must keep the notifier active")
|
||||||
|
|
||||||
cover = subprocess.Popen([str(root / "client"), "cover"], env=env, stdout=log,
|
cover = subprocess.Popen([str(root / "client"), "cover"], env=ipc_environment(env), stdout=log,
|
||||||
stderr=log, start_new_session=True)
|
stderr=log, start_new_session=True)
|
||||||
processes.append(cover)
|
processes.append(cover)
|
||||||
current = windows(2)
|
current = windows(2)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ before the 300 ms deadline; it does not measure frame pacing or GPU performance.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import select
|
import select
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -50,10 +51,10 @@ class ResizeDependencies(unittest.TestCase):
|
|||||||
ctl = REPO / os.environ.get("FERESE_TEST_CTL", "target/debug/feresectl")
|
ctl = REPO / os.environ.get("FERESE_TEST_CTL", "target/debug/feresectl")
|
||||||
with (root / "compositor.log").open("w") as log:
|
with (root / "compositor.log").open("w") as log:
|
||||||
try:
|
try:
|
||||||
compositor = subprocess.Popen([str(binary), "--backend=nested"], env=env, stdout=log, stderr=log)
|
compositor = subprocess.Popen([str(binary), "--backend=nested"], env=ipc_environment(env), stdout=log, stderr=log)
|
||||||
processes.append(compositor)
|
processes.append(compositor)
|
||||||
deadline = time.monotonic() + 10
|
deadline = time.monotonic() + 10
|
||||||
while not (runtime / "ferese/control.sock").exists():
|
while not (ipc_socket(runtime)).exists():
|
||||||
if compositor.poll() is not None or time.monotonic() > deadline:
|
if compositor.poll() is not None or time.monotonic() > deadline:
|
||||||
self.fail((root / "compositor.log").read_text())
|
self.fail((root / "compositor.log").read_text())
|
||||||
time.sleep(.02)
|
time.sleep(.02)
|
||||||
@@ -61,10 +62,10 @@ class ResizeDependencies(unittest.TestCase):
|
|||||||
client_env = dict(env, WAYLAND_DISPLAY=str(socket))
|
client_env = dict(env, WAYLAND_DISPLAY=str(socket))
|
||||||
|
|
||||||
def call(*args):
|
def call(*args):
|
||||||
return json.loads(subprocess.check_output([str(ctl), "-j", *args], env=env, timeout=5))
|
return json.loads(subprocess.check_output([str(ctl), "-j", *args], env=ipc_environment(env), timeout=5))
|
||||||
|
|
||||||
def action(*args):
|
def action(*args):
|
||||||
subprocess.run([str(ctl), *args], env=env, check=True, capture_output=True, timeout=5)
|
subprocess.run([str(ctl), *args], env=ipc_environment(env), check=True, capture_output=True, timeout=5)
|
||||||
|
|
||||||
def client(name):
|
def client(name):
|
||||||
process = subprocess.Popen([str(root / "client"), name], env=client_env,
|
process = subprocess.Popen([str(root / "client"), name], env=client_env,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ No host permission store, compositor configuration or portal service is changed.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
@@ -94,10 +95,10 @@ def private_test():
|
|||||||
env.pop("WAYLAND_SOCKET", None)
|
env.pop("WAYLAND_SOCKET", None)
|
||||||
log = (root / "test.log").open("w")
|
log = (root / "test.log").open("w")
|
||||||
try:
|
try:
|
||||||
compositor = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=env, stdout=log, stderr=log, start_new_session=True)
|
compositor = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
processes.append(compositor)
|
processes.append(compositor)
|
||||||
destination = runtime / "ferese/control.sock"
|
wait_for(lambda: ipc_socket(runtime).is_socket(), "Nested compositor did not start")
|
||||||
wait_for(lambda: destination.exists(), "Nested compositor did not start")
|
destination = ipc_socket(runtime)
|
||||||
admin = IPC(destination)
|
admin = IPC(destination)
|
||||||
outputs = admin.call("get-outputs")
|
outputs = admin.call("get-outputs")
|
||||||
name = next(output["name"] for output in outputs if output["enabled"])
|
name = next(output["name"] for output in outputs if output["enabled"])
|
||||||
@@ -122,7 +123,7 @@ def private_test():
|
|||||||
threading.Thread(target=accept, daemon=True).start()
|
threading.Thread(target=accept, daemon=True).start()
|
||||||
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||||
bus.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "RequestName", GLib.Variant("(su)", ("org.freedesktop.portal.Desktop", 0)), None, Gio.DBusCallFlags.NONE, 2000, None)
|
bus.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "RequestName", GLib.Variant("(su)", ("org.freedesktop.portal.Desktop", 0)), None, Gio.DBusCallFlags.NONE, 2000, None)
|
||||||
backend = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese")], env=env, stdout=log, stderr=log, start_new_session=True)
|
backend = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese")], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
processes.append(backend)
|
processes.append(backend)
|
||||||
def call(method, params=None, path=PATH, interface=INTERFACE):
|
def call(method, params=None, path=PATH, interface=INTERFACE):
|
||||||
return bus.call_sync(NAME, path, interface, method, params, None, Gio.DBusCallFlags.NO_AUTO_START, 12000, None).unpack()
|
return bus.call_sync(NAME, path, interface, method, params, None, Gio.DBusCallFlags.NO_AUTO_START, 12000, None).unpack()
|
||||||
@@ -180,7 +181,7 @@ def private_test():
|
|||||||
fallback("replacement", saved=stale)
|
fallback("replacement", saved=stale)
|
||||||
print("PASS: other-app and changed-device restore require fresh consent", flush=True)
|
print("PASS: other-app and changed-device restore require fresh consent", flush=True)
|
||||||
|
|
||||||
mismatch = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese"), "--stream", name, "hidden", str(2**32 - 1)], env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, start_new_session=True)
|
mismatch = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese"), "--stream", name, "hidden", str(2**32 - 1)], env=ipc_environment(env), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, start_new_session=True)
|
||||||
processes.append(mismatch)
|
processes.append(mismatch)
|
||||||
mismatch.wait(timeout=8)
|
mismatch.wait(timeout=8)
|
||||||
error = mismatch.stderr.read()
|
error = mismatch.stderr.read()
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""Check IPC discovery for isolated compositor drivers."""
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
|
|
||||||
|
|
||||||
|
class SessionSocketTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.directory = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.directory.cleanup)
|
||||||
|
self.runtime = Path(self.directory.name)
|
||||||
|
# Only path discovery is under test; endpoint permissions and actual
|
||||||
|
# socket binding are covered by the compositor's IPC tests.
|
||||||
|
self.socket_type = patch.object(Path, 'is_socket', Path.is_file)
|
||||||
|
self.socket_type.start()
|
||||||
|
self.addCleanup(self.socket_type.stop)
|
||||||
|
|
||||||
|
def endpoint(self, suffix):
|
||||||
|
path = self.runtime / suffix
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.touch()
|
||||||
|
return path
|
||||||
|
|
||||||
|
def test_discovery_changes_when_the_nested_listener_appears(self):
|
||||||
|
self.assertEqual(ipc_socket(self.runtime), self.runtime / 'ferese/control.sock')
|
||||||
|
nested = self.endpoint('ferese/instances/123-abc/control.sock')
|
||||||
|
self.assertEqual(ipc_socket(self.runtime), nested)
|
||||||
|
|
||||||
|
def test_client_environment_replaces_an_inherited_host_target(self):
|
||||||
|
nested = self.endpoint('ferese/instances/123-abc/control.sock')
|
||||||
|
environment = {'XDG_RUNTIME_DIR': str(self.runtime), 'FERESE_SOCKET': '/run/user/1000/ferese/control.sock'}
|
||||||
|
self.assertEqual(ipc_environment(environment)['FERESE_SOCKET'], str(nested))
|
||||||
|
self.assertEqual(environment['FERESE_SOCKET'], '/run/user/1000/ferese/control.sock')
|
||||||
|
|
||||||
|
def test_proxy_or_drm_endpoint_remains_supported(self):
|
||||||
|
default = self.endpoint('ferese/control.sock')
|
||||||
|
self.assertEqual(ipc_socket(self.runtime), default)
|
||||||
|
|
||||||
|
def test_multiple_instances_require_an_explicit_test_target(self):
|
||||||
|
self.endpoint('ferese/instances/123-abc/control.sock')
|
||||||
|
self.endpoint('ferese/instances/456-def/control.sock')
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
ipc_socket(self.runtime)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -32,6 +32,7 @@ class SessionStartupTest(unittest.TestCase):
|
|||||||
path.write_text('#!/bin/bash\n' + body)
|
path.write_text('#!/bin/bash\n' + body)
|
||||||
path.chmod(0o755)
|
path.chmod(0o755)
|
||||||
env = dict(os.environ, PATH=f'{root}:{os.environ["PATH"]}', TEST_ROOT=directory,
|
env = dict(os.environ, PATH=f'{root}:{os.environ["PATH"]}', TEST_ROOT=directory,
|
||||||
|
FERESE_SESSION_MODE='desktop',
|
||||||
FERESE_SESSION_IMPORT_ENV=str(int(direct)), WAYLAND_DISPLAY='private-display',
|
FERESE_SESSION_IMPORT_ENV=str(int(direct)), WAYLAND_DISPLAY='private-display',
|
||||||
FERESE_PUBLIC_WAYLAND_DISPLAY='public-display', WAYLAND_SOCKET='77',
|
FERESE_PUBLIC_WAYLAND_DISPLAY='public-display', WAYLAND_SOCKET='77',
|
||||||
TEST_FAIL=str(int(fail_target)), TEST_STATUS=str(shell_status), TEST_WAIT=str(int(terminate)))
|
TEST_FAIL=str(int(fail_target)), TEST_STATUS=str(shell_status), TEST_WAIT=str(int(terminate)))
|
||||||
@@ -71,6 +72,11 @@ class SessionStartupTest(unittest.TestCase):
|
|||||||
'--user try-restart xdg-desktop-portal-ferese.service', 'shell:77',
|
'--user try-restart xdg-desktop-portal-ferese.service', 'shell:77',
|
||||||
'--user stop ferese-session.target'])
|
'--user stop ferese-session.target'])
|
||||||
|
|
||||||
|
def test_direct_session_imports_its_control_socket(self):
|
||||||
|
status, events = self.run_session(extra_env={'FERESE_SOCKET': '/run/user/1000/ferese/control.sock'})
|
||||||
|
self.assertEqual(status, 0)
|
||||||
|
self.assertIn('FERESE_SOCKET=/run/user/1000/ferese/control.sock', self.imported_vars(events))
|
||||||
|
|
||||||
def test_a_managed_x11_endpoint_is_imported_for_dbus_activators(self):
|
def test_a_managed_x11_endpoint_is_imported_for_dbus_activators(self):
|
||||||
status, events = self.run_session(display=':7', xauthority='/run/user/1000/ferese-xauth-abcd')
|
status, events = self.run_session(display=':7', xauthority='/run/user/1000/ferese-xauth-abcd')
|
||||||
self.assertEqual(status, 0)
|
self.assertEqual(status, 0)
|
||||||
@@ -105,6 +111,11 @@ class SessionStartupTest(unittest.TestCase):
|
|||||||
self.assertEqual(status, 0)
|
self.assertEqual(status, 0)
|
||||||
self.assertEqual(events, ['shell:77'])
|
self.assertEqual(events, ['shell:77'])
|
||||||
|
|
||||||
|
def test_embedded_policy_blocks_session_services_despite_an_inherited_import_flag(self):
|
||||||
|
status, events = self.run_session(direct=True, extra_env={'FERESE_SESSION_MODE': 'embedded'})
|
||||||
|
self.assertEqual(status, 0)
|
||||||
|
self.assertEqual(events, ['shell:77'])
|
||||||
|
|
||||||
def test_failed_target_does_not_prevent_shell_or_stop_unowned_target(self):
|
def test_failed_target_does_not_prevent_shell_or_stop_unowned_target(self):
|
||||||
status, events = self.run_session(fail_target=True)
|
status, events = self.run_session(fail_target=True)
|
||||||
self.assertEqual(status, 0)
|
self.assertEqual(status, 0)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Protocol traces contain only this test's shell traffic, never the host desktop.
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -53,7 +54,7 @@ class ShellMotionTest(unittest.TestCase):
|
|||||||
'<allow receive_sender="*"/><allow own="*"/></policy></busconfig>')
|
'<allow receive_sender="*"/><allow own="*"/></policy></busconfig>')
|
||||||
|
|
||||||
def launch(command, **kwargs):
|
def launch(command, **kwargs):
|
||||||
process = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
process = subprocess.Popen(command, env=ipc_environment(env), start_new_session=True, **kwargs)
|
||||||
children.append(process)
|
children.append(process)
|
||||||
return process
|
return process
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ class ShellMotionTest(unittest.TestCase):
|
|||||||
return subprocess.run(["gdbus", "call", "--session", "--dest", "org.freedesktop.Notifications",
|
return subprocess.run(["gdbus", "call", "--session", "--dest", "org.freedesktop.Notifications",
|
||||||
"--object-path", "/org/freedesktop/Notifications", "--method",
|
"--object-path", "/org/freedesktop/Notifications", "--method",
|
||||||
"org.freedesktop.Notifications." + method, *args],
|
"org.freedesktop.Notifications." + method, *args],
|
||||||
env=env, text=True, capture_output=True, timeout=5, check=check)
|
env=ipc_environment(env), text=True, capture_output=True, timeout=5, check=check)
|
||||||
|
|
||||||
def layer_surface(namespace):
|
def layer_surface(namespace):
|
||||||
matches = list(re.finditer(r'get_layer_surface\([^\n]*?wl_surface[@#](\d+)[^\n]*"' + namespace + '"', log_path.read_text()))
|
matches = list(re.finditer(r'get_layer_surface\([^\n]*?wl_surface[@#](\d+)[^\n]*"' + namespace + '"', log_path.read_text()))
|
||||||
@@ -132,7 +133,7 @@ class ShellMotionTest(unittest.TestCase):
|
|||||||
compositor = launch([str(binary), "--backend", "nested", "--grant-effects", "--grant-shell-control",
|
compositor = launch([str(binary), "--backend", "nested", "--grant-effects", "--grant-shell-control",
|
||||||
"--", "env", "WAYLAND_DEBUG=client", "ICED_BACKEND=tiny-skia", str(shell)],
|
"--", "env", "WAYLAND_DEBUG=client", "ICED_BACKEND=tiny-skia", str(shell)],
|
||||||
stdout=log, stderr=log)
|
stdout=log, stderr=log)
|
||||||
wait_for(lambda: (runtime / "ferese/control.sock").is_socket())
|
wait_for(lambda: (ipc_socket(runtime)).is_socket())
|
||||||
wait_for(lambda: dbus("GetServerInformation", check=False).returncode == 0)
|
wait_for(lambda: dbus("GetServerInformation", check=False).returncode == 0)
|
||||||
bar = wait_for(lambda: layer_surface("ferese-shell-top-bar"))
|
bar = wait_for(lambda: layer_surface("ferese-shell-top-bar"))
|
||||||
if bar_layout == "islands":
|
if bar_layout == "islands":
|
||||||
@@ -165,11 +166,11 @@ class ShellMotionTest(unittest.TestCase):
|
|||||||
"notification materials did not follow their fade")
|
"notification materials did not follow their fade")
|
||||||
print(f"Toast: {opening_frames} opening callback requests, {idle_frames / 2:g} settled commits/s; destroyed", flush=True)
|
print(f"Toast: {opening_frames} opening callback requests, {idle_frames / 2:g} settled commits/s; destroyed", flush=True)
|
||||||
|
|
||||||
subprocess.run([str(ctl), "toggle-keybinding-guide"], env=env, check=True, capture_output=True)
|
subprocess.run([str(ctl), "toggle-keybinding-guide"], env=ipc_environment(env), check=True, capture_output=True)
|
||||||
modal = wait_for(lambda: layer_surface("ferese-system-modal"))
|
modal = wait_for(lambda: layer_surface("ferese-system-modal"))
|
||||||
wait_for(lambda: frames(modal) >= 4)
|
wait_for(lambda: frames(modal) >= 4)
|
||||||
time.sleep(.3)
|
time.sleep(.3)
|
||||||
subprocess.run([str(ctl), "toggle-keybinding-guide"], env=env, check=True, capture_output=True)
|
subprocess.run([str(ctl), "toggle-keybinding-guide"], env=ipc_environment(env), check=True, capture_output=True)
|
||||||
wait_for(lambda: destroyed(modal), timeout=5)
|
wait_for(lambda: destroyed(modal), timeout=5)
|
||||||
self.assertGreater(frames(modal), 10, "modal close did not follow frames")
|
self.assertGreater(frames(modal), 10, "modal close did not follow frames")
|
||||||
self.assertIsNone(compositor.poll(), log_path.read_text()[-8000:])
|
self.assertIsNone(compositor.poll(), log_path.read_text()[-8000:])
|
||||||
@@ -181,7 +182,7 @@ class ShellMotionTest(unittest.TestCase):
|
|||||||
source = config_file.read_text()
|
source = config_file.read_text()
|
||||||
config_file.write_text(re.sub(
|
config_file.write_text(re.sub(
|
||||||
r'bar-layout "[^"]+"', f'bar-layout "{layout_name}"', source))
|
r'bar-layout "[^"]+"', f'bar-layout "{layout_name}"', source))
|
||||||
subprocess.run([str(ctl), "reload-config"], env=env, check=True, capture_output=True)
|
subprocess.run([str(ctl), "reload-config"], env=ipc_environment(env), check=True, capture_output=True)
|
||||||
wait_for(lambda: bar_region_count(bar) == (expected_count or 2))
|
wait_for(lambda: bar_region_count(bar) == (expected_count or 2))
|
||||||
|
|
||||||
self.assertIsNone(compositor.poll(), log_path.read_text()[-8000:])
|
self.assertIsNone(compositor.poll(), log_path.read_text()[-8000:])
|
||||||
@@ -192,7 +193,7 @@ class ShellMotionTest(unittest.TestCase):
|
|||||||
source = config_file.read_text()
|
source = config_file.read_text()
|
||||||
config_file.write_text(re.sub(
|
config_file.write_text(re.sub(
|
||||||
r'bar-island-padding \d+', f'bar-island-padding {padding}', source))
|
r'bar-island-padding \d+', f'bar-island-padding {padding}', source))
|
||||||
subprocess.run([str(ctl), "reload-config"], env=env, check=True, capture_output=True)
|
subprocess.run([str(ctl), "reload-config"], env=ipc_environment(env), check=True, capture_output=True)
|
||||||
expected_width = original_input[0][2] - 2 * (12 - padding)
|
expected_width = original_input[0][2] - 2 * (12 - padding)
|
||||||
wait_for(lambda: len(bar_input_rectangles(bar)) == 2
|
wait_for(lambda: len(bar_input_rectangles(bar)) == 2
|
||||||
and bar_input_rectangles(bar)[0][2] == expected_width)
|
and bar_input_rectangles(bar)[0][2] == expected_width)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
@@ -31,12 +32,12 @@ class ShortcutConnections(unittest.TestCase):
|
|||||||
env = dict(os.environ, XDG_RUNTIME_DIR=str(runtime), XDG_CONFIG_HOME=str(root / "config"), WAYLAND_DISPLAY=str(host_display))
|
env = dict(os.environ, XDG_RUNTIME_DIR=str(runtime), XDG_CONFIG_HOME=str(root / "config"), WAYLAND_DISPLAY=str(host_display))
|
||||||
env.pop("WAYLAND_SOCKET", None)
|
env.pop("WAYLAND_SOCKET", None)
|
||||||
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
||||||
process = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=env,
|
process = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=ipc_environment(env),
|
||||||
stdout=log, stderr=log, start_new_session=True)
|
stdout=log, stderr=log, start_new_session=True)
|
||||||
connections = []
|
connections = []
|
||||||
try:
|
try:
|
||||||
deadline = time.monotonic() + 15
|
deadline = time.monotonic() + 15
|
||||||
while not (runtime / "ferese/control.sock").exists():
|
while not (ipc_socket(runtime)).exists():
|
||||||
if process.poll() is not None or time.monotonic() >= deadline:
|
if process.poll() is not None or time.monotonic() >= deadline:
|
||||||
self.fail((root / "compositor.log").read_text())
|
self.fail((root / "compositor.log").read_text())
|
||||||
time.sleep(0.02)
|
time.sleep(0.02)
|
||||||
@@ -44,7 +45,7 @@ class ShortcutConnections(unittest.TestCase):
|
|||||||
def connect():
|
def connect():
|
||||||
connection = socket.socket(socket.AF_UNIX)
|
connection = socket.socket(socket.AF_UNIX)
|
||||||
connection.settimeout(2)
|
connection.settimeout(2)
|
||||||
connection.connect(str(runtime / "ferese/control.sock"))
|
connection.connect(str(ipc_socket(runtime)))
|
||||||
connections.append(connection)
|
connections.append(connection)
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Requires target/debug/ferese, feresectl, and a running Wayland desktop.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import select
|
import select
|
||||||
import signal
|
import signal
|
||||||
import shutil
|
import shutil
|
||||||
@@ -38,12 +39,12 @@ class ThemeTest(unittest.TestCase):
|
|||||||
XDG_CONFIG_HOME=str(root / "config"))
|
XDG_CONFIG_HOME=str(root / "config"))
|
||||||
|
|
||||||
def launch(command, **kwargs):
|
def launch(command, **kwargs):
|
||||||
child = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
child = subprocess.Popen(command, env=ipc_environment(env), start_new_session=True, **kwargs)
|
||||||
children.append(child)
|
children.append(child)
|
||||||
return child
|
return child
|
||||||
|
|
||||||
def command(*args):
|
def command(*args):
|
||||||
return json.loads(subprocess.check_output([str(repo / "target/debug/feresectl"), "-j", *args], env=env, timeout=5))
|
return json.loads(subprocess.check_output([str(repo / "target/debug/feresectl"), "-j", *args], env=ipc_environment(env), timeout=5))
|
||||||
|
|
||||||
def until(predicate, seconds=5):
|
def until(predicate, seconds=5):
|
||||||
deadline = time.monotonic() + seconds
|
deadline = time.monotonic() + seconds
|
||||||
@@ -67,7 +68,7 @@ class ThemeTest(unittest.TestCase):
|
|||||||
bus = launch(["dbus-daemon", "--session", "--nofork", "--print-address=1"], stdout=subprocess.PIPE, text=True)
|
bus = launch(["dbus-daemon", "--session", "--nofork", "--print-address=1"], stdout=subprocess.PIPE, text=True)
|
||||||
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
||||||
compositor = launch([str(repo / "target/debug/ferese"), "--backend", "nested"], stdout=log, stderr=log)
|
compositor = launch([str(repo / "target/debug/ferese"), "--backend", "nested"], stdout=log, stderr=log)
|
||||||
until(lambda: (runtime / "ferese/control.sock").is_socket())
|
until(lambda: (ipc_socket(runtime)).is_socket())
|
||||||
self.assertEqual(command("theme", "get")["theme"]["appearance"], "dark")
|
self.assertEqual(command("theme", "get")["theme"]["appearance"], "dark")
|
||||||
subscriber = launch([str(repo / "target/debug/feresectl"), "theme", "subscribe"], stdout=subprocess.PIPE, text=True)
|
subscriber = launch([str(repo / "target/debug/feresectl"), "theme", "subscribe"], stdout=subprocess.PIPE, text=True)
|
||||||
self.assertTrue(select.select([subscriber.stdout], [], [], 3)[0])
|
self.assertTrue(select.select([subscriber.stdout], [], [], 3)[0])
|
||||||
@@ -116,10 +117,10 @@ class ThemeTest(unittest.TestCase):
|
|||||||
command("theme", "mode", "light")
|
command("theme", "mode", "light")
|
||||||
until(lambda: command("theme", "get")["theme"]["tokens"]["colors"]["surface_base"] == "#FBF1C7")
|
until(lambda: command("theme", "get")["theme"]["tokens"]["colors"]["surface_base"] == "#FBF1C7")
|
||||||
if shutil.which("gsettings"):
|
if shutil.which("gsettings"):
|
||||||
subprocess.run(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "prefer-dark"], env=env, check=True, timeout=5)
|
subprocess.run(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "prefer-dark"], env=ipc_environment(env), check=True, timeout=5)
|
||||||
replace(source, 'theme { mode "auto"; family "everforest"; schedule { source "system"; }; }\n')
|
replace(source, 'theme { mode "auto"; family "everforest"; schedule { source "system"; }; }\n')
|
||||||
until(lambda: (snapshot := command("theme", "get"))["mode"] == "auto" and snapshot["theme"]["appearance"] == "dark" and snapshot["fallback_note"] is None)
|
until(lambda: (snapshot := command("theme", "get"))["mode"] == "auto" and snapshot["theme"]["appearance"] == "dark" and snapshot["fallback_note"] is None)
|
||||||
subprocess.run(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "prefer-light"], env=env, check=True, timeout=5)
|
subprocess.run(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "prefer-light"], env=ipc_environment(env), check=True, timeout=5)
|
||||||
until(lambda: (snapshot := command("theme", "get"))["theme"]["appearance"] == "light" and snapshot["presented"] == snapshot["theme"])
|
until(lambda: (snapshot := command("theme", "get"))["theme"]["appearance"] == "light" and snapshot["presented"] == snapshot["theme"])
|
||||||
self.assertIsNone(compositor.poll())
|
self.assertIsNone(compositor.poll())
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Requires cc, pkg-config, wayland-scanner, and wayland-protocols.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import select
|
import select
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -62,7 +63,7 @@ class UnmappedToplevelTest(unittest.TestCase):
|
|||||||
with (root / "compositor.log").open("w") as log:
|
with (root / "compositor.log").open("w") as log:
|
||||||
compositor = subprocess.Popen(
|
compositor = subprocess.Popen(
|
||||||
[str(compositor_binary), "--backend", "nested"],
|
[str(compositor_binary), "--backend", "nested"],
|
||||||
env=env,
|
env=ipc_environment(env),
|
||||||
stdout=log,
|
stdout=log,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
)
|
)
|
||||||
@@ -101,7 +102,7 @@ class UnmappedToplevelTest(unittest.TestCase):
|
|||||||
while True:
|
while True:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[str(ctl_binary), "-j", "focused-window"],
|
[str(ctl_binary), "-j", "focused-window"],
|
||||||
env=env,
|
env=ipc_environment(env),
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
check=True,
|
check=True,
|
||||||
@@ -116,7 +117,7 @@ class UnmappedToplevelTest(unittest.TestCase):
|
|||||||
visible_ids.append(focused["id"])
|
visible_ids.append(focused["id"])
|
||||||
if phase == "visible":
|
if phase == "visible":
|
||||||
# Leave a configure/resize outstanding when the buffer is detached.
|
# Leave a configure/resize outstanding when the buffer is detached.
|
||||||
subprocess.run([str(ctl_binary), "toggle-maximized"], env=env,
|
subprocess.run([str(ctl_binary), "toggle-maximized"], env=ipc_environment(env),
|
||||||
capture_output=True, check=True)
|
capture_output=True, check=True)
|
||||||
time.sleep(.08)
|
time.sleep(.08)
|
||||||
if phase == "detached":
|
if phase == "detached":
|
||||||
@@ -131,7 +132,7 @@ class UnmappedToplevelTest(unittest.TestCase):
|
|||||||
deadline = time.monotonic() + 3
|
deadline = time.monotonic() + 3
|
||||||
while True:
|
while True:
|
||||||
remaining = json.loads(subprocess.check_output(
|
remaining = json.loads(subprocess.check_output(
|
||||||
[str(ctl_binary), "-j", "windows"], env=env, text=True
|
[str(ctl_binary), "-j", "windows"], env=ipc_environment(env), text=True
|
||||||
))
|
))
|
||||||
if not remaining:
|
if not remaining:
|
||||||
break
|
break
|
||||||
@@ -139,10 +140,10 @@ class UnmappedToplevelTest(unittest.TestCase):
|
|||||||
self.fail(f"destroyed window still managed: {remaining}")
|
self.fail(f"destroyed window still managed: {remaining}")
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
|
||||||
subprocess.run([str(ctl_binary), "focus-last-window"], env=env,
|
subprocess.run([str(ctl_binary), "focus-last-window"], env=ipc_environment(env),
|
||||||
capture_output=True, check=True)
|
capture_output=True, check=True)
|
||||||
focused = json.loads(subprocess.check_output(
|
focused = json.loads(subprocess.check_output(
|
||||||
[str(ctl_binary), "-j", "focused-window"], env=env, text=True
|
[str(ctl_binary), "-j", "focused-window"], env=ipc_environment(env), text=True
|
||||||
))
|
))
|
||||||
self.assertIsNone(focused, "focus history must not restore a destroyed window")
|
self.assertIsNone(focused, "focus history must not restore a destroyed window")
|
||||||
deadline = time.monotonic() + 3
|
deadline = time.monotonic() + 3
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import io
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import signal
|
import signal
|
||||||
import selectors
|
import selectors
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -58,10 +59,10 @@ class WindowCapture(unittest.TestCase):
|
|||||||
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
||||||
log = (root / "compositor.log").open("w")
|
log = (root / "compositor.log").open("w")
|
||||||
try:
|
try:
|
||||||
compositor = subprocess.Popen([str(COMPOSITOR), "--backend=nested"], env=env, stdout=log, stderr=log, start_new_session=True)
|
compositor = subprocess.Popen([str(COMPOSITOR), "--backend=nested"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
processes.append(compositor)
|
processes.append(compositor)
|
||||||
deadline = time.monotonic() + 10
|
deadline = time.monotonic() + 10
|
||||||
while not (runtime / "ferese/control.sock").exists():
|
while not (ipc_socket(runtime)).exists():
|
||||||
if compositor.poll() is not None or time.monotonic() > deadline:
|
if compositor.poll() is not None or time.monotonic() > deadline:
|
||||||
self.fail((root / "compositor.log").read_text())
|
self.fail((root / "compositor.log").read_text())
|
||||||
time.sleep(0.02)
|
time.sleep(0.02)
|
||||||
@@ -69,7 +70,7 @@ class WindowCapture(unittest.TestCase):
|
|||||||
env["WAYLAND_DISPLAY"] = str(next(path for path in sockets if not path.name.endswith(".lock")))
|
env["WAYLAND_DISPLAY"] = str(next(path for path in sockets if not path.name.endswith(".lock")))
|
||||||
|
|
||||||
def call(*args):
|
def call(*args):
|
||||||
return subprocess.check_output([str(CTL), "-j", *args], env=env, timeout=10)
|
return subprocess.check_output([str(CTL), "-j", *args], env=ipc_environment(env), timeout=10)
|
||||||
|
|
||||||
def windows(count):
|
def windows(count):
|
||||||
deadline = time.monotonic() + 10
|
deadline = time.monotonic() + 10
|
||||||
@@ -80,9 +81,9 @@ class WindowCapture(unittest.TestCase):
|
|||||||
time.sleep(0.02)
|
time.sleep(0.02)
|
||||||
self.fail((root / "compositor.log").read_text())
|
self.fail((root / "compositor.log").read_text())
|
||||||
|
|
||||||
processes.append(subprocess.Popen([str(root / "client")], env=env, stdout=log, stderr=log, start_new_session=True))
|
processes.append(subprocess.Popen([str(root / "client")], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True))
|
||||||
target = windows(1)[0]
|
target = windows(1)[0]
|
||||||
processes.append(subprocess.Popen([str(root / "client"), "cover"], env=env, stdout=log, stderr=log, start_new_session=True))
|
processes.append(subprocess.Popen([str(root / "client"), "cover"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True))
|
||||||
current = windows(2)
|
current = windows(2)
|
||||||
cover = next(window for window in current if window["title"] == "Occluding window")
|
cover = next(window for window in current if window["title"] == "Occluding window")
|
||||||
self.assertEqual((target["x"], target["y"], target["width"], target["height"]),
|
self.assertEqual((target["x"], target["y"], target["width"], target["height"]),
|
||||||
@@ -131,7 +132,7 @@ class WindowCapture(unittest.TestCase):
|
|||||||
worker.stdin.close()
|
worker.stdin.close()
|
||||||
worker.stdout.close()
|
worker.stdout.close()
|
||||||
rejected = subprocess.run([str(CTL), "screenshot-window", str(target["id"])],
|
rejected = subprocess.run([str(CTL), "screenshot-window", str(target["id"])],
|
||||||
env=env, capture_output=True, timeout=10)
|
env=ipc_environment(env), capture_output=True, timeout=10)
|
||||||
self.assertNotEqual(rejected.returncode, 0)
|
self.assertNotEqual(rejected.returncode, 0)
|
||||||
self.assertIn(b"protected", rejected.stderr)
|
self.assertIn(b"protected", rejected.stderr)
|
||||||
self.assertTrue(next(window for window in windows(2) if window["id"] == target["id"])["mapped"])
|
self.assertTrue(next(window for window in windows(2) if window["id"] == target["id"])["mapped"])
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Only this test's compositor, windows and streams are stopped.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import select
|
import select
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -65,21 +66,21 @@ class WindowStream(unittest.TestCase):
|
|||||||
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
||||||
log = (root / "test.log").open("w")
|
log = (root / "test.log").open("w")
|
||||||
try:
|
try:
|
||||||
compositor = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=env, stdout=log, stderr=log, start_new_session=True)
|
compositor = subprocess.Popen([str(REPO / "target/debug/ferese"), "--backend=nested"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
processes.append(compositor)
|
processes.append(compositor)
|
||||||
socket = runtime / "ferese/control.sock"
|
wait_for(lambda: ipc_socket(runtime).is_socket(), "Nested compositor did not start")
|
||||||
wait_for(socket.exists, "Nested compositor did not start")
|
socket = ipc_socket(runtime)
|
||||||
admin = IPC(socket)
|
admin = IPC(socket)
|
||||||
sockets = [path for path in runtime.glob("wayland-*") if not path.name.endswith(".lock")]
|
sockets = [path for path in runtime.glob("wayland-*") if not path.name.endswith(".lock")]
|
||||||
env["WAYLAND_DISPLAY"] = str(sockets[0])
|
env["WAYLAND_DISPLAY"] = str(sockets[0])
|
||||||
target = subprocess.Popen([str(root / "client"), "resize"], env=env, stdin=subprocess.PIPE, stdout=log, stderr=log, start_new_session=True)
|
target = subprocess.Popen([str(root / "client"), "resize"], env=ipc_environment(env), stdin=subprocess.PIPE, stdout=log, stderr=log, start_new_session=True)
|
||||||
processes.append(target)
|
processes.append(target)
|
||||||
windows = wait_for(lambda: [window for window in admin.call("get-windows") if window["capture_width"] == 640], "Window fixture did not map; private manager must be hidden from ordinary clients")
|
windows = wait_for(lambda: [window for window in admin.call("get-windows") if window["capture_width"] == 640], "Window fixture did not map; private manager must be hidden from ordinary clients")
|
||||||
target_id = windows[0]["id"]
|
target_id = windows[0]["id"]
|
||||||
cover = subprocess.Popen([str(root / "client"), "cover"], env=env, stdout=log, stderr=log, start_new_session=True)
|
cover = subprocess.Popen([str(root / "client"), "cover"], env=ipc_environment(env), stdout=log, stderr=log, start_new_session=True)
|
||||||
processes.append(cover)
|
processes.append(cover)
|
||||||
wait_for(lambda: len(admin.call("get-windows")) == 2, "Occluding window did not map")
|
wait_for(lambda: len(admin.call("get-windows")) == 2, "Occluding window did not map")
|
||||||
helper = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese"), "--stream-window", str(target_id), "hidden"], env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=log, text=True, start_new_session=True)
|
helper = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese"), "--stream-window", str(target_id), "hidden"], env=ipc_environment(env), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=log, text=True, start_new_session=True)
|
||||||
processes.append(helper)
|
processes.append(helper)
|
||||||
self.assertTrue(select.select([helper.stdout], [], [], 10)[0], "Stream did not announce a PipeWire node")
|
self.assertTrue(select.select([helper.stdout], [], [], 10)[0], "Stream did not announce a PipeWire node")
|
||||||
ready = json.loads(helper.stdout.readline())
|
ready = json.loads(helper.stdout.readline())
|
||||||
@@ -126,11 +127,11 @@ class WindowStream(unittest.TestCase):
|
|||||||
pipeline.set_state(Gst.State.NULL)
|
pipeline.set_state(Gst.State.NULL)
|
||||||
pipeline = None
|
pipeline = None
|
||||||
cover_id = next(window["id"] for window in admin.call("get-windows") if window["title"] == "Occluding window")
|
cover_id = next(window["id"] for window in admin.call("get-windows") if window["title"] == "Occluding window")
|
||||||
paused = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese"), "--stream-window", str(cover_id), "hidden"], env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=log, text=True, start_new_session=True)
|
paused = subprocess.Popen([str(REPO / "target/debug/xdg-desktop-portal-ferese"), "--stream-window", str(cover_id), "hidden"], env=ipc_environment(env), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=log, text=True, start_new_session=True)
|
||||||
processes.append(paused)
|
processes.append(paused)
|
||||||
self.assertTrue(select.select([paused.stdout], [], [], 10)[0])
|
self.assertTrue(select.select([paused.stdout], [], [], 10)[0])
|
||||||
self.assertGreater(json.loads(paused.stdout.readline())["width"], 0)
|
self.assertGreater(json.loads(paused.stdout.readline())["width"], 0)
|
||||||
locker = subprocess.Popen([str(root / "locker")], env=env, stdout=subprocess.PIPE, stderr=log, text=True, start_new_session=True)
|
locker = subprocess.Popen([str(root / "locker")], env=ipc_environment(env), stdout=subprocess.PIPE, stderr=log, text=True, start_new_session=True)
|
||||||
processes.append(locker)
|
processes.append(locker)
|
||||||
self.assertTrue(select.select([locker.stdout], [], [], 5)[0], "Nested lock request did not complete")
|
self.assertTrue(select.select([locker.stdout], [], [], 5)[0], "Nested lock request did not complete")
|
||||||
self.assertIn(locker.stdout.readline().strip(), ("locked", "requested"))
|
self.assertIn(locker.stdout.readline().strip(), ("locked", "requested"))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Requires target/debug/ferese, feresectl, foot, dbus-daemon, and a Wayland host.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from session_socket import ipc_environment, ipc_socket
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -35,13 +36,13 @@ class WorkspaceHistoryTest(unittest.TestCase):
|
|||||||
children = []
|
children = []
|
||||||
|
|
||||||
def launch(command, **kwargs):
|
def launch(command, **kwargs):
|
||||||
child = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
child = subprocess.Popen(command, env=ipc_environment(env), start_new_session=True, **kwargs)
|
||||||
children.append(child)
|
children.append(child)
|
||||||
return child
|
return child
|
||||||
|
|
||||||
def command(*args):
|
def command(*args):
|
||||||
return json.loads(subprocess.check_output(
|
return json.loads(subprocess.check_output(
|
||||||
[str(repo / "target/debug/feresectl"), "-j", *args], env=env, timeout=5
|
[str(repo / "target/debug/feresectl"), "-j", *args], env=ipc_environment(env), timeout=5
|
||||||
))
|
))
|
||||||
|
|
||||||
def active():
|
def active():
|
||||||
@@ -55,7 +56,7 @@ class WorkspaceHistoryTest(unittest.TestCase):
|
|||||||
compositor = launch([str(repo / "target/debug/ferese"), "--backend", "nested"],
|
compositor = launch([str(repo / "target/debug/ferese"), "--backend", "nested"],
|
||||||
stdout=log, stderr=log)
|
stdout=log, stderr=log)
|
||||||
deadline = time.monotonic() + 10
|
deadline = time.monotonic() + 10
|
||||||
while not (runtime / "ferese/control.sock").is_socket():
|
while not (ipc_socket(runtime)).is_socket():
|
||||||
if compositor.poll() is not None or time.monotonic() >= deadline:
|
if compositor.poll() is not None or time.monotonic() >= deadline:
|
||||||
self.fail((root / "compositor.log").read_text())
|
self.fail((root / "compositor.log").read_text())
|
||||||
time.sleep(.025)
|
time.sleep(.025)
|
||||||
|
|||||||
@@ -221,7 +221,9 @@ async fn session_subject(
|
|||||||
connection: &zbus::Connection,
|
connection: &zbus::Connection,
|
||||||
) -> Result<(String, HashMap<String, Value<'static>>), Box<dyn std::error::Error>> {
|
) -> Result<(String, HashMap<String, Value<'static>>), Box<dyn std::error::Error>> {
|
||||||
// Nested previews must not take over authentication for the host desktop.
|
// Nested previews must not take over authentication for the host desktop.
|
||||||
if std::env::var("FERESE_SESSION_IMPORT_ENV").as_deref() == Ok("0") {
|
if std::env::var("FERESE_SESSION_MODE").as_deref() == Ok("embedded")
|
||||||
|
|| std::env::var("FERESE_SESSION_IMPORT_ENV").as_deref() == Ok("0")
|
||||||
|
{
|
||||||
return process_subject().map_err(Into::into);
|
return process_subject().map_err(Into::into);
|
||||||
}
|
}
|
||||||
let manager = zbus::Proxy::new(
|
let manager = zbus::Proxy::new(
|
||||||
|
|||||||
@@ -228,11 +228,10 @@ fn save_with(
|
|||||||
|
|
||||||
pub fn reload_running() -> bool {
|
pub fn reload_running() -> bool {
|
||||||
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
|
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
|
||||||
let Some(runtime) = std::env::var_os("XDG_RUNTIME_DIR") else {
|
let Ok(path) = ferese_ipc::socket::resolve(None) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let Ok(mut socket) = std::os::unix::net::UnixStream::connect(PathBuf::from(runtime).join("ferese/control.sock"))
|
let Ok(mut socket) = std::os::unix::net::UnixStream::connect(path) else {
|
||||||
else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let _ = socket.set_read_timeout(Some(Duration::from_secs(2)));
|
let _ = socket.set_read_timeout(Some(Duration::from_secs(2)));
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
@@ -15,10 +14,7 @@ struct SessionConnection {
|
|||||||
|
|
||||||
impl SessionConnection {
|
impl SessionConnection {
|
||||||
fn connect() -> Result<Self, String> {
|
fn connect() -> Result<Self, String> {
|
||||||
let path = std::env::var_os("XDG_RUNTIME_DIR")
|
let path = ferese_ipc::socket::resolve(None).map_err(|error| error.to_string())?;
|
||||||
.map(PathBuf::from)
|
|
||||||
.ok_or("Missing runtime directory")?
|
|
||||||
.join("ferese/control.sock");
|
|
||||||
let stream = UnixStream::connect(path).map_err(|error| error.to_string())?;
|
let stream = UnixStream::connect(path).map_err(|error| error.to_string())?;
|
||||||
stream
|
stream
|
||||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||||
|
|||||||
Reference in new issue
Block a user