devserver: display documentation for exposed methods

* A new doc/<method> URL for displaying the docstring of various exposed
  devserver methods.

* The index page lists all available methods, and clicking them jumps to
  their respective doc page.

* Added/fixed docstrings of some exposed methods.

BUG=None
TEST=List of methods displayed, documentation displayed; unit tests pass

Change-Id: Ib13ce8f4ec70bf99688ea40aa156822035422d8c
Reviewed-on: https://gerrit.chromium.org/gerrit/33893
Commit-Ready: Gilad Arnold <garnold@chromium.org>
Reviewed-by: Gilad Arnold <garnold@chromium.org>
Tested-by: Gilad Arnold <garnold@chromium.org>
diff --git a/devserver.py b/devserver.py
index e54932f..7a45959 100755
--- a/devserver.py
+++ b/devserver.py
@@ -224,6 +224,9 @@
     For paths http://myhost/update/dir1/dir2, you can use *args so that
     cherrypy uses the update method and puts the extra paths in args.
   """
+  # Method names that should not be listed on the index page.
+  _UNLISTED_METHODS = ['index', 'doc']
+
   api = ApiRoot()
 
   def __init__(self):
@@ -264,8 +267,8 @@
       archive_url: Google Storage URL for the build.
 
     Example URL:
-      'http://myhost/download?archive_url=gs://chromeos-image-archive/'
-      'x86-generic/R17-1208.0.0-a1-b338'
+      http://myhost/download?archive_url=gs://chromeos-image-archive/
+      x86-generic/R17-1208.0.0-a1-b338
     """
     archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
 
@@ -298,8 +301,8 @@
       archive_url: Google Storage URL for the build.
 
     Example URL:
-      'http://myhost/wait_for_status?archive_url=gs://chromeos-image-archive/'
-      'x86-generic/R17-1208.0.0-a1-b338'
+      http://myhost/wait_for_status?archive_url=gs://chromeos-image-archive/
+      x86-generic/R17-1208.0.0-a1-b338
     """
     archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
     downloader_instance = self._downloader_dict.get(archive_url)
@@ -328,8 +331,8 @@
       archive_url: Google Storage URL for the build.
 
     Example URL:
-      'http://myhost/stage_debug?archive_url=gs://chromeos-image-archive/'
-      'x86-generic/R17-1208.0.0-a1-b338'
+      http://myhost/stage_debug?archive_url=gs://chromeos-image-archive/
+      x86-generic/R17-1208.0.0-a1-b338
     """
     archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
     return downloader.SymbolDownloader(updater.static_dir).Download(archive_url)
@@ -453,12 +456,70 @@
     return (downloader.ImagesDownloader(
         updater.static_dir).Download(archive_url, image_types))
 
+  def _get_exposed_method(self, name, unlisted=[]):
+    """Checks whether a method is exposed as CherryPy URL.
+
+    Args:
+      name: method name to check
+      unlisted: methods to be excluded regardless of their exposed status
+    Returns:
+      Function object if method is exposed and not unlisted, None otherwise.
+    """
+    method = name not in unlisted and self.__class__.__dict__.get(name)
+    if method and hasattr(method, 'exposed') and method.exposed:
+      return method
+    return None
+
+  def _find_exposed_methods(self, unlisted=[]):
+    """Finds exposed CherryPy methods.
+
+    Args:
+      unlisted: methods to be excluded regardless of their exposed status
+    Returns:
+      List of exposed methods that are not unlisted.
+    """
+    return [name for name in self.__class__.__dict__.keys()
+                 if self._get_exposed_method(name, unlisted)]
+
   @cherrypy.expose
   def index(self):
-    return 'Welcome to the Dev Server!'
+    """Presents a welcome message and documentation links."""
+    method_dict = DevServerRoot.__dict__
+    return ('Welcome to the Dev Server!<br>\n'
+            '<br>\n'
+            'Here are the available methods, click for documentation:<br>\n'
+            '<br>\n'
+            '%s' %
+            '<br>\n'.join(
+                [('<a href=doc/%s>%s</a>' % (name, name))
+                 for name in self._find_exposed_methods(
+                     unlisted=self._UNLISTED_METHODS)]))
+
+  @cherrypy.expose
+  def doc(self, *args):
+    """Shows the documentation for available methods / URLs.
+
+    Example:
+      http://myhost/doc/update
+    """
+    name = args[0]
+    method = self._get_exposed_method(name)
+    if not method:
+      raise DevServerError("No exposed method named `%s'" % name)
+    if not method.__doc__:
+      raise DevServerError("No documentation for exposed method `%s'" % name)
+    return '<pre>\n%s</pre>' % method.__doc__
 
   @cherrypy.expose
   def update(self, *args):
+    """Handles an update check from a Chrome OS client.
+
+    The HTTP request should contain the standard Omaha-style XML blob. The URL
+    line may contain an additional intermediate path to the update payload.
+
+    Example:
+      http://myhost/update/optional/path/to/payload
+    """
     label = '/'.join(args)
     body_length = int(cherrypy.request.headers.get('Content-Length', 0))
     data = cherrypy.request.rfile.read(body_length)