blob: 77bcb6c950d1fba58c8044943cc1af4d82c26797 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// // Use of this source code is governed by a BSD-style license that can be
// // found in the LICENSE file.
use {
anyhow::{format_err, Error},
difference::{
self,
Difference::{Add, Rem, Same},
},
fidl_fuchsia_diagnostics::Selector,
fuchsia_inspect_node_hierarchy::{
self,
serialization::{
json::{JsonNodeHierarchySerializer, RawJsonNodeHierarchySerializer},
HierarchyDeserializer, HierarchySerializer,
},
InspectHierarchyMatcher, NodeHierarchy,
},
regex::Regex,
selectors,
std::cmp::{max, min},
std::convert::TryInto,
std::fs::read_to_string,
std::io::{stdin, stdout, Write},
std::path::PathBuf,
std::sync::Arc,
structopt::StructOpt,
termion::{
cursor,
event::{Event, Key},
input::TermRead,
raw::IntoRawMode,
},
};
static JSON_MONIKER_KEY: &str = "path";
static JSON_PAYLOAD_KEY: &str = "contents";
#[derive(Debug, StructOpt)]
struct Options {
#[structopt(short, long, help = "Inspect JSON file to read")]
bugreport: String,
#[structopt(subcommand)]
command: Command,
}
#[derive(Debug, StructOpt)]
enum Command {
#[structopt(name = "generate")]
Generate {
#[structopt(
short,
name = "component",
help = "Generate selectors for only this component"
)]
component_name: Option<String>,
#[structopt(help = "The output file to generate")]
selector_file: String,
},
#[structopt(name = "apply")]
Apply {
#[structopt(
short,
name = "component",
help = "Apply selectors from the provided selector_file for only this component"
)]
component_name: Option<String>,
#[structopt(help = "The selector file to apply to the bugreport")]
selector_file: String,
},
}
#[derive(Debug)]
struct Line {
value: String,
removed: bool,
}
impl Line {
fn new(s: impl ToString) -> Self {
Self { value: s.to_string(), removed: false }
}
fn removed(s: impl ToString) -> Self {
Self { value: s.to_string(), removed: true }
}
fn len(&self) -> usize {
self.value.len()
}
}
struct Output {
lines: Vec<Line>,
offset_top: usize,
offset_left: usize,
max_line_len: usize,
filter_removed: bool,
}
impl Output {
fn new(lines: Vec<Line>) -> Self {
let max_line_len = lines.iter().map(|l| l.len()).max().unwrap_or(0);
Output { lines, offset_top: 0, offset_left: 0, max_line_len, filter_removed: false }
}
fn set_lines(&mut self, lines: Vec<Line>) {
self.max_line_len = lines.iter().map(|l| l.len()).max().unwrap_or(0);
self.lines = lines;
self.scroll(0, 0);
}
fn refresh(&self, stdout: &mut impl Write) {
let (w, h) = termion::terminal_size().unwrap();
let max_lines = h as usize - 2; // Leave 2 lines for info.
self.lines
.iter()
.filter(|l| !self.filter_removed || !l.removed)
.skip(self.offset_top)
.take(max_lines)
.enumerate()
.for_each(|(i, line)| {
if self.offset_left >= line.value.len() {
return;
}
let end = min(line.value.len(), self.offset_left + w as usize);
if line.removed {
write!(stdout, "{}", termion::color::Fg(termion::color::Red)).unwrap();
}
write!(
stdout,
"{}{}{}",
termion::cursor::Goto(1, (i + 1) as u16),
line.value[self.offset_left..end].to_string(),
termion::color::Fg(termion::color::Reset),
)
.unwrap();
});
write!(
stdout,
"{}------------------- T: {}/{}, L: {}/{}{}Controls: [Q]uit. [R]efresh. {} filtered data. Arrow keys scroll.",
termion::cursor::Goto(1, h - 1),
self.offset_top, self.visible_line_count(), self.offset_left, self.max_line_len,
termion::cursor::Goto(1, h),
if self.filter_removed { "S[h]ow" } else { "[H]ide" },
)
.unwrap();
}
fn visible_line_count(&self) -> usize {
self.lines.iter().filter(|l| !self.filter_removed || !l.removed).count()
}
fn scroll(&mut self, down: i64, right: i64) {
let (w, h) = termion::terminal_size().unwrap();
self.offset_top = max(0, self.offset_top as i64 + down) as usize;
self.offset_left = max(0, self.offset_left as i64 + right) as usize;
self.offset_top =
min(self.offset_top as i64, max(0, self.visible_line_count() as i64 - h as i64))
as usize;
self.offset_left =
min(self.offset_left as i64, max(0, self.max_line_len as i64 - w as i64)) as usize;
}
fn set_filter_removed(&mut self, val: bool) {
if self.filter_removed == val {
return;
}
self.filter_removed = val;
if self.filter_removed {
// Starting to filter, tweak offset_top to remove offsets from newly filtered lines.
self.offset_top -= self.lines.iter().take(self.offset_top).filter(|l| l.removed).count()
} else {
// TODO: Fix this
}
}
}
fn parse_path_to_moniker(path: &str) -> Result<Vec<String>, Error> {
// First try to parse the paths according to the legacy bugreport "path" format.
let legacy_bugreport_path = Regex::new(r"/[cr]/([^/]*)/\d+")
.unwrap()
.captures_iter(path)
.map(|cap| cap.get(1).unwrap().as_str().to_owned())
.collect::<Vec<String>>();
match legacy_bugreport_path.as_slice() {
[] => {
// If the legacy bugreport regex failed to produce a moniker vector, then
// the path is generated by the platform and already exists as a moniker. Tokenize
// by unescaped path delimiters.
selectors::tokenize_string(path, selectors::PATH_NODE_DELIMITER).map_err(|e| {
format_err!(
"Failed to parse bugreport path: {} using legacy or new tokenizers. Error: {}",
path,
e
)
})
}
legacy_moniker => Ok(legacy_moniker.to_vec()),
}
}
fn filter_json_schema_by_selectors(
mut value: serde_json::Value,
selector_vec: &Vec<Arc<Selector>>,
) -> Option<serde_json::Value> {
let moniker_string_opt = value[JSON_MONIKER_KEY].as_str();
let deserialized_hierarchy =
RawJsonNodeHierarchySerializer::deserialize(value[JSON_PAYLOAD_KEY].clone());
match (moniker_string_opt, deserialized_hierarchy) {
(Some(moniker_path), Ok(hierarchy)) => {
// A failure here implies a malformed bugreport. We want to panic.
let moniker = parse_path_to_moniker(moniker_path)
.expect("Bugreport contained an unparsable path.");
match selectors::match_component_moniker_against_selectors(&moniker, &selector_vec) {
Ok(matched_selectors) => {
if matched_selectors.is_empty() {
return None;
}
let inspect_matcher: InspectHierarchyMatcher =
(&matched_selectors).try_into().unwrap();
match fuchsia_inspect_node_hierarchy::filter_inspect_snapshot(
hierarchy,
&inspect_matcher,
) {
Ok(Some(filtered)) => {
let serialized_hierarchy =
RawJsonNodeHierarchySerializer::serialize(filtered);
value[JSON_PAYLOAD_KEY] = serialized_hierarchy;
Some(value)
}
Ok(None) => {
// Ok(None) implies the tree was fully filtered. This means that
// it genuinely should not be included in the output.
None
}
Err(e) => {
value[JSON_PAYLOAD_KEY] = serde_json::json!(format!(
"Filtering the hierarchy of {}, an error occurred: {:?}",
moniker_path, e
));
Some(value)
}
}
}
Err(e) => {
value[JSON_PAYLOAD_KEY] = serde_json::json!(format!(
"Evaulating selectors for {} met an unexpected error condition: {:?}",
moniker_path, e
));
Some(value)
}
}
}
(potential_errorful_moniker, potential_errorful_hierarchy) => {
let mut errorful_report = String::new();
if potential_errorful_moniker.is_none() {
errorful_report.push_str(
"The moniker entry in the provided schema was missing or an incorrect type. \n",
);
}
if potential_errorful_hierarchy.is_err() {
errorful_report.push_str(&format!(
"The hierarchy entry was missing or failed to deserialize: {:?}",
potential_errorful_hierarchy
.err()
.expect("We've already verified that the deserialization failed.")
))
}
value[JSON_PAYLOAD_KEY] = serde_json::json!(errorful_report);
Some(value)
}
}
}
/// Consumes a file containing Inspect selectors and applies them to an array of node hierarchies
/// which had previously been serialized to their json schema.
///
/// Returns a vector of Line printed diffs between the unfiltered and filtered hierarchies,
/// or an Error.
fn filter_data_to_lines(
selector_file: &str,
data: &serde_json::Value,
requested_name_opt: &Option<String>,
) -> Result<Vec<Line>, Error> {
let selector_vec: Vec<Arc<Selector>> =
selectors::parse_selector_file(&PathBuf::from(selector_file))?
.into_iter()
.map(Arc::new)
.collect();
let arr: Vec<serde_json::Value> = match data {
serde_json::Value::Array(arr) => arr.to_vec(),
_ => return Err(format_err!("Input Inspect JSON must be an array.")),
};
// Filter the source data that we diff against to only contain the component
// of interest.
let diffable_source = match requested_name_opt {
Some(requested_name) => arr
.into_iter()
.filter(|value| match value[JSON_MONIKER_KEY].as_str() {
Some(moniker_str) => {
let moniker = parse_path_to_moniker(moniker_str)
.expect("Bugreport contained an unparsable path.");
let component_name = moniker
.last()
.expect("Monikers in provided data dumps are required to be non-empty.");
requested_name == component_name
}
None => false,
})
.collect(),
None => arr,
};
let filtered_node_hierarchies: Vec<serde_json::Value> = diffable_source
.clone()
.into_iter()
.filter_map(|value| filter_json_schema_by_selectors(value, &selector_vec))
.collect();
let unfiltered_collection_array = serde_json::Value::Array(diffable_source);
// TODO(43937): Move inspect formatting utilities to the hierarchy library.
let filtered_collection_array = serde_json::Value::Array(filtered_node_hierarchies);
let orig_str = serde_json::to_string_pretty(&unfiltered_collection_array).unwrap();
let new_str = serde_json::to_string_pretty(&filtered_collection_array).unwrap();
let cs = difference::Changeset::new(&orig_str, &new_str, "\n");
Ok(cs
.diffs
.into_iter()
.map(|change| match change {
Same(val) | Add(val) => val.split("\n").map(|l| Line::new(l)).collect::<Vec<Line>>(),
Rem(val) => val.split("\n").map(|l| Line::removed(l)).collect::<Vec<Line>>(),
})
.flatten()
.collect())
}
fn generate_selectors<'a>(
data: &'a serde_json::Value,
component_name: Option<String>,
) -> Result<String, Error> {
let arr = match data {
serde_json::Value::Array(arr) => arr,
_ => return Err(format_err!("Input Inspect JSON must be an array.")),
};
struct MatchedHierarchy {
moniker: Vec<String>,
hierarchy: NodeHierarchy,
}
let matching_hierarchies: Vec<MatchedHierarchy> = arr
.iter()
.filter_map(|value| {
let moniker = parse_path_to_moniker(value[JSON_MONIKER_KEY].as_str().expect(&format!(
"Bugreport had an entry missing the moniker key: {}",
JSON_MONIKER_KEY
)))
.expect("Bugreport contained an unparsable path.");
let component_name_matches = component_name.is_none()
|| component_name.as_ref().unwrap()
== moniker
.last()
.expect("Monikers in provided data dumps are required to be non-empty.");
if component_name_matches {
let hierarchy =
JsonNodeHierarchySerializer::deserialize(value[JSON_PAYLOAD_KEY].to_string())
.unwrap();
Some(MatchedHierarchy { moniker, hierarchy: hierarchy })
} else {
None
}
})
.collect();
let mut output: Vec<String> = vec![];
for matching_hierarchy in matching_hierarchies {
let sanitized_moniker = matching_hierarchy
.moniker
.iter()
.map(|s| selectors::sanitize_string_for_selectors(s))
.collect::<Vec<String>>()
.join("/");
for (node_path, property) in matching_hierarchy.hierarchy.property_iter() {
let formatted_node_path = node_path
.iter()
.map(|s| selectors::sanitize_string_for_selectors(s))
.collect::<Vec<String>>()
.join("/");
let sanitized_property = selectors::sanitize_string_for_selectors(property.name());
output.push(format!(
"{}:{}:{}",
sanitized_moniker.clone(),
formatted_node_path,
sanitized_property
));
}
}
// NodeHierarchy has an intentionally non-deterministic iteration order, but for client
// facing tools we'll want to sort the outputs.
output.sort();
Ok(output.join("\n"))
}
fn interactive_apply(
data: &serde_json::Value,
selector_file: &str,
component_name: Option<String>,
) -> Result<(), Error> {
let stdin = stdin();
let mut stdout = stdout().into_raw_mode().unwrap();
let mut output = Output::new(filter_data_to_lines(&selector_file, &data, &component_name)?);
write!(stdout, "{}{}{}", cursor::Restore, cursor::Hide, termion::clear::All).unwrap();
output.refresh(&mut stdout);
stdout.flush().unwrap();
for c in stdin.events() {
let evt = c.unwrap();
match evt {
Event::Key(Key::Char('q')) => break,
Event::Key(Key::Char('h')) => output.set_filter_removed(!output.filter_removed),
Event::Key(Key::Char('r')) => {
output.set_lines(vec![Line::new("Refreshing filtered hierarchies...")]);
write!(stdout, "{}", termion::clear::All).unwrap();
output.refresh(&mut stdout);
stdout.flush().unwrap();
output.set_lines(filter_data_to_lines(&selector_file, &data, &component_name)?)
}
Event::Key(Key::Up) => {
output.scroll(-1, 0);
}
Event::Key(Key::Down) => {
output.scroll(1, 0);
}
Event::Key(Key::Left) => {
output.scroll(0, -1);
}
Event::Key(Key::Right) => {
output.scroll(0, 1);
}
e => {
println!("{:?}", e);
}
}
write!(stdout, "{}", termion::clear::All).unwrap();
output.refresh(&mut stdout);
stdout.flush().unwrap();
}
write!(stdout, "{}{}{}", cursor::Restore, cursor::Show, termion::clear::All,).unwrap();
stdout.flush().unwrap();
Ok(())
}
fn main() -> Result<(), Error> {
let opts = Options::from_args();
let filename = &opts.bugreport;
let data: serde_json::Value = serde_json::from_str(
&read_to_string(filename).expect(&format!("Failed to read {} ", filename)),
)
.expect(&format!("Failed to parse {} as JSON", filename));
match opts.command {
Command::Generate { selector_file, component_name } => {
std::fs::write(
&selector_file,
generate_selectors(&data, component_name)
.expect(&format!("failed to generate selectors")),
)?;
}
Command::Apply { selector_file, component_name } => {
interactive_apply(&data, &selector_file, component_name)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile;
#[test]
fn parse_path_to_moniker_test() {
assert_eq!(
parse_path_to_moniker("/hub/r/sys/1234/c/my.cmx/1/out/diagnostics")
.expect("test moniker is parsable."),
vec!["sys", "my.cmx"]
);
assert_eq!(
parse_path_to_moniker("/hub/r/12345/28464/c/account_handler.cmx/29647/out/diagnostics")
.expect("test moniker is parsable."),
vec!["12345", "account_handler.cmx"]
);
assert_eq!(
parse_path_to_moniker("a/realm1/b/account_handler.cmx")
.expect("test moniker is parsable."),
vec!["a", "realm1", "b", "account_handler.cmx"]
);
}
#[test]
fn generate_selectors_test() {
let json_dump = get_legacy_json_dump();
eprintln!("json dump: {}", json_dump);
let named_selector_string =
generate_selectors(&json_dump, Some("account_manager.cmx".to_string()))
.expect("Generating selectors with matching name should succeed.");
let expected_named_selector_string = "account_manager.cmx:root/accounts:active
account_manager.cmx:root/accounts:total
account_manager.cmx:root/auth_providers:types
account_manager.cmx:root/listeners:active
account_manager.cmx:root/listeners:events
account_manager.cmx:root/listeners:total_opened";
assert_eq!(named_selector_string, expected_named_selector_string);
assert_eq!(
generate_selectors(&json_dump, Some("bloop.cmx".to_string()))
.expect("Generating selectors with unmatching name should succeed"),
""
);
assert_eq!(
generate_selectors(&json_dump, None)
.expect("Generating selectors with no name should succeed"),
expected_named_selector_string
);
}
fn setup_and_run_selector_filtering(
selector_string: &str,
source_hierarchy: serde_json::Value,
golden_json: serde_json::Value,
requested_component: Option<String>,
) {
let mut selector_path =
tempfile::NamedTempFile::new().expect("Creating tmp selector file should succeed.");
selector_path
.write_all(selector_string.as_bytes())
.expect("writing selectors to file should be fine...");
let filtered_data_string = filter_data_to_lines(
&selector_path.path().to_string_lossy(),
&source_hierarchy,
&requested_component,
)
.expect("filtering hierarchy should have succeeded.")
.into_iter()
.filter(|line| !line.removed)
.fold(String::new(), |mut acc, line| {
acc.push_str(&line.value);
acc
});
let filtered_json_value: serde_json::Value = serde_json::from_str(&filtered_data_string)
.expect(&format!(
"Resultant json dump should be parsable json: {}",
filtered_data_string
));
assert_eq!(filtered_json_value, golden_json);
}
#[test]
fn legacy_filter_data_to_lines_test() {
let full_tree_selector = "account_manager.cmx:root/accounts:active
account_manager.cmx:root/accounts:total
account_manager.cmx:root/auth_providers:types
account_manager.cmx:root/listeners:active
account_manager.cmx:root/listeners:events
account_manager.cmx:root/listeners:total_opened";
setup_and_run_selector_filtering(
full_tree_selector,
get_legacy_json_dump(),
get_legacy_json_dump(),
None,
);
setup_and_run_selector_filtering(
full_tree_selector,
get_legacy_json_dump(),
get_legacy_json_dump(),
Some("account_manager.cmx".to_string()),
);
let single_value_selector = "account_manager.cmx:root/accounts:active";
setup_and_run_selector_filtering(
single_value_selector,
get_legacy_json_dump(),
get_legacy_single_value_json(),
None,
);
setup_and_run_selector_filtering(
single_value_selector,
get_legacy_json_dump(),
get_legacy_single_value_json(),
Some("account_manager.cmx".to_string()),
);
setup_and_run_selector_filtering(
single_value_selector,
get_legacy_json_dump(),
get_empty_value_json(),
Some("bloop.cmx".to_string()),
);
}
#[test]
fn filter_data_to_lines_test() {
let full_tree_selector = "realm1/realm2/session5/account_manager.cmx:root/accounts:active
realm1/realm2/session5/account_manager.cmx:root/accounts:total
realm1/realm2/session5/account_manager.cmx:root/auth_providers:types
realm1/realm2/session5/account_manager.cmx:root/listeners:active
realm1/realm2/session5/account_manager.cmx:root/listeners:events
realm1/realm2/session5/account_manager.cmx:root/listeners:total_opened";
setup_and_run_selector_filtering(
full_tree_selector,
get_json_dump(),
get_json_dump(),
None,
);
setup_and_run_selector_filtering(
full_tree_selector,
get_json_dump(),
get_json_dump(),
Some("account_manager.cmx".to_string()),
);
let single_value_selector =
"realm1/realm2/session5/account_manager.cmx:root/accounts:active";
setup_and_run_selector_filtering(
single_value_selector,
get_json_dump(),
get_single_value_json(),
None,
);
setup_and_run_selector_filtering(
single_value_selector,
get_json_dump(),
get_single_value_json(),
Some("account_manager.cmx".to_string()),
);
setup_and_run_selector_filtering(
single_value_selector,
get_json_dump(),
get_empty_value_json(),
Some("bloop.cmx".to_string()),
);
}
fn get_legacy_json_dump() -> serde_json::Value {
serde_json::json!(
[
{
"contents": {
"root": {
"accounts": {
"active": 0,
"total": 0
},
"auth_providers": {
"types": "google"
},
"listeners": {
"active": 1,
"events": 0,
"total_opened": 1
}
}
},
"path": "/hub/c/account_manager.cmx/25181/out/diagnostics/root.inspect"
}
]
)
}
fn get_legacy_single_value_json() -> serde_json::Value {
serde_json::json!(
[
{
"contents": {
"root": {
"accounts": {
"active": 0
}
}
},
"path": "/hub/c/account_manager.cmx/25181/out/diagnostics/root.inspect"
}
]
)
}
fn get_json_dump() -> serde_json::Value {
serde_json::json!(
[
{
"contents": {
"root": {
"accounts": {
"active": 0,
"total": 0
},
"auth_providers": {
"types": "google"
},
"listeners": {
"active": 1,
"events": 0,
"total_opened": 1
}
}
},
"path": "realm1/realm2/session5/account_manager.cmx"
}
]
)
}
fn get_single_value_json() -> serde_json::Value {
serde_json::json!(
[
{
"contents": {
"root": {
"accounts": {
"active": 0
}
}
},
"path": "realm1/realm2/session5/account_manager.cmx"
}
]
)
}
fn get_empty_value_json() -> serde_json::Value {
serde_json::json!([])
}
}