From 53fc5f49173bb39a03b7d7f308b30cedb406ce50 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Wed, 21 Aug 2024 15:24:02 +0200 Subject: [PATCH] Add `stderr` Support for Auto Splitters (#835) Auto splitters can now print to `stderr`, which is the same as printing via `print_message`, except that like a usual terminal it is line buffered and either flushed on new lines or manually. This then ends up in the same `log_auto_splitter` call that `print_message` ends up in. The logging now also differentiates between messages that come from the auto splitter and messages that come from the runtime. Additionally this bumps `wasmtime` to version 24. --- crates/livesplit-auto-splitting/Cargo.toml | 8 +- crates/livesplit-auto-splitting/README.md | 52 +++++---- crates/livesplit-auto-splitting/src/lib.rs | 49 ++++---- .../src/runtime/api/process.rs | 25 +++-- .../src/runtime/api/runtime.rs | 12 +- .../src/runtime/api/wasi.rs | 106 +++++++++++++++++- .../src/runtime/mod.rs | 40 ++++--- crates/livesplit-auto-splitting/src/timer.rs | 22 +++- .../tests/sandboxing.rs | 5 +- src/auto_splitting/mod.rs | 65 +++++++---- 10 files changed, 282 insertions(+), 102 deletions(-) diff --git a/crates/livesplit-auto-splitting/Cargo.toml b/crates/livesplit-auto-splitting/Cargo.toml index 5b16b357..728c072b 100644 --- a/crates/livesplit-auto-splitting/Cargo.toml +++ b/crates/livesplit-auto-splitting/Cargo.toml @@ -8,13 +8,15 @@ license = "MIT OR Apache-2.0" description = "livesplit-auto-splitting is a library that provides a runtime for running auto splitters that can control a speedrun timer. These auto splitters are provided as WebAssembly modules." keywords = ["speedrun", "timer", "livesplit", "auto-splitting"] edition = "2021" -rust-version = "1.74" +rust-version = "1.79" [dependencies] anyhow = { version = "1.0.45", default-features = false } arc-swap = "1.6.0" async-trait = "0.1.73" +bstr = "1.10.0" bytemuck = { version = "1.14.0", features = ["min_const_generics"] } +bytes = "1.6.1" indexmap = "2.0.2" proc-maps = { version = "0.3.0", default-features = false } read-process-memory = { version = "0.1.4", default-features = false } @@ -25,13 +27,13 @@ sysinfo = { version = "0.31.2", default-features = false, features = [ "system", ] } time = { version = "0.3.3", default-features = false } -wasmtime = { version = "23.0.0", default-features = false, features = [ +wasmtime = { version = "24.0.0", default-features = false, features = [ "cranelift", "gc", "parallel-compilation", "runtime", ] } -wasmtime-wasi = { version = "23.0.0", default-features = false, features = [ +wasmtime-wasi = { version = "24.0.0", default-features = false, features = [ "preview1", ] } diff --git a/crates/livesplit-auto-splitting/README.md b/crates/livesplit-auto-splitting/README.md index 907739df..5c4e9d6c 100644 --- a/crates/livesplit-auto-splitting/README.md +++ b/crates/livesplit-auto-splitting/README.md @@ -34,7 +34,7 @@ pub struct Address(pub u64); pub struct NonZeroAddress(pub NonZeroU64); #[repr(transparent)] -pub struct Process(NonZeroU64); +pub struct AttachedProcess(NonZeroU64); #[repr(transparent)] pub struct ProcessId(u64); @@ -130,12 +130,14 @@ extern "C" { pub fn timer_resume_game_time(); /// Attaches to a process based on its name. The pointer needs to point to - /// valid UTF-8 encoded text with the given length. - pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option; + /// valid UTF-8 encoded text with the given length. If multiple processes + /// with the same name are running, the process that most recently started + /// is being attached to. + 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) -> Option; + pub fn process_attach_by_pid(pid: ProcessId) -> Option; /// Detaches from a process. - pub fn process_detach(process: Process); + pub fn process_detach(process: AttachedProcess); /// Lists processes based on their name. The name pointer needs to point to /// valid UTF-8 encoded text with the given length. Returns `false` if /// listing the processes failed. If it was successful, the buffer is now @@ -156,11 +158,11 @@ extern "C" { ) -> bool; /// Checks whether 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: Process) -> bool; + pub fn process_is_open(process: AttachedProcess) -> 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: Process, + process: AttachedProcess, address: Address, buf_ptr: *mut u8, buf_len: usize, @@ -168,14 +170,14 @@ extern "C" { /// Gets the address of a module in a process. The pointer needs to point to /// valid UTF-8 encoded text with the given length. pub fn process_get_module_address( - process: Process, + process: AttachedProcess, name_ptr: *const u8, name_len: usize, ) -> Option; /// Gets the size of a module in a process. The pointer needs to point to /// valid UTF-8 encoded text with the given length. pub fn process_get_module_size( - process: Process, + process: AttachedProcess, name_ptr: *const u8, name_len: usize, ) -> Option; @@ -190,7 +192,7 @@ extern "C" { /// the path or the module does not exist or it failed to get read. The path /// is guaranteed to be valid UTF-8 and is not nul-terminated. pub fn process_get_module_path( - process: Process, + process: AttachedProcess, name_ptr: *const u8, name_len: usize, buf_ptr: *mut u8, @@ -205,15 +207,22 @@ extern "C" { /// `buf_len_ptr` got set to 0, the path does not exist or failed to get /// read. The path is guaranteed to be valid UTF-8 and is not /// nul-terminated. - pub fn process_get_path(process: Process, buf_ptr: *mut u8, buf_len_ptr: *mut usize) -> bool; + pub fn process_get_path( + process: AttachedProcess, + buf_ptr: *mut u8, + buf_len_ptr: *mut usize, + ) -> bool; /// Gets the number of memory ranges in a given process. - pub fn process_get_memory_range_count(process: Process) -> Option; + pub fn process_get_memory_range_count(process: AttachedProcess) -> Option; /// Gets the start address of a memory range by its index. - pub fn process_get_memory_range_address(process: Process, idx: u64) -> Option; + pub fn process_get_memory_range_address( + process: AttachedProcess, + 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; + pub fn process_get_memory_range_size(process: AttachedProcess, 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; + pub fn process_get_memory_range_flags(process: AttachedProcess, idx: u64) -> Option; /// Sets the tick rate of the runtime. This influences the amount of /// times the `update` function is called per second. @@ -525,13 +534,13 @@ extern "C" { } ``` -On top of the runtime's API, there's also WASI support. Considering WASI -itself is still in preview, the API is subject to change. Auto splitters -using WASI may need to be recompiled in the future. Limitations of the WASI -support: +On top of the runtime's API, there's also WASI 0.1 support. Considering WASI +itself is still in preview, the API is subject to change. Auto splitters using +WASI may need to be recompiled in the future. Limitations of the WASI support: -- `stdout` / `stderr` / `stdin` are unbound. Those streams currently do - nothing. +- `stdout` and `stdin` are unbound. Those streams currently do nothing. +- `stderr` is available for logging purposes. It is line buffered. Only + completed lines or flushing it will cause the output to be logged. - The file system is currently almost entirely empty. The host's file system is accessible through `/mnt`. It is entirely read-only. Windows paths are mapped to `/mnt/c`, `/mnt/d`, etc. to match WSL. @@ -539,3 +548,4 @@ support: - There are no command line arguments. - There is no networking. - There is no threading. +- Time and random numbers are available. diff --git a/crates/livesplit-auto-splitting/src/lib.rs b/crates/livesplit-auto-splitting/src/lib.rs index 6e81873a..0ba8c2a5 100644 --- a/crates/livesplit-auto-splitting/src/lib.rs +++ b/crates/livesplit-auto-splitting/src/lib.rs @@ -34,7 +34,7 @@ //! pub struct NonZeroAddress(pub NonZeroU64); //! //! #[repr(transparent)] -//! pub struct Process(NonZeroU64); +//! pub struct AttachedProcess(NonZeroU64); //! //! #[repr(transparent)] //! pub struct ProcessId(u64); @@ -130,12 +130,14 @@ //! pub fn timer_resume_game_time(); //! //! /// Attaches to a process based on its name. The pointer needs to point to -//! /// valid UTF-8 encoded text with the given length. -//! pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option; +//! /// valid UTF-8 encoded text with the given length. If multiple processes +//! /// with the same name are running, the process that most recently started +//! /// is being attached to. +//! 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) -> Option; +//! pub fn process_attach_by_pid(pid: ProcessId) -> Option; //! /// Detaches from a process. -//! pub fn process_detach(process: Process); +//! pub fn process_detach(process: AttachedProcess); //! /// Lists processes based on their name. The name pointer needs to point to //! /// valid UTF-8 encoded text with the given length. Returns `false` if //! /// listing the processes failed. If it was successful, the buffer is now @@ -156,11 +158,11 @@ //! ) -> bool; //! /// Checks whether 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: Process) -> bool; +//! pub fn process_is_open(process: AttachedProcess) -> 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: Process, +//! process: AttachedProcess, //! address: Address, //! buf_ptr: *mut u8, //! buf_len: usize, @@ -168,14 +170,14 @@ //! /// Gets the address of a module in a process. The pointer needs to point to //! /// valid UTF-8 encoded text with the given length. //! pub fn process_get_module_address( -//! process: Process, +//! process: AttachedProcess, //! name_ptr: *const u8, //! name_len: usize, //! ) -> Option; //! /// Gets the size of a module in a process. The pointer needs to point to //! /// valid UTF-8 encoded text with the given length. //! pub fn process_get_module_size( -//! process: Process, +//! process: AttachedProcess, //! name_ptr: *const u8, //! name_len: usize, //! ) -> Option; @@ -190,7 +192,7 @@ //! /// the path or the module does not exist or it failed to get read. The path //! /// is guaranteed to be valid UTF-8 and is not nul-terminated. //! pub fn process_get_module_path( -//! process: Process, +//! process: AttachedProcess, //! name_ptr: *const u8, //! name_len: usize, //! buf_ptr: *mut u8, @@ -205,15 +207,22 @@ //! /// `buf_len_ptr` got set to 0, the path does not exist or failed to get //! /// read. The path is guaranteed to be valid UTF-8 and is not //! /// nul-terminated. -//! pub fn process_get_path(process: Process, buf_ptr: *mut u8, buf_len_ptr: *mut usize) -> bool; +//! pub fn process_get_path( +//! process: AttachedProcess, +//! buf_ptr: *mut u8, +//! buf_len_ptr: *mut usize, +//! ) -> bool; //! /// Gets the number of memory ranges in a given process. -//! pub fn process_get_memory_range_count(process: Process) -> Option; +//! pub fn process_get_memory_range_count(process: AttachedProcess) -> Option; //! /// Gets the start address of a memory range by its index. -//! pub fn process_get_memory_range_address(process: Process, idx: u64) -> Option; +//! pub fn process_get_memory_range_address( +//! process: AttachedProcess, +//! 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; +//! pub fn process_get_memory_range_size(process: AttachedProcess, 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; +//! pub fn process_get_memory_range_flags(process: AttachedProcess, idx: u64) -> Option; //! //! /// Sets the tick rate of the runtime. This influences the amount of //! /// times the `update` function is called per second. @@ -525,13 +534,14 @@ //! } //! ``` //! -//! On top of the runtime's API, there's also WASI support. Considering WASI +//! On top of the runtime's API, there's also WASI 0.1 support. Considering WASI //! itself is still in preview, the API is subject to change. Auto splitters //! using WASI may need to be recompiled in the future. Limitations of the WASI //! support: //! -//! - `stdout` / `stderr` / `stdin` are unbound. Those streams currently do -//! nothing. +//! - `stdout` and `stdin` are unbound. Those streams currently do nothing. +//! - `stderr` is available for logging purposes. It is line buffered. Only +//! completed lines or flushing it will cause the output to be logged. //! - The file system is currently almost entirely empty. The host's file system //! is accessible through `/mnt`. It is entirely read-only. Windows paths are //! mapped to `/mnt/c`, `/mnt/d`, etc. to match WSL. @@ -539,6 +549,7 @@ //! - There are no command line arguments. //! - There is no networking. //! - There is no threading. +//! - Time and random numbers are available. #![warn( clippy::complexity, @@ -564,7 +575,7 @@ pub use runtime::{ Runtime, }; pub use time; -pub use timer::{Timer, TimerState}; +pub use timer::{LogLevel, Timer, TimerState}; const _: () = { const fn assert_send_sync() {} diff --git a/crates/livesplit-auto-splitting/src/runtime/api/process.rs b/crates/livesplit-auto-splitting/src/runtime/api/process.rs index 079d1191..30167c72 100644 --- a/crates/livesplit-auto-splitting/src/runtime/api/process.rs +++ b/crates/livesplit-auto-splitting/src/runtime/api/process.rs @@ -6,6 +6,7 @@ use wasmtime::{Caller, Linker}; use crate::{ runtime::{Context, ProcessKey}, + timer::LogLevel, CreationError, Process, Timer, }; @@ -19,10 +20,13 @@ pub fn bind(linker: &mut Linker>) -> Result<(), CreationErr 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: {}", - p.name().unwrap_or("") - )); + context.timer.log_runtime( + format_args!( + "Attached to a new process: {}", + p.name().unwrap_or("") + ), + LogLevel::Debug, + ); context.processes.insert(p).data().as_ffi() } else { 0 @@ -43,10 +47,13 @@ pub fn bind(linker: &mut Linker>) -> Result<(), CreationErr .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.timer.log_runtime( + format_args!( + "Attached to a new process: {}", + p.name().unwrap_or("") + ), + LogLevel::Debug, + ); context.processes.insert(p).data().as_ffi() } else { 0 @@ -68,7 +75,7 @@ pub fn bind(linker: &mut Linker>) -> Result<(), CreationErr caller .data_mut() .timer - .log(format_args!("Detached from a process.")); + .log_runtime(format_args!("Detached from a process."), LogLevel::Debug); Ok(()) } }) diff --git a/crates/livesplit-auto-splitting/src/runtime/api/runtime.rs b/crates/livesplit-auto-splitting/src/runtime/api/runtime.rs index fdd16b12..f59b9d05 100644 --- a/crates/livesplit-auto-splitting/src/runtime/api/runtime.rs +++ b/crates/livesplit-auto-splitting/src/runtime/api/runtime.rs @@ -6,7 +6,7 @@ use std::{ use anyhow::{ensure, Result}; use wasmtime::{Caller, Linker}; -use crate::{runtime::Context, CreationError, Timer}; +use crate::{runtime::Context, timer::LogLevel, CreationError, Timer}; use super::{get_arr_mut, get_slice_mut, get_str, memory_and_context}; @@ -14,10 +14,10 @@ pub fn bind(linker: &mut Linker>) -> Result<(), CreationErr linker .func_wrap("env", "runtime_set_tick_rate", { |mut caller: Caller<'_, Context>, ticks_per_sec: f64| -> Result<()> { - caller - .data_mut() - .timer - .log(format_args!("New Tick Rate: {ticks_per_sec}")); + caller.data_mut().timer.log_runtime( + format_args!("New Tick Rate: {ticks_per_sec}"), + LogLevel::Debug, + ); ensure!( ticks_per_sec > 0.0, @@ -45,7 +45,7 @@ pub fn bind(linker: &mut Linker>) -> Result<(), CreationErr |mut caller: Caller<'_, Context>, ptr: u32, len: u32| { let (memory, context) = memory_and_context(&mut caller); let message = get_str(memory, ptr, len)?; - context.timer.log(format_args!("{message}")); + context.timer.log_auto_splitter(format_args!("{message}")); Ok(()) } }) diff --git a/crates/livesplit-auto-splitting/src/runtime/api/wasi.rs b/crates/livesplit-auto-splitting/src/runtime/api/wasi.rs index e2d3faf7..3952d3f7 100644 --- a/crates/livesplit-auto-splitting/src/runtime/api/wasi.rs +++ b/crates/livesplit-auto-splitting/src/runtime/api/wasi.rs @@ -1,11 +1,107 @@ -use std::path::Path; +use std::{ + collections::VecDeque, + path::Path, + sync::{ + atomic::{self, AtomicUsize}, + Arc, Mutex, + }, +}; -use wasmtime_wasi::{preview1::WasiP1Ctx, DirPerms, FilePerms, WasiCtxBuilder}; +use bstr::ByteSlice; +use wasmtime_wasi::{ + preview1::WasiP1Ctx, DirPerms, FilePerms, HostOutputStream, StdoutStream, StreamError, + Subscribe, WasiCtxBuilder, +}; -use crate::wasi_path; +use crate::{wasi_path, Timer}; -pub fn build(script_path: Option<&Path>) -> WasiP1Ctx { +const ERR_CAPACITY: usize = 1 << 20; + +#[derive(Clone)] +pub struct StdErr { + buffer: Arc, +} + +struct Buf { + flush_idx: AtomicUsize, + buf: Mutex>, +} + +impl StdoutStream for StdErr { + fn stream(&self) -> Box { + Box::new(self.clone()) + } + + fn isatty(&self) -> bool { + false + } +} + +impl StdErr { + pub fn new() -> Self { + StdErr { + buffer: Arc::new(Buf { + flush_idx: AtomicUsize::new(0), + buf: Mutex::new(VecDeque::new()), + }), + } + } + + pub fn print_lines(&self, timer: &mut T) { + let flush_idx = self.buffer.flush_idx.swap(0, atomic::Ordering::Relaxed); + if flush_idx == 0 { + return; + } + let buf = &mut *self.buffer.buf.lock().unwrap(); + let (first, _) = buf.as_slices(); + let to_print = match first.get(..flush_idx) { + Some(to_print) => to_print, + None => &buf.make_contiguous()[..flush_idx], + }; + timer.log_auto_splitter(format_args!("{}", to_print.trim().as_bstr())); + buf.drain(..flush_idx); + } +} + +impl HostOutputStream for StdErr { + fn write(&mut self, bytes: bytes::Bytes) -> Result<(), StreamError> { + let buffer = &mut *self.buffer.buf.lock().unwrap(); + if bytes.len() > ERR_CAPACITY - buffer.len() { + return Err(StreamError::Trap(anyhow::format_err!( + "write beyond capacity of StdErr" + ))); + } + + self.buffer.flush_idx.store( + buffer.len() + bytes.iter().rposition(|&b| b == b'\n').unwrap_or_default(), + atomic::Ordering::Relaxed, + ); + + buffer.extend(bytes.as_ref()); + Ok(()) + } + + fn flush(&mut self) -> Result<(), StreamError> { + let len = self.buffer.buf.lock().unwrap().len(); + self.buffer.flush_idx.store(len, atomic::Ordering::Relaxed); + Ok(()) + } + + fn check_write(&mut self) -> Result { + let consumed = self.buffer.buf.lock().unwrap().len(); + Ok(ERR_CAPACITY.saturating_sub(consumed)) + } +} + +#[async_trait::async_trait] +impl Subscribe for StdErr { + async fn ready(&mut self) {} +} + +pub fn build(script_path: Option<&Path>) -> (WasiP1Ctx, StdErr) { let mut wasi = WasiCtxBuilder::new(); + let stderr = StdErr::new(); + wasi.stderr(stderr.clone()); if let Some(script_path) = script_path { if let Some(path) = wasi_path::from_native(script_path) { @@ -44,5 +140,5 @@ pub fn build(script_path: Option<&Path>) -> WasiP1Ctx { // Unfortunate if this fails, but we should still continue. let _ = wasi.preopened_dir("/", "/mnt", DirPerms::READ, FilePerms::READ); } - wasi.build_p1() + (wasi.build_p1(), stderr) } diff --git a/crates/livesplit-auto-splitting/src/runtime/mod.rs b/crates/livesplit-auto-splitting/src/runtime/mod.rs index 15dd4e8e..05325cf8 100644 --- a/crates/livesplit-auto-splitting/src/runtime/mod.rs +++ b/crates/livesplit-auto-splitting/src/runtime/mod.rs @@ -1,8 +1,13 @@ #![allow(clippy::unnecessary_cast)] -use crate::{process::Process, settings, timer::Timer}; +use crate::{ + process::Process, + settings, + timer::{LogLevel, Timer}, +}; use anyhow::Result; +use api::wasi::StdErr; use arc_swap::ArcSwap; use indexmap::IndexMap; use slotmap::SlotMap; @@ -90,6 +95,7 @@ pub struct Context { memory: Option, process_list: ProcessList, wasi: WasiP1Ctx, + stderr: StdErr, } /// A thread-safe handle used to interrupt the execution of the script. @@ -251,17 +257,19 @@ impl ExecutionGuard<'_, T> { if data.trapped { return Ok(()); } - match data.update.call(&mut data.store, ()) { - Ok(()) => { - self.settings_widgets - .store(data.store.data().settings_widgets.clone()); - Ok(()) - } - Err(e) => { - data.trapped = true; - Err(e) - } + let result = data.update.call(&mut data.store, ()); + + if result.is_ok() { + self.settings_widgets + .store(data.store.data().settings_widgets.clone()); + } else { + data.trapped = true; } + + let data = data.store.data_mut(); + data.stderr.print_lines(&mut data.timer); + + result } /// Accesses the memory of the WebAssembly module. This may be useful for @@ -373,6 +381,8 @@ impl CompiledAutoSplitter { tick_rate: AtomicU64::new(f64::to_bits(1.0 / 120.0)), }); + let (wasi, stderr) = api::wasi::build(interpreter_script_path); + let mut store = Store::new( engine, Context { @@ -385,7 +395,8 @@ impl CompiledAutoSplitter { timer, memory: None, process_list: ProcessList::new(), - wasi: api::wasi::build(interpreter_script_path), + wasi, + stderr, }, ); @@ -417,7 +428,10 @@ impl CompiledAutoSplitter { || self.module.get_export("_initialize").is_some() || self.module.get_export("_start").is_some() { - store.data_mut().timer.log(format_args!("This auto splitter uses WASI. The API is subject to change, because WASI is still in preview. Auto splitters using WASI may need to be recompiled in the future.")); + store.data_mut().timer.log_runtime( + format_args!("This auto splitter uses WASI. The API is subject to change, because WASI is still in preview. Auto splitters using WASI may need to be recompiled in the future."), + LogLevel::Warning, + ); // These may be different in future WASI versions. if let Ok(func) = instance.get_typed_func::<(), ()>(&mut store, "_initialize") { diff --git a/crates/livesplit-auto-splitting/src/timer.rs b/crates/livesplit-auto-splitting/src/timer.rs index 96258b7b..a0cf5217 100644 --- a/crates/livesplit-auto-splitting/src/timer.rs +++ b/crates/livesplit-auto-splitting/src/timer.rs @@ -16,6 +16,21 @@ pub enum TimerState { Ended = 3, } +/// The level of criticalness of a log message. +pub enum LogLevel { + /// A trace message. This is the least critical and most verbose message. + Trace, + /// A debug message. This is a message that is useful for debugging. + Debug, + /// An info message. This is a message that provides information. + Info, + /// A warning message. This is a message that warns about something that + /// may be problematic. + Warning, + /// An error message. This is a message that indicates an error. + Error, +} + /// A timer that can be controlled by an auto splitter. pub trait Timer: Send { /// Returns the current state of the timer. @@ -41,7 +56,8 @@ pub trait Timer: Send { /// Sets a custom key value pair. This may be arbitrary information that the /// auto splitter wants to provide for visualization. fn set_variable(&mut self, key: &str, value: &str); - /// Logs a message either from the auto splitter directly or from the - /// runtime. - fn log(&mut self, message: fmt::Arguments<'_>); + /// Logs a message from the auto splitter. + fn log_auto_splitter(&mut self, message: fmt::Arguments<'_>); + /// Logs a message from the runtime. + fn log_runtime(&mut self, message: fmt::Arguments<'_>, log_level: LogLevel); } diff --git a/crates/livesplit-auto-splitting/tests/sandboxing.rs b/crates/livesplit-auto-splitting/tests/sandboxing.rs index 2ce5bf5e..de4b9d8b 100644 --- a/crates/livesplit-auto-splitting/tests/sandboxing.rs +++ b/crates/livesplit-auto-splitting/tests/sandboxing.rs @@ -1,4 +1,4 @@ -use livesplit_auto_splitting::{AutoSplitter, Config, Runtime, Timer, TimerState}; +use livesplit_auto_splitting::{AutoSplitter, Config, LogLevel, Runtime, Timer, TimerState}; use std::{ ffi::OsStr, fmt, fs, @@ -23,7 +23,8 @@ impl Timer for DummyTimer { fn pause_game_time(&mut self) {} fn resume_game_time(&mut self) {} fn set_variable(&mut self, _key: &str, _value: &str) {} - fn log(&mut self, _message: fmt::Arguments<'_>) {} + fn log_auto_splitter(&mut self, _message: fmt::Arguments<'_>) {} + fn log_runtime(&mut self, _message: fmt::Arguments<'_>, _log_level: LogLevel) {} } #[track_caller] diff --git a/src/auto_splitting/mod.rs b/src/auto_splitting/mod.rs index e73770ce..49f23440 100644 --- a/src/auto_splitting/mod.rs +++ b/src/auto_splitting/mod.rs @@ -34,7 +34,7 @@ //! pub struct NonZeroAddress(pub NonZeroU64); //! //! #[repr(transparent)] -//! pub struct Process(NonZeroU64); +//! pub struct AttachedProcess(NonZeroU64); //! //! #[repr(transparent)] //! pub struct ProcessId(u64); @@ -130,12 +130,14 @@ //! pub fn timer_resume_game_time(); //! //! /// Attaches to a process based on its name. The pointer needs to point to -//! /// valid UTF-8 encoded text with the given length. -//! pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option; +//! /// valid UTF-8 encoded text with the given length. If multiple processes +//! /// with the same name are running, the process that most recently started +//! /// is being attached to. +//! 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) -> Option; +//! pub fn process_attach_by_pid(pid: ProcessId) -> Option; //! /// Detaches from a process. -//! pub fn process_detach(process: Process); +//! pub fn process_detach(process: AttachedProcess); //! /// Lists processes based on their name. The name pointer needs to point to //! /// valid UTF-8 encoded text with the given length. Returns `false` if //! /// listing the processes failed. If it was successful, the buffer is now @@ -154,13 +156,13 @@ //! list_ptr: *mut ProcessId, //! list_len_ptr: *mut usize, //! ) -> bool; -//! /// Checks whether is a process is still open. You should detach from a +//! /// Checks whether 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: Process) -> bool; +//! pub fn process_is_open(process: AttachedProcess) -> 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: Process, +//! process: AttachedProcess, //! address: Address, //! buf_ptr: *mut u8, //! buf_len: usize, @@ -168,14 +170,14 @@ //! /// Gets the address of a module in a process. The pointer needs to point to //! /// valid UTF-8 encoded text with the given length. //! pub fn process_get_module_address( -//! process: Process, +//! process: AttachedProcess, //! name_ptr: *const u8, //! name_len: usize, //! ) -> Option; //! /// Gets the size of a module in a process. The pointer needs to point to //! /// valid UTF-8 encoded text with the given length. //! pub fn process_get_module_size( -//! process: Process, +//! process: AttachedProcess, //! name_ptr: *const u8, //! name_len: usize, //! ) -> Option; @@ -190,7 +192,7 @@ //! /// the path or the module does not exist or it failed to get read. The path //! /// is guaranteed to be valid UTF-8 and is not nul-terminated. //! pub fn process_get_module_path( -//! process: Process, +//! process: AttachedProcess, //! name_ptr: *const u8, //! name_len: usize, //! buf_ptr: *mut u8, @@ -205,15 +207,22 @@ //! /// `buf_len_ptr` got set to 0, the path does not exist or failed to get //! /// read. The path is guaranteed to be valid UTF-8 and is not //! /// nul-terminated. -//! pub fn process_get_path(process: Process, buf_ptr: *mut u8, buf_len_ptr: *mut usize) -> bool; +//! pub fn process_get_path( +//! process: AttachedProcess, +//! buf_ptr: *mut u8, +//! buf_len_ptr: *mut usize, +//! ) -> bool; //! /// Gets the number of memory ranges in a given process. -//! pub fn process_get_memory_range_count(process: Process) -> Option; +//! pub fn process_get_memory_range_count(process: AttachedProcess) -> Option; //! /// Gets the start address of a memory range by its index. -//! pub fn process_get_memory_range_address(process: Process, idx: u64) -> Option; +//! pub fn process_get_memory_range_address( +//! process: AttachedProcess, +//! 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; +//! pub fn process_get_memory_range_size(process: AttachedProcess, 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; +//! pub fn process_get_memory_range_flags(process: AttachedProcess, idx: u64) -> Option; //! //! /// Sets the tick rate of the runtime. This influences the amount of //! /// times the `update` function is called per second. @@ -525,13 +534,14 @@ //! } //! ``` //! -//! On top of the runtime's API, there's also WASI support. Considering WASI +//! On top of the runtime's API, there's also WASI 0.1 support. Considering WASI //! itself is still in preview, the API is subject to change. Auto splitters //! using WASI may need to be recompiled in the future. Limitations of the WASI //! support: //! -//! - `stdout` / `stderr` / `stdin` are unbound. Those streams currently do -//! nothing. +//! - `stdout` and `stdin` are unbound. Those streams currently do nothing. +//! - `stderr` is available for logging purposes. It is line buffered. Only +//! completed lines or flushing it will cause the output to be logged. //! - The file system is currently almost entirely empty. The host's file system //! is accessible through `/mnt`. It is entirely read-only. Windows paths are //! mapped to `/mnt/c`, `/mnt/d`, etc. to match WSL. @@ -539,6 +549,7 @@ //! - There are no command line arguments. //! - There is no networking. //! - There is no threading. +//! - Time and random numbers are available. use crate::{ event::{self, TimerQuery}, @@ -547,7 +558,8 @@ use crate::{ }; pub use livesplit_auto_splitting::{settings, wasi_path}; use livesplit_auto_splitting::{ - AutoSplitter, Config, CreationError, InterruptHandle, Timer as AutoSplitTimer, TimerState, + AutoSplitter, Config, CreationError, InterruptHandle, LogLevel, Timer as AutoSplitTimer, + TimerState, }; use snafu::Snafu; use std::{fmt, fs, io, path::PathBuf, thread, time::Duration}; @@ -764,9 +776,20 @@ impl AutoSplitTimer for Timer { drop(self.0.set_custom_variable(name.into(), value.into())); } - fn log(&mut self, message: fmt::Arguments<'_>) { + fn log_auto_splitter(&mut self, message: fmt::Arguments<'_>) { log::info!(target: "Auto Splitter", "{message}"); } + + fn log_runtime(&mut self, message: fmt::Arguments<'_>, log_level: LogLevel) { + let level = match log_level { + LogLevel::Trace => log::Level::Trace, + LogLevel::Debug => log::Level::Debug, + LogLevel::Info => log::Level::Info, + LogLevel::Warning => log::Level::Warn, + LogLevel::Error => log::Level::Error, + }; + log::log!(target: "Auto Splitter", level, "{message}"); + } } async fn run(