Build API: Allow output with non-zero return codes.

BUG=None
TEST=run_tests

Change-Id: Iec5d02ced71755c3e97d611e2c269ae9e4479ac8
Reviewed-on: https://chromium-review.googlesource.com/1534542
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Tested-by: Alex Klein <saklein@chromium.org>
Reviewed-by: Lann Martin <lannm@chromium.org>
diff --git a/scripts/build_api.py b/scripts/build_api.py
index 9119133..9f41400 100644
--- a/scripts/build_api.py
+++ b/scripts/build_api.py
@@ -8,6 +8,7 @@
 from __future__ import print_function
 
 import importlib
+import os
 
 from google.protobuf import json_format
 from google.protobuf import symbol_database
@@ -28,10 +29,18 @@
   """Base error class for the module."""
 
 
+class InvalidInputFileError(Error):
+  """Raised when the input file cannot be read."""
+
+
 class InvalidInputFormatError(Error):
   """Raised when the passed input protobuf can't be parsed."""
 
 
+class InvalidOutputFileError(Error):
+  """Raised when the output file cannot be written."""
+
+
 # API Service Errors.
 class UnknownServiceError(Error):
   """Error raised when the requested service has not been registered."""
@@ -94,6 +103,9 @@
   opts.service = parts[0]
   opts.method = parts[1]
 
+  if not os.path.exists(opts.input_json):
+    parser.error('Input file does not exist.')
+
   opts.Freeze()
   return opts
 
@@ -142,23 +154,30 @@
 
     return sorted(services)
 
-  def Route(self, service_name, method_name, input_json):
+  def Route(self, service_name, method_name, input_path, output_path):
     """Dispatch the request.
 
     Args:
       service_name (str): The fully qualified service name.
       method_name (str): The name of the method being called.
-      input_json (str): The JSON encoded input message data.
+      input_path (str): The path to the input message file.
+      output_path (str): The path where the output message should be written.
 
     Returns:
-      google.protobuf.message.Message: An instance of the method's output
-        message class.
+      int: The return code.
 
     Raises:
+      InvalidInputFileError when the input file cannot be read.
+      InvalidOutputFileError when the output file cannot be written.
       ServiceModuleNotFoundError when the service module cannot be imported.
       MethodNotFoundError when the method cannot be retrieved from the module.
     """
     try:
+      input_json = osutils.ReadFile(input_path)
+    except IOError as e:
+      raise InvalidInputFileError('Unable to read input file: %s' % e.message)
+
+    try:
       svc, module_name = self._services[service_name]
     except KeyError:
       raise UnknownServiceError('The %s service has not been registered.'
@@ -194,8 +213,16 @@
     method_impl = self._GetMethod(module_name, method_name)
 
     # Successfully located; call and return.
-    method_impl(input_msg, output_msg)
-    return output_msg
+    return_code = method_impl(input_msg, output_msg)
+    if return_code is None:
+      return_code = 0
+
+    try:
+      osutils.WriteFile(output_path, json_format.MessageToJson(output_msg))
+    except IOError as e:
+      raise InvalidOutputFileError('Cannot write output file: %s' % e.message)
+
+    return return_code
 
   def _HandleChrootAssert(self, service_options, method_options):
     """Check the chroot assert options and execute assertion as needed.
@@ -261,18 +288,8 @@
   opts = _ParseArgs(argv, router)
 
   try:
-    input_proto = osutils.ReadFile(opts.input_json)
-  except IOError as e:
-    cros_build_lib.Die('Unable to read input file: %s' % e.message)
-
-  try:
-    output_msg = router.Route(opts.service, opts.method, input_proto)
+    return router.Route(opts.service, opts.method, opts.input_json,
+                        opts.output_json)
   except Error as e:
     # Error derivatives are handled nicely, but let anything else bubble up.
     cros_build_lib.Die(e.message)
-
-  output_content = json_format.MessageToJson(output_msg)
-  try:
-    osutils.WriteFile(opts.output_json, output_content)
-  except IOError as e:
-    cros_build_lib.Die('Unable to write output file: %s' % e.message)