diff --git a/crates/hearthspace-common/src/lib.rs b/crates/hearthspace-common/src/lib.rs index fbf0e8b..75c761b 100644 --- a/crates/hearthspace-common/src/lib.rs +++ b/crates/hearthspace-common/src/lib.rs @@ -11,6 +11,7 @@ pub const LOG_FILE_ENV: &str = "HEARTHSPACE_LOG_FILE"; pub const SETTINGS_SOCKET_ENV: &str = "HEARTHSPACE_SETTINGS_SOCKET"; pub const SESSION_SOCKET_ENV: &str = "HEARTHSPACE_SESSION_SOCKET"; pub const SESSION_DIR_ENV: &str = "HEARTHSPACE_SESSION_DIR"; +pub const HOST_RUNTIME_DIR_ENV: &str = "HEARTHSPACE_HOST_XDG_RUNTIME_DIR"; pub const REPO_ROOT_ENV: &str = "HEARTHSPACE_REPO_ROOT"; pub const DEV_ENV: &str = "HEARTHSPACE_DEV"; pub const CURRENT_DESKTOP: &str = "Hearthspace"; diff --git a/crates/hearthspace-compositor/src/compositor/mod.rs b/crates/hearthspace-compositor/src/compositor/mod.rs index 1708f10..339d619 100644 --- a/crates/hearthspace-compositor/src/compositor/mod.rs +++ b/crates/hearthspace-compositor/src/compositor/mod.rs @@ -1,6 +1,7 @@ #[cfg(feature = "winit")] use std::time::Instant; use std::{ + collections::HashSet, os::unix::{fs::MetadataExt, net::UnixListener as CommandListener}, sync::Arc, }; @@ -183,6 +184,7 @@ struct App { next_spawn_position: CanvasPoint, next_spawn_output_name: Option, spawn_offset: i32, + snap_wayland_socket_paths: HashSet, pointer_location: Point, raw_pointer_location: Point, active_touch_slot: Option, @@ -489,6 +491,7 @@ pub(in crate::compositor) fn initialize_app( next_spawn_position: CanvasPoint { x: 80, y: 96 }, next_spawn_output_name: None, spawn_offset: 0, + snap_wayland_socket_paths: HashSet::new(), pointer_location: (0.0, 0.0).into(), raw_pointer_location: (0.0, 0.0).into(), active_touch_slot: None, diff --git a/crates/hearthspace-compositor/src/compositor/shell_integration.rs b/crates/hearthspace-compositor/src/compositor/shell_integration.rs index db42893..a6a26f2 100644 --- a/crates/hearthspace-compositor/src/compositor/shell_integration.rs +++ b/crates/hearthspace-compositor/src/compositor/shell_integration.rs @@ -1,16 +1,18 @@ use std::{ env, fs, io::{self, ErrorKind}, - os::unix::fs::symlink, - path::PathBuf, + path::{Path, PathBuf}, process::Command, + sync::Arc, }; use smithay::{ backend::input::{ButtonState, KeyState}, + reexports::calloop::{Interest, Mode as CalloopMode, PostAction, generic::Generic}, utils::Point, }; use tracing::{debug, error, info, warn}; +use wayland_server::ListeningSocket; use crate::{config::*, geometry::CanvasPoint}; use hearthspace_shell::{ @@ -19,6 +21,7 @@ use hearthspace_shell::{ }; use super::App; +use super::client::ClientState; mod command_socket; @@ -33,6 +36,13 @@ fn runtime_path(name: &str) -> PathBuf { .join(name) } +fn host_runtime_dir() -> PathBuf { + env::var_os(hearthspace_common::HOST_RUNTIME_DIR_ENV) + .map(PathBuf::from) + .or_else(|| env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from)) + .unwrap_or_else(env::temp_dir) +} + /// Snap instance names are embedded into a runtime directory path, so reject any /// value containing path separators or other characters that could escape the /// intended `snap.` directory. @@ -42,7 +52,17 @@ fn is_valid_snap_instance_name(instance_name: &str) -> bool { .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.')) } -fn ensure_snap_wayland_socket(instance_name: &str) -> std::io::Result { +fn snap_wayland_socket_path( + host_runtime_dir: &Path, + instance_name: &str, + wayland_display: &str, +) -> PathBuf { + host_runtime_dir + .join(format!("snap.{instance_name}")) + .join(wayland_display) +} + +fn ensure_snap_instance_name(instance_name: &str) -> std::io::Result<()> { if !is_valid_snap_instance_name(instance_name) { return Err(io::Error::new( ErrorKind::InvalidInput, @@ -50,22 +70,7 @@ fn ensure_snap_wayland_socket(instance_name: &str) -> std::io::Result { )); } - let snap_runtime_dir = runtime_path(&format!("snap.{instance_name}")); - fs::create_dir_all(&snap_runtime_dir)?; - let wayland_display = wayland_display_name(); - let snap_socket_path = snap_runtime_dir.join(&wayland_display); - - match fs::remove_file(&snap_socket_path) { - Ok(()) => {} - Err(error) if error.kind() == ErrorKind::NotFound => {} - Err(error) => return Err(error), - } - - symlink( - PathBuf::from("..").join(&wayland_display), - &snap_socket_path, - )?; - Ok(wayland_display) + Ok(()) } fn launch_environment_for_app(app: &DesktopApp) -> std::io::Result> { @@ -316,7 +321,7 @@ impl App { } fn launch_app(&mut self, app_id: &str, output_name: Option<&str>) { - let Some(app) = self.app_catalog.app_by_id(app_id) else { + let Some(app) = self.app_catalog.app_by_id(app_id).cloned() else { warn!(app_id, "no launchable desktop app found"); return; }; @@ -342,7 +347,7 @@ impl App { }; let wayland_display = if let Some(instance_name) = &app.snap_instance_name { - match ensure_snap_wayland_socket(instance_name) { + match self.ensure_snap_wayland_listener(instance_name) { Ok(display_name) => display_name, Err(error) => { error!(app_id, %error, "failed to prepare Snap Wayland socket"); @@ -353,7 +358,7 @@ impl App { wayland_display_name() }; - let launch_env = match launch_environment_for_app(app) { + let launch_env = match launch_environment_for_app(&app) { Ok(env) => env, Err(error) => { error!(app_id, %error, "failed to prepare app environment"); @@ -372,6 +377,65 @@ impl App { info!(app_id, wayland_display, argv = ?command, "launched desktop app"); } } + + fn ensure_snap_wayland_listener(&mut self, instance_name: &str) -> io::Result { + ensure_snap_instance_name(instance_name)?; + + let wayland_display = wayland_display_name(); + let socket_path = + snap_wayland_socket_path(&host_runtime_dir(), instance_name, &wayland_display); + if self.snap_wayland_socket_paths.contains(&socket_path) { + return Ok(wayland_display); + } + if let Some(parent) = socket_path.parent() { + fs::create_dir_all(parent)?; + } + + let socket = ListeningSocket::bind_absolute(socket_path.clone()).map_err(|error| { + io::Error::other(format!( + "failed to bind Snap Wayland socket {}: {error}", + socket_path.display() + )) + })?; + let source = Generic::new(socket, Interest::READ, CalloopMode::Level); + let log_path = socket_path.clone(); + self.loop_handle + .insert_source(source, move |_, socket, data| { + while let Some(stream) = socket.accept()? { + match data + .display + .handle() + .insert_client(stream, Arc::new(ClientState::default())) + { + Ok(client_id) => { + debug!(?client_id, socket = %log_path.display(), "accepted Snap Wayland client") + } + Err(error) => error!( + %error, + socket = %log_path.display(), + "failed to insert Snap Wayland client" + ), + } + data.state.request_redraw(); + } + Ok(PostAction::Continue) + }) + .map_err(|error| { + io::Error::other(format!( + "failed to register Snap Wayland socket {}: {error}", + socket_path.display() + )) + })?; + + info!( + socket = %socket_path.display(), + instance_name, + wayland_display, + "listening for Snap Wayland clients" + ); + self.snap_wayland_socket_paths.insert(socket_path); + Ok(wayland_display) + } } #[cfg(test)] @@ -397,4 +461,15 @@ mod tests { assert!(!is_valid_snap_instance_name("with/slash")); assert!(!is_valid_snap_instance_name("space here")); } + + #[test] + fn snap_wayland_socket_path_uses_host_snap_runtime() { + let socket_path = + snap_wayland_socket_path(Path::new("/run/user/1000"), "firefox", "wayland-99"); + + assert_eq!( + socket_path, + PathBuf::from("/run/user/1000/snap.firefox/wayland-99") + ); + } } diff --git a/crates/hearthspace-session/src/session_env.rs b/crates/hearthspace-session/src/session_env.rs index 998c733..e33f689 100644 --- a/crates/hearthspace-session/src/session_env.rs +++ b/crates/hearthspace-session/src/session_env.rs @@ -9,6 +9,7 @@ use crate::cli::{Args, RunMode}; #[derive(Debug)] pub(crate) struct SessionPaths { + pub(crate) host_runtime_dir: PathBuf, pub(crate) session_dir: PathBuf, pub(crate) applications_dir: PathBuf, pub(crate) settings_socket: PathBuf, @@ -24,7 +25,8 @@ pub(crate) struct SessionEnv { pub(crate) fn create_session_paths() -> Result> { let host_runtime_dir = env::var_os("XDG_RUNTIME_DIR").ok_or("XDG_RUNTIME_DIR is not set")?; - let session_dir = PathBuf::from(host_runtime_dir) + let host_runtime_dir = PathBuf::from(host_runtime_dir); + let session_dir = host_runtime_dir .join("hearthspace") .join(format!("session-{}", process::id())); let applications_dir = session_dir.join("share/applications"); @@ -33,6 +35,7 @@ pub(crate) fn create_session_paths() -> Result