Add version string to AOS timing report

This sets us up to more readily indicate what version of an application
crashed when the starter observes an application crash. This also will
help us to identify situations where there are mixed versions of
binaries on sprayers.

Change-Id: I4d265552d6c3cadd43b834f343da50aec46be4ef
Signed-off-by: James Kuszmaul <james.kuszmaul@bluerivertech.com>
diff --git a/aos/events/event_loop.cc b/aos/events/event_loop.cc
index 072630f..2122814 100644
--- a/aos/events/event_loop.cc
+++ b/aos/events/event_loop.cc
@@ -35,6 +35,8 @@
 }
 }  // namespace
 
+std::optional<std::string> EventLoop::default_version_string_;
+
 std::pair<SharedSpan, absl::Span<uint8_t>> MakeSharedSpan(size_t size) {
   AlignedOwningSpan *const span = reinterpret_cast<AlignedOwningSpan *>(
       malloc(sizeof(AlignedOwningSpan) + size + kChannelDataAlignment - 1));
@@ -146,7 +148,8 @@
 PhasedLoopHandler::~PhasedLoopHandler() {}
 
 EventLoop::EventLoop(const Configuration *configuration)
-    : timing_report_(flatbuffers::DetachedBuffer()),
+    : version_string_(default_version_string_),
+      timing_report_(flatbuffers::DetachedBuffer()),
       configuration_(configuration) {}
 
 EventLoop::~EventLoop() {
@@ -472,8 +475,13 @@
   flatbuffers::Offset<flatbuffers::String> name_offset =
       fbb.CreateString(name());
 
+  const flatbuffers::Offset<flatbuffers::String> version_offset =
+      version_string_.has_value() ? fbb.CreateString(version_string_.value())
+                                  : flatbuffers::Offset<flatbuffers::String>();
+
   timing::Report::Builder report_builder(fbb);
   report_builder.add_name(name_offset);
+  report_builder.add_version(version_offset);
   report_builder.add_pid(GetTid());
   if (timer_offsets.size() > 0) {
     report_builder.add_timers(timers_offset);
@@ -642,6 +650,18 @@
 
 cpu_set_t EventLoop::DefaultAffinity() { return aos::DefaultAffinity(); }
 
+void EventLoop::SetDefaultVersionString(std::string_view version) {
+  default_version_string_ = version;
+}
+
+void EventLoop::SetVersionString(std::string_view version) {
+  CHECK(!is_running())
+      << ": Can't do things that might alter the timing report while running.";
+  version_string_ = version;
+
+  UpdateTimingReport();
+}
+
 void WatcherState::set_timing_report(timing::Watcher *watcher) {
   watcher_ = watcher;
   if (!watcher) {
diff --git a/aos/events/event_loop.fbs b/aos/events/event_loop.fbs
index c0aaf19..15c6149 100644
--- a/aos/events/event_loop.fbs
+++ b/aos/events/event_loop.fbs
@@ -95,6 +95,11 @@
   // Identifier for the event loop.  This should change every time a process
   // gets restarted.
   pid:int (id: 1);
+  // Version string associated with this application. This can be empty or
+  // anything (an actual version, a git sha, etc.). This provides a convenient
+  // way for applications to self-report their version in a way that gets
+  // logged.
+  version:string (id: 8);
 
   // List of statistics for each watcher, sender, fetcher, timer, and
   // phased loop.
diff --git a/aos/events/event_loop.h b/aos/events/event_loop.h
index 3926e26..1ed4337 100644
--- a/aos/events/event_loop.h
+++ b/aos/events/event_loop.h
@@ -808,6 +808,16 @@
   // Returns the boot UUID.
   virtual const UUID &boot_uuid() const = 0;
 
+  // Sets the version string that will be used in any newly constructed
+  // EventLoop objects. This can be overridden for individual EventLoop's by
+  // calling EventLoop::SetVersionString(). The version string is populated into
+  // the timing report message. Makes a copy of the provided string_view.
+  static void SetDefaultVersionString(std::string_view version);
+
+  // Overrides the version string for this event loop. Makes a copy of the
+  // provided string_view.
+  void SetVersionString(std::string_view version);
+
  protected:
   // Sets the name of the event loop.  This is the application name.
   virtual void set_name(const std::string_view name) = 0;
@@ -898,6 +908,13 @@
  private:
   virtual pid_t GetTid() = 0;
 
+  // Default version string to be used in the timing report for any newly
+  // created EventLoop objects.
+  static std::optional<std::string> default_version_string_;
+
+  // Timing report version string for this event loop.
+  std::optional<std::string> version_string_;
+
   FlatbufferDetachedBuffer<timing::Report> timing_report_;
 
   ::std::atomic<bool> is_running_{false};
diff --git a/aos/events/event_loop_param_test.cc b/aos/events/event_loop_param_test.cc
index ba2d426..c861284 100644
--- a/aos/events/event_loop_param_test.cc
+++ b/aos/events/event_loop_param_test.cc
@@ -1488,6 +1488,84 @@
   }
 }
 
+// Test that setting a default version string results in it getting populated
+// correctly.
+TEST_P(AbstractEventLoopTest, DefaultVersionStringInTimingReport) {
+  gflags::FlagSaver flag_saver;
+  FLAGS_timing_report_ms = 1000;
+
+  EventLoop::SetDefaultVersionString("default_version_string");
+
+  auto loop = MakePrimary();
+
+  Fetcher<timing::Report> report_fetcher =
+      loop->MakeFetcher<timing::Report>("/aos");
+
+  TimerHandler *exit_timer = loop->AddTimer([this]() { Exit(); });
+  loop->OnRun([exit_timer, &loop, &report_fetcher]() {
+    report_fetcher.Fetch();
+    exit_timer->Schedule(loop->monotonic_now() + std::chrono::seconds(2));
+  });
+
+  Run();
+
+  bool found_primary_report = false;
+  while (report_fetcher.FetchNext()) {
+    if (report_fetcher->name()->string_view() == "primary") {
+      found_primary_report = true;
+      EXPECT_EQ("default_version_string",
+                report_fetcher->version()->string_view());
+    } else {
+      FAIL() << report_fetcher->name()->string_view();
+    }
+  }
+
+  if (do_timing_reports() == DoTimingReports::kYes) {
+    EXPECT_TRUE(found_primary_report);
+  } else {
+    EXPECT_FALSE(found_primary_report);
+  }
+}
+
+// Test that overriding the default version string results in it getting
+// populated correctly.
+TEST_P(AbstractEventLoopTest, OverrideDersionStringInTimingReport) {
+  gflags::FlagSaver flag_saver;
+  FLAGS_timing_report_ms = 1000;
+
+  EventLoop::SetDefaultVersionString("default_version_string");
+
+  auto loop = MakePrimary();
+  loop->SetVersionString("override_version");
+
+  Fetcher<timing::Report> report_fetcher =
+      loop->MakeFetcher<timing::Report>("/aos");
+
+  TimerHandler *exit_timer = loop->AddTimer([this]() { Exit(); });
+  loop->OnRun([exit_timer, &loop, &report_fetcher]() {
+    report_fetcher.Fetch();
+    exit_timer->Schedule(loop->monotonic_now() + std::chrono::seconds(2));
+  });
+
+  Run();
+
+  bool found_primary_report = false;
+  while (report_fetcher.FetchNext()) {
+    if (report_fetcher->name()->string_view() == "primary") {
+      found_primary_report = true;
+      EXPECT_EQ("override_version", report_fetcher->version()->string_view());
+    } else {
+      FAIL() << report_fetcher->name()->string_view();
+    }
+  }
+
+  if (do_timing_reports() == DoTimingReports::kYes) {
+    EXPECT_TRUE(found_primary_report);
+  } else {
+    EXPECT_FALSE(found_primary_report);
+  }
+}
+
 // Verify that we can change a timer's parameters during execution.
 TEST_P(AbstractEventLoopTest, TimerChangeParameters) {
   auto loop = MakePrimary();
@@ -3409,6 +3487,19 @@
                "May only send the buffer detached from this Sender");
 }
 
+// Tests that senders fail when created on the wrong node.
+TEST_P(AbstractEventLoopDeathTest, SetVersionWhileRunning) {
+  auto loop1 = MakePrimary();
+
+  loop1->OnRun([&loop1, this]() {
+    EXPECT_DEATH({ loop1->SetVersionString("abcdef"); },
+                 "timing report while running");
+    Exit();
+  });
+
+  Run();
+}
+
 int TestChannelFrequency(EventLoop *event_loop) {
   return event_loop->GetChannel<TestMessage>("/test")->frequency();
 }
diff --git a/aos/events/logging/multinode_logger_test_lib.h b/aos/events/logging/multinode_logger_test_lib.h
index e207179..e94f626 100644
--- a/aos/events/logging/multinode_logger_test_lib.h
+++ b/aos/events/logging/multinode_logger_test_lib.h
@@ -60,13 +60,13 @@
 };
 
 constexpr std::string_view kCombinedConfigSha1() {
-  return "d018002a9b780d45a69172a1e5dd1d6df49a7c6c63b9bae9125cdc0458ddc6ca";
+  return "55c8aead4cffd4a3880572daa18be9828cca642edfc756bf9dd6f1037369a9fb";
 }
 constexpr std::string_view kSplitConfigSha1() {
-  return "562f80087c0e95d9304127c4cb46962659b4bfc11def84253c67702b4213e6cf";
+  return "df321a9362217f3b3e4a8e3069e27c70e918256ff6aa9bb138a0734c3d9588e3";
 }
 constexpr std::string_view kReloggedSplitConfigSha1() {
-  return "cb560559ee3111d7c67314e3e1a5fd7fc88e8b4cfd9d15ea71c8d1cae1c0480b";
+  return "faf336f121573a5de62e5c0547a8a0a29c5143f4712bef806d348fede87435f2";
 }
 
 LoggerState MakeLoggerState(NodeEventLoopFactory *node,
diff --git a/aos/events/ping.cc b/aos/events/ping.cc
index 83a066b..975f2e4 100644
--- a/aos/events/ping.cc
+++ b/aos/events/ping.cc
@@ -11,6 +11,7 @@
 
 int main(int argc, char **argv) {
   aos::InitGoogle(&argc, &argv);
+  aos::EventLoop::SetDefaultVersionString("ping_version");
 
   aos::FlatbufferDetachedBuffer<aos::Configuration> config =
       aos::configuration::ReadConfig(FLAGS_config);
diff --git a/aos/events/pong.cc b/aos/events/pong.cc
index 6a22b6b..9162567 100644
--- a/aos/events/pong.cc
+++ b/aos/events/pong.cc
@@ -11,6 +11,7 @@
 
 int main(int argc, char **argv) {
   aos::InitGoogle(&argc, &argv);
+  aos::EventLoop::SetDefaultVersionString("pong_version");
 
   aos::FlatbufferDetachedBuffer<aos::Configuration> config =
       aos::configuration::ReadConfig(FLAGS_config);
diff --git a/aos/events/timing_report_dump_lib.cc b/aos/events/timing_report_dump_lib.cc
index 86936f1..15a8eb9 100644
--- a/aos/events/timing_report_dump_lib.cc
+++ b/aos/events/timing_report_dump_lib.cc
@@ -181,10 +181,16 @@
     LOG(INFO) << "Failed to send " << report.send_failures()
               << " timing report(s) in " << report.name()->string_view();
   }
+  std::string version_string;
+  if (report.has_version()) {
+    version_string =
+        absl::StrCat("version: \"", report.version()->string_view(), "\" ");
+  }
   std::cout << report.name()->string_view() << "[" << report.pid() << "] ("
-            << MaybeNodeName("", event_loop_->node()) << ") ("
-            << event_loop_->context().monotonic_event_time << ","
+            << MaybeNodeName("", event_loop_->node()) << ") " << version_string
+            << "(" << event_loop_->context().monotonic_event_time << ","
             << event_loop_->context().realtime_event_time << "):" << std::endl;
+
   if (report.has_watchers() && report.watchers()->size() > 0) {
     PrintWatchers(&std::cout, *report.watchers());
   }
@@ -314,6 +320,9 @@
   if (accumulated_statistics_.count(map_key) == 0) {
     accumulated_statistics_[map_key].name = raw_report.name()->str();
     accumulated_statistics_[map_key].pid = raw_report.pid();
+    if (raw_report.has_version()) {
+      accumulated_statistics_[map_key].version = raw_report.version()->str();
+    }
   }
 
   timing::ReportT report;