Environment detection & logging on startup

Incl. OctoPi version & RPi model through bundled plugin that only
gets loaded if OctoPi is detected.
This commit is contained in:
Gina Häußge 2017-11-06 17:11:04 +01:00
parent 0bb343e1d3
commit b2d70de144
19 changed files with 494 additions and 53 deletions

View file

@ -40,7 +40,7 @@ INSTALL_REQUIRES = [
"pkginfo>=1.2.1,<1.3",
"requests>=2.18.4,<3",
"semantic_version>=2.4.2,<2.5",
"psutil>=5.4.0,<6",
"psutil>=5.4.1,<6",
"Click>=6.2,<6.3",
"awesome-slugify>=1.6.5,<1.7",
"feedparser>=5.2.1,<5.3",

View file

@ -60,7 +60,7 @@ def init_platform(basedir, configfile, use_logging_file=True, logging_file=None,
uncaught_handler=None, safe_mode=False, ignore_blacklist=False, after_preinit_logging=None,
after_settings=None, after_logging=None, after_safe_mode=None,
after_event_manager=None, after_connectivity_checker=None,
after_plugin_manager=None):
after_plugin_manager=None, after_environment_detector=None):
kwargs = dict()
logger, recorder = preinit_logging(debug, verbosity, uncaught_logger, uncaught_handler)
@ -115,8 +115,14 @@ def init_platform(basedir, configfile, use_logging_file=True, logging_file=None,
if callable(after_plugin_manager):
after_plugin_manager(**kwargs)
environment_detector = init_environment_detector(plugin_manager)
kwargs["environment_detector"] = environment_detector
if callable(after_environment_detector):
after_environment_detector(**kwargs)
return settings, logger, safe_mode, event_manager, connectivity_checker, plugin_manager
return settings, logger, safe_mode, event_manager, connectivity_checker, plugin_manager, environment_detector
def init_settings(basedir, configfile):
@ -522,6 +528,10 @@ def init_connectivity_checker(settings, event_manager):
return connectivityChecker
def init_environment_detector(plugin_manager):
from octoprint.environment import EnvironmentDetector
return EnvironmentDetector(plugin_manager)
#~~ server main method
def main():

View file

@ -48,7 +48,7 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi
"https://urllib3.readthedocs.org/en/latest/security.html#openssl-pyopenssl")
logger.info(get_divider_line("*"))
def log_register_rollover(safe_mode=None, plugin_manager=None, **kwargs):
def log_register_rollover(safe_mode=None, plugin_manager=None, environment_detector=None, **kwargs):
from octoprint.logging import get_handler, log_to_handler, get_divider_line
from octoprint.logging.handlers import OctoPrintLogHandler
@ -67,22 +67,24 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi
if safe_mode:
_log("SAFE MODE is active. Third party plugins are disabled!")
plugin_manager.log_all_plugins(only_to_handler=handler)
environment_detector.log_detected_environment(only_to_handler=handler)
_log(get_divider_line("-"))
OctoPrintLogHandler.registerRolloverCallback(rollover_callback)
try:
settings, _, safe_mode, event_manager, \
connectivity_checker, plugin_manager = init_platform(basedir,
configfile,
logging_file=logging_config,
debug=debug,
verbosity=verbosity,
uncaught_logger=__name__,
safe_mode=safe_mode,
ignore_blacklist=ignore_blacklist,
after_safe_mode=log_startup,
after_plugin_manager=log_register_rollover)
components = init_platform(basedir, configfile,
logging_file=logging_config,
debug=debug,
verbosity=verbosity,
uncaught_logger=__name__,
safe_mode=safe_mode,
ignore_blacklist=ignore_blacklist,
after_safe_mode=log_startup,
after_plugin_manager=log_register_rollover)
settings, _, safe_mode, event_manager, connectivity_checker, plugin_manager, environment_detector = components
except FatalStartupError as e:
click.echo(e.message, err=True)
click.echo("There was a fatal error starting up OctoPrint.", err=True)
@ -92,6 +94,7 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi
plugin_manager=plugin_manager,
event_manager=event_manager,
connectivity_checker=connectivity_checker,
environment_detector=environment_detector,
host=host,
port=port,
debug=debug,

View file

@ -0,0 +1,133 @@
from __future__ import absolute_import
import copy
import logging
import os
import platform
import sys
import threading
import yaml
import psutil
from octoprint.plugin import EnvironmentDetectionPlugin
from octoprint.util import get_formatted_size
from octoprint.util.platform import get_os
class EnvironmentDetector(object):
def __init__(self, plugin_manager):
self._plugin_manager = plugin_manager
self._cache = None
self._cache_lock = threading.RLock()
self._environment_plugins = self._plugin_manager.get_implementations(EnvironmentDetectionPlugin)
self._logger = logging.getLogger(__name__)
@property
def environment(self):
with self._cache_lock:
if self._cache is None:
self.run_detection()
return copy.deepcopy(self._cache)
def run_detection(self, notify_plugins=True):
environment = dict()
environment["os"] = self._detect_os()
environment["python"] = self._detect_python()
environment["hardware"] = self._detect_hardware()
plugin_result = self._detect_from_plugins()
if plugin_result:
environment["plugins"] = plugin_result
with self._cache_lock:
self._cache = environment
if notify_plugins:
self.notify_plugins()
return environment
def _detect_os(self):
return dict(id=get_os(),
platform=sys.platform)
def _detect_python(self):
result = dict()
# determine python version
result["version"] = platform.python_version()
# determine if we are running from a virtual environment
if hasattr(sys, "real_prefix") or (hasattr(sys, "base_prefix") and os.path.realpath(sys.prefix) != os.path.realpath(sys.base_prefix)):
result["virtualenv"] = sys.prefix
# try to find pip version
try:
import pip
result["pip"] = pip.__version__
except:
result["pip"] = "unknown"
return result
def _detect_hardware(self):
return dict(cores=psutil.cpu_count(),
freq=psutil.cpu_freq().max,
ram=get_formatted_size(psutil.virtual_memory().total))
def _detect_from_plugins(self):
result = dict()
for implementation in self._environment_plugins:
try:
additional = implementation.get_additional_environment()
if additional is not None and isinstance(additional, dict) and len(additional):
result[implementation._identifier] = additional
except:
self._logger.exception("Error while fetching additional "
"environment data from plugin {}".format(implementation._identifier))
return result
def log_detected_environment(self, only_to_handler=None):
def _log(message, level=logging.INFO):
if only_to_handler is not None:
import octoprint.logging
octoprint.logging.log_to_handler(self._logger, only_to_handler, level, message, [])
else:
self._logger.log(level, message)
_log(self._format())
def _format(self):
with self._cache_lock:
if self._cache is None:
self.run_detection()
environment = copy.deepcopy(self._cache)
dumped_environment = yaml.safe_dump(environment,
default_flow_style=False,
indent=" ",
allow_unicode=True).strip()
environment_lines = "\n".join(map(lambda l: "| {}".format(l), dumped_environment.split("\n")))
return u"Detected environment is Python {} under {} ({}). Details:\n{}".format(environment["python"]["version"],
environment["os"]["id"].title(),
environment["os"]["platform"],
environment_lines)
def notify_plugins(self):
with self._cache_lock:
if self._cache is None:
self.run_detection(notify_plugins=False)
environment = copy.deepcopy(self._cache)
for implementation in self._environment_plugins:
try:
implementation.on_environment_detected(environment)
except:
self._logger.exception("Error while sending environment "
"detection result to plugin {}".format(implementation._identifier))

View file

@ -91,7 +91,8 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
else:
if init:
if plugin_types is None:
plugin_types = [StartupPlugin,
plugin_types = [EnvironmentDetectionPlugin,
StartupPlugin,
ShutdownPlugin,
TemplatePlugin,
SettingsPlugin,

View file

@ -105,6 +105,16 @@ class ReloadNeedingPlugin(Plugin):
Mixin for plugin types that need a reload of the UI after enabling/disabling them.
"""
class EnvironmentDetectionPlugin(OctoPrintPlugin, RestartNeedingPlugin):
def get_additional_environment(self):
pass
def on_environment_detected(self, environment, *args, **kwargs):
pass
class StartupPlugin(OctoPrintPlugin, SortablePlugin):
"""
The ``StartupPlugin`` allows hooking into the startup of OctoPrint. It can be used to start up additional services

View file

@ -0,0 +1,154 @@
# coding=utf-8
from __future__ import absolute_import, division, print_function
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2017 The OctoPrint Project - Released under terms of the AGPLv3 License"
import flask
import os
import octoprint.plugin
_OCTOPI_VERSION_PATH = "/etc/octopi_version"
_CPUINFO_PATH = "/proc/cpuinfo"
# based on https://elinux.org/RPi_HardwareHistory#Which_Pi_have_I_got.3F
_RPI_REVISION_MAP = {
"Beta": "1B (Beta)",
"0002": "1B",
"0003": "1B",
"0004": "1B",
"0005": "1B",
"0006": "1B",
"0007": "1A",
"0008": "1A",
"0009": "1A",
"000d": "1B",
"000e": "1B",
"000f": "1B",
"0010": "B+",
"0011": "CM1",
"0012": "A+",
"0013": "B+",
"0014": "CM1",
"0015": "A+",
"a01040": "2B",
"a01041": "2B",
"a21041": "2B",
"a22042": "2B",
"900021": "A+",
"900032": "B+",
"900092": "Zero",
"900093": "Zero",
"920093": "Zero",
"9000c1": "Zero W",
"a02082": "3B",
"a020a0": "CM3",
"a22082": "3B",
"a32082": "3B",
}
def get_octopi_version():
with open(_OCTOPI_VERSION_PATH, "r") as f:
version_line = f.readline()
return version_line.strip()
def get_pi_revision():
with open(_CPUINFO_PATH) as f:
for line in f:
if line and line.startswith("Revision:"):
return line[line.index(":") + 1:].strip()
return "unknown"
def get_pi_model(revision):
if revision.startswith("1000"):
# strip flag for over-volted (https://elinux.org/RPi_HardwareHistory#Which_Pi_have_I_got.3F)
revision = revision[4:]
return _RPI_REVISION_MAP.get(revision.lower(), "unknown")
class OctoPiSupportPlugin(octoprint.plugin.EnvironmentDetectionPlugin,
octoprint.plugin.SimpleApiPlugin,
octoprint.plugin.AssetPlugin,
octoprint.plugin.TemplatePlugin):
def __init__(self):
self._version = None
self._revision = None
self._model = None
#~~ EnvironmentDetectionPlugin
def get_additional_environment(self):
return dict(version=self._get_version(),
revision=self._get_revision(),
model=self._get_model())
#~~ SimpleApiPlugin
def on_api_get(self, request):
return flask.jsonify(version=self._get_version(),
revision=self._get_revision(),
model=self._get_model())
#~~ AssetPlugin
def get_assets(self):
return dict(
js=["js/octopi_support.js"],
css=["css/octopi_support.css"]
)
#~~ TemplatePlugin
def get_template_configs(self):
return [
dict(type="about", name="About OctoPi", template="octopi_support_about.jinja2")
]
def get_template_vars(self):
version = self._get_version()
revision = self._get_revision()
model = self._get_model()
return dict(version=version,
rpi=(revision is not None and revision != "unknown" and model is not None and model != "unknown"),
rpi_revision=revision,
rpi_model=model)
#~~ Helpers
def _get_version(self):
if self._version is None:
try:
self._version = get_octopi_version()
except:
self._logger.exception("Error while reading OctoPi version from file {}".format(_OCTOPI_VERSION_PATH))
return self._version
def _get_model(self):
if self._model is None:
try:
self._model = get_pi_model(self._get_revision())
except:
self._logger.exception("Error while detecting RPi model")
return self._model
def _get_revision(self):
if self._revision is None:
try:
self._revision = get_pi_revision()
except:
self._logger.exception("Error while detecting RPi revision")
return self._revision
def __plugin_check__():
from octoprint.util.platform import get_os
return get_os() == "linux" and os.path.exists(_OCTOPI_VERSION_PATH)
def __plugin_load__():
global __plugin_implementation__
__plugin_implementation__ = OctoPiSupportPlugin()

View file

@ -0,0 +1,3 @@
#octopi_support_footer {
white-space: nowrap;
}

View file

@ -0,0 +1,53 @@
(function (global, factory) {
if (typeof define === "function" && define.amd) {
define(["OctoPrintClient"], factory);
} else {
factory(global.OctoPrintClient);
}
})(this, function(OctoPrintClient) {
var OctoPrintOctoPiSupportClient = function(base) {
this.base = base;
};
OctoPrintOctoPiSupportClient.prototype.get = function(opts) {
return this.base.get(this.base.getSimpleApiUrl("octopi_support"));
};
OctoPrintClient.registerPluginComponent("octopi_support", OctoPrintOctoPiSupportClient);
return OctoPrintOctoPiSupportClient;
});
$(function() {
function OctoPiSupportViewModel(parameters) {
var self = this;
self.requestData = function() {
OctoPrint.plugins.octopi_support.get()
.done(function(response) {
$("#octopi_support_footer").remove();
if (!response.version) return;
var octoPrintVersion = $(".footer span.version");
var octoPiVersion = $("<span id='octopi_support_footer'> " + gettext("running on") + " " + gettext("OctoPi")
+ " <span class='octopi_version'>" + response.version + "</span></span>")
$(octoPiVersion).insertAfter(octoPrintVersion);
})
};
self.onStartup = function() {
self.requestData();
};
self.onServerReconnect = function() {
self.requestData();
};
}
// view model class, parameters for constructor, container to bind to
ADDITIONAL_VIEWMODELS.push([
OctoPiSupportViewModel,
[],
[]
]);
});

View file

@ -0,0 +1,41 @@
<h3>{{ _('About OctoPi') }}</h3>
<h4>{{ _('The ready-to-go Raspberry Pi image with OctoPrint') }}</h4>
<p>Version <span class="plugin_octopi_support_version">{{ plugin_octopi_support_version }}</span></p>
<ul>
<li>Website: <a href="http://octoprint.org/download/" target="_blank" rel="noreferrer noopener">octoprint.org/download</a> &amp; <a href="https://octopi.octoprint.org" target="_blank" rel="noreferrer noopener">octopi.octoprint.org</a></li>
<li>Source Code: <a href="https://github.com/guysoft/OctoPi" target="_blank" rel="noreferrer noopener">github.com/guysoft/OctoPi</a></li>
</ul>
<p>
&copy; 2013-{{ now.strftime("%Y") }} <a href="https://github.com/guysoft/OctoPi/graphs/contributors" target="_blank" rel="noreferrer noopener">The OctoPi Authors</a>
</p>
<p>
OctoPi is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
</p>
<p>
OctoPi is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
</p>
<p>
For a copy of the GNU General Public License, see
<a href="https://www.gnu.org/licenses/gpl-3.0.en.html" target="_blank" rel="noreferrer noopener">www.gnu.org/licenses/gpl-3.0.en.html</a>.
</p>
{% if plugin_octopi_support_rpi %}
<h3>{{ _('About this Raspberry Pi') }}</h3>
<ul>
<li>Model: {{ plugin_octopi_support_rpi_model }}</li>
<li>Revision: {{ plugin_octopi_support_rpi_revision }}</li>
</ul>
{% endif %}

View file

@ -15,6 +15,7 @@ from octoprint.server.util.flask import restricted_access, with_revalidation_che
from octoprint.server import admin_permission, VERSION
from octoprint.util.pip import LocalPipCaller, UnknownPip
from octoprint.util.version import get_octoprint_version_string, get_octoprint_version, is_octoprint_compatible
from octoprint.util.platform import get_os
from flask import jsonify, make_response
from flask.ext.babel import gettext
@ -241,7 +242,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
available=self._repository_available,
plugins=self._repository_plugins
),
os=self._get_os(),
os=get_os(),
octoprint=get_octoprint_version_string(),
pip=dict(
available=self._pip_caller.available,
@ -759,7 +760,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
if repo_data is None:
return False
current_os = self._get_os()
current_os = get_os()
octoprint_version = get_octoprint_version(base=True)
def map_repository_entry(entry):
@ -878,14 +879,6 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
return positive_match and not negative_match
@classmethod
def _get_os(cls):
for identifier, platforms in cls.OPERATING_SYSTEMS.items():
if (callable(platforms) and platforms(sys.platform)) or (isinstance(platforms, list) and sys.platform in platforms):
return identifier
else:
return "unmapped"
@property
def _reconnect_hooks(self):
reconnect_hooks = self.__class__.RECONNECT_HOOKS

View file

@ -146,11 +146,13 @@ def load_user(id):
class Server(object):
def __init__(self, settings=None, plugin_manager=None, connectivity_checker=None, event_manager=None,
host="0.0.0.0", port=5000, debug=False, safe_mode=False, allow_root=False, octoprint_daemon=None):
def __init__(self, settings=None, plugin_manager=None, connectivity_checker=None, environment_detector=None,
event_manager=None, host="0.0.0.0", port=5000, debug=False, safe_mode=False, allow_root=False,
octoprint_daemon=None):
self._settings = settings
self._plugin_manager = plugin_manager
self._connectivity_checker = connectivity_checker
self._environment_detector = environment_detector
self._event_manager = event_manager
self._host = host
self._port = port
@ -290,7 +292,8 @@ class Server(object):
app_session_manager=appSessionManager,
plugin_lifecycle_manager=pluginLifecycleManager,
preemptive_cache=preemptiveCache,
connectivity_checker=connectivityChecker
connectivity_checker=connectivityChecker,
environment_detector=self._environment_detector
)
# create user manager instance
@ -390,6 +393,9 @@ class Server(object):
pluginManager.implementation_post_inits=[settings_plugin_config_migration_and_cleanup]
pluginManager.log_all_plugins()
# log environment data now
self._environment_detector.log_detected_environment()
# initialize file manager and register it for changes in the registered plugins
fileManager.initialize()

View file

@ -274,7 +274,7 @@ default_settings = {
],
"usersettings": ["access", "interface"],
"wizard": ["access"],
"about": ["about", "supporters", "authors", "changelog", "license", "thirdparty", "plugin_pluginmanager"],
"about": ["about", "plugin_octopi_support", "supporters", "authors", "changelog", "license", "thirdparty", "plugin_pluginmanager"],
"generic": []
},
"disabled": {

File diff suppressed because one or more lines are too long

View file

@ -942,6 +942,11 @@ ul.dropdown-menu li a {
}
}
#footer_version,
#footer_link {
max-width: 50%;
}
}
/** Notifications */

View file

@ -110,14 +110,14 @@
{% endif %}
</div>
<div class="footer">
<ul class="pull-left muted">
<ul id="footer_version" class="pull-left muted">
<li><small>{{ _('OctoPrint') }} <span class="version">{{ display_version|e }}</span></small></li>
</ul>
<ul class="pull-right">
<ul id="footer_links" class="pull-right">
<li><a href="http://octoprint.org" target="_blank" rel="noreferrer noopener"><i class="fa fa-home"></i> {{ _('Homepage') }}</a></li>
<li><a href="https://github.com/foosel/OctoPrint/" target="_blank" rel="noreferrer noopener"><i class="fa fa-github"></i> {{ _('Sourcecode') }}</a></li>
<li><a href="http://docs.octoprint.org" target="_blank" rel="noreferrer noopener"><i class="fa fa-book"></i> {{ _('Documentation') }}</a></li>
<li><a href="https://github.com/foosel/OctoPrint/issues" target="_blank" rel="noreferrer noopener"><i class="fa fa-flag"></i> {{ _('Bugs and Requests') }}</a></li>
<li><a href="https://github.com/foosel/OctoPrint/" target="_blank" rel="noreferrer noopener"><i class="fa fa-github"></i> {{ _('Source') }}</a></li>
<li><a href="http://docs.octoprint.org" target="_blank" rel="noreferrer noopener"><i class="fa fa-book"></i> {{ _('Docs') }}</a></li>
<li><a href="https://github.com/foosel/OctoPrint/issues" target="_blank" rel="noreferrer noopener"><i class="fa fa-flag"></i> {{ _('Issues') }}</a></li>
<li><a href="https://github.com/foosel/OctoPrint/wiki/FAQ" target="_blank" rel="noreferrer noopener"><i class="fa fa-question-circle"></i> {{ _('FAQ') }}</a></li>
<li id="footer_about"><a href="javascript:void(0)" data-bind="click: show"><i class="fa fa-info-circle"></i> {{ _('About') }}</a></li>
</ul>

View file

@ -39,3 +39,19 @@ else:
def set_close_exec(handle):
# no-op
pass
# current os
_OPERATING_SYSTEMS = dict(windows=["win32"],
linux=lambda x: x.startswith("linux"),
macos=["darwin"],
freebsd=lambda x: x.startswith("freebsd"))
OPERATING_SYSTEM_UNMAPPED = "unmapped"
def get_os():
for identifier, platforms in _OPERATING_SYSTEMS.items():
if (callable(platforms) and platforms(sys.platform)) or (isinstance(platforms, list) and sys.platform in platforms):
return identifier
else:
return OPERATING_SYSTEM_UNMAPPED

View file

@ -42,20 +42,3 @@ class PluginManagerPluginTests(unittest.TestCase):
with mock.patch("sys.platform", sys_platform):
actual = PluginManagerPlugin._is_os_compatible(current_os, entries)
self.assertEqual(actual, expected)
@ddt.data(
("win32", "windows"),
("linux2", "linux"),
("darwin", "macos"),
("linux", "linux"),
("linux3", "linux"),
("freebsd", "freebsd"),
("freebsd2342", "freebsd"),
("os2", "unmapped"),
("sunos5", "unmapped")
)
@ddt.unpack
def test_get_os(self, sys_platform, expected):
with mock.patch("sys.platform", sys_platform):
actual = PluginManagerPlugin._get_os()
self.assertEqual(actual, expected)

View file

@ -0,0 +1,30 @@
# coding=utf-8
from __future__ import absolute_import
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2017 The OctoPrint Project - Released under terms of the AGPLv3 License"
import unittest
import ddt
import mock
@ddt.ddt
class PlatformUtilTest(unittest.TestCase):
@ddt.data(
("win32", "windows"),
("linux2", "linux"),
("darwin", "macos"),
("linux", "linux"),
("linux3", "linux"),
("freebsd", "freebsd"),
("freebsd2342", "freebsd"),
("os2", "unmapped"),
("sunos5", "unmapped")
)
@ddt.unpack
def test_get_os(self, sys_platform, expected):
with mock.patch("sys.platform", sys_platform):
from octoprint.util.platform import get_os
actual = get_os()
self.assertEqual(actual, expected)