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::private_client::ClientCapabilities;
|
||||
use crate::session_environment::SessionPolicy;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum BackendKind {
|
||||
@@ -17,6 +18,7 @@ pub enum BackendKind {
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct LaunchConfig {
|
||||
pub backend: BackendKind,
|
||||
pub(crate) session_policy: SessionPolicy,
|
||||
pub client: Vec<OsString>,
|
||||
pub client_capabilities: ClientCapabilities,
|
||||
}
|
||||
@@ -75,6 +77,10 @@ impl LaunchConfig {
|
||||
|
||||
Ok(Self {
|
||||
backend,
|
||||
session_policy: match backend {
|
||||
BackendKind::Drm => SessionPolicy::Desktop,
|
||||
BackendKind::Nested => SessionPolicy::Embedded,
|
||||
},
|
||||
client,
|
||||
client_capabilities,
|
||||
})
|
||||
@@ -110,6 +116,7 @@ mod tests {
|
||||
let config = LaunchConfig::parse([OsString::from("foot")], true).unwrap();
|
||||
|
||||
assert_eq!(config.backend, BackendKind::Nested);
|
||||
assert_eq!(config.session_policy, SessionPolicy::Embedded);
|
||||
assert_eq!(config.client, [OsString::from("foot")]);
|
||||
assert!(config.client_capabilities.is_empty());
|
||||
}
|
||||
@@ -119,6 +126,7 @@ mod tests {
|
||||
let config = LaunchConfig::parse(Vec::<OsString>::new(), false).unwrap();
|
||||
|
||||
assert_eq!(config.backend, BackendKind::Drm);
|
||||
assert_eq!(config.session_policy, SessionPolicy::Desktop);
|
||||
assert!(config.client.is_empty());
|
||||
assert!(config.client_capabilities.is_empty());
|
||||
}
|
||||
@@ -178,4 +186,15 @@ mod tests {
|
||||
|
||||
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::{Interest, Mode, PostAction, RegistrationToken};
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::config::DaemonConfig;
|
||||
use crate::private_client::ClientCapabilities;
|
||||
use crate::process::{pidfd, spawn_client, terminate_child};
|
||||
use crate::{Ferese, SessionPolicy};
|
||||
|
||||
struct Service {
|
||||
config: DaemonConfig,
|
||||
@@ -31,7 +31,7 @@ impl Runner {
|
||||
pub fn start(state: &mut Ferese) -> Rc<RefCell<Self>> {
|
||||
let runner = Rc::new(RefCell::new(Self::new(
|
||||
&state.autostart,
|
||||
state.direct_backend.is_none(),
|
||||
state.session_environment.policy,
|
||||
)));
|
||||
Self::refresh(&runner, state);
|
||||
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);
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
@@ -148,11 +151,11 @@ impl Runner {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(configs: &[DaemonConfig], nested: bool) -> Self {
|
||||
pub fn new(configs: &[DaemonConfig], policy: SessionPolicy) -> Self {
|
||||
Self(
|
||||
configs
|
||||
.iter()
|
||||
.filter(|config| config.enabled && (!nested || config.nested))
|
||||
.filter(|config| config.enabled && policy.allows_autostart(config.nested))
|
||||
.map(|config| Service {
|
||||
config: config.clone(),
|
||||
child: None,
|
||||
@@ -166,7 +169,7 @@ impl Runner {
|
||||
}
|
||||
|
||||
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();
|
||||
for service in &mut self.0 {
|
||||
if let Some(child) = &mut service.child {
|
||||
@@ -244,17 +247,17 @@ mod tests {
|
||||
restart: 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());
|
||||
let pid = runner.0[0].child.as_ref().unwrap().id();
|
||||
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!(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);
|
||||
config.enabled = false;
|
||||
runner.reconcile(&[config], false);
|
||||
runner.reconcile(&[config], SessionPolicy::Desktop);
|
||||
assert!(runner.0.is_empty());
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while unsafe { libc::kill(pid as i32, 0) } == 0 {
|
||||
@@ -271,22 +274,72 @@ mod tests {
|
||||
restart: true,
|
||||
nested: false,
|
||||
}];
|
||||
assert!(Runner::new(&configs, true).0.is_empty());
|
||||
let mut direct = Runner::new(&configs, false);
|
||||
assert!(Runner::new(&configs, SessionPolicy::Embedded).0.is_empty());
|
||||
let mut direct = Runner::new(&configs, SessionPolicy::Desktop);
|
||||
assert_eq!(direct.0.len(), 1);
|
||||
direct.stop();
|
||||
assert!(direct.0[0].finished);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_login_items_never_start_on_either_backend() {
|
||||
fn disabled_login_items_never_start_under_either_session_policy() {
|
||||
let configs = vec![DaemonConfig {
|
||||
command: vec!["not-executed".into()],
|
||||
enabled: false,
|
||||
restart: true,
|
||||
nested: true,
|
||||
}];
|
||||
assert!(Runner::new(&configs, true).0.is_empty());
|
||||
assert!(Runner::new(&configs, false).0.is_empty());
|
||||
assert!(Runner::new(&configs, SessionPolicy::Embedded).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::mpsc::{SyncSender, sync_channel};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{env, fs, io, thread};
|
||||
use std::{fs, io, thread};
|
||||
|
||||
use ferese_core::LayoutMode;
|
||||
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
|
||||
@@ -78,6 +78,41 @@ pub(crate) struct IpcSocketGuard {
|
||||
path: PathBuf,
|
||||
device: 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 {
|
||||
@@ -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>> {
|
||||
let path = socket_path()?;
|
||||
pub(crate) fn init(
|
||||
event_loop: &mut EventLoop<'static, Ferese>,
|
||||
endpoint: Endpoint,
|
||||
) -> Result<ScreenshotInit, Box<dyn std::error::Error>> {
|
||||
let Endpoint { path, instance } = endpoint;
|
||||
prepare_parent(&path)?;
|
||||
let listener = bind_listener(&path)?;
|
||||
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(),
|
||||
device: metadata.dev(),
|
||||
inode: metadata.ino(),
|
||||
_instance: instance,
|
||||
};
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
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> {
|
||||
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) {
|
||||
Ok(metadata) => {
|
||||
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),
|
||||
}
|
||||
|
||||
@@ -1613,6 +1652,7 @@ fn effective_uid() -> u32 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@@ -1818,6 +1858,52 @@ mod tests {
|
||||
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]
|
||||
fn listener_reclaims_only_a_stale_owned_socket() {
|
||||
let root = unique_test_directory("stale-socket");
|
||||
|
||||
@@ -46,7 +46,7 @@ mod window_rules;
|
||||
mod winit;
|
||||
mod xwayland;
|
||||
|
||||
pub(crate) use session_environment::{SessionEnvironment, X11Environment};
|
||||
pub(crate) use session_environment::{SessionEnvironment, SessionPolicy, X11Environment};
|
||||
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
@@ -95,7 +95,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
state.loop_signal.stop();
|
||||
})?;
|
||||
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_sections = state
|
||||
.config_source
|
||||
|
||||
@@ -8,10 +8,31 @@ pub(crate) struct X11Environment {
|
||||
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.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct SessionEnvironment {
|
||||
pub wayland_display: OsString,
|
||||
pub control_socket: PathBuf,
|
||||
pub policy: SessionPolicy,
|
||||
pub x11: Option<X11Environment>,
|
||||
}
|
||||
|
||||
@@ -21,6 +42,8 @@ impl SessionEnvironment {
|
||||
command
|
||||
.env("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("FERESE_SHELL_CONTROL_SOCKET")
|
||||
.env_remove("DISPLAY")
|
||||
@@ -29,6 +52,9 @@ impl SessionEnvironment {
|
||||
if let Some(x11) = &self.x11 {
|
||||
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 {
|
||||
SessionEnvironment {
|
||||
wayland_display: OsString::from("wayland-7"),
|
||||
control_socket: PathBuf::from("/run/user/1000/ferese/instances/preview/control.sock"),
|
||||
policy: SessionPolicy::Desktop,
|
||||
x11,
|
||||
}
|
||||
}
|
||||
@@ -135,4 +163,42 @@ mod tests {
|
||||
|
||||
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::os::unix::fs::FileTypeExt;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
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()
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn 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 {
|
||||
#[cfg(test)]
|
||||
pub fn new(
|
||||
event_loop: &mut EventLoop<'static, Self>,
|
||||
display: Display<Self>,
|
||||
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>> {
|
||||
let display_handle = display.handle();
|
||||
|
||||
@@ -657,6 +674,8 @@ impl Ferese {
|
||||
|
||||
session_environment: crate::SessionEnvironment {
|
||||
wayland_display: socket_name.clone(),
|
||||
control_socket: ipc_endpoint.path.clone(),
|
||||
policy: session_policy,
|
||||
x11: None,
|
||||
},
|
||||
socket_name,
|
||||
@@ -791,7 +810,7 @@ impl Ferese {
|
||||
xdg_toplevel_icon_manager,
|
||||
};
|
||||
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
|
||||
.screenshot
|
||||
.set_deadline_observer(move |deadline| screenshot.deadline_timer.update(deadline));
|
||||
|
||||
@@ -349,6 +349,8 @@ mod tests {
|
||||
|
||||
fn environment() -> SessionEnvironment {
|
||||
SessionEnvironment {
|
||||
control_socket: std::path::PathBuf::from("/run/user/1000/ferese/control.sock"),
|
||||
policy: crate::SessionPolicy::Desktop,
|
||||
wayland_display: OsString::from("wayland-7"),
|
||||
x11: None,
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ impl Fixture {
|
||||
|
||||
fn environment(&self) -> SessionEnvironment {
|
||||
SessionEnvironment {
|
||||
control_socket: PathBuf::from("/run/user/1000/ferese/control.sock"),
|
||||
policy: crate::SessionPolicy::Desktop,
|
||||
wayland_display: std::ffi::OsString::from("wayland-it"),
|
||||
x11: Some(X11Environment {
|
||||
display: std::ffi::OsString::from(self.reservation.display_name()),
|
||||
|
||||
Reference in new issue
Block a user