305 lines
10 KiB
Rust
305 lines
10 KiB
Rust
#![allow(irrefutable_let_patterns)]
|
|
|
|
mod backends;
|
|
mod config;
|
|
mod cursor;
|
|
mod daemon;
|
|
mod dimming;
|
|
mod display_presentation;
|
|
mod effects;
|
|
mod floating;
|
|
mod frame_scheduler;
|
|
mod gestures;
|
|
mod grabs;
|
|
mod handlers;
|
|
mod input;
|
|
mod input_capture;
|
|
mod ipc;
|
|
mod metrics;
|
|
mod overview;
|
|
mod portal_session;
|
|
mod portal_shortcuts;
|
|
mod presentation;
|
|
mod presentation_policy;
|
|
mod private_client;
|
|
mod reload;
|
|
mod render;
|
|
#[cfg(feature = "resize-metrics")]
|
|
mod resize_metrics;
|
|
mod resize_transaction;
|
|
mod session_lock;
|
|
mod shell_control;
|
|
mod stacking;
|
|
mod state;
|
|
mod theme;
|
|
mod wallpaper;
|
|
mod window_rules;
|
|
mod winit;
|
|
|
|
use std::error::Error;
|
|
use std::process::{Child, Command};
|
|
use std::time::Duration;
|
|
use std::{io, thread};
|
|
|
|
use calloop::signals::{Signal, Signals};
|
|
use smithay::reexports::calloop::EventLoop;
|
|
use smithay::reexports::wayland_server::Display;
|
|
pub use state::{Ferese, RuntimeConfig};
|
|
use tracing::{info, warn};
|
|
|
|
use crate::backends::LaunchConfig;
|
|
|
|
fn main() -> Result<(), Box<dyn Error>> {
|
|
init_logging();
|
|
|
|
// Settings validates a candidate before atomically replacing the user's file.
|
|
// This mode never opens a display, input device, or compositor socket.
|
|
let args = std::env::args_os().skip(1).collect::<Vec<_>>();
|
|
if args.first().is_some_and(|arg| arg == "--check-config") {
|
|
if args.len() != 2 {
|
|
return Err("usage: ferese --check-config PATH".into());
|
|
}
|
|
let source = std::fs::read_to_string(&args[1])?;
|
|
theme::prepare(
|
|
&source,
|
|
std::path::Path::new(&args[1])
|
|
.parent()
|
|
.unwrap_or(std::path::Path::new(".")),
|
|
)?;
|
|
println!("Configuration is valid");
|
|
return Ok(());
|
|
}
|
|
|
|
let launch =
|
|
LaunchConfig::from_environment().map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
|
|
let initial_source = config::config_path()
|
|
.filter(|path| path.exists())
|
|
.map(|path| theme::read_source(&path))
|
|
.transpose()?;
|
|
let source = initial_source.as_deref().unwrap_or("");
|
|
let directory = config::config_path()
|
|
.and_then(|path| path.parent().map(ToOwned::to_owned))
|
|
.unwrap_or_else(|| std::path::PathBuf::from("."));
|
|
let (config, runtime, candidate) = theme::prepare(source, &directory)?;
|
|
let mut event_loop = EventLoop::try_new()?;
|
|
let signals = Signals::new(&[Signal::SIGINT, Signal::SIGTERM])?;
|
|
event_loop
|
|
.handle()
|
|
.insert_source(signals, |event, _, state: &mut Ferese| {
|
|
info!(signal = ?event.signal(), "stopping Ferese");
|
|
state.loop_signal.stop();
|
|
})?;
|
|
let display = Display::new()?;
|
|
let mut state = Ferese::new(&mut event_loop, display, runtime)?;
|
|
state.config_source = initial_source.filter(|source| source.len() <= 60 * 1024);
|
|
theme::init(&mut event_loop, &mut state, candidate)?;
|
|
overview::init_font_loader(&mut event_loop, &mut state)?;
|
|
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
|
use calloop::timer::{TimeoutAction, Timer};
|
|
info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients");
|
|
let mut child = spawn_client(&mut state, launch.client, launch.client_capabilities);
|
|
let runner = std::rc::Rc::new(std::cell::RefCell::new(daemon::Runner::new(
|
|
&config.autostart,
|
|
launch.backend == backends::BackendKind::Nested,
|
|
)));
|
|
runner.borrow_mut().tick(&mut state);
|
|
let services = runner.clone();
|
|
event_loop.handle().insert_source(
|
|
Timer::from_duration(Duration::from_secs(1)),
|
|
move |_, _, state: &mut Ferese| {
|
|
services.borrow_mut().tick(state);
|
|
TimeoutAction::ToDuration(Duration::from_secs(1))
|
|
},
|
|
)?;
|
|
let reload_handle = event_loop.handle();
|
|
let result = event_loop.run(None, &mut state, |state| {
|
|
state.poll_theme();
|
|
// The decoder wakes the loop after publishing its result, including
|
|
// when no output has a pending frame.
|
|
let wallpaper_changed = state.wallpaper.poll();
|
|
// All input/Wayland callbacks have returned, releasing seat locks.
|
|
if state.focus_cycle.is_some() && (state.session_lock.active || state.input_capture.captures(1)) {
|
|
state.cancel_focus_cycle();
|
|
}
|
|
// Coalesce cursor changes and redraw here, never inside cursor_image.
|
|
if state.input_capture.restore_focus {
|
|
state.restore_input_capture_focus();
|
|
}
|
|
state.apply_unlocked_pointer_hint();
|
|
let wallpaper_retry = state.wallpaper.take_retry_wakeup();
|
|
let cursor_changed = std::mem::take(&mut state.cursor_redraw_pending);
|
|
let output_redraws = std::mem::take(&mut state.output_redraw_pending);
|
|
if wallpaper_changed || wallpaper_retry {
|
|
backends::direct::render_all(state);
|
|
} else {
|
|
if cursor_changed {
|
|
backends::direct::render_cursor(state);
|
|
}
|
|
|
|
if !output_redraws.is_empty() {
|
|
backends::direct::render_on(state, &output_redraws);
|
|
}
|
|
}
|
|
if let Err(error) = state
|
|
.wallpaper
|
|
.arm_retry_timer(&reload_handle, std::time::Instant::now())
|
|
{
|
|
warn!(%error, "could not schedule wallpaper upload retry");
|
|
}
|
|
// Registry/sync/configure replies must not depend on a submitted
|
|
// frame: at startup clients need these before they can draw anything.
|
|
// This also keeps idle DRM sessions responsive when there is no damage.
|
|
if let Err(error) = state.display_handle.flush_clients() {
|
|
tracing::warn!(%error, "failed to flush Wayland clients");
|
|
}
|
|
});
|
|
|
|
if let Some(backend) = state.direct_backend.as_ref() {
|
|
backend.dump_scheduling_metrics();
|
|
}
|
|
|
|
#[cfg(feature = "resize-metrics")]
|
|
state.resize_metrics.dump();
|
|
|
|
runner.borrow_mut().stop();
|
|
if let Some(child) = &mut child {
|
|
terminate_child(child);
|
|
}
|
|
|
|
result.map_err(Into::into)
|
|
}
|
|
|
|
fn init_logging() {
|
|
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("ferese=info"));
|
|
tracing_subscriber::fmt().with_env_filter(filter).init();
|
|
}
|
|
|
|
fn spawn_client(
|
|
state: &mut Ferese,
|
|
mut args: Vec<std::ffi::OsString>,
|
|
capabilities: private_client::ClientCapabilities,
|
|
) -> Option<Child> {
|
|
if args.is_empty() {
|
|
info!("no client requested; pass one after `--`, for example `-- foot`");
|
|
return None;
|
|
}
|
|
let program = args.remove(0);
|
|
|
|
let mut command = Command::new(&program);
|
|
command.args(args);
|
|
// Wallpaper decoding and drivers may already have worker threads. Set
|
|
// the child's display explicitly instead of mutating the process env.
|
|
command.env("WAYLAND_DISPLAY", &state.socket_name);
|
|
command.env_remove("WAYLAND_SOCKET");
|
|
command.env_remove("FERESE_SHELL_CONTROL_SOCKET");
|
|
command.env("FERESE_COMPOSITOR_WALLPAPER", "1");
|
|
let private_connection = if capabilities.is_empty() {
|
|
None
|
|
} else {
|
|
match private_client::prepare_command(state, &mut command, capabilities) {
|
|
Ok(connection) => Some(connection),
|
|
Err(error) => {
|
|
warn!(program = ?program, %error, "failed to create private Wayland connection");
|
|
return None;
|
|
}
|
|
}
|
|
};
|
|
|
|
let child = match command.spawn() {
|
|
Ok(child) => {
|
|
info!(program = ?program, pid = child.id(), "spawned Wayland client");
|
|
Some(child)
|
|
}
|
|
Err(error) => {
|
|
warn!(program = ?program, %error, "failed to spawn Wayland client");
|
|
None
|
|
}
|
|
};
|
|
drop(private_connection);
|
|
child
|
|
}
|
|
|
|
fn terminate_child(child: &mut Child) {
|
|
if child.try_wait().ok().flatten().is_some() {
|
|
return;
|
|
}
|
|
|
|
// SAFETY: the process ID comes from the live Child owned by Ferese.
|
|
unsafe {
|
|
libc::kill(child.id() as i32, libc::SIGTERM);
|
|
}
|
|
|
|
for _ in 0..20 {
|
|
if child.try_wait().ok().flatten().is_some() {
|
|
return;
|
|
}
|
|
thread::sleep(Duration::from_millis(10));
|
|
}
|
|
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod startup_tests {
|
|
use std::io::{Read, Write};
|
|
use std::os::unix::net::UnixStream;
|
|
use std::sync::Arc;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn deferred_cursor_redraw_runs_after_input_lock_is_released() {
|
|
use std::sync::Mutex;
|
|
|
|
let mut event_loop = EventLoop::<bool>::try_new().unwrap();
|
|
let pointer_lock = Arc::new(Mutex::new(()));
|
|
let callback_lock = pointer_lock.clone();
|
|
event_loop.handle().insert_idle(move |pending| {
|
|
// Mirrors Smithay's cursor_image callback during pointer motion.
|
|
let _guard = callback_lock.lock().unwrap();
|
|
*pending = true;
|
|
*pending = true; // Multiple cursor changes must coalesce.
|
|
assert!(callback_lock.try_lock().is_err());
|
|
});
|
|
|
|
let signal = event_loop.get_signal();
|
|
let mut pending = false;
|
|
let mut redraws = 0;
|
|
event_loop
|
|
.run(Some(Duration::ZERO), &mut pending, |pending| {
|
|
if std::mem::take(pending) {
|
|
// Rendering reads the pointer location at this point.
|
|
let _guard = pointer_lock.try_lock().expect("input lock still held");
|
|
redraws += 1;
|
|
}
|
|
assert!(!std::mem::take(pending));
|
|
signal.stop();
|
|
})
|
|
.unwrap();
|
|
assert_eq!(redraws, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn wayland_roundtrip_completes_without_a_rendered_frame() {
|
|
let mut display = Display::<()>::new().unwrap();
|
|
let (server, mut client) = UnixStream::pair().unwrap();
|
|
display
|
|
.handle()
|
|
.insert_client(server, Arc::new(state::ClientState::default()))
|
|
.unwrap();
|
|
// wl_display.sync(new_id=2), using native-endian Wayland wire format.
|
|
for word in [1u32, 12u32 << 16, 2u32] {
|
|
client.write_all(&word.to_ne_bytes()).unwrap();
|
|
}
|
|
display.dispatch_clients(&mut ()).unwrap();
|
|
display.handle().flush_clients().unwrap();
|
|
client.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
|
|
let mut reply = [0u8; 12];
|
|
client.read_exact(&mut reply).unwrap();
|
|
assert_eq!(u32::from_ne_bytes(reply[0..4].try_into().unwrap()), 2);
|
|
assert_eq!(u32::from_ne_bytes(reply[4..8].try_into().unwrap()) & 0xffff, 0);
|
|
}
|
|
}
|