Skip to content

Commit

Permalink
Add stderr Support for Auto Splitters (#835)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
CryZe authored Aug 21, 2024
1 parent a996a16 commit 53fc5f4
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 102 deletions.
8 changes: 5 additions & 3 deletions crates/livesplit-auto-splitting/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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",
] }

Expand Down
52 changes: 31 additions & 21 deletions crates/livesplit-auto-splitting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<Process>;
/// 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<AttachedProcess>;
/// Attaches to a process based on its process id.
pub fn process_attach_by_pid(pid: ProcessId) -> Option<Process>;
pub fn process_attach_by_pid(pid: ProcessId) -> Option<AttachedProcess>;
/// 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
Expand All @@ -156,26 +158,26 @@ 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,
) -> bool;
/// 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<NonZeroAddress>;
/// 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<NonZeroU64>;
Expand All @@ -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,
Expand All @@ -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<NonZeroU64>;
pub fn process_get_memory_range_count(process: AttachedProcess) -> Option<NonZeroU64>;
/// Gets the start address of a memory range by its index.
pub fn process_get_memory_range_address(process: Process, idx: u64) -> Option<NonZeroAddress>;
pub fn process_get_memory_range_address(
process: AttachedProcess,
idx: u64,
) -> Option<NonZeroAddress>;
/// Gets the size of a memory range by its index.
pub fn process_get_memory_range_size(process: Process, idx: u64) -> Option<NonZeroU64>;
pub fn process_get_memory_range_size(process: AttachedProcess, idx: u64) -> Option<NonZeroU64>;
/// Gets the flags of a memory range by its index.
pub fn process_get_memory_range_flags(process: Process, idx: u64) -> Option<NonZeroU64>;
pub fn process_get_memory_range_flags(process: AttachedProcess, idx: u64) -> Option<MemoryRangeFlags>;

/// Sets the tick rate of the runtime. This influences the amount of
/// times the `update` function is called per second.
Expand Down Expand Up @@ -525,17 +534,18 @@ 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.
- There are no environment variables.
- There are no command line arguments.
- There is no networking.
- There is no threading.
- Time and random numbers are available.
49 changes: 30 additions & 19 deletions crates/livesplit-auto-splitting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<Process>;
//! /// 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<AttachedProcess>;
//! /// Attaches to a process based on its process id.
//! pub fn process_attach_by_pid(pid: ProcessId) -> Option<Process>;
//! pub fn process_attach_by_pid(pid: ProcessId) -> Option<AttachedProcess>;
//! /// 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
Expand All @@ -156,26 +158,26 @@
//! ) -> 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,
//! ) -> bool;
//! /// 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<NonZeroAddress>;
//! /// 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<NonZeroU64>;
Expand All @@ -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,
Expand All @@ -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<NonZeroU64>;
//! pub fn process_get_memory_range_count(process: AttachedProcess) -> Option<NonZeroU64>;
//! /// Gets the start address of a memory range by its index.
//! pub fn process_get_memory_range_address(process: Process, idx: u64) -> Option<NonZeroAddress>;
//! pub fn process_get_memory_range_address(
//! process: AttachedProcess,
//! idx: u64,
//! ) -> Option<NonZeroAddress>;
//! /// Gets the size of a memory range by its index.
//! pub fn process_get_memory_range_size(process: Process, idx: u64) -> Option<NonZeroU64>;
//! pub fn process_get_memory_range_size(process: AttachedProcess, idx: u64) -> Option<NonZeroU64>;
//! /// Gets the flags of a memory range by its index.
//! pub fn process_get_memory_range_flags(process: Process, idx: u64) -> Option<NonZeroU64>;
//! pub fn process_get_memory_range_flags(process: AttachedProcess, idx: u64) -> Option<MemoryRangeFlags>;
//!
//! /// Sets the tick rate of the runtime. This influences the amount of
//! /// times the `update` function is called per second.
Expand Down Expand Up @@ -525,20 +534,22 @@
//! }
//! ```
//!
//! 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.
//! - There are no environment variables.
//! - There are no command line arguments.
//! - There is no networking.
//! - There is no threading.
//! - Time and random numbers are available.
#![warn(
clippy::complexity,
Expand All @@ -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<T: Send + Sync>() {}
Expand Down
25 changes: 16 additions & 9 deletions crates/livesplit-auto-splitting/src/runtime/api/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use wasmtime::{Caller, Linker};

use crate::{
runtime::{Context, ProcessKey},
timer::LogLevel,
CreationError, Process, Timer,
};

Expand All @@ -19,10 +20,13 @@ pub fn bind<T: Timer>(linker: &mut Linker<Context<T>>) -> 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("<Unnamed Process>")
));
context.timer.log_runtime(
format_args!(
"Attached to a new process: {}",
p.name().unwrap_or("<Unnamed Process>")
),
LogLevel::Debug,
);
context.processes.insert(p).data().as_ffi()
} else {
0
Expand All @@ -43,10 +47,13 @@ pub fn bind<T: Timer>(linker: &mut Linker<Context<T>>) -> 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("<Unnamed Process>")
));
context.timer.log_runtime(
format_args!(
"Attached to a new process: {}",
p.name().unwrap_or("<Unnamed Process>")
),
LogLevel::Debug,
);
context.processes.insert(p).data().as_ffi()
} else {
0
Expand All @@ -68,7 +75,7 @@ pub fn bind<T: Timer>(linker: &mut Linker<Context<T>>) -> Result<(), CreationErr
caller
.data_mut()
.timer
.log(format_args!("Detached from a process."));
.log_runtime(format_args!("Detached from a process."), LogLevel::Debug);
Ok(())
}
})
Expand Down
12 changes: 6 additions & 6 deletions crates/livesplit-auto-splitting/src/runtime/api/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ 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};

pub fn bind<T: Timer>(linker: &mut Linker<Context<T>>) -> Result<(), CreationError> {
linker
.func_wrap("env", "runtime_set_tick_rate", {
|mut caller: Caller<'_, Context<T>>, 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,
Expand Down Expand Up @@ -45,7 +45,7 @@ pub fn bind<T: Timer>(linker: &mut Linker<Context<T>>) -> Result<(), CreationErr
|mut caller: Caller<'_, Context<T>>, 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(())
}
})
Expand Down
Loading

0 comments on commit 53fc5f4

Please sign in to comment.