diff --git a/etc/jay-portals.conf b/etc/jay-portals.conf new file mode 100644 index 00000000..a3e4e097 --- /dev/null +++ b/etc/jay-portals.conf @@ -0,0 +1,3 @@ +[preferred] +default=gtk +org.freedesktop.impl.portal.ScreenCast=jay diff --git a/etc/jay.portal b/etc/jay.portal index 019f7796..99a7d867 100644 --- a/etc/jay.portal +++ b/etc/jay.portal @@ -1,4 +1,3 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.jay Interfaces=org.freedesktop.impl.portal.ScreenCast; -UseIn=jay diff --git a/etc/org.freedesktop.impl.portal.desktop.jay.service b/etc/org.freedesktop.impl.portal.desktop.jay.service deleted file mode 100644 index b3b6344b..00000000 --- a/etc/org.freedesktop.impl.portal.desktop.jay.service +++ /dev/null @@ -1,4 +0,0 @@ -[D-BUS Service] -Name=org.freedesktop.impl.portal.desktop.jay -Exec=/bin/false -SystemdService=xdg-desktop-portal-jay.service diff --git a/etc/xdg-desktop-portal-jay.service b/etc/xdg-desktop-portal-jay.service deleted file mode 100644 index 4fe86765..00000000 --- a/etc/xdg-desktop-portal-jay.service +++ /dev/null @@ -1,7 +0,0 @@ -[Unit] -Description=Jay Portal - -[Service] -Type=dbus -BusName=org.freedesktop.impl.portal.desktop.jay -ExecStart=/home/julian/bin/jay portal diff --git a/src/cli.rs b/src/cli.rs index 9162103b..4ced1a84 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -226,7 +226,7 @@ pub fn main() { Cmd::Unlock => unlock::main(cli.global), Cmd::RunPrivileged(a) => run_privileged::main(cli.global, a), Cmd::SeatTest(a) => seat_test::main(cli.global, a), - Cmd::Portal => portal::run(cli.global), + Cmd::Portal => portal::run_freestanding(cli.global), Cmd::Randr(a) => randr::main(cli.global, a), Cmd::Input(a) => input::main(cli.global, a), #[cfg(feature = "it")] diff --git a/src/compositor.rs b/src/compositor.rs index 8e4e5fd6..aeb2287f 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -23,6 +23,7 @@ use { io_uring::{IoUring, IoUringError}, leaks, logger::Logger, + portal::{self, PortalStartup}, scale::Scale, sighand::{self, SighandError}, state::{ConnectorData, IdleState, ScreenlockState, State, XWaylandState}, @@ -52,8 +53,16 @@ pub const MAX_EXTENTS: i32 = (1 << 22) - 1; pub fn start_compositor(global: GlobalArgs, args: RunArgs) { let forker = create_forker(); + let portal = portal::run_from_compositor(global.log_level.into()); let logger = Logger::install_compositor(global.log_level.into()); - let res = start_compositor2(Some(forker), Some(logger.clone()), args, None); + let portal = match portal { + Ok(p) => Some(p), + Err(e) => { + log::error!("Could not spawn portal: {}", ErrorFmt(e)); + None + } + }; + let res = start_compositor2(Some(forker), portal, Some(logger.clone()), args, None); leaks::log_leaked(); if let Err(e) = res { let e = ErrorFmt(e); @@ -67,7 +76,7 @@ pub fn start_compositor(global: GlobalArgs, args: RunArgs) { #[cfg(feature = "it")] pub fn start_compositor_for_test(future: TestFuture) -> Result<(), CompositorError> { - let res = start_compositor2(None, None, RunArgs::default(), Some(future)); + let res = start_compositor2(None, None, None, RunArgs::default(), Some(future)); leaks::log_leaked(); res } @@ -106,6 +115,7 @@ pub type TestFuture = Box) -> Box>>; fn start_compositor2( forker: Option>, + portal: Option, logger: Option>, run_args: RunArgs, test_future: Option, @@ -161,7 +171,7 @@ fn start_compositor2( pending_float_titles: Default::default(), dbus: Dbus::new(&engine, &ring, &run_toplevel), fdcloser: FdCloser::new(), - logger, + logger: logger.clone(), connectors: Default::default(), outputs: Default::default(), drm_devs: Default::default(), @@ -225,6 +235,10 @@ fn start_compositor2( forker.setenv(key.as_bytes(), val.as_bytes()); } } + let mut _portal = None; + if let (Some(portal), Some(logger)) = (portal, &logger) { + _portal = Some(engine.spawn(portal.spawn(engine.clone(), ring.clone(), logger.clone()))); + } let _compositor = engine.spawn(start_compositor3(state.clone(), test_future)); ring.run()?; state.clear(); diff --git a/src/forker.rs b/src/forker.rs index b5796923..0067198d 100644 --- a/src/forker.rs +++ b/src/forker.rs @@ -1,18 +1,19 @@ -mod clone3; mod io; use { crate::{ async_engine::{AsyncEngine, SpawnedFuture}, compositor::{DISPLAY, WAYLAND_DISPLAY}, - forker::{ - clone3::{fork_with_pidfd, Forked}, - io::{IoIn, IoOut}, - }, + forker::io::{IoIn, IoOut}, io_uring::IoUring, state::State, utils::{ - buffd::BufFdError, copyhashmap::CopyHashMap, errorfmt::ErrorFmt, numcell::NumCell, + buffd::BufFdError, + clone3::{fork_with_pidfd, Forked}, + copyhashmap::CopyHashMap, + errorfmt::ErrorFmt, + numcell::NumCell, + process_name::set_process_name, queue::AsyncQueue, }, xwayland, @@ -323,7 +324,7 @@ impl Forker { env::set_var("XDG_SESSION_TYPE", "wayland"); env::remove_var(DISPLAY); env::remove_var(WAYLAND_DISPLAY); - setup_name("the ol' forker"); + set_process_name("the ol' forker"); setup_deathsig(ppid); reset_signals(); let socket = Rc::new(setup_fds(socket)); @@ -568,13 +569,6 @@ fn setup_deathsig(ppid: c::pid_t) { } } -fn setup_name(name: &str) { - unsafe { - let name = name.into_ustr(); - c::prctl(c::PR_SET_NAME, name.as_ptr()); - } -} - fn map_fds(fds: Vec<(i32, OwnedFd)>) -> Result, SpawnError> { let mut desired: Vec<_> = fds.iter().map(|v| v.0).collect(); desired.sort_by(|a, b| b.cmp(a)); diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 2b9487be..696ae7fa 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -88,11 +88,10 @@ impl JayCompositor { let log_file = Rc::new(JayLogFile::new(req.id, &self.client)); track!(self.client, log_file); self.client.add_client_obj(&log_file)?; - let path = match &self.client.state.logger { - Some(logger) => logger.path(), - _ => "".as_bytes().as_bstr(), + match &self.client.state.logger { + Some(logger) => log_file.send_path(logger.path().as_bstr()), + _ => log_file.send_path(b"".as_bstr()), }; - log_file.send_path(path); Ok(()) } diff --git a/src/logger.rs b/src/logger.rs index 274604a2..373e84d3 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,8 +1,9 @@ use { crate::utils::{errorfmt::ErrorFmt, oserror::OsError}, backtrace::Backtrace, - bstr::{BStr, BString, ByteSlice}, + bstr::BString, log::{Level, Log, Metadata, Record}, + parking_lot::Mutex, std::{ cell::Cell, fs::DirBuilder, @@ -10,12 +11,12 @@ use { os::unix::{ffi::OsStringExt, fs::DirBuilderExt}, ptr, sync::{ - atomic::{AtomicU32, Ordering::Relaxed}, + atomic::{AtomicI32, AtomicU32, Ordering::Relaxed}, Arc, }, time::SystemTime, }, - uapi::{c, format_ustr, Errno, Fd, OwnedFd}, + uapi::{c, format_ustr, Errno, Fd, OwnedFd, Ustring}, }; thread_local! { @@ -24,8 +25,9 @@ thread_local! { pub struct Logger { level: AtomicU32, - path: BString, - file: OwnedFd, + path: Mutex>, + _file: Mutex, + file_fd: AtomicI32, } impl Logger { @@ -37,67 +39,31 @@ impl Logger { fatal!("Error: Could not dup stderr: {}", ErrorFmt(e)); } }; - Self::install(level, b"", file) + Self::install(level, b"STDERR", file) } pub fn install_compositor(level: Level) -> Arc { - let log_dir = create_log_dir(); - let (path, file) = 'file: { - for i in 0.. { - let file_name = format_ustr!( - "{}/jay-{}-{}.txt", - log_dir, - humantime::format_rfc3339_millis(SystemTime::now()), - i, - ); - match uapi::open( - &file_name, - c::O_CREAT | c::O_EXCL | c::O_CLOEXEC | c::O_WRONLY, - 0o644, - ) { - Ok(f) => break 'file (file_name, f), - Err(Errno(c::EEXIST)) => {} - Err(e) => { - let e: OsError = e.into(); - fatal!("Error: Could not create log file: {}", ErrorFmt(e)); - } - } - } - unreachable!(); - }; - std::panic::set_hook(Box::new(|p| { - if let Some(loc) = p.location() { - log::error!( - "Panic at {} line {} column {}", - loc.file(), - loc.line(), - loc.column() - ); - } else { - log::error!("Panic at unknown location"); - } - if let Some(msg) = p.payload().downcast_ref::<&str>() { - log::error!("Message: {}", msg); - } - if let Some(msg) = p.payload().downcast_ref::() { - log::error!("Message: {}", msg); - } - log::error!("Backtrace:\n{:?}", Backtrace::new()); - })); + let (path, file) = open_log_file("jay"); Self::install(level, path.as_bytes(), file) } + pub fn install_pipe(file: OwnedFd, level: Level) -> Arc { + Self::install(level, b"PIPE", file) + } + fn install(level: Level, path: &[u8], file: OwnedFd) -> Arc { let slf = Arc::new(Self { level: AtomicU32::new(level as _), - path: path.to_vec().into(), - file, + path: Mutex::new(Arc::new(path.to_vec().into())), + file_fd: AtomicI32::new(file.raw()), + _file: Mutex::new(file), }); log::set_boxed_logger(Box::new(LogWrapper { logger: slf.clone(), })) .unwrap(); log::set_max_level(level.to_level_filter()); + set_panic_hook(); slf } @@ -106,17 +72,57 @@ impl Logger { log::set_max_level(level.to_level_filter()); } - pub fn path(&self) -> &BStr { - self.path.as_bstr() + pub fn path(&self) -> Arc { + self.path.lock().clone() + } + + pub fn redirect(&self, ty: &str) { + let (file, fd) = open_log_file(ty); + log::info!("Redirecting logs to {}", file.display()); + *self.path.lock() = Arc::new(file.as_bytes().into()); + self.file_fd.store(fd.raw(), Relaxed); + *self._file.lock() = fd; + } + + pub fn write_raw(&self, buf: &[u8]) { + let mut fd = Fd::new(self.file_fd.load(Relaxed)); + let _ = fd.write_all(buf); } } -fn create_log_dir() -> BString { +pub fn open_log_file(ty: &str) -> (Ustring, OwnedFd) { + let log_dir = create_log_dir(ty); + for i in 0.. { + let file_name = format_ustr!( + "{}/{ty}-{}-{}.txt", + log_dir, + humantime::format_rfc3339_millis(SystemTime::now()), + i, + ); + match uapi::open( + &file_name, + c::O_CREAT | c::O_EXCL | c::O_CLOEXEC | c::O_WRONLY, + 0o644, + ) { + Ok(f) => return (file_name, f), + Err(Errno(c::EEXIST)) => {} + Err(e) => { + let e: OsError = e.into(); + fatal!("Error: Could not create log file: {}", ErrorFmt(e)); + } + } + } + unreachable!() +} + +fn create_log_dir(ty: &str) -> BString { let mut log_dir = match dirs::data_local_dir() { Some(d) => d, None => fatal!("Error: $HOME is not set"), }; log_dir.push("jay"); + log_dir.push("logs"); + log_dir.push(ty); let res = DirBuilder::new() .recursive(true) .mode(0o755) @@ -131,6 +137,28 @@ fn create_log_dir() -> BString { log_dir.into_os_string().into_vec().into() } +fn set_panic_hook() { + std::panic::set_hook(Box::new(|p| { + if let Some(loc) = p.location() { + log::error!( + "Panic at {} line {} column {}", + loc.file(), + loc.line(), + loc.column() + ); + } else { + log::error!("Panic at unknown location"); + } + if let Some(msg) = p.payload().downcast_ref::<&str>() { + log::error!("Message: {}", msg); + } + if let Some(msg) = p.payload().downcast_ref::() { + log::error!("Message: {}", msg); + } + log::error!("Backtrace:\n{:?}", Backtrace::new()); + })); +} + struct LogWrapper { logger: Arc, } @@ -170,7 +198,7 @@ impl Log for LogWrapper { record.args(), ) }; - let mut fd = Fd::new(self.logger.file.raw()); + let mut fd = Fd::new(self.logger.file_fd.load(Relaxed)); let _ = fd.write_all(buffer); } diff --git a/src/portal.rs b/src/portal.rs index 18a2b383..c66f6cfe 100644 --- a/src/portal.rs +++ b/src/portal.rs @@ -11,8 +11,9 @@ use { Dbus, DbusSocket, BUS_DEST, BUS_PATH, DBUS_NAME_FLAG_DO_NOT_QUEUE, DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER, }, + forker::ForkerError, io_uring::IoUring, - logger, + logger::Logger, pipewire::pw_con::{PwCon, PwConHolder, PwConOwner}, portal::{ ptl_display::{watch_displays, PortalDisplay, PortalDisplayId}, @@ -20,15 +21,29 @@ use { ptl_screencast::{add_screencast_dbus_members, ScreencastSession}, }, utils::{ - copyhashmap::CopyHashMap, errorfmt::ErrorFmt, numcell::NumCell, - run_toplevel::RunToplevel, xrd::xrd, + buf::Buf, + clone3::{fork_with_pidfd, Forked}, + copyhashmap::CopyHashMap, + errorfmt::ErrorFmt, + numcell::NumCell, + oserror::OsError, + process_name::set_process_name, + run_toplevel::RunToplevel, + vecdeque_ext::VecDequeExt, + xrd::xrd, }, video::dmabuf::DmaBufIds, wheel::Wheel, wire_dbus::org, }, - std::rc::{Rc, Weak}, - uapi::c, + log::Level, + std::{ + collections::VecDeque, + rc::{Rc, Weak}, + sync::Arc, + }, + thiserror::Error, + uapi::{c, OwnedFd}, }; const PORTAL_SUCCESS: u32 = 0; @@ -37,8 +52,110 @@ const PORTAL_CANCELLED: u32 = 1; #[allow(dead_code)] const PORTAL_ENDED: u32 = 2; -pub fn run(global: GlobalArgs) { - logger::Logger::install_stderr(global.log_level.into()); +pub fn run_freestanding(global: GlobalArgs) { + let logger = Logger::install_stderr(global.log_level.into()); + run(logger); +} + +#[derive(Debug, Error)] +pub enum PortalError { + #[error("Could not create pipe")] + CreatePipe(#[source] OsError), + #[error("Could not fork")] + Fork(#[source] ForkerError), +} + +pub struct PortalStartup { + logs: Rc, + pid: c::pid_t, + pidfd: Rc, +} + +impl PortalStartup { + pub async fn spawn(self, eng: Rc, ring: Rc, logger: Arc) { + let f1 = eng.spawn({ + let ring = ring.clone(); + async move { + if let Err(e) = ring.readable(&self.pidfd).await { + log::error!( + "Could not wait for portal pidfd to become readable: {}", + ErrorFmt(e) + ); + return; + } + let (_, status) = match uapi::waitpid(self.pid, 0) { + Ok(r) => r, + Err(e) => { + log::error!( + "Could not retrieve exit status of portal ({}): {}", + self.pid, + ErrorFmt(OsError::from(e)) + ); + return; + } + }; + let status = uapi::WEXITSTATUS(status); + if status != 0 { + log::error!("Portal exited with non-0 exit code: {status}"); + } + } + }); + let f2 = eng.spawn({ + let ring = ring.clone(); + let logger = logger.clone(); + async move { + let mut buf = VecDeque::::new(); + let mut buf2 = Buf::new(1024); + let mut done = false; + while !done { + match ring.read(&self.logs, buf2.clone()).await { + Ok(n) if n > 0 => buf.extend(&buf2[..n]), + Ok(_) => done = true, + Err(e) => { + log::error!("Could not read portal logs: {}", ErrorFmt(e)); + return; + } + }; + while let Some(pos) = buf.iter().position(|b| b == &b'\n') { + let (left, right) = buf.get_slices(..pos); + logger.write_raw(left); + logger.write_raw(right); + logger.write_raw(b" (portal)\n"); + buf.drain(..=pos); + } + } + } + }); + f1.await; + f2.await; + } +} + +pub fn run_from_compositor(level: Level) -> Result { + let (read, write) = match uapi::pipe2(c::O_CLOEXEC) { + Ok(p) => p, + Err(e) => return Err(PortalError::CreatePipe(e.into())), + }; + let fork = match fork_with_pidfd(false) { + Ok(f) => f, + Err(e) => return Err(PortalError::Fork(e)), + }; + match fork { + Forked::Parent { pidfd, pid } => Ok(PortalStartup { + logs: Rc::new(read), + pid, + pidfd: Rc::new(pidfd), + }), + Forked::Child { .. } => { + drop(read); + let logger = Logger::install_pipe(write, level); + run(logger); + std::process::exit(0); + } + } +} + +fn run(logger: Arc) { let eng = AsyncEngine::new(); let ring = match IoUring::new(&eng, 32) { Ok(r) => r, @@ -46,13 +163,16 @@ pub fn run(global: GlobalArgs) { fatal!("Could not create an IO-uring: {}", ErrorFmt(e)); } }; - let _f = eng.spawn(run_async(eng.clone(), ring.clone())); + let _f = eng.spawn(run_async(eng.clone(), ring.clone(), logger)); if let Err(e) = ring.run() { fatal!("The IO-uring returned an error: {}", ErrorFmt(e)); } } -async fn run_async(eng: Rc, ring: Rc) { +async fn run_async(eng: Rc, ring: Rc, logger: Arc) { + let (_rtl_future, rtl) = RunToplevel::install(&eng); + let dbus = Dbus::new(&eng, &ring, &rtl); + let dbus = init_dbus_session(&dbus, logger).await; let xrd = match xrd() { Some(xrd) => xrd, _ => { @@ -71,9 +191,6 @@ async fn run_async(eng: Rc, ring: Rc) { fatal!("Could not connect to pipewire: {}", ErrorFmt(e)); } }; - let (_rtl_future, rtl) = RunToplevel::install(&eng); - let dbus = Dbus::new(&eng, &ring, &rtl); - let dbus = init_dbus_session(&dbus).await; let state = Rc::new(PortalState { xrd, ring, @@ -101,37 +218,53 @@ async fn run_async(eng: Rc, ring: Rc) { const UNIQUE_NAME: &str = "org.freedesktop.impl.portal.desktop.jay"; -async fn init_dbus_session(dbus: &Dbus) -> Rc { +async fn init_dbus_session(dbus: &Dbus, logger: Arc) -> Rc { let session = match dbus.session().await { Ok(s) => s, Err(e) => { fatal!("Could not connect to dbus session daemon: {}", ErrorFmt(e)); } }; - session.call( - BUS_DEST, - BUS_PATH, - org::freedesktop::dbus::RequestName { - name: UNIQUE_NAME.into(), - flags: DBUS_NAME_FLAG_DO_NOT_QUEUE, - }, - |rv| match rv { - Ok(r) if r.rv == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER => { - log::info!("Acquired unique name {}", UNIQUE_NAME); - return; + let rv = session + .call_async( + BUS_DEST, + BUS_PATH, + org::freedesktop::dbus::RequestName { + name: UNIQUE_NAME.into(), + flags: DBUS_NAME_FLAG_DO_NOT_QUEUE, + }, + ) + .await; + match rv { + Ok(r) if r.get().rv == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER => { + log::info!("Acquired unique name {}", UNIQUE_NAME); + logger.redirect("portal"); + let fork = match fork_with_pidfd(false) { + Ok(f) => f, + Err(e) => fatal!("Could not fork: {}", ErrorFmt(e)), + }; + match fork { + Forked::Parent { .. } => std::process::exit(0), + Forked::Child { .. } => { + if let Err(e) = uapi::setsid() { + log::error!("setsid failed: {}", ErrorFmt(OsError::from(e))); + } + } } - Ok(r) => { - fatal!("Could not acquire unique name {}: {}", UNIQUE_NAME, r.rv); - } - Err(e) => { - fatal!( - "Could not communicate with the session bus: {}", - ErrorFmt(e) - ); - } - }, - ); - session + set_process_name("jay portal"); + session + } + Ok(_) => { + log::info!("Portal is already running"); + std::process::exit(0); + } + Err(e) => { + fatal!( + "Could not communicate with the session bus: {}", + ErrorFmt(e) + ); + } + } } struct PortalState { diff --git a/src/utils.rs b/src/utils.rs index db7b8e05..5764751b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,6 +7,7 @@ pub mod buf; pub mod buffd; pub mod bufio; pub mod cell_ext; +pub mod clone3; pub mod clonecell; pub mod copyhashmap; pub mod debug_fn; @@ -27,6 +28,7 @@ pub mod opaque_cell; pub mod option_ext; pub mod oserror; pub mod page_size; +pub mod process_name; pub mod ptr_ext; pub mod queue; pub mod rc_eq; @@ -45,6 +47,7 @@ pub mod tri; pub mod trim; pub mod unlink_on_drop; pub mod vec_ext; +pub mod vecdeque_ext; pub mod vecstorage; pub mod windows; pub mod xrd; diff --git a/src/forker/clone3.rs b/src/utils/clone3.rs similarity index 97% rename from src/forker/clone3.rs rename to src/utils/clone3.rs index 111f3571..6590eff3 100644 --- a/src/forker/clone3.rs +++ b/src/utils/clone3.rs @@ -30,6 +30,7 @@ pub fn fork_with_pidfd(pidfd_for_child: bool) -> Result { let mut args = clone_args { flags: c::CLONE_PIDFD as u64, pidfd: (&mut pidfd as *mut c::c_int) as _, + exit_signal: c::SIGCHLD as _, ..Default::default() }; let mut child_pidfd = None; diff --git a/src/utils/process_name.rs b/src/utils/process_name.rs new file mode 100644 index 00000000..2f7ab4c2 --- /dev/null +++ b/src/utils/process_name.rs @@ -0,0 +1,8 @@ +use uapi::{c, IntoUstr}; + +pub fn set_process_name(name: &str) { + unsafe { + let name = name.into_ustr(); + c::prctl(c::PR_SET_NAME, name.as_ptr()); + } +} diff --git a/src/utils/vecdeque_ext.rs b/src/utils/vecdeque_ext.rs new file mode 100644 index 00000000..6d48d0e5 --- /dev/null +++ b/src/utils/vecdeque_ext.rs @@ -0,0 +1,35 @@ +use std::{ + collections::{Bound, VecDeque}, + ops::RangeBounds, +}; + +pub trait VecDequeExt { + fn get_slices(&self, range: impl RangeBounds) -> (&[T], &[T]); +} + +impl VecDequeExt for VecDeque { + fn get_slices(&self, range: impl RangeBounds) -> (&[T], &[T]) { + let (l, r) = self.as_slices(); + let start = match range.start_bound().cloned() { + Bound::Included(n) => n, + Bound::Excluded(n) => n + 1, + Bound::Unbounded => 0, + }; + let end = match range.end_bound().cloned() { + Bound::Included(n) => n + 1, + Bound::Excluded(n) => n, + Bound::Unbounded => self.len(), + }; + let left = { + let lo = start.min(l.len()); + let hi = end.min(l.len()); + &l[lo..hi] + }; + let right = { + let lo = start.saturating_sub(l.len()); + let hi = end.saturating_sub(l.len()); + &r[lo..hi] + }; + (left, right) + } +}