Add version command and diagnostic logging
authorJeremy Stanley <fungi@yuggoth.org>
Tue, 8 May 2018 18:46:54 +0000 (18:46 +0000)
committerJeremy Stanley <fungi@yuggoth.org>
Tue, 8 May 2018 18:55:16 +0000 (18:55 +0000)
Implement a new Versions class which is instantiated at startup and
reflects the versions of mudpy as well as the Python interpreter on
which it's running and versions of associated Python dependencies
plus any other importable Python packages which are found to be
present in the environment.

Include a show version command which provides a relevant summary of
this information and a selftest routine to make sure it's exercised.
Also log detailed version and diagnostic information at service
start, for ease of troubleshooting and defect reporting.

doc/source/api.rst
mudpy/__init__.py
mudpy/misc.py
mudpy/tests/selftest.py
mudpy/version.py [new file with mode: 0644]

index 966c24d..875bd5f 100644 (file)
@@ -50,6 +50,14 @@ mudpy\.telnet module
     :undoc-members:
     :show-inheritance:
 
+mudpy\.version module
+---------------------
+
+.. automodule:: mudpy.version
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
 
 Module contents
 ---------------
index 5f2d578..8c12ee2 100644 (file)
@@ -1,6 +1,6 @@
 """Core modules package for the mudpy engine."""
 
-# Copyright (c) 2004-2017 Jeremy Stanley <fungi@yuggoth.org>. Permission
+# Copyright (c) 2004-2018 Jeremy Stanley <fungi@yuggoth.org>. Permission
 # to use, copy, modify, and distribute this software is granted under
 # terms provided in the LICENSE file distributed with this software.
 
@@ -35,5 +35,5 @@ def load():
 
 
 # load the modules contained in this package
-modules = ["data", "misc", "password", "telnet"]
+modules = ["data", "misc", "password", "telnet", "version"]
 load()
index 154b555..4355586 100644 (file)
@@ -335,6 +335,7 @@ class Universe:
         self.startdir = os.getcwd()
         self.terminate_flag = False
         self.userlist = []
+        self.versions = None
         if not filename:
             possible_filenames = [
                 "etc/mudpy.yaml",
@@ -2102,6 +2103,8 @@ def command_show(actor, parameters):
     arguments = parameters.split()
     if not parameters:
         message = "What do you want to show?"
+    elif arguments[0] == "version":
+        message = repr(universe.versions)
     elif arguments[0] == "time":
         message = universe.groups["internal"]["counters"].get(
             "elapsed"
@@ -2481,9 +2484,6 @@ def setup():
         log(*logline)
     universe.setup_loglines = []
 
-    # log an initial message
-    log("Started mudpy with command line: " + " ".join(sys.argv))
-
     # fork and disassociate
     daemonize(universe)
 
@@ -2496,6 +2496,17 @@ def setup():
     # make the pidfile
     create_pidfile(universe)
 
+    # load and store diagnostic info
+    universe.versions = mudpy.version.Versions("mudpy")
+
+    # log startup diagnostic messages
+    log("On %s at %s" % (universe.versions.python_version, sys.executable), 1)
+    log("Import path: %s" % ", ".join(sys.path), 1)
+    log("Installed dependencies: %s" % universe.versions.dependencies_text, 1)
+    log("Other python packages: %s" % universe.versions.environment_text, 1)
+    log("Started %s with command line: %s" % (
+        universe.versions.version, " ".join(sys.argv)), 1)
+
     # pass the initialized universe back
     return universe
 
index 06cdbab..b859484 100644 (file)
@@ -186,6 +186,11 @@ test_set_refused = (
     (2, r'The "mudpy\.limit" element is kept in read-only file', ""),
 )
 
+test_show_version = (
+    (2, "> ", "show version"),
+    (2, r"Running mudpy .* on .* Python 3.*with.*pyyaml.*> ", ""),
+)
+
 test_show_files = (
     (2, "> ", "show files"),
     (2, r'These are the current files containing the universe:.*'
@@ -263,6 +268,7 @@ dialogue = (
     (test_reload, "reload"),
     (test_set_facet, "set facet"),
     (test_set_refused, "refuse altering read-only element"),
+    (test_show_version, "show version and diagnostic info"),
     (test_show_files, "show a list of loaded files"),
     (test_show_file, "show nodes from a specific file"),
     (test_show_groups, "show groups"),
diff --git a/mudpy/version.py b/mudpy/version.py
new file mode 100644 (file)
index 0000000..58d7e7f
--- /dev/null
@@ -0,0 +1,87 @@
+"""Version and diagnostic information for the mudpy engine."""
+
+# Copyright (c) 2018 Jeremy Stanley <fungi@yuggoth.org>. Permission
+# to use, copy, modify, and distribute this software is granted under
+# terms provided in the LICENSE file distributed with this software.
+
+import json
+import pkg_resources
+import sys
+
+
+class VersionDetail:
+
+    """Version detail for a Python package."""
+
+    def __init__(self, package):
+        self.project_name = _normalize_project(package.project_name)
+        version = package.version
+        self.version_info = tuple(version.split('.'))
+
+        # Build up a human-friendly version string for display purposes
+        self.text = "%s %s" % (self.project_name, version)
+
+        # Obtain Git commit ID from PBR metadata if present
+        dist = pkg_resources.get_distribution(self.project_name)
+        try:
+            self.git_version = json.loads(
+                dist.get_metadata("pbr.json"))["git_version"]
+            self.text = "%s (%s)" % (self.text, self.git_version)
+        except (IOError, KeyError):
+            self.git_version = None
+
+    def __repr__(self):
+        return self.text
+
+
+class Versions:
+
+    """Tracks info on known Python package versions."""
+
+    def __init__(self, project_name):
+        # Normalize the supplied name
+        project_name = _normalize_project(project_name)
+
+        # Python info for convenience
+        self.python_version = "%s Python %s" % (
+            sys.platform, sys.version.split(" ")[0])
+
+        # List of package names for this package's declared dependencies
+        requirements = []
+        for package in pkg_resources.get_distribution(project_name).requires():
+            requirements.append(_normalize_project(package.project_name))
+
+        # Accumulators for Python package versions
+        self.dependencies = {}
+        self.environment = {}
+
+        # Loop over all installed packages
+        for package in pkg_resources.working_set:
+            version = VersionDetail(package)
+            # Sort packages into the corresponding buckets
+            if version.project_name in requirements:
+                # This is a dependency
+                self.dependencies[version.project_name] = version
+            elif version.project_name == project_name:
+                # This is our main package
+                self.version = version
+            else:
+                # This may be a transitive dep, or merely installed
+                self.environment[version.project_name] = version
+
+        self.dependencies_text = ", ".join(
+            sorted([x.text for x in self.dependencies.values()]))
+        self.environment_text = ", ".join(
+            sorted([x.text for x in self.environment.values()]))
+
+    def __repr__(self):
+        return "Running %s on %s with dependencies %s." % (
+            self.version.text,
+            self.python_version,
+            self.dependencies_text,
+            )
+
+
+def _normalize_project(project_name):
+    """Convenience function to normalize Python project names."""
+    return pkg_resources.safe_name(project_name).lower()