[fidlcat] Integration test for fidlcat.

Also includes support for cleanly shutting down the agent on target
from fidlcat.

Also includes changes to core product configuration to run e2e tests.

Bug: 6515

Change-Id: I04668a31489525341b3d44fc54a00e625dca9d8f
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/376998
Commit-Queue: Jeremy Manson <jeremymanson@google.com>
Reviewed-by: Vincent Belliard <vbelliard@google.com>
Reviewed-by: Brett Wilson <brettw@google.com>
Testability-Review: Brett Wilson <brettw@google.com>
diff --git a/src/tests/end_to_end/fidlcat/BUILD.gn b/src/tests/end_to_end/fidlcat/BUILD.gn
new file mode 100644
index 0000000..414b7e3
--- /dev/null
+++ b/src/tests/end_to_end/fidlcat/BUILD.gn
@@ -0,0 +1,99 @@
+# 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/dart/test.gni")
+import("//build/host.gni")
+import("//build/testing/environments.gni")
+
+_shared_out_dir = get_label_info(":bogus(${target_toolchain})", "root_out_dir")
+
+dart_test("fidlcat_test") {
+  sources = [ "fidlcat_test.dart" ]
+
+  deps = [
+    "//sdk/testing/sl4f/client",
+    "//third_party/dart-pkg/pub/logging",
+    "//third_party/dart-pkg/pub/mockito",
+    "//third_party/dart-pkg/pub/test",
+  ]
+
+  non_dart_deps = [ ":runtime_deps" ]
+
+  args = [ "--data-dir=" + rebase_path(_shared_out_dir) ]
+
+  environments = [
+    # Runs on "main" builders (try and ci) in QEMU environments.
+    qemu_env,
+  ]
+}
+
+# Extract the symbols for the given ELF file from the .build-id directory.
+template("generate_symbols") {
+  assert(defined(invoker.library_label), "Must define 'library_label'")
+  assert(defined(invoker.library_path), "Must define 'library_path'")
+  assert(defined(invoker.output), "Must define 'output'")
+
+  action(target_name) {
+    deps = [ invoker.library_label ]
+    inputs = [ invoker.library_path ]
+    outputs = [ invoker.output ]
+
+    script = "generate_debug.sh"
+
+    args = [
+      "--build-id-dir",
+      rebase_path("$root_build_dir/.build-id"),
+      "--build-id-script",
+      rebase_path("//build/images/elfinfo.py"),
+      "--binary",
+      rebase_path(invoker.library_path),
+      "--output",
+      rebase_path(invoker.output),
+    ]
+  }
+}
+
+generate_symbols("echo_client_cpp_sym") {
+  library_label =
+      "//garnet/examples/fidl/echo_client_cpp:bin($target_toolchain)"
+  library_path = "$_shared_out_dir/echo_client_cpp"
+  output = "${target_gen_dir}/echo_client_cpp.debug"
+}
+
+copy("runtime_deps") {
+  testonly = true
+
+  _data_dir = "$target_gen_dir/runtime_deps"
+
+  sources = [
+    "$_shared_out_dir/fidling/gen/garnet/examples/fidl/services/echo.fidl.json",
+    "$_shared_out_dir/gen/sdk/core.fidl_json.txt",
+    "$host_tools_dir/fidlcat",
+    "${target_gen_dir}/echo_client_cpp.debug",
+  ]
+
+  outputs = [ "$_data_dir/{{source_file_part}}" ]
+
+  metadata = {
+    test_runtime_deps = [ "$_data_dir/fidlcat" ]
+  }
+
+  deps = [
+    ":echo_client_cpp_sym",
+    "//garnet/examples/fidl/echo_client_cpp",
+    "//garnet/examples/fidl/echo_server_cpp",
+    "//garnet/examples/fidl/services:echo",
+    "//garnet/packages/prod:run",
+    "//garnet/packages/tools:sl4f",
+    "//sdk:core_fidl_json($target_toolchain)",
+    "//src/developer/debug/debug_agent",
+    "//tools/fidlcat:fidlcat_host($host_toolchain)",
+  ]
+}
+
+group("tests") {
+  testonly = true
+
+  deps = [ ":fidlcat_test($host_toolchain)" ]
+}
diff --git a/src/tests/end_to_end/fidlcat/OWNERS b/src/tests/end_to_end/fidlcat/OWNERS
new file mode 100644
index 0000000..b04eacb
--- /dev/null
+++ b/src/tests/end_to_end/fidlcat/OWNERS
@@ -0,0 +1,3 @@
+jeremymanson@google.com
+vbelliard@google.com
+*
\ No newline at end of file
diff --git a/src/tests/end_to_end/fidlcat/analysis_options.yaml b/src/tests/end_to_end/fidlcat/analysis_options.yaml
new file mode 100644
index 0000000..05dcaef
--- /dev/null
+++ b/src/tests/end_to_end/fidlcat/analysis_options.yaml
@@ -0,0 +1,5 @@
+# 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.
+
+include: ../../../../topaz/tools/analysis_options.yaml
diff --git a/src/tests/end_to_end/fidlcat/generate_debug.sh b/src/tests/end_to_end/fidlcat/generate_debug.sh
new file mode 100755
index 0000000..557a662
--- /dev/null
+++ b/src/tests/end_to_end/fidlcat/generate_debug.sh
@@ -0,0 +1,44 @@
+#!/bin/sh
+# 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.
+
+# Copies the debug symbol file in the |build-id-dir| directory with the build
+# id of the given |binary| to |output|.  Requires a |build-id-script| to read
+# the build id of |binary|.  The goal is to provide the debug symbols with a
+# filename statically known to the build system, so that it can be depended on
+# explicitly.
+
+set -e
+
+while [ $# != 0 ]; do
+  case "$1" in
+    --output)
+      OUTPUT="$2"
+      shift
+      ;;
+    --build-id-script)
+      BUILD_ID_SCRIPT="$2"
+      shift
+      ;;
+    --build-id-dir)
+      BUILD_ID_DIR="$2"
+      shift
+      ;;
+    --binary)
+      BINARY="$2"
+      shift
+      ;;
+    *)
+      break
+      ;;
+  esac
+  shift
+done
+
+BUILD_ID=$("${BUILD_ID_SCRIPT}" --build-id "${BINARY}")
+
+BUILD_ID_DIR_1=$(printf "${BUILD_ID}" | head -c 2)
+BUILD_ID_DIR_2=$(printf "${BUILD_ID}" | tail -c +3)
+
+/bin/cp "${BUILD_ID_DIR}"/"${BUILD_ID_DIR_1}"/"${BUILD_ID_DIR_2}".debug "${OUTPUT}"
diff --git a/src/tests/end_to_end/fidlcat/pubspec.yaml b/src/tests/end_to_end/fidlcat/pubspec.yaml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/tests/end_to_end/fidlcat/pubspec.yaml
diff --git a/src/tests/end_to_end/fidlcat/test/fidlcat_test.dart b/src/tests/end_to_end/fidlcat/test/fidlcat_test.dart
new file mode 100644
index 0000000..7b3b198
--- /dev/null
+++ b/src/tests/end_to_end/fidlcat/test/fidlcat_test.dart
@@ -0,0 +1,147 @@
+// 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 'dart:convert';
+import 'dart:io'
+    show
+        Directory,
+        File,
+        FileMode,
+        FileSystemException,
+        Platform,
+        Process,
+        ProcessResult;
+
+import 'package:args/args.dart';
+import 'package:logging/logging.dart';
+import 'package:test/test.dart';
+import 'package:sl4f/sl4f.dart' as sl4f;
+
+const _timeout = Timeout(Duration(minutes: 5));
+
+void main(List<String> arguments) {
+  final log = Logger('fidlcat_test');
+
+  /// fuchsia-pkg URL for the debug agent.
+  const String debugAgentUrl =
+      'fuchsia-pkg://fuchsia.com/debug_agent#meta/debug_agent.cmx';
+
+  /// Location of the fidlcat executable.
+  final String fidlcatPath =
+      Platform.script.resolve('runtime_deps/fidlcat').toFilePath();
+
+  /// Location of the IR for the core SDK APIs.
+  String fidlIrPath;
+
+  /// A convenient directory for temporary files
+  Directory tempDir;
+
+  /// The core.fidl_json.txt file passed to this program contains a list of
+  /// FIDL IR files relative to the root build directory.  This test program
+  /// is not run out of the root build directory, so we create a new file
+  /// containing absolute paths.
+  void setPath() {
+    final parser = ArgParser()..addOption('data-dir');
+    final argResults = parser.parse(arguments);
+    final String dataDir = argResults['data-dir'];
+
+    tempDir = Directory.systemTemp.createTempSync();
+
+    final int timestamp = (DateTime.now()).microsecondsSinceEpoch;
+    final String tempPath = tempDir.path;
+    fidlIrPath = '$tempPath/core.fidl_json_processed-$timestamp.txt';
+
+    final outFile = File(fidlIrPath)..createSync(recursive: true);
+
+    final String fidlIrPathInit =
+        Platform.script.resolve('runtime_deps/core.fidl_json.txt').toFilePath();
+    File(fidlIrPathInit)
+        .openRead()
+        .transform(utf8.decoder)
+        .transform(LineSplitter())
+        .forEach((line) => {
+              outFile.writeAsStringSync('$dataDir/$line\n',
+                  mode: FileMode.append)
+            });
+  }
+
+  void cleanup() {
+    try {
+      tempDir.deleteSync(recursive: true);
+    } on FileSystemException {
+      // Do nothing.
+    }
+  }
+
+  sl4f.Sl4f sl4fDriver;
+
+  setUp(() async {
+    sl4fDriver = sl4f.Sl4f.fromEnvironment();
+    await sl4fDriver.startServer();
+    setPath();
+  });
+
+  tearDown(() async {
+    await sl4fDriver.stopServer();
+    sl4fDriver.close();
+    cleanup();
+  });
+
+  /// Formats an IP address so that fidlcat can understand it (removes % part,
+  /// adds brackets around it.)
+  String formatTarget(String target) {
+    log.info('$target: target');
+    try {
+      Uri.parseIPv4Address(target);
+      return target;
+    } on FormatException {
+      try {
+        Uri.parseIPv6Address(target);
+        return '[$target]';
+      } on FormatException {
+        try {
+          Uri.parseIPv6Address(target.split('%')[0]);
+          return '[$target]';
+        } on FormatException {
+          return null;
+        }
+      }
+    }
+  }
+
+  group('fidlcat', () {
+    test('Simple test of echo client output and shutdown', () async {
+      int port = await sl4fDriver.ssh.pickUnusedPort();
+      log.info('Chose port: $port');
+      Future<ProcessResult> agentResult =
+          sl4fDriver.ssh.run('run $debugAgentUrl --port=$port');
+      String target = formatTarget(sl4fDriver.ssh.target);
+      log.info('Target: $target');
+
+      final String symbolPath = Platform.script
+          .resolve('runtime_deps/echo_client_cpp.debug')
+          .toFilePath();
+      final String echoIr =
+          Platform.script.resolve('runtime_deps/echo.fidl.json').toFilePath();
+      ProcessResult processResult;
+      do {
+        processResult = await Process.run(fidlcatPath, [
+          '--connect=$target:$port',
+          '--quit-agent-on-exit',
+          '--fidl-ir-path=@$fidlIrPath',
+          '--fidl-ir-path=$echoIr',
+          '-s',
+          '$symbolPath',
+          'run',
+          'fuchsia-pkg://fuchsia.com/echo_client_cpp#meta/echo_client_cpp.cmx',
+        ]);
+      } while (processResult.exitCode == 2); // 2 means can't connect (yet).
+      expect(
+          processResult.stdout.toString(),
+          contains(
+              'sent request fidl.examples.echo/Echo.EchoString = {"value":"hello world"}'));
+      await agentResult;
+    });
+  }, timeout: _timeout);
+}
diff --git a/tools/fidlcat/command_line_options.cc b/tools/fidlcat/command_line_options.cc
index f2662f0..de4035c 100644
--- a/tools/fidlcat/command_line_options.cc
+++ b/tools/fidlcat/command_line_options.cc
@@ -30,13 +30,18 @@
   record all fidl calls invoked by the process.  The command may be of the form
   "run <component URL>", in which case the given component will be launched.
 
+  fidlcat will return the code 1 if its parameters are invalid.
+
+  fidlcat expects a debug agent to be running on the target device.  It will
+  return the code 2 if it cannot connect to the debug agent.
+
 Options:
 
 )";
 
 const char* const kRemoteHostHelp = R"(  --connect
-      The host and port of the target Fuchsia instance, of the form
-      [<ipv6_addr>]:port.)";
+      The host and port of the debug agent running on the target Fuchsia
+      instance, of the form [<ipv6_addr>]:port.)";
 
 const char* const kRemotePidHelp = R"(  --remote-pid
       The koid of the remote process. Can be passed multiple times.)";
@@ -144,6 +149,11 @@
 const char* const kCompareHelp = R"(  --compare=<path>
       Compare output with the one stored in the given file)";
 
+const char kQuitAgentOnExit[] = R"(  --quit-agent-on-exit
+      Will send a quit message to a connected debug agent in order for it to
+      shutdown. This is so that fidlcat doesn't leak unwanted debug agents on
+      "on-the-fly" debugging sessions.)";
+
 const char* const kHelpHelp = R"(  --help
   -h
       Prints all command-line switches.)";
@@ -217,6 +227,8 @@
   parser.AddSwitch("verbose", 'v', kVerbosityHelp, &CommandLineOptions::verbose);
   parser.AddSwitch("quiet", 'q', kQuietHelp, &CommandLineOptions::quiet);
   parser.AddSwitch("log-file", 0, kLogFileHelp, &CommandLineOptions::log_file);
+  parser.AddSwitch("quit-agent-on-exit", 0, kQuitAgentOnExit,
+                   &CommandLineOptions::quit_agent_on_exit);
   bool requested_help = false;
   parser.AddGeneralSwitch("help", 'h', kHelpHelp, [&requested_help]() { requested_help = true; });
 
diff --git a/tools/fidlcat/command_line_options.h b/tools/fidlcat/command_line_options.h
index 745a541..155b7eb 100644
--- a/tools/fidlcat/command_line_options.h
+++ b/tools/fidlcat/command_line_options.h
@@ -29,6 +29,7 @@
   std::string colors = "auto";
   int columns = 0;
   bool dump_messages = false;
+  bool quit_agent_on_exit = false;
 
   std::optional<std::string> verbose;
   std::optional<std::string> quiet;
diff --git a/tools/fidlcat/command_line_options_test.cc b/tools/fidlcat/command_line_options_test.cc
index 004ac88..567a587 100644
--- a/tools/fidlcat/command_line_options_test.cc
+++ b/tools/fidlcat/command_line_options_test.cc
@@ -93,8 +93,9 @@
 // Test to ensure that non-existent files are reported accordingly.
 TEST_F(CommandLineOptionsTest, BadOptionsTest) {
   // Parse the command line.
-  std::vector<const char*> argv = {"fakebinary", "--fidl-ir-path", "blah.fidl.json", "--remote-pid",
-                                   "3141",       "--fidl-ir-path", "@all_files.txt"};
+  std::vector<const char*> argv = {
+      "fakebinary", "--fidl-ir-path", "blah.fidl.json", "--remote-pid",
+      "3141",       "--fidl-ir-path", "@all_files.txt", "--quit-agent-on-exit"};
   CommandLineOptions options;
   DecodeOptions decode_options;
   DisplayOptions display_options;
diff --git a/tools/fidlcat/interception_tests/interception_workflow_test.cc b/tools/fidlcat/interception_tests/interception_workflow_test.cc
index 83dd472..ec97d3d 100644
--- a/tools/fidlcat/interception_tests/interception_workflow_test.cc
+++ b/tools/fidlcat/interception_tests/interception_workflow_test.cc
@@ -430,7 +430,7 @@
   initialized_ = true;
   global_dispatcher = dispatcher.get();
   std::vector<std::string> blank;
-  workflow_.Initialize(blank, blank, "", blank, std::move(dispatcher));
+  workflow_.Initialize(blank, blank, "", blank, std::move(dispatcher), false);
 
   // Create fake processes and threads.
   InjectProcesses(session);
diff --git a/tools/fidlcat/lib/interception_workflow.cc b/tools/fidlcat/lib/interception_workflow.cc
index 328cb74..ddde0e8 100644
--- a/tools/fidlcat/lib/interception_workflow.cc
+++ b/tools/fidlcat/lib/interception_workflow.cc
@@ -189,8 +189,13 @@
 void InterceptionWorkflow::Initialize(
     const std::vector<std::string>& symbol_paths, const std::vector<std::string>& symbol_repo_paths,
     const std::string& symbol_cache_path, const std::vector<std::string>& symbol_servers,
-    std::unique_ptr<SyscallDecoderDispatcher> syscall_decoder_dispatcher) {
+    std::unique_ptr<SyscallDecoderDispatcher> syscall_decoder_dispatcher, bool quit_agent_on_exit) {
   syscall_decoder_dispatcher_ = std::move(syscall_decoder_dispatcher);
+
+  if (quit_agent_on_exit) {
+    session_->system().settings().SetBool(zxdb::ClientSettings::System::kQuitAgentOnExit, true);
+  }
+
   // 1) Set up symbol index.
 
   // Stolen from console/console_main.cc
diff --git a/tools/fidlcat/lib/interception_workflow.h b/tools/fidlcat/lib/interception_workflow.h
index c0f2273..ab35eba 100644
--- a/tools/fidlcat/lib/interception_workflow.h
+++ b/tools/fidlcat/lib/interception_workflow.h
@@ -101,7 +101,8 @@
                   const std::vector<std::string>& symbol_repo_paths,
                   const std::string& symbol_cache_path,
                   const std::vector<std::string>& symbol_servers,
-                  std::unique_ptr<SyscallDecoderDispatcher> syscall_decoder_dispatcher);
+                  std::unique_ptr<SyscallDecoderDispatcher> syscall_decoder_dispatcher,
+                  bool quit_agent_on_exit);
 
   // Connect the workflow to the host/port pair given.  |and_then| is posted to
   // the loop on completion.
diff --git a/tools/fidlcat/main.cc b/tools/fidlcat/main.cc
index 470ac53..73ce94e 100644
--- a/tools/fidlcat/main.cc
+++ b/tools/fidlcat/main.cc
@@ -79,14 +79,15 @@
   uint16_t port;
   zxdb::Err parse_err = zxdb::ParseHostPort(*(options.connect), &host, &port);
   if (!parse_err.ok()) {
-    FXL_LOG(FATAL) << "Could not parse host/port pair: " << parse_err.msg();
+    fprintf(stderr, "Could not parse host/port pair: %s", parse_err.msg().c_str());
+    exit(1);
   }
 
   auto attach = [workflow, process_koids, remote_name = options.remote_name,
                  params](const zxdb::Err& err) {
     if (!err.ok()) {
-      FXL_LOG(FATAL) << "Unable to connect: " << err.msg();
-      return;
+      fprintf(stderr, "Unable to connect: %s", err.msg().c_str());
+      exit(2);
     }
     FXL_LOG(INFO) << "Connected!";
     if (!process_koids.empty()) {
@@ -164,7 +165,8 @@
 
   InterceptionWorkflow workflow;
   workflow.Initialize(options.symbol_paths, options.symbol_repo_paths, options.symbol_cache_path,
-                      options.symbol_servers, std::move(decoder_dispatcher));
+                      options.symbol_servers, std::move(decoder_dispatcher),
+                      options.quit_agent_on_exit);
 
   if (workflow.HasSymbolServers()) {
     for (const auto& server : workflow.GetSymbolServers()) {