diff --git a/crates/livesplit-auto-splitting/Cargo.toml b/crates/livesplit-auto-splitting/Cargo.toml index 702c67b2..6a2edb73 100644 --- a/crates/livesplit-auto-splitting/Cargo.toml +++ b/crates/livesplit-auto-splitting/Cargo.toml @@ -20,12 +20,13 @@ sysinfo = { version = "0.29.0", default-features = false, features = [ "multithread", ] } time = { version = "0.3.3", default-features = false } -wasmtime = { version = "12.0.1", default-features = false, features = [ +wasmtime = { version = "13.0.0", default-features = false, features = [ "cranelift", "parallel-compilation", ] } -wasmtime-wasi = "12.0.1" -wasi-common = "12.0.1" +wasmtime-wasi = "13.0.0" +wasi-common = "13.0.0" +bytemuck = { version = "1.14.0", features = ["min_const_generics"] } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.48.0", features = ["Win32_Storage_FileSystem"] } diff --git a/crates/livesplit-auto-splitting/README.md b/crates/livesplit-auto-splitting/README.md index c3b2dd7c..8ff04918 100644 --- a/crates/livesplit-auto-splitting/README.md +++ b/crates/livesplit-auto-splitting/README.md @@ -34,7 +34,10 @@ pub struct Address(pub u64); pub struct NonZeroAddress(pub NonZeroU64); #[repr(transparent)] -pub struct ProcessId(NonZeroU64); +pub struct Process(NonZeroU64); + +#[repr(transparent)] +pub struct ProcessId(u64); #[repr(transparent)] pub struct TimerState(u32); @@ -98,16 +101,35 @@ extern "C" { pub fn timer_resume_game_time(); /// Attaches to a process based on its name. - pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option; + pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option; + /// Attaches to a process based on its process id. + pub fn process_attach_by_pid(pid: ProcessId); /// Detaches from a process. - pub fn process_detach(process: ProcessId); + pub fn process_detach(process: Process); + /// Lists processes based on their name. Returns `false` if listing the + /// processes failed. If it was successful, the buffer is now filled + /// with the process ids. They are in no specific order. The + /// `list_len_ptr` will be updated to the amount of process ids that + /// were found. If this is larger than the original value provided, the + /// buffer provided was too small and not all process ids could be + /// stored. This is still considered successful and can optionally be + /// treated as an error condition by the caller by checking if the + /// length increased and potentially reallocating a larger buffer. If + /// the length decreased after the call, the buffer was larger than + /// needed and the remaining entries are untouched. + pub fn process_list_by_name( + name_ptr: *const u8, + name_len: usize, + list_ptr: *mut ProcessId, + list_len_ptr: *mut usize, + ) -> bool; /// Checks whether is a process is still open. You should detach from a /// process and stop using it if this returns `false`. - pub fn process_is_open(process: ProcessId) -> bool; + pub fn process_is_open(process: Process) -> bool; /// Reads memory from a process at the address given. This will write /// the memory to the buffer given. Returns `false` if this fails. pub fn process_read( - process: ProcessId, + process: Process, address: Address, buf_ptr: *mut u8, buf_len: usize, @@ -115,41 +137,41 @@ extern "C" { /// Gets the address of a module in a process. pub fn process_get_module_address( - process: ProcessId, + process: Process, name_ptr: *const u8, name_len: usize, ) -> Option; /// Gets the size of a module in a process. pub fn process_get_module_size( - process: ProcessId, + process: Process, name_ptr: *const u8, name_len: usize, ) -> Option; /// Gets the number of memory ranges in a given process. - pub fn process_get_memory_range_count(process: ProcessId) -> Option; + pub fn process_get_memory_range_count(process: Process) -> Option; /// Gets the start address of a memory range by its index. pub fn process_get_memory_range_address( - process: ProcessId, + process: Process, idx: u64, ) -> Option; /// Gets the size of a memory range by its index. - pub fn process_get_memory_range_size(process: ProcessId, idx: u64) -> Option; + pub fn process_get_memory_range_size(process: Process, idx: u64) -> Option; /// Gets the flags of a memory range by its index. pub fn process_get_memory_range_flags( - process: ProcessId, + process: Process, idx: u64, ) -> Option; /// Stores the file system path of the executable in the buffer given. The - /// path is a pa thth that is accessiblerough the WASI file system, so a + /// path is a path that is accessible through the WASI file system, so a /// Windows path of `C:\foo\bar.exe` would be returned as /// `/mnt/c/foo/bar.exe`. Returns `false` if the buffer is too small. After /// this call, no matter whether it was successful or not, the /// `buf_len_ptr` will be set to the required buffer size. The path is /// guaranteed to be valid UTF-8 and is not nul-terminated. pub fn process_get_path( - process: ProcessId, + process: Process, buf_ptr: *mut u8, buf_len_ptr: *mut usize, ) -> bool; diff --git a/crates/livesplit-auto-splitting/src/lib.rs b/crates/livesplit-auto-splitting/src/lib.rs index 6c206034..26783d93 100644 --- a/crates/livesplit-auto-splitting/src/lib.rs +++ b/crates/livesplit-auto-splitting/src/lib.rs @@ -34,7 +34,10 @@ //! pub struct NonZeroAddress(pub NonZeroU64); //! //! #[repr(transparent)] -//! pub struct ProcessId(NonZeroU64); +//! pub struct Process(NonZeroU64); +//! +//! #[repr(transparent)] +//! pub struct ProcessId(u64); //! //! #[repr(transparent)] //! pub struct TimerState(u32); @@ -98,23 +101,35 @@ //! pub fn timer_resume_game_time(); //! //! /// Attaches to a process based on its name. -//! pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option; -//! /// Attaches to a process based on its pid. -//! pub fn process_attach_pid(pid: u32); +//! pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option; +//! /// Attaches to a process based on its process id. +//! pub fn process_attach_by_pid(pid: ProcessId); //! /// Detaches from a process. -//! pub fn process_detach(process: ProcessId); -//! /// Lists processes (as pids) based on their name. Returns `false` -//! /// if the buffer is too small. After this call, no matter whether -//! /// it was successful or not, the `buf_len_ptr` will be set to the -//! /// required buffer size. -//! pub fn process_list(name_ptr: *const u8, name_len: usize, list_ptr: *mut u8, list_len_ptr: *mut usize) -> bool +//! pub fn process_detach(process: Process); +//! /// Lists processes based on their name. Returns `false` if listing the +//! /// processes failed. If it was successful, the buffer is now filled +//! /// with the process ids. They are in no specific order. The +//! /// `list_len_ptr` will be updated to the amount of process ids that +//! /// were found. If this is larger than the original value provided, the +//! /// buffer provided was too small and not all process ids could be +//! /// stored. This is still considered successful and can optionally be +//! /// treated as an error condition by the caller by checking if the +//! /// length increased and potentially reallocating a larger buffer. If +//! /// the length decreased after the call, the buffer was larger than +//! /// needed and the remaining entries are untouched. +//! pub fn process_list_by_name( +//! name_ptr: *const u8, +//! name_len: usize, +//! list_ptr: *mut ProcessId, +//! list_len_ptr: *mut usize, +//! ) -> bool; //! /// Checks whether is a process is still open. You should detach from a //! /// process and stop using it if this returns `false`. -//! pub fn process_is_open(process: ProcessId) -> bool; +//! pub fn process_is_open(process: Process) -> bool; //! /// Reads memory from a process at the address given. This will write //! /// the memory to the buffer given. Returns `false` if this fails. //! pub fn process_read( -//! process: ProcessId, +//! process: Process, //! address: Address, //! buf_ptr: *mut u8, //! buf_len: usize, @@ -122,29 +137,29 @@ //! //! /// Gets the address of a module in a process. //! pub fn process_get_module_address( -//! process: ProcessId, +//! process: Process, //! name_ptr: *const u8, //! name_len: usize, //! ) -> Option; //! /// Gets the size of a module in a process. //! pub fn process_get_module_size( -//! process: ProcessId, +//! process: Process, //! name_ptr: *const u8, //! name_len: usize, //! ) -> Option; //! //! /// Gets the number of memory ranges in a given process. -//! pub fn process_get_memory_range_count(process: ProcessId) -> Option; +//! pub fn process_get_memory_range_count(process: Process) -> Option; //! /// Gets the start address of a memory range by its index. //! pub fn process_get_memory_range_address( -//! process: ProcessId, +//! process: Process, //! idx: u64, //! ) -> Option; //! /// Gets the size of a memory range by its index. -//! pub fn process_get_memory_range_size(process: ProcessId, idx: u64) -> Option; +//! pub fn process_get_memory_range_size(process: Process, idx: u64) -> Option; //! /// Gets the flags of a memory range by its index. //! pub fn process_get_memory_range_flags( -//! process: ProcessId, +//! process: Process, //! idx: u64, //! ) -> Option; //! @@ -156,7 +171,7 @@ //! /// `buf_len_ptr` will be set to the required buffer size. The path is //! /// guaranteed to be valid UTF-8 and is not nul-terminated. //! pub fn process_get_path( -//! process: ProcessId, +//! process: Process, //! buf_ptr: *mut u8, //! buf_len_ptr: *mut usize, //! ) -> bool; diff --git a/crates/livesplit-auto-splitting/src/process.rs b/crates/livesplit-auto-splitting/src/process.rs index 79b4397c..0f932680 100644 --- a/crates/livesplit-auto-splitting/src/process.rs +++ b/crates/livesplit-auto-splitting/src/process.rs @@ -81,7 +81,9 @@ impl Process { pub(super) fn with_pid(pid: u32, process_list: &mut ProcessList) -> Result { process_list.refresh(); - let process = process_list.get(sysinfo::Pid::from_u32(pid)).context(ProcessDoesntExist)?; + let process = process_list + .get(sysinfo::Pid::from_u32(pid)) + .context(ProcessDoesntExist)?; let path = build_path(process.exe()); @@ -100,21 +102,14 @@ impl Process { }) } - pub(super) fn list_pids_by_name(name: &str, process_list: &mut ProcessList) -> Result, OpenError> { - let mut result = Vec::new(); - + pub(super) fn list_pids_by_name<'a>( + name: &'a str, + process_list: &'a mut ProcessList, + ) -> impl Iterator + 'a { process_list.refresh(); - let processes = process_list.processes_by_name(name); - - for process in processes { - result.push(process.pid().as_u32()); - } - - if result.is_empty() { - Err(OpenError::ProcessDoesntExist) - } else { - Ok(result) - } + process_list + .processes_by_name(name) + .map(|p| p.pid().as_u32()) } pub(super) fn is_open(&mut self, process_list: &mut ProcessList) -> bool { @@ -203,6 +198,15 @@ impl Process { self.path.as_deref() } + /// Returns the name of the executable of the process. + pub fn name(&self) -> Option<&str> { + let path = &self.path.as_deref()?; + Some(match path.rsplit_once('/') { + Some((_, name)) => name, + None => path, + }) + } + fn refresh_memory_ranges(&mut self) -> Result<(), ModuleError> { let now = Instant::now(); if now >= self.next_memory_range_check { diff --git a/crates/livesplit-auto-splitting/src/runtime.rs b/crates/livesplit-auto-splitting/src/runtime.rs index 62128338..480756a1 100644 --- a/crates/livesplit-auto-splitting/src/runtime.rs +++ b/crates/livesplit-auto-splitting/src/runtime.rs @@ -14,8 +14,7 @@ use std::{ env::consts::{ARCH, OS}, path::{Path, PathBuf}, str, - time::{Duration, Instant}, - mem, + time::{Duration, Instant}, }; use sysinfo::{ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt}; use wasi_common::{ @@ -655,9 +654,10 @@ fn bind_interface(linker: &mut Linker>) -> Result<(), Creat let process_name = get_str(memory, ptr, len)?; Ok( if let Ok(p) = Process::with_name(process_name, &mut context.process_list) { - context - .timer - .log(format_args!("Attached to a new process: {process_name}")); + context.timer.log(format_args!( + "Attached to a new process: {}", + p.name().unwrap_or("") + )); context.processes.insert(p).data().as_ffi() } else { 0 @@ -669,14 +669,19 @@ fn bind_interface(linker: &mut Linker>) -> Result<(), Creat source, name: "process_attach", })? - .func_wrap("env", "process_attach_pid", { - |mut caller: Caller<'_, Context>, pid: u32| { + .func_wrap("env", "process_attach_by_pid", { + |mut caller: Caller<'_, Context>, pid: u64| { let (_, context) = memory_and_context(&mut caller); Ok( - if let Ok(p) = Process::with_pid(pid, &mut context.process_list) { - context - .timer - .log(format_args!("Attached to a new process with pid {pid}")); + if let Some(p) = pid + .try_into() + .ok() + .and_then(|pid| Process::with_pid(pid, &mut context.process_list).ok()) + { + context.timer.log(format_args!( + "Attached to a new process: {}", + p.name().unwrap_or("") + )); context.processes.insert(p).data().as_ffi() } else { 0 @@ -686,7 +691,7 @@ fn bind_interface(linker: &mut Linker>) -> Result<(), Creat }) .map_err(|source| CreationError::LinkFunction { source, - name: "process_attach_pid", + name: "process_attach_by_pid", })? .func_wrap("env", "process_detach", { |mut caller: Caller<'_, Context>, process: u64| { @@ -706,32 +711,52 @@ fn bind_interface(linker: &mut Linker>) -> Result<(), Creat source, name: "process_detach", })? - .func_wrap("env", "process_list", { - |mut caller: Caller<'_, Context>, name_ptr: u32, name_len: u32, list_ptr: u32, list_len_ptr: u32| { + .func_wrap("env", "process_list_by_name", { + |mut caller: Caller<'_, Context>, + name_ptr: u32, + name_len: u32, + list_ptr: u32, + list_len_ptr: u32| { let (memory, context) = memory_and_context(&mut caller); - let process_name = get_str(memory, name_ptr, name_len)?; - if let Ok(list) = Process::list_pids_by_name(process_name, &mut context.process_list) { - let list_len_bytes = get_arr_mut(memory, list_len_ptr)?; - let list_len = u32::from_le_bytes(*list_len_bytes) as usize; - *list_len_bytes = (list.len() as u32).to_le_bytes(); - if list_len < list.len() { - return Ok(0u32); - } - let mut list_bytes = Vec::new(); - for i in &list { - list_bytes.extend_from_slice(&i.to_le_bytes()); - } - let buf = get_slice_mut(memory, list_ptr, (list.len() * mem::size_of::()) as _)?; - buf.copy_from_slice(list_bytes.as_slice()); - Ok(1u32) - } else { - Ok(0u32) + + let list_len_bytes = get_arr_mut(memory, list_len_ptr)?; + let list_len = u32::from_le_bytes(*list_len_bytes); + + let [name, list] = get_two_slice_mut( + memory, + name_ptr, + name_len, + list_ptr, + list_len + .checked_mul(8) + .context("The list length overflows the size of the address space.")?, + )?; + + let mut count = 0u32; + + let mut iter = + Process::list_pids_by_name(str::from_utf8(name)?, &mut context.process_list) + .inspect(|_| { + count = count.saturating_add(1); + }); + + for (pid, list_element) in iter.by_ref().zip(bytemuck::cast_slice_mut(list)) { + *list_element = (pid as u64).to_le_bytes(); } + // Consume the rest of the PIDs to ensure we fully count them. + iter.for_each(drop); + + let list_len_bytes = get_arr_mut(memory, list_len_ptr)?; + *list_len_bytes = count.to_le_bytes(); + + // Currently this can't fail, but that's only because `sysinfo` + // doesn't report any errors when listing the processes fails. + Ok(1u32) } }) .map_err(|source| CreationError::LinkFunction { source, - name: "process_list", + name: "process_list_by_name", })? .func_wrap("env", "process_is_open", { |mut caller: Caller<'_, Context>, process: u64| { @@ -972,13 +997,17 @@ fn get_arr_mut(memory: &mut [u8], ptr: u32) -> Result<&mut [u8; fn get_slice(memory: &[u8], ptr: u32, len: u32) -> Result<&[u8]> { memory - .get(ptr as usize..(ptr + len) as usize) + .get(ptr as usize..) + .context("Out of bounds pointer and length pair.")? + .get(..len as usize) .context("Out of bounds pointer and length pair.") } fn get_slice_mut(memory: &mut [u8], ptr: u32, len: u32) -> Result<&mut [u8]> { memory - .get_mut(ptr as usize..(ptr + len) as usize) + .get_mut(ptr as usize..) + .context("Out of bounds pointer and length pair.")? + .get_mut(..len as usize) .context("Out of bounds pointer and length pair.") } @@ -986,3 +1015,45 @@ fn get_str(memory: &[u8], ptr: u32, len: u32) -> Result<&str> { let slice = get_slice(memory, ptr, len)?; str::from_utf8(slice).map_err(Into::into) } + +fn get_two_slice_mut( + memory: &mut [u8], + ptr1: u32, + len1: u32, + ptr2: u32, + len2: u32, +) -> Result<[&mut [u8]; 2]> { + let (ptr1, ptr2) = (ptr1 as usize, ptr2 as usize); + let (len1, len2) = (len1 as usize, len2 as usize); + if ptr1 < ptr2 { + if ptr2 >= memory.len() { + return Err(format_err!("Out of bounds pointer and length pair.")); + } + let (first, second) = memory.split_at_mut(ptr2); + Ok([ + first + .get_mut(ptr1..) + .context("Out of bounds pointer and length pair.")? + .get_mut(..len1) + .context("Overlapping pair of pointer ranges.")?, + second + .get_mut(..len2) + .context("Out of bounds pointer and length pair.")?, + ]) + } else { + if ptr1 >= memory.len() { + return Err(format_err!("Out of bounds pointer and length pair.")); + } + let (first, second) = memory.split_at_mut(ptr1); + Ok([ + first + .get_mut(ptr2..) + .context("Out of bounds pointer and length pair.")? + .get_mut(..len2) + .context("Overlapping pair of pointer ranges.")?, + second + .get_mut(..len1) + .context("Out of bounds pointer and length pair.")?, + ]) + } +} diff --git a/src/auto_splitting/mod.rs b/src/auto_splitting/mod.rs index d8c60349..10ec3b20 100644 --- a/src/auto_splitting/mod.rs +++ b/src/auto_splitting/mod.rs @@ -34,7 +34,10 @@ //! pub struct NonZeroAddress(pub NonZeroU64); //! //! #[repr(transparent)] -//! pub struct ProcessId(NonZeroU64); +//! pub struct Process(NonZeroU64); +//! +//! #[repr(transparent)] +//! pub struct ProcessId(u64); //! //! #[repr(transparent)] //! pub struct TimerState(u32); @@ -51,6 +54,20 @@ //! pub const ENDED: Self = Self(3); //! } //! +//! #[repr(transparent)] +//! pub struct MemoryRangeFlags(NonZeroU64); +//! +//! impl MemoryRangeFlags { +//! /// The memory range is readable. +//! pub const READ: Self = Self(NonZeroU64::new(1 << 1).unwrap()); +//! /// The memory range is writable. +//! pub const WRITE: Self = Self(NonZeroU64::new(1 << 2).unwrap()); +//! /// The memory range is executable. +//! pub const EXECUTE: Self = Self(NonZeroU64::new(1 << 3).unwrap()); +//! /// The memory range has a file path. +//! pub const PATH: Self = Self(NonZeroU64::new(1 << 4).unwrap()); +//! } +//! //! extern "C" { //! /// Gets the state that the timer currently is in. //! pub fn timer_get_state() -> TimerState; @@ -84,33 +101,68 @@ //! pub fn timer_resume_game_time(); //! //! /// Attaches to a process based on its name. -//! pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option; +//! pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option; +//! /// Attaches to a process based on its process id. +//! pub fn process_attach_by_pid(pid: ProcessId); //! /// Detaches from a process. -//! pub fn process_detach(process: ProcessId); +//! pub fn process_detach(process: Process); +//! /// Lists processes based on their name. Returns `false` if listing the +//! /// processes failed. If it was successful, the buffer is now filled +//! /// with the process ids. They are in no specific order. The +//! /// `list_len_ptr` will be updated to the amount of process ids that +//! /// were found. If this is larger than the original value provided, the +//! /// buffer provided was too small and not all process ids could be +//! /// stored. This is still considered successful and can optionally be +//! /// treated as an error condition by the caller by checking if the +//! /// length increased and potentially reallocating a larger buffer. If +//! /// the length decreased after the call, the buffer was larger than +//! /// needed and the remaining entries are untouched. +//! pub fn process_list_by_name( +//! name_ptr: *const u8, +//! name_len: usize, +//! list_ptr: *mut ProcessId, +//! list_len_ptr: *mut usize, +//! ) -> bool; //! /// Checks whether is a process is still open. You should detach from a //! /// process and stop using it if this returns `false`. -//! pub fn process_is_open(process: ProcessId) -> bool; +//! pub fn process_is_open(process: Process) -> bool; //! /// Reads memory from a process at the address given. This will write //! /// the memory to the buffer given. Returns `false` if this fails. //! pub fn process_read( -//! process: ProcessId, +//! process: Process, //! address: Address, //! buf_ptr: *mut u8, //! buf_len: usize, //! ) -> bool; +//! //! /// Gets the address of a module in a process. //! pub fn process_get_module_address( -//! process: ProcessId, +//! process: Process, //! name_ptr: *const u8, //! name_len: usize, //! ) -> Option; //! /// Gets the size of a module in a process. //! pub fn process_get_module_size( -//! process: ProcessId, +//! process: Process, //! name_ptr: *const u8, //! name_len: usize, //! ) -> Option; //! +//! /// Gets the number of memory ranges in a given process. +//! pub fn process_get_memory_range_count(process: Process) -> Option; +//! /// Gets the start address of a memory range by its index. +//! pub fn process_get_memory_range_address( +//! process: Process, +//! idx: u64, +//! ) -> Option; +//! /// Gets the size of a memory range by its index. +//! pub fn process_get_memory_range_size(process: Process, idx: u64) -> Option; +//! /// Gets the flags of a memory range by its index. +//! pub fn process_get_memory_range_flags( +//! process: Process, +//! idx: u64, +//! ) -> Option; +//! //! /// Stores the file system path of the executable in the buffer given. The //! /// path is a path that is accessible through the WASI file system, so a //! /// Windows path of `C:\foo\bar.exe` would be returned as @@ -119,7 +171,7 @@ //! /// `buf_len_ptr` will be set to the required buffer size. The path is //! /// guaranteed to be valid UTF-8 and is not nul-terminated. //! pub fn process_get_path( -//! process: ProcessId, +//! process: Process, //! buf_ptr: *mut u8, //! buf_len_ptr: *mut usize, //! ) -> bool; @@ -581,12 +633,8 @@ async fn run( Request::ReloadScript(ret) => { let mut config = Config::default(); config.settings_store = Some(runtime.settings_store().clone()); - - match ScriptRuntime::new( - &script_path, - Timer(timer.clone()), - config, - ) { + + match ScriptRuntime::new(&script_path, Timer(timer.clone()), config) { Ok(r) => { ret.send(Ok(())).ok(); runtime = r;