From a36ecfa80ee60f30cce09590e030d79abe542d04 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Tue, 8 May 2018 18:46:54 +0000 Subject: [PATCH] Add version command and diagnostic logging 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 | 8 +++++ mudpy/__init__.py | 4 +-- mudpy/misc.py | 17 ++++++++-- mudpy/tests/selftest.py | 6 ++++ mudpy/version.py | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 mudpy/version.py diff --git a/doc/source/api.rst b/doc/source/api.rst index 966c24d..875bd5f 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -50,6 +50,14 @@ mudpy\.telnet module :undoc-members: :show-inheritance: +mudpy\.version module +--------------------- + +.. automodule:: mudpy.version + :members: + :undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/mudpy/__init__.py b/mudpy/__init__.py index 5f2d578..8c12ee2 100644 --- a/mudpy/__init__.py +++ b/mudpy/__init__.py @@ -1,6 +1,6 @@ """Core modules package for the mudpy engine.""" -# Copyright (c) 2004-2017 Jeremy Stanley . Permission +# Copyright (c) 2004-2018 Jeremy Stanley . 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() diff --git a/mudpy/misc.py b/mudpy/misc.py index 154b555..4355586 100644 --- a/mudpy/misc.py +++ b/mudpy/misc.py @@ -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 diff --git a/mudpy/tests/selftest.py b/mudpy/tests/selftest.py index 06cdbab..b859484 100644 --- a/mudpy/tests/selftest.py +++ b/mudpy/tests/selftest.py @@ -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 index 0000000..58d7e7f --- /dev/null +++ b/mudpy/version.py @@ -0,0 +1,87 @@ +"""Version and diagnostic information for the mudpy engine.""" + +# Copyright (c) 2018 Jeremy Stanley . 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() -- 2.11.0