[detect] Detect skeleton
Near-final v0 implementation for Detect.
Test: fx build && fx test triage-detect-tests && fx test triage
Change-Id: I21aacdb458773f154c33c5de537ffee01ed41304
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/426775
Commit-Queue: Chris Phoenix <cphoenix@google.com>
Testability-Review: Christopher Johnson <crjohns@google.com>
Reviewed-by: Christopher Johnson <crjohns@google.com>
diff --git a/src/diagnostics/BUILD.gn b/src/diagnostics/BUILD.gn
index 53b4ab0..2a6321a 100644
--- a/src/diagnostics/BUILD.gn
+++ b/src/diagnostics/BUILD.gn
@@ -18,6 +18,7 @@
deps = [
"archivist:tests",
"config:tests",
+ "detect:tests",
"iquery:tests",
"lib:tests",
"log-stats:tests",
diff --git a/src/diagnostics/config/triage/BUILD.gn b/src/diagnostics/config/triage/BUILD.gn
index b448f4d..86244d5 100644
--- a/src/diagnostics/config/triage/BUILD.gn
+++ b/src/diagnostics/config/triage/BUILD.gn
@@ -2,6 +2,7 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import("//build/config.gni")
import("//src/diagnostics/triage/build/triage_config_test.gni")
# The list of triage configuration files in the current directory that
@@ -28,6 +29,15 @@
"timezone.triage",
]
+detect_files = [ "detect/wlan-detect.triage" ]
+
+triage_files += detect_files
+
+config_data("triage-detect") {
+ for_pkg = "triage-detect"
+ sources = detect_files
+}
+
group("triage") {
testonly = true
deps = [ ":tests" ]
diff --git a/src/diagnostics/config/triage/detect/wlan-detect.triage b/src/diagnostics/config/triage/detect/wlan-detect.triage
new file mode 100644
index 0000000..2f67180
--- /dev/null
+++ b/src/diagnostics/config/triage/detect/wlan-detect.triage
@@ -0,0 +1,17 @@
+{
+ select: {
+ disconnects: "INSPECT:wlanstack.cmx:root/client_stats/disconnect/*:@time",
+ },
+ eval: {
+ last_24_hours: "Fn([time], time > Now() - Hours(24))",
+ n_disconnects_today: "Count(Filter(last_24_hours, disconnects))",
+ },
+ act: {
+ too_many_disconnects: {
+ type: "Snapshot",
+ trigger: "n_disconnects_today >= 5",
+ repeat: "Hours(24)",
+ signature: "five-disconnects-today",
+ },
+ },
+}
diff --git a/src/diagnostics/detect/BUILD.gn b/src/diagnostics/detect/BUILD.gn
new file mode 100644
index 0000000..4f30cc0
--- /dev/null
+++ b/src/diagnostics/detect/BUILD.gn
@@ -0,0 +1,69 @@
+# Copyright 2020 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.
+
+import("//build/config.gni")
+import("//build/rust/rustc_binary.gni")
+import("//src/sys/build/components.gni")
+
+rustc_binary("bin") {
+ name = "triage-detect"
+ edition = "2018"
+ with_unit_tests = true
+
+ deps = [
+ "//sdk/fidl/fuchsia.diagnostics:fuchsia.diagnostics-rustc",
+ "//sdk/fidl/fuchsia.feedback:fuchsia.feedback-rustc",
+ "//sdk/fidl/fuchsia.logger:fuchsia.logger-rustc",
+ "//src/diagnostics/lib/injectable-time",
+ "//src/diagnostics/lib/triage",
+ "//src/lib/diagnostics/inspect/contrib/rust",
+ "//src/lib/fdio/rust:fdio",
+ "//src/lib/fidl/rust/fidl",
+ "//src/lib/fuchsia-async",
+ "//src/lib/fuchsia-component",
+ "//src/lib/syslog/rust:syslog",
+ "//src/lib/zircon/rust:fuchsia-zircon",
+ "//third_party/rust_crates:anyhow",
+ "//third_party/rust_crates:argh",
+ "//third_party/rust_crates:async-trait",
+ "//third_party/rust_crates:futures",
+ "//third_party/rust_crates:glob",
+ "//third_party/rust_crates:log",
+ "//third_party/rust_crates:maplit",
+ "//third_party/rust_crates:matches",
+ "//third_party/rust_crates:thiserror",
+ ]
+
+ sources = [
+ "src/delay_tracker.rs",
+ "src/diagnostics.rs",
+ "src/main.rs",
+ "src/snapshot.rs",
+ "src/triage_shim.rs",
+ ]
+
+ test_deps = []
+}
+
+fuchsia_component("triage-detect-component") {
+ deps = [
+ ":bin",
+ "//src/diagnostics/config/triage:triage-detect",
+ ]
+ manifest = "meta/triage-detect.cml"
+}
+
+fuchsia_package("triage-detect") {
+ deps = [ ":triage-detect-component" ]
+}
+
+fuchsia_unittest_package("triage-detect-tests") {
+ manifest = "meta/triage-detect-tests.cmx"
+ deps = [ ":bin_test" ]
+}
+
+group("tests") {
+ testonly = true
+ deps = [ ":triage-detect-tests" ]
+}
diff --git a/src/diagnostics/detect/README.md b/src/diagnostics/detect/README.md
new file mode 100644
index 0000000..a500183
--- /dev/null
+++ b/src/diagnostics/detect/README.md
@@ -0,0 +1,6 @@
+# detect
+
+Detect runs on-device, getting diagnostic data periodically and using Triage-lib
+to decide whether to take actions such as generating a crash report/snapshot.
+
+TODO(fxbug.dev/61333): Add more info.
diff --git a/src/diagnostics/detect/meta/triage-detect-tests.cmx b/src/diagnostics/detect/meta/triage-detect-tests.cmx
new file mode 100644
index 0000000..23a53b2
--- /dev/null
+++ b/src/diagnostics/detect/meta/triage-detect-tests.cmx
@@ -0,0 +1,13 @@
+{
+ "program": {
+ "binary": "bin/triage_detect_bin_test"
+ },
+ "sandbox": {
+ "features": [
+ "build-info"
+ ],
+ "services": [
+ "fuchsia.logger.LogSink"
+ ]
+ }
+}
diff --git a/src/diagnostics/detect/meta/triage-detect.cml b/src/diagnostics/detect/meta/triage-detect.cml
new file mode 100644
index 0000000..4e8a160
--- /dev/null
+++ b/src/diagnostics/detect/meta/triage-detect.cml
@@ -0,0 +1,29 @@
+{
+ program: {
+ binary: "bin/triage_detect",
+ args: [
+ "--check-every",
+ "Minutes(2)",
+ ],
+ },
+ use: [
+ { runner: "elf" },
+ {
+ protocol: "fuchsia.feedback.CrashReporter",
+ from: "parent",
+ },
+ {
+ protocol: "fuchsia.logger.LogSink",
+ from: "parent",
+ },
+ {
+ protocol: "fuchsia.diagnostics.FeedbackArchiveAccessor",
+ from: "parent",
+ },
+ {
+ directory: "config-data",
+ rights: [ "r*" ],
+ path: "/config/data",
+ },
+ ],
+}
diff --git a/src/diagnostics/detect/src/delay_tracker.rs b/src/diagnostics/detect/src/delay_tracker.rs
new file mode 100644
index 0000000..875f758
--- /dev/null
+++ b/src/diagnostics/detect/src/delay_tracker.rs
@@ -0,0 +1,98 @@
+// Copyright 2020 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.
+
+/// DelayTracker keeps track of how recently we sent a crash report of each type, and whether we
+/// should mute too-frequent requests.
+use {
+ crate::{Mode, MINIMUM_SIGNATURE_INTERVAL_NANOS},
+ injectable_time::TimeSource,
+ log::warn,
+ std::collections::HashMap,
+ triage::SnapshotTrigger,
+};
+
+pub struct DelayTracker<'a> {
+ last_sent: HashMap<String, i64>,
+ time_source: &'a dyn TimeSource,
+ program_mode: Mode,
+}
+
+impl<'a> DelayTracker<'a> {
+ pub fn new(time_source: &'a dyn TimeSource, program_mode: Mode) -> DelayTracker<'a> {
+ DelayTracker { last_sent: HashMap::new(), time_source, program_mode }
+ }
+
+ fn appropriate_report_interval(&self, desired_interval: i64) -> i64 {
+ if self.program_mode == Mode::Test || desired_interval >= MINIMUM_SIGNATURE_INTERVAL_NANOS {
+ desired_interval
+ } else {
+ MINIMUM_SIGNATURE_INTERVAL_NANOS
+ }
+ }
+
+ // If it's OK to send, remember the time and return true.
+ pub fn ok_to_send(&mut self, snapshot: &SnapshotTrigger) -> bool {
+ let now = self.time_source.now();
+ let interval = self.appropriate_report_interval(snapshot.interval);
+ let should_send = match self.last_sent.get(&snapshot.signature) {
+ None => true,
+ Some(time) => time <= &(now - interval),
+ };
+ if should_send {
+ self.last_sent.insert(snapshot.signature.to_string(), now);
+ if snapshot.interval < MINIMUM_SIGNATURE_INTERVAL_NANOS {
+ // To reduce logspam, put this warning here rather than above where the
+ // calculation is. The calculation may happen every time we check diagnostics; this
+ // will happen at most every MINIMUM_SIGNATURE_INTERVAL (except in tests).
+ warn!(
+ "Signature {} has interval {} nanos, less than minimum {}",
+ snapshot.signature, snapshot.interval, MINIMUM_SIGNATURE_INTERVAL_NANOS
+ );
+ }
+ }
+ should_send
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use injectable_time::FakeTime;
+
+ #[test]
+ fn verify_test_mode() {
+ let time = FakeTime::new();
+ let mut tracker = DelayTracker::new(&time, Mode::Test);
+ time.set(1);
+ let trigger_slow = SnapshotTrigger { signature: "slow".to_string(), interval: 10 };
+ let trigger_fast = SnapshotTrigger { signature: "fast".to_string(), interval: 1 };
+ let ok_slow_1 = tracker.ok_to_send(&trigger_slow);
+ let ok_fast_1 = tracker.ok_to_send(&trigger_fast);
+ time.set(3);
+ let ok_slow_2 = tracker.ok_to_send(&trigger_slow);
+ let ok_fast_2 = tracker.ok_to_send(&trigger_fast);
+ // This one should obviously succeed.
+ assert_eq!(ok_slow_1, true);
+ // It should allow a different snapshot signature too.
+ assert_eq!(ok_fast_1, true);
+ // It should reject the first (slow) signature the second time.
+ assert_eq!(ok_slow_2, false);
+ // The second (fast) signature should be accepted repeatedly.
+ assert_eq!(ok_fast_2, true);
+ }
+
+ #[test]
+ fn verify_appropriate_report_interval() {
+ assert!(MINIMUM_SIGNATURE_INTERVAL_NANOS > 1);
+ let time = FakeTime::new();
+ let test_tracker = DelayTracker::new(&time, Mode::Test);
+ let production_tracker = DelayTracker::new(&time, Mode::Production);
+
+ assert_eq!(test_tracker.appropriate_report_interval(1), 1);
+ assert_eq!(
+ production_tracker.appropriate_report_interval(1),
+ MINIMUM_SIGNATURE_INTERVAL_NANOS
+ );
+ }
+}
diff --git a/src/diagnostics/detect/src/diagnostics.rs b/src/diagnostics/detect/src/diagnostics.rs
new file mode 100644
index 0000000..8a61e8c
--- /dev/null
+++ b/src/diagnostics/detect/src/diagnostics.rs
@@ -0,0 +1,96 @@
+// Copyright 2020 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.
+
+// Fetches diagnostic data.
+
+use {
+ anyhow::Error,
+ fuchsia_inspect_contrib::reader::{ArchiveReader, DataType},
+ log::error,
+ triage::DiagnosticData,
+ triage::Source,
+};
+
+// Selectors for Inspect data must start with this exact string.
+const INSPECT_PREFIX: &str = "INSPECT:";
+
+// Durable connection to Archivist
+#[derive(Debug)]
+pub struct DiagnosticFetcher {
+ inspect: InspectFetcher,
+}
+
+#[derive(Debug)]
+pub struct Selectors {
+ pub(crate) inspect_selectors: Vec<String>,
+}
+
+impl DiagnosticFetcher {
+ pub fn create(selectors: Selectors) -> Result<DiagnosticFetcher, Error> {
+ Ok(DiagnosticFetcher { inspect: InspectFetcher::create(selectors.inspect_selectors)? })
+ }
+
+ pub async fn get_diagnostics(&mut self) -> Result<Vec<DiagnosticData>, Error> {
+ let inspect_data = DiagnosticData::new(
+ "inspect.json".to_string(),
+ Source::Inspect,
+ self.inspect.fetch().await?,
+ )?;
+ Ok(vec![inspect_data])
+ }
+}
+
+impl Selectors {
+ pub fn new() -> Selectors {
+ Selectors { inspect_selectors: Vec::new() }
+ }
+
+ pub fn with_inspect_selectors(mut self, selectors: Vec<String>) -> Self {
+ self.inspect_selectors.extend(selectors);
+ self
+ }
+}
+
+struct InspectFetcher {
+ // If we have no selectors, we don't want to actually fetch anything.
+ // (Fetching with no selectors fetches all Inspect data.)
+ reader: Option<ArchiveReader>,
+}
+
+impl std::fmt::Debug for InspectFetcher {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("InspectFetcher").field("reader", &"opaque-ArchiveReader").finish()
+ }
+}
+
+impl InspectFetcher {
+ pub fn create(selectors: Vec<String>) -> Result<InspectFetcher, Error> {
+ if selectors.len() == 0 {
+ return Ok(InspectFetcher { reader: None });
+ }
+ let reader = ArchiveReader::new();
+ let get_inspect = |s: String| -> Option<std::string::String> {
+ if &s[..INSPECT_PREFIX.len()] == INSPECT_PREFIX {
+ Some(s[INSPECT_PREFIX.len()..].to_string())
+ } else {
+ error!("All selectors should begin with 'INSPECT:' - '{}'", s);
+ None
+ }
+ };
+ let selectors = selectors.into_iter().filter_map(get_inspect);
+ let reader = reader.add_selectors(selectors);
+ Ok(InspectFetcher { reader: Some(reader) })
+ }
+
+ /// This returns a String in JSON format because that's what TriageLib needs.
+ pub async fn fetch(&mut self) -> Result<String, Error> {
+ match &self.reader {
+ None => Ok("[]".to_string()),
+ Some(reader) => {
+ // TODO(fxbug.dev/62480): Make TriageLib accept structured data
+ Ok(reader.snapshot_raw(DataType::Inspect).await?.to_string())
+ }
+ }
+ }
+}
diff --git a/src/diagnostics/detect/src/main.rs b/src/diagnostics/detect/src/main.rs
new file mode 100644
index 0000000..3d14698
--- /dev/null
+++ b/src/diagnostics/detect/src/main.rs
@@ -0,0 +1,245 @@
+// Copyright 2020 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.
+
+//! `triage-detect` is responsible for auto-triggering crash reports in Fuchsia.
+
+// TODO(fxbug.dev/61333): Several things
+// need to be answered and implemented before this is deployed.
+//
+// How and whether to gate/space crash report requests - should we queue them (with limited slots)
+// the way PowerManager code does? (probably not)
+// Should we throttle crash report requests (N per day) and where to enforce this?
+// Do signatures need to be unique for each action? Namespaced between files?
+// Integration test
+// Restrict signature to lowercase-and-hyphens.
+
+mod delay_tracker;
+mod diagnostics;
+mod snapshot;
+mod triage_shim;
+
+use {
+ anyhow::{bail, Context, Error},
+ argh::FromArgs,
+ delay_tracker::DelayTracker,
+ fuchsia_async as fasync, fuchsia_zircon as zx,
+ glob::glob,
+ injectable_time::UtcTime,
+ log::{error, info, warn},
+ snapshot::SnapshotRequest,
+ std::collections::HashMap,
+};
+
+const MINIMUM_CHECK_TIME_NANOS: i64 = 60 * 1_000_000_000;
+const CONFIG_GLOB: &str = "/config/data/*";
+const SIGNATURE_PREFIX: &str = "fuchsia-detect-";
+const MINIMUM_SIGNATURE_INTERVAL_NANOS: i64 = 3600 * 1_000_000_000;
+
+/// Command line args
+#[derive(FromArgs, Debug)]
+struct CommandLine {
+ /// how often to scan Diagnostic data
+ #[argh(option)]
+ check_every: Option<String>,
+
+ /// ignore minimum times for testing. Never check in code with this flag set.
+ #[argh(switch)]
+ test_only: bool,
+}
+
+#[derive(PartialEq, Debug)]
+pub enum Mode {
+ Test,
+ Production,
+}
+
+fn load_configuration_files() -> Result<HashMap<String, String>, Error> {
+ fn file_stem(file_path: &std::path::PathBuf) -> Result<String, Error> {
+ if let Some(s) = file_path.file_stem() {
+ if let Some(s) = s.to_str() {
+ return Ok(s.to_owned());
+ }
+ }
+ bail!("Bad path {:?} - can't find file_stem", file_path)
+ }
+
+ let mut file_contents = HashMap::new();
+ for file_path in glob(CONFIG_GLOB)? {
+ let file_path = file_path?;
+ let stem = file_stem(&file_path)?;
+ let config_text = std::fs::read_to_string(file_path)?;
+ file_contents.insert(stem, config_text);
+ }
+ Ok(file_contents)
+}
+
+fn load_command_line() -> Result<CommandLine, Error> {
+ // We can't just use the one-line argh parse, because that writes to stdout
+ // and stdout doesn't currently work in v2 components. Instead, grab and
+ // log the output.
+ let arg_strings = std::env::args().collect::<Vec<_>>();
+ let arg_strs: Vec<&str> = arg_strings.iter().map(|s| s.as_str()).collect();
+ match CommandLine::from_args(&[arg_strs[0]], &arg_strs[1..]) {
+ Ok(args) => Ok(args),
+ Err(output) => {
+ for line in output.output.split("\n") {
+ warn!("CmdLine: {}", line);
+ }
+ match output.status {
+ Ok(()) => bail!("Exited as requested by command line args"),
+ Err(()) => bail!("Exited due to bad command line args"),
+ }
+ }
+ }
+}
+
+/// appropriate_check_interval determines the interval to check diagnostics, or signals error.
+///
+/// If the command line arg is empty, the interval is set to MINIMUM_CHECK_TIME_NANOS.
+/// If the command line can't be evaluated to an integer, an error is returned.
+/// If the integer is below minimum and mode isn't Test, an error is returned.
+/// If a valid integer is determined, it is returned as a zx::Duration.
+fn appropriate_check_interval(
+ command_line_option: &Option<String>,
+ mode: &Mode,
+) -> Result<zx::Duration, Error> {
+ let check_every = match &command_line_option {
+ None => MINIMUM_CHECK_TIME_NANOS,
+ Some(expression) => triage_shim::evaluate_int_math(&expression).or_else(|e| {
+ bail!("Check_every argument must be Minutes(n), Hours(n), etc. but: {}", e)
+ })?,
+ };
+ if check_every < MINIMUM_CHECK_TIME_NANOS && *mode != Mode::Test {
+ bail!(
+ "Minimum time to check is {} seconds; {} nanos is too small",
+ MINIMUM_CHECK_TIME_NANOS / 1_000_000_000,
+ check_every
+ );
+ }
+ info!(
+ "Checking every {} seconds from command line '{:?}'",
+ check_every / 1_000_000_000,
+ command_line_option
+ );
+ Ok(zx::Duration::from_nanos(check_every))
+}
+
+// on_error logs any errors from `value` and then returns a Result.
+// value must return a Result; error_message must contain one {} to put the error in.
+macro_rules! on_error {
+ ($value:expr, $error_message:expr) => {
+ $value.or_else(|e| {
+ let message = format!($error_message, e);
+ error!("{}", message);
+ bail!("{}", message)
+ })
+ };
+}
+
+#[fasync::run_singlethreaded]
+async fn main() -> Result<(), Error> {
+ fuchsia_syslog::init_with_tags(&["detect"]).context("initializing logging").unwrap();
+ let args = on_error!(load_command_line(), "Command line error: {}")?;
+ let mode = match args.test_only {
+ true => Mode::Test,
+ false => Mode::Production,
+ };
+ let check_every = on_error!(
+ appropriate_check_interval(&args.check_every, &mode),
+ "Invalid command line arg for check time: {}"
+ )?;
+ let configuration =
+ on_error!(load_configuration_files(), "Error reading configuration files: {}")?;
+ let triage_engine = on_error!(
+ triage_shim::TriageLib::new(configuration),
+ "Failed to parse Detect configuration files: {}"
+ )?;
+ info!("Loaded and parsed .triage files");
+ let selectors = triage_engine.selectors();
+ let mut diagnostic_source = diagnostics::DiagnosticFetcher::create(selectors)?;
+ let snapshot_service = snapshot::CrashReportHandlerBuilder::new().build()?;
+ let system_time = UtcTime::new();
+ let mut delay_tracker = DelayTracker::new(&system_time, mode);
+
+ // Start the first scan as soon as the program starts, via the "missed deadline" logic below.
+ let mut next_check_time = fasync::Time::INFINITE_PAST;
+ loop {
+ if next_check_time < fasync::Time::now() {
+ // We missed a deadline, so don't wait at all; start the check. But first
+ // schedule the next check time at now() + check_every.
+ if next_check_time != fasync::Time::INFINITE_PAST {
+ warn!(
+ "Missed diagnostic check deadline {:?} by {:?} nanos",
+ next_check_time,
+ fasync::Time::now() - next_check_time
+ );
+ }
+ next_check_time = fasync::Time::now() + check_every;
+ } else {
+ // Wait until time for the next check.
+ fasync::Timer::new(next_check_time).await;
+ // Now it should be approximately next_check_time o'clock. To avoid drift from
+ // delays, calculate the next check time by adding check_every to the current
+ // next_check_time.
+ next_check_time += check_every;
+ }
+ let diagnostics = diagnostic_source.get_diagnostics().await;
+ let diagnostics = match diagnostics {
+ Ok(diagnostics) => diagnostics,
+ Err(e) => {
+ error!("Fetching diagnostics failed: {}", e);
+ continue;
+ }
+ };
+ let snapshot_requests = triage_engine.evaluate(diagnostics);
+ for snapshot in snapshot_requests.into_iter() {
+ if delay_tracker.ok_to_send(&snapshot) {
+ let signature = format!("{}{}", SIGNATURE_PREFIX, snapshot.signature);
+ if let Err(e) = snapshot_service.request_snapshot(SnapshotRequest::new(signature)) {
+ error!("Snapshot request failed: {}", e);
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn verify_appropriate_check_interval() -> Result<(), Error> {
+ let error_a = Some("a".to_string());
+ let error_empty = Some("".to_string());
+ let raw_1 = Some("1".to_string());
+ let raw_1_result = zx::Duration::from_nanos(1);
+ let short_time = Some(format!("Nanos({})", MINIMUM_CHECK_TIME_NANOS - 1));
+ let short_time_result = zx::Duration::from_nanos(MINIMUM_CHECK_TIME_NANOS - 1);
+ let minimum_time = Some(format!("Nanos({})", MINIMUM_CHECK_TIME_NANOS));
+ let minimum_time_result = zx::Duration::from_nanos(MINIMUM_CHECK_TIME_NANOS);
+ let long_time = Some(format!("Nanos({})", MINIMUM_CHECK_TIME_NANOS + 1));
+ let long_time_result = zx::Duration::from_nanos(MINIMUM_CHECK_TIME_NANOS + 1);
+
+ assert!(appropriate_check_interval(&error_a, &Mode::Test).is_err());
+ assert!(appropriate_check_interval(&error_empty, &Mode::Test).is_err());
+ assert_eq!(appropriate_check_interval(&None, &Mode::Test)?, minimum_time_result);
+ assert_eq!(appropriate_check_interval(&raw_1, &Mode::Test)?, raw_1_result);
+ assert_eq!(appropriate_check_interval(&short_time, &Mode::Test)?, short_time_result);
+ assert_eq!(appropriate_check_interval(&minimum_time, &Mode::Test)?, minimum_time_result);
+ assert_eq!(appropriate_check_interval(&long_time, &Mode::Test)?, long_time_result);
+
+ assert!(appropriate_check_interval(&error_a, &Mode::Production).is_err());
+ assert!(appropriate_check_interval(&error_empty, &Mode::Production).is_err());
+ assert_eq!(appropriate_check_interval(&None, &Mode::Production)?, minimum_time_result);
+ assert!(appropriate_check_interval(&raw_1, &Mode::Production).is_err());
+ assert!(appropriate_check_interval(&short_time, &Mode::Production).is_err());
+ assert_eq!(
+ appropriate_check_interval(&minimum_time, &Mode::Production)?,
+ minimum_time_result
+ );
+ assert_eq!(appropriate_check_interval(&long_time, &Mode::Test)?, long_time_result);
+ assert_eq!(appropriate_check_interval(&long_time, &Mode::Production)?, long_time_result);
+ Ok(())
+ }
+}
diff --git a/src/diagnostics/detect/src/snapshot.rs b/src/diagnostics/detect/src/snapshot.rs
new file mode 100644
index 0000000..7e63ce4
--- /dev/null
+++ b/src/diagnostics/detect/src/snapshot.rs
@@ -0,0 +1,327 @@
+// Copyright 2020 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.
+
+// Triggers a snapshot via FIDL
+
+use log::{error, warn};
+
+const CRASH_PROGRAM_NAME: &str = "triage_detect";
+
+#[derive(Debug)]
+pub struct SnapshotRequest {
+ signature: String,
+}
+
+impl SnapshotRequest {
+ pub fn new(signature: String) -> SnapshotRequest {
+ SnapshotRequest { signature }
+ }
+}
+
+// Code shamelessly stolen and slightly adapted from
+// garnet/bin/power_manager/src/crash_report_handler.rs
+
+use anyhow::{anyhow, format_err, Error};
+//use async_trait::async_trait;
+use fidl_fuchsia_feedback as fidl_feedback;
+use fuchsia_async as fasync;
+use futures::channel::mpsc;
+use futures::stream::StreamExt;
+use std::cell::RefCell;
+use std::rc::Rc;
+
+/// CrashReportHandler
+///
+/// Summary: Provides a mechanism for filing crash reports.
+///
+/// FIDL dependencies:
+/// - fuchsia.feedback.CrashReporter: CrashReportHandler uses this protocol to communicate
+/// with the CrashReporter service in order to file crash reports.
+///
+
+/// Path to the CrashReporter service.
+const CRASH_REPORTER_SVC: &'static str = "/svc/fuchsia.feedback.CrashReporter";
+
+/// The maximum number of pending crash report requests. This is needed because the FIDL API to file
+/// a crash report does not return until the crash report has been fully generated, which can take
+/// many seconds. Supporting pending crash reports means Detect can file
+/// a new crash report for any other reason within that window, but the CrashReportHandler will
+/// handle rate limiting to the CrashReporter service.
+const MAX_PENDING_CRASH_REPORTS: usize = 10;
+
+/// A builder for constructing the CrashReportHandler node.
+pub struct CrashReportHandlerBuilder {
+ proxy: Option<fidl_feedback::CrashReporterProxy>,
+ max_pending_crash_reports: usize,
+}
+
+// This function is from fuchsia-mirror/garnet/bin/power_manager/src/utils.rs
+/// Create and connect a FIDL proxy to the service at `path`
+fn connect_proxy<T: fidl::endpoints::ServiceMarker>(
+ path: &String,
+) -> Result<T::Proxy, anyhow::Error> {
+ let (proxy, server) = fidl::endpoints::create_proxy::<T>()
+ .map_err(|e| anyhow::format_err!("Failed to create proxy: {}", e))?;
+
+ fdio::service_connect(path, server.into_channel())
+ .map_err(|s| anyhow::format_err!("Failed to connect to service at {}: {}", path, s))?;
+ Ok(proxy)
+}
+
+/// Logs an error message if the passed in `result` is an error.
+#[macro_export]
+macro_rules! log_if_err {
+ ($result:expr, $log_prefix:expr) => {
+ if let Err(e) = $result.as_ref() {
+ log::error!("{}: {}", $log_prefix, e);
+ }
+ };
+}
+
+impl CrashReportHandlerBuilder {
+ pub fn new() -> Self {
+ Self { proxy: None, max_pending_crash_reports: MAX_PENDING_CRASH_REPORTS }
+ }
+
+ #[cfg(test)]
+ pub fn with_proxy(mut self, proxy: fidl_feedback::CrashReporterProxy) -> Self {
+ self.proxy = Some(proxy);
+ self
+ }
+
+ #[cfg(test)]
+ pub fn with_max_pending_crash_reports(mut self, max: usize) -> Self {
+ self.max_pending_crash_reports = max;
+ self
+ }
+
+ pub fn build(self) -> Result<Rc<CrashReportHandler>, Error> {
+ // Connect to the CrashReporter service if a proxy wasn't specified
+ let proxy = if self.proxy.is_some() {
+ self.proxy.unwrap()
+ } else {
+ connect_proxy::<fidl_feedback::CrashReporterMarker>(&CRASH_REPORTER_SVC.to_string())?
+ };
+
+ // Set up the crash report sender that runs asynchronously
+ let (channel, receiver) = mpsc::channel(self.max_pending_crash_reports);
+ CrashReportHandler::begin_crash_report_sender(proxy, receiver);
+
+ Ok(Rc::new(CrashReportHandler {
+ channel_size: self.max_pending_crash_reports,
+ crash_report_sender: RefCell::new(channel),
+ }))
+ }
+}
+
+pub struct CrashReportHandler {
+ /// The channel to send new crash report requests to the asynchronous crash report sender
+ /// future. The maximum pending crash reports are implicitly enforced by the channel length.
+ crash_report_sender: RefCell<mpsc::Sender<SnapshotRequest>>,
+ channel_size: usize,
+}
+
+impl CrashReportHandler {
+ /// Default name to use for `program_name` in the crash report. Using "device" here to align
+ /// with other device/hardware crash types that use the same name (brownout, hardware watchdog
+ /// timeout, power cycles, etc.).
+ const DEFAULT_PROGRAM_NAME: &'static str = CRASH_PROGRAM_NAME;
+
+ /// Handle a FileCrashReport message by sending the specified crash report signature over the
+ /// channel to the crash report sender.
+ pub fn request_snapshot(&self, request: SnapshotRequest) -> Result<(), Error> {
+ // Try to send the crash report signature over the channel. If the channel is full, return
+ // an error
+ match self.crash_report_sender.borrow_mut().try_send(request) {
+ Ok(()) => Ok(()),
+ Err(e) if e.is_full() => {
+ warn!("Too many crash reports pending: {}", e);
+ Err(anyhow!("Pending crash reports exceeds max ({})", self.channel_size))
+ }
+ Err(e) => {
+ warn!("Error sending crash report: {}", e);
+ Err(anyhow!("{}", e))
+ }
+ }
+ }
+
+ /// Spawn and detach a future that receives crash report signatures over the channel and uses
+ /// the proxy to send a File FIDL request to the CrashReporter service with the specified
+ /// signatures.
+ fn begin_crash_report_sender(
+ proxy: fidl_feedback::CrashReporterProxy,
+ mut receive_channel: mpsc::Receiver<SnapshotRequest>,
+ ) {
+ fasync::Task::local(async move {
+ while let Some(request) = receive_channel.next().await {
+ log_if_err!(
+ Self::send_crash_report(&proxy, request).await,
+ "Failed to file crash report"
+ );
+ }
+ error!("Crash reporter task ended. Crash reports will no longer be filed. This should not happen.")
+ })
+ .detach();
+ }
+
+ /// Send a File request to the CrashReporter service with the specified crash report signature.
+ async fn send_crash_report(
+ proxy: &fidl_feedback::CrashReporterProxy,
+ payload: SnapshotRequest,
+ ) -> Result<(), Error> {
+ warn!("Filing crash report, signature '{}'", payload.signature);
+ let report = fidl_feedback::CrashReport {
+ program_name: Some(CrashReportHandler::DEFAULT_PROGRAM_NAME.to_string()),
+ specific_report: Some(fidl_feedback::SpecificCrashReport::Generic(
+ fidl_feedback::GenericCrashReport { crash_signature: Some(payload.signature) },
+ )),
+ ..fidl_feedback::CrashReport::empty()
+ };
+
+ let result = proxy.file(report).await.map_err(|e| format_err!("IPC error: {}", e))?;
+ result.map_err(|e| format_err!("Service error: {}", e))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use futures::TryStreamExt;
+ use matches::assert_matches;
+
+ /// Tests that the node responds to the FileCrashReport message and that the expected crash
+ /// report is received by the CrashReporter service.
+ #[fasync::run_singlethreaded(test)]
+ async fn test_crash_report_content() {
+ // The crash report signature to use and verify against
+ let crash_report_signature = "TestCrashReportSignature";
+
+ // Set up the CrashReportHandler node
+ let (proxy, mut stream) =
+ fidl::endpoints::create_proxy_and_stream::<fidl_feedback::CrashReporterMarker>()
+ .unwrap();
+ let crash_report_handler =
+ CrashReportHandlerBuilder::new().with_proxy(proxy).build().unwrap();
+
+ // File a crash report
+ crash_report_handler
+ .request_snapshot(SnapshotRequest::new(crash_report_signature.to_string()))
+ .unwrap();
+
+ // Verify the fake service receives the crash report with expected data
+ if let Ok(Some(fidl_feedback::CrashReporterRequest::File { responder: _, report })) =
+ stream.try_next().await
+ {
+ assert_eq!(
+ report,
+ fidl_feedback::CrashReport {
+ program_name: Some(CRASH_PROGRAM_NAME.to_string()),
+ specific_report: Some(fidl_feedback::SpecificCrashReport::Generic(
+ fidl_feedback::GenericCrashReport {
+ crash_signature: Some(crash_report_signature.to_string())
+ },
+ )),
+ ..fidl_feedback::CrashReport::empty()
+ }
+ );
+ } else {
+ panic!("Did not receive a crash report");
+ }
+ }
+
+ /// Tests that the number of pending crash reports is correctly bounded.
+ #[test]
+ fn test_crash_report_pending_reports() {
+ let mut exec = fasync::Executor::new().unwrap();
+
+ // Set up the proxy/stream and node outside of the large future used below. This way we can
+ // still poll the stream after the future completes.
+ let (proxy, mut stream) =
+ fidl::endpoints::create_proxy_and_stream::<fidl_feedback::CrashReporterMarker>()
+ .unwrap();
+ let crash_report_handler = CrashReportHandlerBuilder::new()
+ .with_proxy(proxy)
+ .with_max_pending_crash_reports(1)
+ .build()
+ .unwrap();
+
+ // Run most of the test logic inside a top level future for better ergonomics
+ exec.run_singlethreaded(async {
+ // Set up the CrashReportHandler node. The request stream is never serviced, so when the
+ // node makes the FIDL call to file the crash report, the call will block indefinitely.
+ // This lets us test the pending crash report counts.
+
+ // The first FileCrashReport should succeed
+ assert_matches!(
+ crash_report_handler
+ .request_snapshot(SnapshotRequest::new("TestCrash1".to_string())),
+ Ok(())
+ );
+
+ // The second FileCrashReport should also succeed because since the first is now in
+ // progress, this is now the first "pending" report request
+ assert_matches!(
+ crash_report_handler
+ .request_snapshot(SnapshotRequest::new("TestCrash2".to_string())),
+ Ok(())
+ );
+
+ // Since the first request has not completed, and there is already one pending request,
+ // this request should fail
+ assert_matches!(
+ crash_report_handler
+ .request_snapshot(SnapshotRequest::new("TestCrash3".to_string())),
+ Err(_)
+ );
+
+ // Verify the signature of the first crash report
+ if let Ok(Some(fidl_feedback::CrashReporterRequest::File { responder, report })) =
+ stream.try_next().await
+ {
+ // Send a reply to allow the node to process the next crash report
+ let _ = responder.send(&mut Ok(()));
+ assert_eq!(
+ report,
+ fidl_feedback::CrashReport {
+ program_name: Some(CRASH_PROGRAM_NAME.to_string()),
+ specific_report: Some(fidl_feedback::SpecificCrashReport::Generic(
+ fidl_feedback::GenericCrashReport {
+ crash_signature: Some("TestCrash1".to_string())
+ },
+ )),
+ ..fidl_feedback::CrashReport::empty()
+ }
+ );
+ } else {
+ panic!("Did not receive a crash report");
+ }
+
+ // Verify the signature of the second crash report
+ if let Ok(Some(fidl_feedback::CrashReporterRequest::File { responder, report })) =
+ stream.try_next().await
+ {
+ // Send a reply to allow the node to process the next crash report
+ let _ = responder.send(&mut Ok(()));
+ assert_eq!(
+ report,
+ fidl_feedback::CrashReport {
+ program_name: Some(CRASH_PROGRAM_NAME.to_string()),
+ specific_report: Some(fidl_feedback::SpecificCrashReport::Generic(
+ fidl_feedback::GenericCrashReport {
+ crash_signature: Some("TestCrash2".to_string())
+ },
+ )),
+ ..fidl_feedback::CrashReport::empty()
+ }
+ );
+ } else {
+ panic!("Did not receive a crash report");
+ }
+ });
+
+ // Verify there are no more crash reports. Use `run_until_stalled` because `next` is
+ // expected to block until a new crash report is ready, which shouldn't happen here.
+ assert!(exec.run_until_stalled(&mut stream.next()).is_pending());
+ }
+}
diff --git a/src/diagnostics/detect/src/triage_shim.rs b/src/diagnostics/detect/src/triage_shim.rs
new file mode 100644
index 0000000..1d3253d
--- /dev/null
+++ b/src/diagnostics/detect/src/triage_shim.rs
@@ -0,0 +1,87 @@
+// Copyright 2020 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.
+
+// Interface to the Triage library.
+
+use {
+ crate::diagnostics::Selectors,
+ anyhow::Error,
+ std::collections::HashMap,
+ triage::{ActionTagDirective, ParseResult, SnapshotTrigger},
+};
+
+pub fn evaluate_int_math(expression: &str) -> Result<i64, Error> {
+ triage::evaluate_int_math(expression)
+}
+
+type ConfigFiles = HashMap<String, String>;
+
+type DiagnosticData = triage::DiagnosticData;
+
+pub struct TriageLib {
+ triage_config: triage::ParseResult,
+}
+
+impl TriageLib {
+ pub fn new(configs: ConfigFiles) -> Result<TriageLib, Error> {
+ let triage_config = ParseResult::new(&configs, &ActionTagDirective::AllowAll)?;
+ Ok(TriageLib { triage_config })
+ }
+
+ pub fn selectors(&self) -> Selectors {
+ Selectors::new().with_inspect_selectors(triage::all_selectors(&self.triage_config))
+ }
+
+ pub fn evaluate(&self, data: Vec<DiagnosticData>) -> Vec<SnapshotTrigger> {
+ triage::snapshots(&data, &self.triage_config)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use maplit::hashmap;
+
+ const CONFIG: &str = r#"{
+ select: {
+ foo: "INSPECT:foo.cm:path/to:leaf",
+ },
+ act: {
+ yes: {
+ type: "Snapshot",
+ trigger: "foo==8",
+ repeat: "Micros(42)",
+ signature: "got-it",
+ },
+ },
+ }"#;
+
+ const INSPECT: &str = r#"[
+ {
+ "moniker": "foo.cm",
+ "payload": {"path": {"to": {"leaf": 8}}}
+ }
+ ]"#;
+
+ #[test]
+ fn library_calls_work() -> Result<(), Error> {
+ fuchsia_syslog::init_with_tags(&["detect"]).unwrap();
+ let configs = hashmap! { "foo.config".to_string() => CONFIG.to_string() };
+ let lib = TriageLib::new(configs)?;
+ let data = vec![DiagnosticData::new(
+ "inspect.json".to_string(),
+ triage::Source::Inspect,
+ INSPECT.to_string(),
+ )?];
+ let expected_trigger =
+ vec![SnapshotTrigger { signature: "got-it".to_string(), interval: 42_000 }];
+
+ assert_eq!(
+ lib.selectors().inspect_selectors,
+ vec!["INSPECT:foo.cm:path/to:leaf".to_string()]
+ );
+ assert_eq!(lib.evaluate(data), expected_trigger);
+ Ok(())
+ }
+}
diff --git a/src/diagnostics/lib/triage/BUILD.gn b/src/diagnostics/lib/triage/BUILD.gn
index b2bd9a1..a3df50a 100644
--- a/src/diagnostics/lib/triage/BUILD.gn
+++ b/src/diagnostics/lib/triage/BUILD.gn
@@ -12,6 +12,7 @@
"//third_party/rust_crates:anyhow",
"//third_party/rust_crates:itertools",
"//third_party/rust_crates:lazy_static",
+ "//third_party/rust_crates:log",
"//third_party/rust_crates:maplit",
"//third_party/rust_crates:nom",
"//third_party/rust_crates:num-derive",
diff --git a/src/diagnostics/lib/triage/src/act.rs b/src/diagnostics/lib/triage/src/act.rs
index 0741cfe..ad4732f 100644
--- a/src/diagnostics/lib/triage/src/act.rs
+++ b/src/diagnostics/lib/triage/src/act.rs
@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#[cfg(target_os = "fuchsia")]
+use log::error;
+
use {
super::{
config::DiagnosticData,
@@ -53,6 +56,7 @@
results: HashMap<String, bool>,
warnings: Vec<String>,
gauges: Vec<String>,
+ snapshots: Vec<SnapshotTrigger>,
sort_gauges: bool,
sub_results: Vec<(String, Box<ActionResults>)>,
}
@@ -63,6 +67,7 @@
results: HashMap::new(),
warnings: Vec::new(),
gauges: Vec::new(),
+ snapshots: Vec::new(),
sort_gauges: true,
sub_results: Vec::new(),
}
@@ -80,6 +85,10 @@
self.gauges.push(gauge);
}
+ pub fn add_snapshot(&mut self, snapshot: SnapshotTrigger) {
+ self.snapshots.push(snapshot);
+ }
+
pub fn set_sort_gauges(&mut self, v: bool) {
self.sort_gauges = v;
}
@@ -105,6 +114,14 @@
}
}
+/// [SnapshotTrigger] is the information needed to generate a request for a crash report.
+/// It can be returned from the library as part of ActionResults.
+#[derive(Debug, Clone, PartialEq)]
+pub struct SnapshotTrigger {
+ pub interval: i64, // zx::Duration but this library has to run on host.
+ pub signature: String,
+}
+
/// [Actions] are stored as a map of maps, both with string keys. The outer key
/// is the namespace for the inner key, which is the name of the [Action].
pub type Actions = HashMap<String, ActionsSchema>;
@@ -121,6 +138,7 @@
pub enum Action {
Warning(Warning),
Gauge(Gauge),
+ Snapshot(Snapshot),
}
/// Action that is triggered if a predicate is met.
@@ -139,6 +157,15 @@
pub tag: Option<String>, // An optional tag to associate with this Action
}
+/// Action that displays percentage of value.
+#[derive(Clone, Debug, Deserialize)]
+pub struct Snapshot {
+ pub trigger: Metric, // Take snapshot when this is true
+ pub repeat: Metric, // Expression evaluating to time delay before repeated triggers
+ pub signature: String, // Sent in the crash report
+ // There's no tag option because snapshot conditions are always news worth seeing.
+}
+
impl Gauge {
pub fn get_formatted_value(&self, metric_value: MetricValue) -> String {
match metric_value {
@@ -162,6 +189,7 @@
match self {
Action::Warning(action) => action.tag.clone(),
Action::Gauge(action) => action.tag.clone(),
+ Action::Snapshot(_) => None,
}
}
}
@@ -182,6 +210,7 @@
match action {
Action::Warning(warning) => self.update_warnings(warning, namespace, name),
Action::Gauge(gauge) => self.update_gauges(gauge, namespace, name),
+ Action::Snapshot(snapshot) => self.update_snapshots(snapshot, namespace, name),
};
}
}
@@ -189,6 +218,18 @@
&self.action_results
}
+ /// Evaluate and return snapshots. Consume self.
+ pub fn into_snapshots(mut self) -> Vec<SnapshotTrigger> {
+ for (namespace, actions) in self.actions.iter() {
+ for (name, action) in actions.iter() {
+ if let Action::Snapshot(snapshot) = action {
+ self.update_snapshots(snapshot, namespace, name)
+ }
+ }
+ }
+ self.action_results.snapshots
+ }
+
/// Update warnings if condition is met.
fn update_warnings(&mut self, action: &Warning, namespace: &String, name: &String) {
let was_triggered = match self.metric_state.eval_action_metric(namespace, &action.trigger) {
@@ -213,6 +254,53 @@
self.action_results.set_result(&format!("{}::{}", namespace, name), was_triggered);
}
+ /// Update snapshots if condition is met.
+ fn update_snapshots(&mut self, action: &Snapshot, namespace: &str, name: &str) {
+ let was_triggered = match self.metric_state.eval_action_metric(namespace, &action.trigger) {
+ MetricValue::Bool(true) => {
+ let interval = self.metric_state.eval_action_metric(namespace, &action.repeat);
+ match interval {
+ MetricValue::Int(interval) => {
+ let signature = action.signature.clone();
+ let output = SnapshotTrigger { interval, signature };
+ self.action_results.add_snapshot(output);
+ true
+ }
+ _ => {
+ self.action_results.add_warning(format!(
+ "Bad interval in config '{}': {:?}",
+ namespace, interval
+ ));
+ #[cfg(target_os = "fuchsia")]
+ error!("Bad interval in config '{}': {:?}", namespace, interval);
+ false
+ }
+ }
+ }
+ MetricValue::Bool(false) => false,
+ MetricValue::Missing(reason) => {
+ #[cfg(target_os = "fuchsia")]
+ error!("Snapshot trigger was missing: {}", reason);
+ self.action_results
+ .add_warning(format!("[MISSING] In config '{}': {}", namespace, reason));
+ false
+ }
+ other => {
+ #[cfg(target_os = "fuchsia")]
+ error!(
+ "[ERROR] Unexpected value type in config '{}' (need boolean): {}",
+ namespace, other
+ );
+ self.action_results.add_warning(format!(
+ "[ERROR] Unexpected value type in config '{}' (need boolean): {}",
+ namespace, other
+ ));
+ false
+ }
+ };
+ self.action_results.set_result(&format!("{}::{}", namespace, name), was_triggered);
+ }
+
/// Update gauges.
fn update_gauges(&mut self, action: &Gauge, namespace: &String, name: &String) {
let value = self.metric_state.eval_action_metric(namespace, &action.value);
@@ -225,7 +313,9 @@
use {
super::*,
crate::config::Source,
- crate::metrics::{Metric, Metrics},
+ crate::metrics::{fetch::SelectorString, Metric, Metrics},
+ anyhow::Error,
+ std::convert::TryFrom,
};
/// Tells whether any of the stored values include a substring.
@@ -385,4 +475,51 @@
action_context.action_results.get_warnings()
);
}
+
+ #[test]
+ fn snapshots_update_correctly() -> Result<(), Error> {
+ let metrics = Metrics::new();
+ let actions = Actions::new();
+ let data = vec![];
+ let mut action_context = ActionContext::new(&metrics, &actions, &data);
+ let selector =
+ Metric::Selector(SelectorString::try_from("INSPECT:foo:bar:baz".to_string())?);
+ let true_value = Metric::Eval("1==1".to_string());
+ let false_value = Metric::Eval("1==2".to_string());
+ let five_value = Metric::Eval("5".to_string());
+ let foo_value = Metric::Eval("'foo'".to_string());
+ let missing_value = Metric::Eval("foo".to_string());
+ let snapshot_5_sig = SnapshotTrigger { interval: 5, signature: "signature".to_string() };
+ // Tester re-uses the same action_context, so results will accumulate.
+ macro_rules! tester {
+ ($trigger:expr, $repeat:expr, $func:expr) => {
+ let selector_interval_action = Snapshot {
+ trigger: $trigger.clone(),
+ repeat: $repeat.clone(),
+ signature: "signature".to_string(),
+ };
+ action_context.update_snapshots(&selector_interval_action, "", "");
+ assert!($func(&action_context.action_results.snapshots));
+ };
+ }
+ type VT = Vec<SnapshotTrigger>;
+
+ // Verify it doesn't crash on bad inputs
+ tester!(true_value, selector, |s: &VT| s.is_empty());
+ tester!(true_value, foo_value, |s: &VT| s.is_empty());
+ tester!(true_value, missing_value, |s: &VT| s.is_empty());
+ tester!(selector, five_value, |s: &VT| s.is_empty());
+ tester!(foo_value, five_value, |s: &VT| s.is_empty());
+ tester!(five_value, five_value, |s: &VT| s.is_empty());
+ tester!(missing_value, five_value, |s: &VT| s.is_empty());
+ assert_eq!(action_context.action_results.warnings.len(), 7);
+ // False trigger shouldn't add a result
+ tester!(false_value, five_value, |s: &VT| s.is_empty());
+ tester!(true_value, five_value, |s| s == &vec![snapshot_5_sig.clone()]);
+ // We can have more than one of the same trigger in the results.
+ tester!(true_value, five_value, |s| s
+ == &vec![snapshot_5_sig.clone(), snapshot_5_sig.clone()]);
+ assert_eq!(action_context.action_results.warnings.len(), 7);
+ Ok(())
+ }
}
diff --git a/src/diagnostics/lib/triage/src/lib.rs b/src/diagnostics/lib/triage/src/lib.rs
index b4a1d98..04e836d 100644
--- a/src/diagnostics/lib/triage/src/lib.rs
+++ b/src/diagnostics/lib/triage/src/lib.rs
@@ -2,7 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-use {crate::act::ActionContext, anyhow::Error};
+use {
+ crate::act::ActionContext,
+ crate::metrics::{MetricState, MetricValue},
+ anyhow::{bail, Error},
+};
pub(crate) mod act; // Perform appropriate actions.
pub(crate) mod config; // Read the config file(s) for metric and action specs.
@@ -11,7 +15,7 @@
pub(crate) mod result_format; // Formats the triage results.
pub(crate) mod validate; // Check config - including that metrics/triggers work correctly.
-pub use act::ActionResults;
+pub use act::{ActionResults, SnapshotTrigger};
pub use config::{ActionTagDirective, DiagnosticData, ParseResult, Source};
pub use result_format::ActionResultFormatter;
@@ -25,3 +29,24 @@
ActionContext::new(&parse_result.metrics, &parse_result.actions, diagnostic_data);
Ok(action_context.process().clone())
}
+
+pub fn snapshots(data: &Vec<DiagnosticData>, parse_result: &ParseResult) -> Vec<SnapshotTrigger> {
+ let evaluator = ActionContext::new(&parse_result.metrics, &parse_result.actions, data);
+ evaluator.into_snapshots()
+}
+
+pub fn all_selectors(parse: &ParseResult) -> Vec<String> {
+ parse.all_selectors()
+}
+
+pub fn evaluate_int_math(expression: &str) -> Result<i64, Error> {
+ match MetricState::evaluate_math(&config::parse::parse_expression(expression)?) {
+ MetricValue::Int(i) => Ok(i),
+ MetricValue::Float(f) => match MetricState::safe_float_to_int(f) {
+ Some(i) => Ok(i),
+ None => bail!("Non-numeric float result {}", f),
+ },
+ MetricValue::Missing(msg) => bail!("Eval error: {}", msg),
+ bad_type => bail!("Non-numeric result: {:?}", bad_type),
+ }
+}
diff --git a/src/diagnostics/lib/triage/src/metrics.rs b/src/diagnostics/lib/triage/src/metrics.rs
index cac6273..3679fa8 100644
--- a/src/diagnostics/lib/triage/src/metrics.rs
+++ b/src/diagnostics/lib/triage/src/metrics.rs
@@ -9,7 +9,7 @@
super::config::{self, DataFetcher, DiagnosticData, Source},
fetch::{InspectFetcher, KeyValueFetcher, SelectorString, SelectorType, TextFetcher},
fuchsia_inspect_node_hierarchy::{ArrayContent, Property as DiagnosticProperty},
- injectable_time::TimeSource,
+ injectable_time::{FakeTime, TimeSource},
lazy_static::lazy_static,
serde::{Deserialize, Deserializer},
serde_json::Value as JsonValue,
@@ -17,9 +17,6 @@
variable::VariableName,
};
-#[cfg(test)]
-use injectable_time::FakeTime;
-
/// The contents of a single Metric. Metrics produce a value for use in Actions or other Metrics.
#[derive(Clone, Debug)]
pub enum Metric {
@@ -60,6 +57,10 @@
/// Contains all the information needed to look up and evaluate a Metric - other
/// [Metric]s that may be referred to, and a source of input values to calculate on.
+///
+/// Note: MetricState uses a single Now() value for all evaluations. If a MetricState is
+/// retained and used for multiple evaluations at different times, provide a way to update
+/// the `now` field.
pub struct MetricState<'a> {
pub metrics: &'a Metrics,
pub fetcher: Fetcher<'a>,
@@ -651,7 +652,6 @@
}
/// Evaluate an Expression which contains only base values, not referring to other Metrics.
- #[cfg(test)]
pub fn evaluate_math(e: &Expression) -> MetricValue {
let values = HashMap::new();
let fetcher = Fetcher::TrialData(TrialDataFetcher::new(&values));
@@ -896,7 +896,7 @@
}
}
- fn safe_float_to_int(float: f64) -> Option<i64> {
+ pub fn safe_float_to_int(float: f64) -> Option<i64> {
if !float.is_finite() {
return None;
}
diff --git a/src/diagnostics/lib/triage/src/validate.rs b/src/diagnostics/lib/triage/src/validate.rs
index 0c9ac9a..02eb309 100644
--- a/src/diagnostics/lib/triage/src/validate.rs
+++ b/src/diagnostics/lib/triage/src/validate.rs
@@ -113,23 +113,26 @@
println!("Action {} not found in trial {}", action_name, trial_name);
return true;
}
- Some(action) => match action {
- Action::Warning(properties) => {
- match metric_state.eval_action_metric(namespace, &properties.trigger) {
- MetricValue::Bool(actual) if actual == expected => return false,
- other => {
- println!(
- "Test {} failed: trigger '{}' of action {} returned {:?}, expected {}",
- trial_name, properties.trigger, action_name, other, expected
- );
- return true;
- }
+ Some(action) => {
+ let trigger = match action {
+ Action::Warning(properties) => &properties.trigger,
+ Action::Snapshot(properties) => &properties.trigger,
+ _ => {
+ println!("Action {:?} cannot be tested", action);
+ return true;
+ }
+ };
+ match metric_state.eval_action_metric(namespace, trigger) {
+ MetricValue::Bool(actual) if actual == expected => return false,
+ other => {
+ println!(
+ "Test {} failed: trigger '{}' of action {} returned {:?}, expected {}",
+ trial_name, trigger, action_name, other, expected
+ );
+ return true;
}
}
- _ => {
- return false;
- }
- },
+ }
},
}
}
diff --git a/src/diagnostics/validator/inspect/src/main.rs b/src/diagnostics/validator/inspect/src/main.rs
index d8c2b14..4c83414 100644
--- a/src/diagnostics/validator/inspect/src/main.rs
+++ b/src/diagnostics/validator/inspect/src/main.rs
@@ -13,14 +13,12 @@
anyhow::{format_err, Error},
argh::FromArgs,
fidl_test_inspect_validate as validate, fuchsia_async as fasync, fuchsia_syslog as syslog,
- log::*,
serde::Serialize,
std::str::FromStr,
};
fn init_syslog() {
syslog::init_with_tags(&[]).expect("should not fail");
- debug!("Driver did init logger");
}
/// Validate Inspect VMO formats written by 'puppet' programs controlled by