276 lines
10 KiB
Rust
276 lines
10 KiB
Rust
|
|
use std::collections::BTreeMap;
|
||
|
|
|
||
|
|
use runa_protocol::{ActorSnapshotDto, StackFrame, Value};
|
||
|
|
use serde_json::{Value as Json, json};
|
||
|
|
|
||
|
|
#[derive(Default)]
|
||
|
|
pub(crate) struct Variables {
|
||
|
|
next: u64,
|
||
|
|
values: BTreeMap<u64, (Value, String)>,
|
||
|
|
references: BTreeMap<String, u64>,
|
||
|
|
root: Option<u64>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Variables {
|
||
|
|
pub fn clear(&mut self) {
|
||
|
|
self.values.clear();
|
||
|
|
self.references.clear();
|
||
|
|
self.root = None;
|
||
|
|
}
|
||
|
|
|
||
|
|
fn reference(&mut self, value: Value, path: String) -> u64 {
|
||
|
|
if !path.is_empty() {
|
||
|
|
if let Some(reference) = self.references.get(&path) {
|
||
|
|
return *reference;
|
||
|
|
}
|
||
|
|
self.references.insert(path.clone(), self.next + 1);
|
||
|
|
}
|
||
|
|
self.next += 1;
|
||
|
|
self.values.insert(self.next, (value, path));
|
||
|
|
self.next
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn capture(&mut self, actor: &ActorSnapshotDto) {
|
||
|
|
self.clear();
|
||
|
|
let mut metadata = plain(json!({
|
||
|
|
"mailbox_id": actor.mailbox_id,
|
||
|
|
"implementation_id": actor.implementation_id,
|
||
|
|
"incarnation": actor.incarnation,
|
||
|
|
"behavior_path": actor.behavior_path,
|
||
|
|
"status": serde_json::to_value(&actor.status).unwrap()["status"],
|
||
|
|
}));
|
||
|
|
if let Value::Record(entries) = &mut metadata {
|
||
|
|
entries.insert(
|
||
|
|
"actor_ref".to_owned(),
|
||
|
|
runa_core::ActorRef::parse(&actor.actor_ref)
|
||
|
|
.map(Value::ActorRef)
|
||
|
|
.unwrap_or_else(|| Value::String(actor.actor_ref.clone())),
|
||
|
|
);
|
||
|
|
if let runa_protocol::ActorStatusDto::Failed(message) = &actor.status {
|
||
|
|
entries.insert("failure".to_owned(), Value::String(message.clone()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let value = record([
|
||
|
|
("committed_state", actor.state.clone()),
|
||
|
|
(
|
||
|
|
"mailbox",
|
||
|
|
record([
|
||
|
|
(
|
||
|
|
"reserved",
|
||
|
|
Value::List(actor.reserved.clone().into_iter().collect()),
|
||
|
|
),
|
||
|
|
("queued", Value::List(actor.queued.clone())),
|
||
|
|
]),
|
||
|
|
),
|
||
|
|
("config", plain(json!({"log_level": actor.log_level}))),
|
||
|
|
("metadata", metadata),
|
||
|
|
]);
|
||
|
|
self.root = Some(self.reference(value, "actor".to_owned()));
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn scope(&self) -> Result<Json, String> {
|
||
|
|
let reference = self
|
||
|
|
.root
|
||
|
|
.ok_or("Actor snapshot unavailable while running")?;
|
||
|
|
Ok(json!({
|
||
|
|
"name": "Actor snapshot (committed; not frame locals)",
|
||
|
|
"variablesReference": reference, "expensive": false, "namedVariables": 4
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn frame_scope(&mut self, frame: &StackFrame) -> Json {
|
||
|
|
let value = plain(serde_json::to_value(frame).unwrap());
|
||
|
|
let reference = self.reference(value, String::new());
|
||
|
|
json!({"name": "Frame metadata (not locals)", "variablesReference": reference, "expensive": false})
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn variables(&mut self, args: &Json) -> Result<Json, String> {
|
||
|
|
let reference = super::integer(args, "variablesReference", None)?;
|
||
|
|
let (value, path) = self
|
||
|
|
.values
|
||
|
|
.get(&reference)
|
||
|
|
.cloned()
|
||
|
|
.ok_or("Stale or unknown variables reference")?;
|
||
|
|
let indexed = matches!(value, Value::List(_));
|
||
|
|
match args.get("filter") {
|
||
|
|
Some(Json::String(filter))
|
||
|
|
if (filter == "named" && indexed) || (filter == "indexed" && !indexed) =>
|
||
|
|
{
|
||
|
|
return Ok(json!({"variables": []}));
|
||
|
|
}
|
||
|
|
None => {}
|
||
|
|
Some(Json::String(filter)) if matches!(filter.as_str(), "named" | "indexed") => {}
|
||
|
|
_ => return Err("Invalid variables filter".to_owned()),
|
||
|
|
}
|
||
|
|
let entries: Vec<_> = match value {
|
||
|
|
Value::Record(entries) => entries.into_iter().collect(),
|
||
|
|
Value::List(entries) => entries
|
||
|
|
.into_iter()
|
||
|
|
.enumerate()
|
||
|
|
.map(|(index, value)| (index.to_string(), value))
|
||
|
|
.collect(),
|
||
|
|
_ => return Err("Variable has no children".to_owned()),
|
||
|
|
};
|
||
|
|
let start = super::integer(args, "start", Some(0))? as usize;
|
||
|
|
let count = super::integer(args, "count", Some(0))? as usize;
|
||
|
|
let variables: Vec<_> = entries
|
||
|
|
.into_iter()
|
||
|
|
.skip(start)
|
||
|
|
.take(if count == 0 { usize::MAX } else { count })
|
||
|
|
.map(|(name, value)| {
|
||
|
|
let child_path = if path.is_empty() {
|
||
|
|
String::new()
|
||
|
|
} else {
|
||
|
|
format!("{path}/{}", name.replace('~', "~0").replace('/', "~1"))
|
||
|
|
};
|
||
|
|
self.variable(&name, value, child_path)
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
Ok(json!({"variables": variables}))
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn evaluate(&mut self, args: &Json) -> Result<Json, String> {
|
||
|
|
if let Some(context) = args.get("context")
|
||
|
|
&& !matches!(context.as_str(), Some("watch" | "repl" | "variables"))
|
||
|
|
{
|
||
|
|
return Err(
|
||
|
|
"Snapshot paths are supported only in Watch, Debug Console and Variables"
|
||
|
|
.to_owned(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
let expression = args["expression"]
|
||
|
|
.as_str()
|
||
|
|
.ok_or("Expected a snapshot path")?;
|
||
|
|
let mut tokens = expression.split('/');
|
||
|
|
if expression.len() > 4096 || tokens.next() != Some("actor") || tokens.clone().count() > 64
|
||
|
|
{
|
||
|
|
return Err(
|
||
|
|
"Use actor or actor/<JSON Pointer path>; Rune expressions are unsupported"
|
||
|
|
.to_owned(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
let root = self
|
||
|
|
.root
|
||
|
|
.ok_or("Actor snapshot unavailable while running")?;
|
||
|
|
let mut value = self.values[&root].0.clone();
|
||
|
|
for token in tokens {
|
||
|
|
let mut chars = token.chars();
|
||
|
|
let mut key = String::new();
|
||
|
|
while let Some(ch) = chars.next() {
|
||
|
|
key.push(if ch == '~' {
|
||
|
|
match chars.next() {
|
||
|
|
Some('0') => '~',
|
||
|
|
Some('1') => '/',
|
||
|
|
_ => return Err("Invalid JSON Pointer escape; use ~0 or ~1".to_owned()),
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
ch
|
||
|
|
});
|
||
|
|
}
|
||
|
|
value = match value {
|
||
|
|
Value::Record(mut entries) => {
|
||
|
|
entries.remove(&key).ok_or("Snapshot field not found")?
|
||
|
|
}
|
||
|
|
Value::List(entries) => {
|
||
|
|
if key.is_empty()
|
||
|
|
|| (key.starts_with('0') && key.len() > 1)
|
||
|
|
|| !key.bytes().all(|byte| byte.is_ascii_digit())
|
||
|
|
{
|
||
|
|
return Err("Invalid snapshot list index".to_owned());
|
||
|
|
}
|
||
|
|
let index: usize = key.parse().map_err(|_| "Invalid snapshot list index")?;
|
||
|
|
entries
|
||
|
|
.get(index)
|
||
|
|
.cloned()
|
||
|
|
.ok_or("Snapshot list index out of range")?
|
||
|
|
}
|
||
|
|
_ => return Err("Snapshot path traverses a scalar value".to_owned()),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
let mut variable = self.variable(expression, value, expression.to_owned());
|
||
|
|
variable["result"] = variable["value"].take();
|
||
|
|
let object = variable.as_object_mut().unwrap();
|
||
|
|
object.remove("name");
|
||
|
|
object.remove("value");
|
||
|
|
Ok(variable)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn variable(&mut self, name: &str, value: Value, path: String) -> Json {
|
||
|
|
let (kind, preview, children, indexed) = match &value {
|
||
|
|
Value::Null => ("null", "null".to_owned(), 0, false),
|
||
|
|
Value::Bool(value) => ("bool", value.to_string(), 0, false),
|
||
|
|
Value::Integer(value) => ("integer", value.to_string(), 0, false),
|
||
|
|
Value::String(value) => {
|
||
|
|
let preview: String = value.chars().take(256).collect();
|
||
|
|
let preview = if preview.len() < value.len() {
|
||
|
|
format!("{}… ({} bytes)", json!(preview), value.len())
|
||
|
|
} else {
|
||
|
|
json!(preview).to_string()
|
||
|
|
};
|
||
|
|
("string", preview, 0, false)
|
||
|
|
}
|
||
|
|
Value::ActorRef(value) => ("actor_ref", format!("@{value}"), 0, false),
|
||
|
|
Value::List(entries) => (
|
||
|
|
"list",
|
||
|
|
format!("List({})", entries.len()),
|
||
|
|
entries.len(),
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
Value::Record(entries) => (
|
||
|
|
"record",
|
||
|
|
format!("Record({})", entries.len()),
|
||
|
|
entries.len(),
|
||
|
|
false,
|
||
|
|
),
|
||
|
|
};
|
||
|
|
let reference = if children == 0 {
|
||
|
|
0
|
||
|
|
} else {
|
||
|
|
self.reference(value, path.clone())
|
||
|
|
};
|
||
|
|
let mut result = json!({
|
||
|
|
"name": name, "value": preview, "type": kind, "variablesReference": reference,
|
||
|
|
"presentationHint": {"attributes": ["readOnly"]}
|
||
|
|
});
|
||
|
|
if !path.is_empty() {
|
||
|
|
result["evaluateName"] = json!(path);
|
||
|
|
}
|
||
|
|
if matches!(kind, "list" | "record") {
|
||
|
|
result[if indexed {
|
||
|
|
"indexedVariables"
|
||
|
|
} else {
|
||
|
|
"namedVariables"
|
||
|
|
}] = json!(children);
|
||
|
|
}
|
||
|
|
result
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn record<const N: usize>(entries: [(&str, Value); N]) -> Value {
|
||
|
|
Value::Record(
|
||
|
|
entries
|
||
|
|
.into_iter()
|
||
|
|
.map(|(key, value)| (key.to_owned(), value))
|
||
|
|
.collect(),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn plain(value: Json) -> Value {
|
||
|
|
match value {
|
||
|
|
Json::Null => Value::Null,
|
||
|
|
Json::Bool(value) => Value::Bool(value),
|
||
|
|
Json::Number(value) => value
|
||
|
|
.as_i64()
|
||
|
|
.map(Value::Integer)
|
||
|
|
.unwrap_or_else(|| Value::String(value.to_string())),
|
||
|
|
Json::String(value) => Value::String(value),
|
||
|
|
Json::Array(values) => Value::List(values.into_iter().map(plain).collect()),
|
||
|
|
Json::Object(values) => Value::Record(
|
||
|
|
values
|
||
|
|
.into_iter()
|
||
|
|
.map(|(key, value)| (key, plain(value)))
|
||
|
|
.collect(),
|
||
|
|
),
|
||
|
|
}
|
||
|
|
}
|