diff --git a/src/wl.rs b/src/wl.rs index 413a4e4..e780fc1 100644 --- a/src/wl.rs +++ b/src/wl.rs @@ -168,15 +168,52 @@ pub fn available() -> bool { } /// A file only we can see, gone from the folder already (for shared memory and keymaps). +/// Its memory is really there before the compositor gets it: the compositor writes screen +/// pixels straight into it, and if the space ran out half-way the compositor would be the one +/// to crash (SIGBUS: a full /run/user once took a whole Hyprland desktop down this way). +/// So it lives in memory of its own (memfd), not in the runtime folder, and with no room the +/// screenshot just fails here. fn scratch_file(len: u64) -> std::io::Result { - let dir = std::env::var_os("XDG_RUNTIME_DIR").map(std::path::PathBuf::from).unwrap_or_else(std::env::temp_dir); - let path = dir.join(format!("anybot-wl-{}-{}", std::process::id(), crate::model::now_ms())); - let f = std::fs::OpenOptions::new().read(true).write(true).create_new(true).open(&path)?; - let _ = std::fs::remove_file(&path); - f.set_len(len)?; + use std::os::fd::FromRawFd as _; + let fd = unsafe { libc::memfd_create(c"anybot-wl".as_ptr(), libc::MFD_CLOEXEC) }; + let f = if fd >= 0 { + unsafe { std::fs::File::from_raw_fd(fd) } + } else { + let dir = std::env::var_os("XDG_RUNTIME_DIR").map(std::path::PathBuf::from).unwrap_or_else(std::env::temp_dir); + let path = dir.join(format!("anybot-wl-{}-{}", std::process::id(), crate::model::now_ms())); + let f = std::fs::OpenOptions::new().read(true).write(true).create_new(true).open(&path)?; + let _ = std::fs::remove_file(&path); + f + }; + if len > 0 { + reserve(&f, len)?; + } Ok(f) } +/// Make every page of the file real now (not a promise the compositor finds broken later). +fn reserve(f: &std::fs::File, len: u64) -> std::io::Result<()> { + use std::os::fd::AsRawFd as _; + let r = unsafe { libc::posix_fallocate(f.as_raw_fd(), 0, len as libc::off_t) }; + if r == 0 { + return Ok(()); + } + if r != libc::EOPNOTSUPP && r != libc::EINVAL { + return Err(std::io::Error::from_raw_os_error(r)); + } + // A file system without fallocate: write the zeros ourselves. + f.set_len(0)?; + let mut w = f; + let zeros = vec![0u8; 1 << 20]; + let mut left = len; + while left > 0 { + let n = left.min(zeros.len() as u64) as usize; + w.write_all(&zeros[..n])?; + left -= n as u64; + } + Ok(()) +} + /// The keymap text: one key per keysym we use; the first four are the modifiers. pub fn keymap_text(keys: &[u32]) -> String { let mut s = String::from("xkb_keymap {\nxkb_keycodes \"anybot\" {\n minimum = 8;\n"); @@ -614,4 +651,19 @@ mod tests { let px = to_rgba(&[1, 2, 3, 0], wl_shm::Format::Abgr8888, 1, 1, 4, false).unwrap(); assert_eq!(px.get_pixel(0, 0).0, [1, 2, 3, 255]); } + + /// The screenshot buffer is real memory before the compositor writes into it, and not a + /// file in the runtime folder (a full /run/user must not crash the desktop). + #[test] + fn screenshot_buffer_is_allocated_up_front() { + use std::os::fd::AsRawFd as _; + let len = 3 * 1024 * 1024 + 17; + let f = scratch_file(len).unwrap(); + assert_eq!(f.metadata().unwrap().len(), len); + let mut st: libc::stat = unsafe { std::mem::zeroed() }; + assert_eq!(unsafe { libc::fstat(f.as_raw_fd(), &mut st) }, 0); + assert!(st.st_blocks as u64 * 512 >= len, "pages not reserved: {} blocks", st.st_blocks); + let link = std::fs::read_link(format!("/proc/self/fd/{}", f.as_raw_fd())).unwrap(); + assert!(link.to_string_lossy().starts_with("/memfd:"), "{link:?}"); + } }