Refactor starter_cmd a bit

This patch switches the order of arguments for starter_cmd. In
particular, the subcommand now comes first and then you specify the
name of the application.

For example:

  $ starter_cmd start <app>
  $ starter_cmd stop <app>
  $ starter_cmd status <app>

This patch also enhances the "status" command to work without any
arguments. In this case, it will print out a high-level status for all
applications.

The error reporting on incorrect arguments to starter_cmd should also
be quite a bit improved with this change.

Change-Id: I4c80e6662b6f3d3e630a4be26d579e1166a89643
diff --git a/aos/starter/BUILD b/aos/starter/BUILD
index 8ad3dbf..de6e249 100644
--- a/aos/starter/BUILD
+++ b/aos/starter/BUILD
@@ -96,6 +96,7 @@
     deps = [
         ":starter_rpc_lib",
         "@com_github_google_glog//:glog",
+        "@com_google_absl//absl/strings:str_format",
     ],
 )
 
diff --git a/aos/starter/starter_cmd.cc b/aos/starter/starter_cmd.cc
index d076afc..1381fa3 100644
--- a/aos/starter/starter_cmd.cc
+++ b/aos/starter/starter_cmd.cc
@@ -1,7 +1,9 @@
 #include <chrono>
+#include <functional>
 #include <iostream>
 #include <unordered_map>
 
+#include "absl/strings/str_format.h"
 #include "aos/init.h"
 #include "aos/json_to_flatbuffer.h"
 #include "gflags/gflags.h"
@@ -9,36 +11,53 @@
 
 DEFINE_string(config, "./config.json", "File path of aos configuration");
 
-static const std::unordered_map<std::string, aos::starter::Command> kCommands{
-    {"start", aos::starter::Command::START},
-    {"stop", aos::starter::Command::STOP},
-    {"restart", aos::starter::Command::RESTART}};
+namespace {
 
-int main(int argc, char **argv) {
-  aos::InitGoogle(&argc, &argv);
+static const std::unordered_map<std::string, aos::starter::Command>
+    kCommandConversions{{"start", aos::starter::Command::START},
+                        {"stop", aos::starter::Command::STOP},
+                        {"restart", aos::starter::Command::RESTART}};
 
-  CHECK(argc == 3) << "Invalid number of command arguments";
-
-  const std::string application_name = argv[1];
-  const std::string command_str = argv[2];
-
-  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
-      aos::configuration::ReadConfig(FLAGS_config);
-
-  if (command_str == "status") {
-    auto status = aos::starter::GetStatus(application_name, &config.message());
+bool GetStarterStatus(int argc, char **argv, const aos::Configuration *config) {
+  if (argc == 1) {
+    // Print status for all processes.
+    auto status = aos::starter::GetStarterStatus(config);
+    for (const aos::starter::ApplicationStatus *app_status :
+         *status.message().statuses()) {
+      absl::PrintF("%-30s %s\n", app_status->name()->string_view(),
+                   aos::starter::EnumNameState(app_status->state()));
+    }
+  } else if (argc == 2) {
+    // Print status for the specified process.
+    const char *application_name = argv[1];
+    auto status = aos::starter::GetStatus(application_name, config);
     std::cout << aos::FlatbufferToJson(&status.message()) << '\n';
+  } else {
+    LOG(ERROR) << "The \"status\" command requires zero or one arguments.";
+    return true;
+  }
+  return false;
+}
 
-    return 0;
+bool InteractWithProgram(int argc, char **argv,
+                         const aos::Configuration *config) {
+  const char *command_string = argv[0];
+
+  if (argc != 2) {
+    LOG(ERROR) << "The \"" << command_string
+               << "\" command requires an application name as an argument.";
+    return true;
   }
 
-  const auto command_search = kCommands.find(command_str);
-  CHECK(command_search != kCommands.end())
-      << "Invalid command \"" << command_str << "\"";
-  const aos::starter::Command command = command_search->second;
+  const auto command_search = kCommandConversions.find(command_string);
+  CHECK(command_search != kCommandConversions.end())
+      << "Internal error: \"" << command_string
+      << "\" is not in kCommandConversions.";
 
-  if (aos::starter::SendCommandBlocking(command, application_name,
-                                        &config.message(),
+  const aos::starter::Command command = command_search->second;
+  const char *application_name = argv[1];
+
+  if (aos::starter::SendCommandBlocking(command, application_name, config,
                                         std::chrono::seconds(3))) {
     switch (command) {
       case aos::starter::Command::START:
@@ -52,6 +71,57 @@
         break;
     }
   } else {
-    std::cout << "Failed to " << command_str << ' ' << application_name << '\n';
+    std::cout << "Failed to " << command_string << ' ' << application_name
+              << '\n';
   }
+  return false;
+}
+
+// This is the set of subcommands we support. Each subcommand accepts argc and
+// argv from its own point of view. So argv[0] is always the name of the
+// subcommand. argv[1] and up are the arguments to the subcommand.
+// The subcommand returns true if there was an error parsing the command line
+// arguments. It returns false when the command line arguments are parsed
+// successfully.
+static const std::unordered_map<
+    std::string, std::function<bool(int argc, char **argv,
+                                    const aos::Configuration *config)>>
+    kCommands{
+        {"status", GetStarterStatus},
+        {"start", InteractWithProgram},
+        {"stop", InteractWithProgram},
+        {"restart", InteractWithProgram},
+    };
+
+}  // namespace
+
+int main(int argc, char **argv) {
+  aos::InitGoogle(&argc, &argv);
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(FLAGS_config);
+
+  bool parsing_failed = false;
+
+  if (argc < 2) {
+    parsing_failed = true;
+  } else {
+    const char *command = argv[1];
+    auto it = kCommands.find(command);
+    if (it == kCommands.end()) {
+      parsing_failed = true;
+    } else {
+      parsing_failed = it->second(argc - 1, argv + 1, &config.message());
+    }
+  }
+
+  if (parsing_failed) {
+    LOG(ERROR) << "Parsing failed. Valid commands are:";
+    for (auto entry: kCommands) {
+      LOG(ERROR) << " - " << entry.first;
+    }
+    return 1;
+  }
+
+  return 0;
 }
diff --git a/aos/starter/starter_rpc_lib.cc b/aos/starter/starter_rpc_lib.cc
index efb4042..89fc2c2 100644
--- a/aos/starter/starter_rpc_lib.cc
+++ b/aos/starter/starter_rpc_lib.cc
@@ -127,5 +127,19 @@
                       aos::starter::ApplicationStatus>::Empty();
 }
 
+const aos::FlatbufferVector<aos::starter::Status> GetStarterStatus(
+    const aos::Configuration *config) {
+  ShmEventLoop event_loop(config);
+  event_loop.SkipAosLog();
+
+  auto status_fetcher = event_loop.MakeFetcher<aos::starter::Status>("/aos");
+  status_fetcher.Fetch();
+  if (status_fetcher) {
+    return status_fetcher.CopyFlatBuffer();
+  } else {
+    return FlatbufferVector<aos::starter::Status>::Empty();
+  }
+}
+
 }  // namespace starter
 }  // namespace aos
diff --git a/aos/starter/starter_rpc_lib.h b/aos/starter/starter_rpc_lib.h
index 57c9e6b..152016a 100644
--- a/aos/starter/starter_rpc_lib.h
+++ b/aos/starter/starter_rpc_lib.h
@@ -30,6 +30,11 @@
 const aos::FlatbufferDetachedBuffer<aos::starter::ApplicationStatus> GetStatus(
     std::string_view name, const aos::Configuration *config);
 
+// Fetches the entire status message of starter. Creates a temporary event loop
+// from the provided config for fetching.
+const aos::FlatbufferVector<aos::starter::Status> GetStarterStatus(
+    const aos::Configuration *config);
+
 }  // namespace starter
 }  // namespace aos