diff --git a/Cargo.lock b/Cargo.lock index defa69612..111d18964 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3386,6 +3386,7 @@ dependencies = [ "crossterm", "futures", "globset", + "libc", "md-5", "mlua", "parking_lot", @@ -3400,6 +3401,7 @@ dependencies = [ "twox-hash", "unicode-width 0.2.0", "uzers", + "windows-sys 0.59.0", "yazi-adapter", "yazi-binding", "yazi-boot", diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index beb9908a4..1550e40a8 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -46,10 +46,12 @@ unicode-width = { workspace = true } yazi-prebuilt = "0.1.0" [target."cfg(unix)".dependencies] +libc = { workspace = true } uzers = { workspace = true } [target."cfg(windows)".dependencies] clipboard-win = "5.4.0" +windows-sys = { version = "0.59.0", features = [ "Win32_Security", "Win32_System_JobObjects", "Win32_System_Threading" ] } [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-plugin/preset/plugins/svg.lua b/yazi-plugin/preset/plugins/svg.lua index 20fefc8cb..7d046b17e 100644 --- a/yazi-plugin/preset/plugins/svg.lua +++ b/yazi-plugin/preset/plugins/svg.lua @@ -25,18 +25,44 @@ function M:preload(job) end -- stylua: ignore - local cmd = require("magick").with_env():args { - "-size", string.format("%dx%d^", rt.preview.max_width, rt.preview.max_height), - string.format("rsvg:%s", job.file.url), "-strip", - "-quality", rt.preview.image_quality, - string.format("JPG:%s", cache), + local cmd = Command("resvg"):args { + "-w", rt.preview.max_width, "-h", rt.preview.max_height, + "--image-rendering", "optimizeSpeed", + tostring(job.file.url), tostring(cache) } + if rt.tasks.image_alloc > 0 then + cmd = cmd:memory(rt.tasks.image_alloc) + end + + local child, err = cmd:spawn() + if not child then + return true, Err("Failed to start `resvg`, error: %s", err) + end + + local status, err + while true do + ya.sleep(0.2) + + status, err = child:try_wait() + if status or err then + break + end + + local id, mem = child:id(), nil + if id then + mem = ya.proc_info(id).mem_resident + end + if mem and mem > rt.tasks.image_alloc then + child:start_kill() + err = Err("memory limit exceeded, pid: %s, memory: %s", id, mem) + break + end + end - local status, err = cmd:status() if status then return status.success else - return true, Err("Failed to start `magick`, error: %s", err) + return true, Err("Error while running `resvg`: %s", err) end end diff --git a/yazi-plugin/preset/plugins/video.lua b/yazi-plugin/preset/plugins/video.lua index 1bc118a41..8f2bd0755 100644 --- a/yazi-plugin/preset/plugins/video.lua +++ b/yazi-plugin/preset/plugins/video.lua @@ -58,7 +58,7 @@ function M:preload(job) "-skip_frame", "nokey", "-ss", ss, "-an", "-sn", "-dn", "-i", tostring(job.file.url), - "-vframes", 1, + "-map", "0:v", "-vframes", 1, "-q:v", qv, "-vf", string.format("scale=-1:'min(%d,ih)':flags=fast_bilinear", rt.preview.max_height), "-f", "image2", diff --git a/yazi-plugin/src/process/child.rs b/yazi-plugin/src/process/child.rs index 7e53e765d..d89865c46 100644 --- a/yazi-plugin/src/process/child.rs +++ b/yazi-plugin/src/process/child.rs @@ -1,7 +1,7 @@ -use std::{ops::DerefMut, time::Duration}; +use std::{ops::DerefMut, process::ExitStatus, time::Duration}; use futures::future::try_join3; -use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Table, UserData, Value}; +use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Table, UserData, UserDataFields, UserDataMethods, Value}; use tokio::{io::{self, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}, process::{ChildStderr, ChildStdin, ChildStdout}, select}; use yazi_binding::Error; @@ -9,40 +9,95 @@ use super::Status; use crate::process::Output; pub struct Child { - inner: tokio::process::Child, - stdin: Option>, - stdout: Option>, - stderr: Option>, + inner: tokio::process::Child, + stdin: Option>, + stdout: Option>, + stderr: Option>, + #[cfg(windows)] + job_handle: Option, +} + +#[cfg(windows)] +impl Drop for Child { + fn drop(&mut self) { + if let Some(h) = self.job_handle.take() { + unsafe { windows_sys::Win32::Foundation::CloseHandle(h) }; + } + } } impl Child { - pub fn new(mut inner: tokio::process::Child) -> Self { + pub fn new( + mut inner: tokio::process::Child, + #[cfg(windows)] job_handle: Option, + ) -> Self { let stdin = inner.stdin.take().map(BufWriter::new); let stdout = inner.stdout.take().map(BufReader::new); let stderr = inner.stderr.take().map(BufReader::new); - Self { inner, stdin, stdout, stderr } + Self { + inner, + stdin, + stdout, + stderr, + #[cfg(windows)] + job_handle, + } } -} -impl UserData for Child { - fn add_methods>(methods: &mut M) { - #[inline] - async fn read_line(me: &mut Child) -> (Option>, u8) { - async fn read(r: Option) -> Option> { - let mut buf = Vec::new(); - match r?.read_until(b'\n', &mut buf).await { - Ok(0) | Err(_) => None, - Ok(_) => Some(buf), - } + pub(super) async fn wait(&mut self) -> io::Result { + drop(self.stdin.take()); + self.inner.wait().await + } + + pub(super) async fn status(&mut self) -> io::Result { + drop(self.stdin.take()); + drop(self.stdout.take()); + drop(self.stderr.take()); + self.inner.wait().await + } + + async fn read_line(&mut self) -> (Option>, u8) { + async fn read(r: Option) -> Option> { + let mut buf = Vec::new(); + match r?.read_until(b'\n', &mut buf).await { + Ok(0) | Err(_) => None, + Ok(_) => Some(buf), } + } + + select! { + r @ Some(_) = read(self.stdout.as_mut()) => (r, 0u8), + r @ Some(_) = read(self.stderr.as_mut()) => (r, 1u8), + else => (None, 2u8), + } + } - select! { - r @ Some(_) = read(me.stdout.as_mut()) => (r, 0u8), - r @ Some(_) = read(me.stderr.as_mut()) => (r, 1u8), - else => (None, 2u8), + pub(super) async fn wait_with_output(mut self) -> io::Result { + async fn read(r: &mut Option) -> io::Result> { + let mut vec = Vec::new(); + if let Some(r) = r.as_mut() { + r.read_to_end(&mut vec).await?; } + Ok(vec) } + // Ensure stdin is closed so the child isn't stuck waiting on input while the + // parent is waiting for it to exit. + drop(self.stdin.take()); + + // Drop happens after `try_join` due to + let mut stdout = self.stdout.take(); + let mut stderr = self.stderr.take(); + + let result = try_join3(self.inner.wait(), read(&mut stdout), read(&mut stderr)).await?; + Ok(std::process::Output { status: result.0, stdout: result.1, stderr: result.2 }) + } +} + +impl UserData for Child { + fn add_methods>(methods: &mut M) { + methods.add_method("id", |_, me, ()| Ok(me.inner.id())); + methods.add_async_method_mut("read", |_, mut me, len: usize| async move { async fn read(r: Option, len: usize) -> Option> { let mut r = r?; @@ -62,14 +117,15 @@ impl UserData for Child { }) }); methods.add_async_method_mut("read_line", |lua, mut me, ()| async move { - match read_line(&mut me).await { + match me.read_line().await { (Some(b), event) => (lua.create_string(b)?, event).into_lua_multi(&lua), (None, event) => (Value::Nil, event).into_lua_multi(&lua), } }); + // TODO: deprecate this method methods.add_async_method_mut("read_line_with", |lua, mut me, options: Table| async move { let timeout = Duration::from_millis(options.raw_get("timeout")?); - let Ok(result) = tokio::time::timeout(timeout, read_line(&mut me)).await else { + let Ok(result) = tokio::time::timeout(timeout, me.read_line()).await else { return (Value::Nil, 3u8).into_lua_multi(&lua); }; match result { @@ -98,38 +154,21 @@ impl UserData for Child { }); methods.add_async_method_mut("wait", |lua, mut me, ()| async move { - drop(me.stdin.take()); - match me.inner.wait().await { + match me.wait().await { Ok(status) => (Status::new(status), Value::Nil).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); methods.add_async_function("wait_with_output", |lua, ud: AnyUserData| async move { - async fn read_to_end(r: &mut Option) -> io::Result> { - let mut vec = Vec::new(); - if let Some(r) = r.as_mut() { - r.read_to_end(&mut vec).await?; - } - Ok(vec) + match ud.take::()?.wait_with_output().await { + Ok(output) => (Output::new(output), Value::Nil).into_lua_multi(&lua), + Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } - - let mut me = ud.take::()?; - let mut stdout_pipe = me.stdout.take(); - let mut stderr_pipe = me.stderr.take(); - - let stdout_fut = read_to_end(&mut stdout_pipe); - let stderr_fut = read_to_end(&mut stderr_pipe); - - drop(me.stdin.take()); - let result = try_join3(me.inner.wait(), stdout_fut, stderr_fut).await; - drop(stdout_pipe); - drop(stderr_pipe); - - match result { - Ok((status, stdout, stderr)) => { - (Output::new(std::process::Output { status, stdout, stderr }), Value::Nil) - .into_lua_multi(&lua) - } + }); + methods.add_async_method_mut("try_wait", |lua, mut me, ()| async move { + match me.inner.try_wait() { + Ok(Some(status)) => (Status::new(status), Value::Nil).into_lua_multi(&lua), + Ok(None) => (Value::Nil, Value::Nil).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); diff --git a/yazi-plugin/src/process/command.rs b/yazi-plugin/src/process/command.rs index 1f5c55ec6..49f99f56c 100644 --- a/yazi-plugin/src/process/command.rs +++ b/yazi-plugin/src/process/command.rs @@ -1,4 +1,4 @@ -use std::process::Stdio; +use std::{io, process::Stdio}; use mlua::{AnyUserData, ExternalError, IntoLuaMulti, Lua, MetaMethod, Table, UserData, Value}; use tokio::process::{ChildStderr, ChildStdin, ChildStdout}; @@ -8,7 +8,8 @@ use super::{Child, output::Output}; use crate::process::Status; pub struct Command { - inner: tokio::process::Command, + inner: tokio::process::Command, + memory: Option, } const NULL: u8 = 0; @@ -21,7 +22,7 @@ impl Command { let mut inner = tokio::process::Command::new(program); inner.kill_on_drop(true).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); - Ok(Self { inner }) + Ok(Self { inner, memory: None }) })?; let command = lua.create_table_from([ @@ -35,6 +36,74 @@ impl Command { lua.globals().raw_set("Command", command) } + + #[cfg(unix)] + fn spawn(&mut self) -> io::Result { + if let Some(max) = self.memory { + unsafe { + self.inner.pre_exec(move || { + let rlp = libc::rlimit { rlim_cur: max as _, rlim_max: max as _ }; + libc::setrlimit(libc::RLIMIT_AS, &rlp); + Ok(()) + }); + } + } + self.inner.spawn().map(Child::new) + } + + #[cfg(windows)] + fn spawn(&mut self) -> io::Result { + use std::os::windows::io::RawHandle; + + use windows_sys::Win32::System::JobObjects::{AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_PROCESS_MEMORY, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation, SetInformationJobObject}; + + fn assign_job(max: usize, handle: RawHandle) -> io::Result { + unsafe { + let job = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()); + if job.is_null() { + return Err(io::Error::last_os_error()); + } + + let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed(); + info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_PROCESS_MEMORY; + info.ProcessMemoryLimit = max; + + let result = SetInformationJobObject( + job, + JobObjectExtendedLimitInformation, + &mut info as *mut _ as *mut _, + std::mem::size_of_val(&info) as u32, + ); + + if result == 0 { + Err(io::Error::last_os_error()) + } else if AssignProcessToJobObject(job, handle) == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(job) + } + } + } + + let child = self.inner.spawn()?; + if let (Some(max), Some(handle)) = (self.memory, child.raw_handle()) { + if let Ok(job) = assign_job(max, handle) { + return Ok(Child::new(child, Some(job))); + } + } + + Ok(Child::new(child, None)) + } + + async fn output(&mut self) -> io::Result { + self.inner.stdin(Stdio::piped()); + self.inner.stdout(Stdio::piped()); + self.spawn()?.wait_with_output().await + } + + async fn status(&mut self) -> io::Result { + self.spawn()?.status().await + } } impl UserData for Command { @@ -102,18 +171,22 @@ impl UserData for Command { ud.borrow_mut::()?.inner.stderr(make_stdio(stdio)?); Ok(ud) }); - methods.add_method_mut("spawn", |lua, me, ()| match me.inner.spawn() { - Ok(child) => (Child::new(child), Value::Nil).into_lua_multi(lua), + methods.add_function_mut("memory", |_, (ud, max): (AnyUserData, usize)| { + ud.borrow_mut::()?.memory = Some(max); + Ok(ud) + }); + methods.add_method_mut("spawn", |lua, me, ()| match me.spawn() { + Ok(child) => (child, Value::Nil).into_lua_multi(lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(lua), }); methods.add_async_method_mut("output", |lua, mut me, ()| async move { - match me.inner.output().await { + match me.output().await { Ok(output) => (Output::new(output), Value::Nil).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); methods.add_async_method_mut("status", |lua, mut me, ()| async move { - match me.inner.status().await { + match me.status().await { Ok(status) => (Status::new(status), Value::Nil).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } diff --git a/yazi-plugin/src/utils/mod.rs b/yazi-plugin/src/utils/mod.rs index 5a320167f..c1f3a5853 100644 --- a/yazi-plugin/src/utils/mod.rs +++ b/yazi-plugin/src/utils/mod.rs @@ -1,5 +1,5 @@ #![allow(clippy::module_inception)] yazi_macro::mod_flat!( - app cache call image json layer log preview spot sync target text time user utils + app cache call image json layer log preview process spot sync target text time user utils ); diff --git a/yazi-plugin/src/utils/process.rs b/yazi-plugin/src/utils/process.rs new file mode 100644 index 000000000..9d8d10082 --- /dev/null +++ b/yazi-plugin/src/utils/process.rs @@ -0,0 +1,29 @@ +use mlua::{Function, Lua}; + +use super::Utils; + +impl Utils { + #[cfg(target_os = "macos")] + pub(super) fn proc_info(lua: &Lua) -> mlua::Result { + lua.create_function(|lua, pid: usize| { + let info = unsafe { + let mut info: libc::proc_taskinfo = std::mem::zeroed(); + libc::proc_pidinfo( + pid as _, + libc::PROC_PIDTASKINFO, + 0, + &mut info as *mut _ as *mut _, + std::mem::size_of_val(&info) as _, + ); + info + }; + + lua.create_table_from([("mem_resident", info.pti_resident_size)]) + }) + } + + #[cfg(not(target_os = "macos"))] + pub(super) fn proc_info(lua: &Lua) -> mlua::Result { + lua.create_function(|lua, ()| lua.create_table()) + } +} diff --git a/yazi-plugin/src/utils/utils.rs b/yazi-plugin/src/utils/utils.rs index 839e3e468..a4d6a490e 100644 --- a/yazi-plugin/src/utils/utils.rs +++ b/yazi-plugin/src/utils/utils.rs @@ -45,6 +45,9 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result { b"preview_code" => Utils::preview_code(lua)?, b"preview_widgets" => Utils::preview_widgets(lua)?, + // Process + b"proc_info" => Utils::proc_info(lua)?, + // Spot b"spot_table" => Utils::spot_table(lua)?, b"spot_widgets" => Utils::spot_widgets(lua)?,