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

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 Futures 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 and Deser deriving for the FairMutex 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)