Interact
Interact is a Rust framework for friendly online introspection of the running program state in an intuitive command-line interactive way.
Interact is useful for server programs that otherwise receive no input. You can use Interact to make your server receive commands using the special prompt from the interact_prompt
crate. The commands can be used to browse your server's internal state, modify it, and call method functions that were specified in interact
derive attributes.
Reference
-
Crate docs of latest release:
-
Crate docs of master (possibly unreleased) version:
Design
Using two traits, Access
and Deser
, Interact exposes types as trait objects, similarly to the Any
trait, but with a functionality of reflection. The Access
trait allows to probe the fields of structs and enums and modify them. It also allows to iterate arrays, vectors, and maps. It also allows to safely punch through Rc
, Arc
, and Mutex
. The traits can be derived using #[derive(Interact)]
.
At the prompt side, predictive parsing is used for providing full auto-complete and hinting, while constructing access paths, and while constructing values used in field assignments and function calls.
Examples
In the section Interact is demoed using modifications of existing Rust programs.
Actix
To get a taste of Interact as applied to actual servers, you can try the Interact-enabled Actix chat demo (originally from here).
While the state of an Actix program is spread across a stack of Future
s that may exist in multiple process thread, Interact has no difficulty in traversing it and presenting a whole picture of the server.
Summary of changes
To enable this example, there were two changes:
- Changes in Actix core (Github link), that enable Interact for the
Addr<T>
Actor messaging proxy. - Changes to Actix chat app (Github link), which add
#[derive(Interact)]
for its types, and invocation of the Interact prompt.
Demo
git clone https://github.com/interact-rs/actix
cd actix/examples/chat
cargo run --bin server
Executing the server presents a prompt in a dedicated Interact thread, while the server functionality runs in the process's background:
Running chat server on 127.0.0.1:12345
Rust `interact`, type '?' for more information
>>>
You can examine the server state:
>>> server
ChatServer { sessions: HashMap {}, rooms: HashMap { "Main": HashSet {} } }
In parallel, run two clients using cargo run --bin client
, and re-examine the server's state:
>>> server
[#1] ChatServer {
sessions: HashMap {
8426954607288880898: ChatSession { id: 8426954607288880898,
addr: [#1], hb: 374.307146ms, room: "Main" },
9536033526192464616: ChatSession { id: 9536033526192464616,
addr: [#1], hb: 513.580812ms, room: "Main" }
},
rooms: HashMap { "Main": HashSet { 8426954607288880898, 9536033526192464616 } }
}
The reason for #[1]
is the loop that is detected by traversal of ChatSession's addr
, which loops back into ChatServer
.
You can use Interact to print only field of rooms:
>>> server.rooms
HashMap { "Main": HashSet { 8426954607288880898, 9536033526192464616 } }
Or access one of the sessions:
>>> server.sessions[8426954607288880898]
[#1] ChatSession {
id: 8426954607288880898,
addr: [#2] ChatServer {
sessions: HashMap {
8426954607288880898: [#1],
9536033526192464616: ChatSession { id: 9536033526192464616,
addr: [#2], hb: 759.986694ms, room: "Main" }
},
rooms: HashMap { "Main": HashSet { 8426954607288880898, 9536033526192464616 } }
},
hb: 632.849822ms,
room: "Main"
}
See the 'hb' field get updated:
>>> server.sessions[8426954607288880898].hb
716.16972ms
>>> server.sessions[8426954607288880898].hb
10.398845ms
Modify the room's name:
>>> server.sessions[8426954607288880898].room = "Boo"
See that it was indeed modified:
>>> server.sessions[8426954607288880898]
[#1] ChatSession {
id: 8426954607288880898,
addr: [#2] ChatServer {
sessions: HashMap {
8426954607288880898: [#1],
9536033526192464616: ChatSession { id: 9536033526192464616,
addr: [#2], hb: 219.435076ms, room: "Main" }
},
rooms: HashMap { "Main": HashSet { 8426954607288880898, 9536033526192464616 } }
},
hb: 112.667608ms,
room: "Boo"
}
Alacritty
By enabling Interact for a program such as Alacritty, we can probe and modify its state while it runs (for example, modify the cursor's position from the Interact prompt).
Summary of changes
The changes in Alacritty do the following:
- Add an invocation of the Interact prompt.
- Add
#[derive(Interact)]
for a small portion of the types. - Add special
Access
andDeser
deriving for theFairMutex
type.
Demo
Here is the interactive state it produces:
>>> term
Term {
grid: Grid {
cols: Column (80),
lines: Line (24),
display_offset: 0,
scroll_limit: 0,
max_scroll_limit: 100000
},
input_needs_wrap: false,
next_title: None,
next_mouse_cursor: None,
alt_grid: Grid {
cols: Column (80),
lines: Line (24),
display_offset: 0,
scroll_limit: 0,
max_scroll_limit: 0
},
alt: false,
cursor: Cursor {
point: Point { line: Line (5), col: Column (45) },
template: Cell { c: ' ' },
charsets: Charsets ([ Ascii, Ascii, Ascii, Ascii ])
},
dirty: false,
next_is_urgent: None,
cursor_save: Cursor {
point: Point { line: Line (0), col: Column (0) },
template: Cell { c: ' ' },
charsets: Charsets ([ Ascii, Ascii, Ascii, Ascii ])
},
cursor_save_alt: Cursor {
point: Point { line: Line (0), col: Column (0) },
template: Cell { c: ' ' },
charsets: Charsets ([ Ascii, Ascii, Ascii, Ascii ])
},
semantic_escape_chars: ",│`|:\"\' ()[]{}<>",
dynamic_title: true,
tabspaces: 8,
auto_scroll: false
}
Using derive
Cargo.toml
The Interact dependency is needed:
[dependencies]
interact = "0.3"
Source
- The a crate's top level,
extern crate interact
is needed. - At places
#[derive(Interact)]
is needed, import the needed proc macro:use interact::Interact;
An example:
# #![allow(unused_variables)] #fn main() { extern crate interact; use interact::Interact; #[derive(Interact)] struct Point { x: i32, y: i32, } #}
Attributes
Container attributes
Following #[derive(Interact)]
, methods to be called from the prompt can be
specified by name, along with their parameters, and whether they take in &self
or &mut self
.
#[interact(mut_fn(function_name(param_a, param_b)))
#[interact(immut_fn(function_name(param_a, param_b)))
For example:
# #![allow(unused_variables)] #fn main() { #[derive(Interact)] #[interact(mut_fn(add(param_a)))] struct Baz(u32); impl Baz { fn add(&mut self, param_a: u32) { self.0 += param_a; } } #}
Field attributes
The skip
attribute allows to make some fields invisible:
#[interact(skip))
The downside is that having any skipped field on a type means that it is
unbuildable, and therefore cannot be passed as value to functions or to be
assigned using =
in an expression.
Registering data
The interact_prompt
crate provides helpers for registering data to be examined. Currently, data must be owned, so if it is shared by threads running in the background in parallel to the prompt, it needs to be provided using Arc<_>
. If the data is expeced to be shared and mutable by the Interact prompt, then it should be wrapped in Arc<Mutex<_>>
.
For example:
use interact_prompt::{SendRegistry};
fn register_global_mutable_data(global_state: Arc<Mutex<MyData>>) {
SendRegistry::insert("global", Box::new(global_state));
}
fn register_global_readable_data(readonly: Arc<MyData>) {
SendRegistry::insert("readonly", Box::new(readonly));
}
Using the prompt
The Interact prompt lets the user probe the registered data, and possibly to modify it to some degree.
This section provides various examples for what is possible at the prompt.
Accessing data
Whole state
Suppose a value of the following simple state is registered:
# #![allow(unused_variables)] #fn main() { #[derive(Interact)] struct Point { x: u32, y: u32, } #}
The whole of it can be printed, and the result is similar to a pretty printed Debug
:
>>> state
Point { x: 3, y: 4 }
Tuple structs are accessed similarly using Rust's .0
, .1
, etc.
Field access
The syntax for field access is similar to Rust's. For example, accessing one of the fields of the previous example:
>>> state.x
3
Enum access
# #![allow(unused_variables)] #fn main() { struct OptPoint { x: Option<u32>, y: Option<u32>, } #}
Suppose that we have an instance of this struct with the following value:
OptPoint { x: None, y: Some(3) }
Unlike in Rust, we can have a full path to the variant's value through variant's name:
>>> state.y.Some
(3)
>>> state.y.Some.0
3
>>> state.x
None
>>> basic.x.None
None
Vec, HashMap, and BTreeMap access
Accessing vectors and maps are done like you'd expected via []
. Currently, ranges are not supported in vectors and sorted maps.
Access via Mutex
, Rc
, Arc
, RefCell
, Box
Interact elides complexity to access paths when wrapper types are used. For Mutex, it uses .try_lock()
behind the scenes. For RefCell
it uses try_borrow()
.
Modifying data
Types that expose a mutable interface, for example via Arc<Mutex<_>>
, can have their fields be assigned and modified from the Interact prompt.
Interact knows the basic types, and is also able to construct values of derived types for which the #[interact(skip)]
attribute was not used for any field.
Assignments
Assignments are done using =
at the prompt.
For example, check cargo run --example large-example
:
>>> complex.tuple
((690498389, VarUnit, (193, 38)), 1262478744)
>>> complex.tuple.0.2
(193, 38)
>>> complex.tuple.0.2 = (1, 1)
>>> complex.tuple
((690498389, VarUnit, (1, 1)), 1262478744)
>>> complex.tuple.0.1 = VarNamed { a: 3, b: 10}
>>> complex.tuple
((690498389, VarNamed { a: 3, b: 10 }, (1, 1)), 1262478744)
Wrapper types
The wrapper types Rc
, RefCell
, Mutex
, Box
are transparent to construction of values, and need not be specified.
>>> complex.boxed = VarNamed { a: 3, b: 10}
>>> complex.boxed
VarNamed { a: 3, b: 10 }
Calling methods
By specifying Interact's special mut_fn
or immut_fn
container attributes, you can add methods that would be reachable from the Interact prompt, upon reaching values that match the types on which the special methods are defined.
For example, given the following type:
#[derive(Interact)]
#[interact(mut_fn(add(param_a)))]
struct Baz(u32);
impl Baz {
fn add(&mut self, param_a: u32) {
self.0 += param_a;
}
}
We can call the add
methods:
>>> state.baz_val
Baz (1)
>>> state.baz_val.add(3)
>>> state.baz_val
Baz (4)