cros: Properly handle KeyboardInterrupt.

This CL makes a few changes so that KeyboardInterrupt/SIGINT can be
handled better in cros.

TLDR: KeyboardInterrupts thrown in a cros_xxx.py file will now properly
bubble up to cros.py, and uploading stats no longer prints a large error
message when KeyboardInterrupts are thrown.

1. Ignore SIGINT in Manager().
parallel.Manager() creates SyncManager objects from the multiprocessing
library. These objects do not handle KeyboardInterrupt properly, causing
socket errors and/or IOErrors. Ignoring SIGINT in the manager allows the
main thread to handle it and shut down the manager on context exit.

2. Favor foreground exceptions in _BackgroundTask.ParallelTasks().
If ParallelTasks() completed with both the main and background threads
throwing exceptions, the background exception was clobbering the
foreground. Propagating the foreground exception instead allows
KeyboardInterrupts in the foreground to continue up the stack.

3. Don't print KeyboardInterrupts in stats.UploadContext().
If the main task catches its KeyboardInterrupt, #2 above won't have
effect so the background task will cause a BackgroundFailure exception.
This printed a large red error message, which isn't necessary for
KeyboardInterrupt since the user knows why the program failed.

BUG=brillo:392
TEST=cbuildbot/run_tests, new unit tests failed before and pass now.

Change-Id: I6b2d35fa7e364c895e354b7954eaa44d4038f590
Reviewed-on: https://chromium-review.googlesource.com/256698
Reviewed-by: David Pursell <dpursell@chromium.org>
Tested-by: David Pursell <dpursell@chromium.org>
Trybot-Ready: David Pursell <dpursell@chromium.org>
Commit-Queue: David Pursell <dpursell@chromium.org>
diff --git a/scripts/cros.py b/scripts/cros.py
index 3120f62..c21c183 100644
--- a/scripts/cros.py
+++ b/scripts/cros.py
@@ -15,6 +15,7 @@
 
 from __future__ import print_function
 
+import logging
 import sys
 
 from chromite.cros import commands
@@ -47,26 +48,30 @@
 
 
 def main(argv):
-  parser = GetOptions(commands.ListCommands())
-  # Cros currently does nothing without a subcmd. Print help if no args are
-  # specified.
-  if not argv:
-    parser.print_help()
+  try:
+    parser = GetOptions(commands.ListCommands())
+    # Cros currently does nothing without a subcmd. Print help if no args are
+    # specified.
+    if not argv:
+      parser.print_help()
+      return 1
+
+    namespace = parser.parse_args(argv)
+    subcommand = namespace.cros_class(namespace)
+    with stats.UploadContext() as queue:
+      if subcommand.upload_stats:
+        cmd_base = subcommand.options.cros_class.command_name
+        cmd_stats = stats.Stats.SafeInit(cmd_line=sys.argv, cmd_base=cmd_base)
+        if cmd_stats:
+          queue.put([cmd_stats, stats.StatsUploader.URL,
+                     subcommand.upload_stats_timeout])
+      # TODO: to make command completion faster, send an interrupt signal to the
+      # stats uploader task after the subcommand completes.
+      code = _RunSubCommand(subcommand)
+      if code is not None:
+        return code
+
+    return 0
+  except KeyboardInterrupt:
+    logging.debug('Aborted due to keyboard interrupt.')
     return 1
-
-  namespace = parser.parse_args(argv)
-  subcommand = namespace.cros_class(namespace)
-  with stats.UploadContext() as queue:
-    if subcommand.upload_stats:
-      cmd_base = subcommand.options.cros_class.command_name
-      cmd_stats = stats.Stats.SafeInit(cmd_line=sys.argv, cmd_base=cmd_base)
-      if cmd_stats:
-        queue.put([cmd_stats, stats.StatsUploader.URL,
-                   subcommand.upload_stats_timeout])
-    # TODO: to make command completion faster, send an interrupt signal to the
-    # stats uploader task after the subcommand completes.
-    code = _RunSubCommand(subcommand)
-    if code is not None:
-      return code
-
-  return 0