Better command line interface

This commit is contained in:
Gina Häußge 2015-07-13 13:51:36 +02:00
parent 75992ef837
commit a65ad50898
7 changed files with 387 additions and 219 deletions

View file

@ -3,11 +3,17 @@
# The init.d script will only run if this variable non-empty.
OCTOPRINT_USER=pi
# base directory to use
#BASEDIR=/home/pi/.octoprint
# configuration file to use
#CONFIGFILE=/home/pi/.octoprint/config.yaml
# On what port to run daemon, default is 5000
PORT=5000
# Path to the OctoPrint executable, use this to override the default setting "/usr/bin/octoprint"
#DAEMON=/path/to/octoprint/executable
# Path to the OctoPrint executable, you need to set this to match your installation!
#DAEMON=/home/pi/OctoPrint/venv/bin/octoprint
# What arguments to pass to octoprint, usually no need to touch this
DAEMON_ARGS="--port=$PORT"

View file

@ -16,17 +16,23 @@
# Author: Sami Olmari
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DESC="Octoprint Daemon"
NAME="Octoprint"
DAEMON=/usr/local/bin/octoprint
PIDFILE=/var/run/$NAME.pid
DESC="OctoPrint Daemon"
NAME="OctoPrint"
PKGNAME=octoprint
PIDFILE=/var/run/$PKGNAME.pid
SCRIPTNAME=/etc/init.d/$PKGNAME
# Read configuration variable file if it is present
[ -r /etc/default/$PKGNAME ] && . /etc/default/$PKGNAME
# Exit if the octoprint is not installed
# Exit if the DAEMON is not set
if [ -z "$DAEMON" ]
then
log_warning_msg "Not starting $PKGNAME, DAEMON not set in /etc/default/$PKGNAME."
exit 0
fi
# Exit if the DAEMON is not installed
[ -x "$DAEMON" ] || exit 0
# Load the VERBOSE setting and other rcS variables
@ -48,6 +54,17 @@ then
exit 0
fi
COMMAND_ARGS=
if [ -z "$BASEDIR" ]
then
COMMAND_ARGS="--basedir $BASEDIR $COMMAND_ARGS"
fi
if [ -z "$CONFIGFILE" ]
then
COMMAND_ARGS="--config $CONFIGFILE $COMMAND_ARGS"
fi
#
# Function to verify if a pid is alive
#
@ -74,7 +91,7 @@ do_start()
if [ $RETVAL != 0 ]; then
start-stop-daemon --start --background --quiet --pidfile $PIDFILE --make-pidfile \
--exec $DAEMON --chuid $OCTOPRINT_USER --user $OCTOPRINT_USER --umask $UMASK --nicelevel=$NICELEVEL \
-- $DAEMON_ARGS
-- $COMMAND_ARGS serve $DAEMON_ARGS
RETVAL="$?"
fi
}

View file

@ -31,7 +31,8 @@ INSTALL_REQUIRES = [
"rsa",
"pkginfo",
"requests",
"semantic_version"
"semantic_version",
"Click"
]
# Additional requirements for optional install options

View file

@ -1,5 +1,14 @@
#!/usr/bin/env python
# coding=utf-8
from __future__ import absolute_import, print_function
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
import sys
import click
from octoprint.daemon import Daemon
from octoprint.server import Server
@ -9,76 +18,283 @@ from ._version import get_versions
__version__ = get_versions()['version']
del get_versions
#~~ main class
class Main(Daemon):
def __init__(self, pidfile, configfile, basedir, host, port, debug, allowRoot, logConf):
def init_platform(basedir, configfile, use_logging_file=True, logging_file=None,
logging_config=None, debug=False, uncaught_logger=None,
uncaught_handler=None):
settings = init_settings(basedir, configfile)
logger = init_logging(settings,
use_logging_file=use_logging_file,
logging_file=logging_file,
default_config=logging_config,
debug=debug,
uncaught_logger=uncaught_logger,
uncaught_handler=uncaught_handler)
plugin_manager = init_pluginsystem(settings)
return settings, logger, plugin_manager
def init_settings(basedir, configfile):
from octoprint.settings import settings
return settings(init=True, basedir=basedir, configfile=configfile)
def init_logging(settings, use_logging_file=True, logging_file=None, default_config=None, debug=False, uncaught_logger=None, uncaught_handler=None):
import logging
import os
from octoprint.util import dict_merge
# default logging configuration
if default_config is None:
default_config = {
"version": 1,
"formatters": {
"simple": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"file": {
"class": "logging.handlers.TimedRotatingFileHandler",
"level": "DEBUG",
"formatter": "simple",
"when": "D",
"backupCount": "1",
"filename": os.path.join(settings.getBaseFolder("logs"), "octoprint.log")
},
"serialFile": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "simple",
"maxBytes": 2 * 1024 * 1024, # let's limit the serial log to 2MB in size
"filename": os.path.join(settings.getBaseFolder("logs"), "serial.log")
}
},
"loggers": {
"SERIAL": {
"level": "CRITICAL",
"handlers": ["serialFile"],
"propagate": False
},
"tornado.application": {
"level": "INFO"
},
"tornado.general": {
"level": "INFO"
}
},
"root": {
"level": "INFO",
"handlers": ["console", "file"]
}
}
if debug:
default_config["root"]["level"] = "DEBUG"
if use_logging_file:
if logging_file is None:
logging_file = os.path.join(settings.getBaseFolder("base"), "logging.yaml")
config_from_file = {}
if os.path.exists(logging_file) and os.path.isfile(logging_file):
import yaml
with open(logging_file, "r") as f:
config_from_file = yaml.safe_load(f)
config = dict_merge(default_config, config_from_file)
else:
config = default_config
logging.config.dictConfig(config)
logging.captureWarnings(True)
import warnings
warnings.simplefilter("always")
if uncaught_logger is None:
logger = logging.getLogger(__name__)
else:
logger = logging.getLogger(uncaught_logger)
if uncaught_handler is None:
def exception_logger(exc_type, exc_value, exc_tb):
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_tb))
uncaught_handler = exception_logger
sys.excepthook = uncaught_handler
return logger
def init_pluginsystem(settings):
from octoprint.plugin import plugin_manager
return plugin_manager(init=True, settings=settings)
#~~ Custom click option to hide from help
class HiddenOption(click.Option):
def get_help_record(self, ctx):
pass
def hidden_option(*param_decls, **attrs):
"""Attaches a hidden option to the command. All positional arguments are
passed as parameter declarations to :class:`Option`; all keyword
arguments are forwarded unchanged. This is equivalent to creating an
:class:`Option` instance manually and attaching it to the
:attr:`Command.params` list.
"""
import inspect
from click.decorators import _param_memo
def decorator(f):
if 'help' in attrs:
attrs['help'] = inspect.cleandoc(attrs['help'])
_param_memo(f, HiddenOption(param_decls, **attrs))
return f
return decorator
#~~ daemon class
class OctoPrintDaemon(Daemon):
def __init__(self, pidfile, basedir, configfile, host, port, debug, allow_root, logging_config):
Daemon.__init__(self, pidfile)
self._configfile = configfile
self._basedir = basedir
self._configfile = configfile
self._host = host
self._port = port
self._debug = debug
self._allowRoot = allowRoot
self._logConf = logConf
self._allow_root = allow_root
self._logging_config = logging_config
def run(self):
octoprint = Server(self._configfile, self._basedir, self._host, self._port, self._debug, self._allowRoot)
octoprint.run()
run_server(self._basedir, self._configfile, self._host, self._port, self._debug, self._allow_root, self._logging_config)
#~~ serve method
def run_server(basedir, configfile, host, port, debug, allow_root, logging_config):
settings, _, plugin_manager = init_platform(basedir,
configfile,
logging_file=logging_config,
debug=debug,
uncaught_logger=__name__)
octoprint = Server(settings=settings, plugin_manager=plugin_manager, host=host, port=port, debug=debug, allow_root=allow_root)
octoprint.run()
@click.group(name="octoprint", invoke_without_command=True)
@click.option("--basedir", "-b", type=click.Path(),
help="Specify the basedir to use for uploads, timelapses etc.")
@click.option("--config", "-c", "configfile", type=click.Path(),
help="Specify the config file to use.")
@hidden_option("--debug", "-d", is_flag=True)
@hidden_option("--host", type=click.STRING)
@hidden_option("--port", type=click.INT)
@hidden_option("--logging", type=click.Path())
@hidden_option("--daemon", type=click.Choice(["start", "stop", "restart"]))
@hidden_option("--pid", type=click.Path())
@hidden_option("--iknowwhatimdoing", "allow_root", is_flag=True)
@click.version_option(version=__version__)
@click.pass_context
def cli(ctx, debug, host, port, basedir, configfile, logging, daemon, pid, allow_root):
class ContextObject(object):
def __init__(self):
self.debug = False
self.configfile = None
self.basedir = None
self.allow_root = None
obj = ContextObject()
obj.debug = debug
obj.configfile = configfile
obj.basedir = basedir
obj.allow_root = allow_root
ctx.obj = obj
if ctx.invoked_subcommand is None:
if daemon:
click.echo("Daemon operation via \"octoprint --daemon "
"(start|stop|restart)\" is deprecated, please use "
"\"octoprint daemon start|stop|restart\" from now on")
ctx.invoke(daemon_command, pid=pid, daemon=daemon)
else:
click.echo("Starting the server via \"octoprint\" is deprecated, "
"please use \"octoprint serve\" from now on.")
ctx.invoke(serve_command, host=host, port=port, logging=logging)
@cli.command(name="serve")
@click.option("--host", type=click.STRING,
help="Specify the host on which to bind the server.")
@click.option("--port", type=click.INT,
help="Specify the port on which to bind the server.")
@click.option("--logging", type=click.Path(),
help="Specify the config file to use for configuring logging.")
@click.option("--debug", "-d", is_flag=True,
help="Enable debug mode.")
@click.option("--iknowwhatimdoing", "allow_root", is_flag=True,
help="Allow OctoPrint to run as user root.")
@click.pass_context
def serve_command(ctx, host, port, logging, debug, allow_root):
"""Starts the OctoPrint server."""
run_server(ctx.obj.basedir, ctx.obj.configfile, host, port, debug,
allow_root, logging)
@cli.command(name="daemon")
@click.option("--pid", type=click.Path(),
help="Pidfile to use for daemonizing.")
@click.option("--host", type=click.STRING,
help="Specify the host on which to bind the server.")
@click.option("--port", type=click.INT,
help="Specify the port on which to bind the server.")
@click.option("--logging", type=click.Path(),
help="Specify the config file to use for configuring logging.")
@click.option("--debug", "-d", is_flag=True,
help="Enable debug mode")
@click.option("--iknowwhatimdoing", "allow_root", is_flag=True,
help="Allow OctoPrint to run as user root.")
@click.argument("command", type=click.Choice(["start", "stop", "restart"]),
metavar="start|stop|restart")
@click.pass_context
def daemon_command(ctx, pid, host, port, logging, debug, allow_root, command):
"""
Starts, stops or restarts in daemon mode.
Please note that daemon mode is only supported under Linux right now.
"""
if sys.platform == "darwin" or sys.platform == "win32":
click.echo("Sorry, daemon mode is only supported under Linux right now",
file=sys.stderr)
sys.exit(2)
daemon = OctoPrintDaemon(pid, ctx.obj.basedir, ctx.obj.configfile,
host, port, debug, allow_root, logging)
if command == "start":
daemon.start()
elif command == "stop":
daemon.stop()
elif command == "restart":
daemon.restart()
def main():
import argparse
cli.main(prog_name="octoprint", auto_envvar_prefix="OCTOPRINT")
parser = argparse.ArgumentParser(prog="run")
parser.add_argument("-v", "--version", action="store_true", dest="version",
help="Output OctoPrint's version and exit")
parser.add_argument("-d", "--debug", action="store_true", dest="debug",
help="Enable debug mode")
parser.add_argument("--host", action="store", type=str, dest="host",
help="Specify the host on which to bind the server")
parser.add_argument("--port", action="store", type=int, dest="port",
help="Specify the port on which to bind the server")
parser.add_argument("-c", "--config", action="store", dest="config",
help="Specify the config file to use. OctoPrint needs to have write access for the settings dialog to work. Defaults to ~/.octoprint/config.yaml")
parser.add_argument("-b", "--basedir", action="store", dest="basedir",
help="Specify the basedir to use for uploads, timelapses etc. OctoPrint needs to have write access. Defaults to ~/.octoprint")
parser.add_argument("--logging", action="store", dest="logConf",
help="Specify the config file to use for configuring logging. Defaults to ~/.octoprint/logging.yaml")
parser.add_argument("--daemon", action="store", type=str, choices=["start", "stop", "restart"],
help="Daemonize/control daemonized OctoPrint instance (only supported under Linux right now)")
parser.add_argument("--pid", action="store", type=str, dest="pidfile", default="/tmp/octoprint.pid",
help="Pidfile to use for daemonizing, defaults to /tmp/octoprint.pid")
parser.add_argument("--iknowwhatimdoing", action="store_true", dest="allowRoot",
help="Allow OctoPrint to run as user root")
args = parser.parse_args()
if args.version:
print "OctoPrint version %s" % __version__
sys.exit(0)
if args.daemon:
if sys.platform == "darwin" or sys.platform == "win32":
print >> sys.stderr, "Sorry, daemon mode is only supported under Linux right now"
sys.exit(2)
daemon = Main(args.pidfile, args.config, args.basedir, args.host, args.port, args.debug, args.allowRoot, args.logConf)
if "start" == args.daemon:
daemon.start()
elif "stop" == args.daemon:
daemon.stop()
elif "restart" == args.daemon:
daemon.restart()
else:
octoprint = Server(args.config, args.basedir, args.host, args.port, args.debug, args.allowRoot, args.logConf)
octoprint.run()
if __name__ == "__main__":
main()

View file

@ -22,7 +22,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import os
import logging
from octoprint.settings import settings
from octoprint.settings import settings as s
from octoprint.plugin.core import (PluginInfo, PluginManager, Plugin)
from octoprint.plugin.types import *
@ -44,7 +44,7 @@ def _validate_plugin(phase, plugin_info):
setattr(plugin_info.instance, PluginInfo.attr_hooks, hooks)
def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_entry_points=None, plugin_disabled_list=None,
plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None, plugin_validators=None):
plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None, plugin_validators=None, settings=None):
"""
Factory method for initially constructing and consecutively retrieving the :class:`~octoprint.plugin.core.PluginManager`
singleton.
@ -87,9 +87,12 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
else:
if init:
if settings is None:
settings = s()
if plugin_folders is None:
plugin_folders = (
settings().getBaseFolder("plugins"),
settings.getBaseFolder("plugins"),
(os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "plugins")), True)
)
if plugin_types is None:
@ -107,7 +110,7 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
if plugin_entry_points is None:
plugin_entry_points = "octoprint.plugin"
if plugin_disabled_list is None:
plugin_disabled_list = settings().get(["plugins", "_disabled"])
plugin_disabled_list = settings.get(["plugins", "_disabled"])
if plugin_restart_needing_hooks is None:
plugin_restart_needing_hooks = [
"octoprint.server.http"
@ -134,7 +137,7 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
return _instance
def plugin_settings(plugin_key, defaults=None, get_preprocessors=None, set_preprocessors=None):
def plugin_settings(plugin_key, defaults=None, get_preprocessors=None, set_preprocessors=None, settings=None):
"""
Factory method for creating a :class:`PluginSettings` instance.
@ -143,12 +146,17 @@ def plugin_settings(plugin_key, defaults=None, get_preprocessors=None, set_prepr
defaults (dict): The default settings for the plugin.
get_preprocessors (dict): The getter preprocessors for the plugin.
set_preprocessors (dict): The setter preprocessors for the plugin.
settings (octoprint.settings.Settings): The settings instance to use.
Returns:
PluginSettings: A fully initialized :class:`PluginSettings` instance to be used to access the plugin's
settings
"""
return PluginSettings(settings(), plugin_key, defaults=defaults, get_preprocessors=get_preprocessors, set_preprocessors=set_preprocessors)
if settings is None:
settings = s()
return PluginSettings(settings, plugin_key, defaults=defaults,
get_preprocessors=get_preprocessors,
set_preprocessors=set_preprocessors)
def call_plugin(types, method, args=None, kwargs=None, callback=None, error_callback=None):

View file

@ -468,8 +468,6 @@ class PluginManager(object):
self.marked_plugins = defaultdict(list)
self.reload_plugins(startup=True, initialize_implementations=False)
@property
def plugins(self):
plugins = dict(self.enabled_plugins)
@ -955,16 +953,14 @@ class PluginManager(object):
self.logger.info("No plugins available")
else:
self.logger.info("{count} plugin(s) registered with the system:\n{plugins}".format(count=len(all_plugins), plugins="\n".join(
sorted(
map(lambda x: "| " + x.long_str(show_bundled=show_bundled,
bundled_strs=bundled_str,
show_location=show_location,
location_str=location_str,
show_enabled=show_enabled,
enabled_strs=enabled_str),
self.enabled_plugins.values())
)
)))
map(lambda x: "| " + x.long_str(show_bundled=show_bundled,
bundled_strs=bundled_str,
show_location=show_location,
location_str=location_str,
show_enabled=show_enabled,
enabled_strs=enabled_str),
sorted(self.enabled_plugins.values(), key=lambda x: x.name.lower()),
))))
def get_plugin(self, identifier, require_enabled=True):
"""

View file

@ -108,14 +108,13 @@ def load_user(id):
class Server():
def __init__(self, configfile=None, basedir=None, host="0.0.0.0", port=5000, debug=False, allowRoot=False, logConf=None):
self._configfile = configfile
self._basedir = basedir
def __init__(self, settings=None, plugin_manager=None, host="0.0.0.0", port=5000, debug=False, allow_root=False):
self._settings = settings
self._plugin_manager = plugin_manager
self._host = host
self._port = port
self._debug = debug
self._allowRoot = allowRoot
self._logConf = logConf
self._allow_root = allow_root
self._server = None
self._logger = None
@ -125,9 +124,14 @@ class Server():
self._template_searchpaths = []
def run(self):
if not self._allowRoot:
if not self._allow_root:
self._check_for_root()
if self._settings is None:
self._settings = settings()
if self._plugin_manager is None:
self._plugin_manager = octoprint.plugin.plugin_manager()
global app
global babel
@ -147,16 +151,16 @@ class Server():
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler
import sys
debug = self._debug
# first initialize the settings singleton and make sure it uses given configfile and basedir if available
s = settings(init=True, basedir=self._basedir, configfile=self._configfile)
self._logger = logging.getLogger(__name__)
pluginManager = self._plugin_manager
# then monkey patch a bunch of stuff
self._logger.info("Starting OctoPrint %s" % DISPLAY_VERSION)
# monkey patch a bunch of stuff
util.tornado.fix_ioloop_scheduling()
util.flask.enable_additional_translations(additional_folders=[s.getBaseFolder("translations")])
util.flask.enable_additional_translations(additional_folders=[self._settings.getBaseFolder("translations")])
# setup app
self._setup_app()
@ -164,23 +168,20 @@ class Server():
# setup i18n
self._setup_i18n(app)
# then initialize logging
self._setup_logging(self._debug, self._logConf)
self._logger = logging.getLogger(__name__)
def exception_logger(exc_type, exc_value, exc_tb):
self._logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_tb))
sys.excepthook = exception_logger
self._logger.info("Starting OctoPrint %s" % DISPLAY_VERSION)
if self._settings.getBoolean(["serial", "log"]):
# enable debug logging to serial.log
logging.getLogger("SERIAL").setLevel(logging.DEBUG)
logging.getLogger("SERIAL").debug("Enabling serial logging")
# then initialize the plugin manager
pluginManager = octoprint.plugin.plugin_manager(init=True)
# load plugins
pluginManager.reload_plugins(startup=True, initialize_implementations=False)
printerProfileManager = PrinterProfileManager()
eventManager = events.eventManager()
analysisQueue = octoprint.filemanager.analysis.AnalysisQueue()
slicingManager = octoprint.slicing.SlicingManager(s.getBaseFolder("slicingProfiles"), printerProfileManager)
slicingManager = octoprint.slicing.SlicingManager(self._settings.getBaseFolder("slicingProfiles"), printerProfileManager)
storage_managers = dict()
storage_managers[octoprint.filemanager.FileDestinations.LOCAL] = octoprint.filemanager.storage.LocalFileStorage(s.getBaseFolder("uploads"))
storage_managers[octoprint.filemanager.FileDestinations.LOCAL] = octoprint.filemanager.storage.LocalFileStorage(self._settings.getBaseFolder("uploads"))
fileManager = octoprint.filemanager.FileManager(analysisQueue, slicingManager, printerProfileManager, initial_storage_managers=storage_managers)
printer = Printer(fileManager, analysisQueue, printerProfileManager)
appSessionManager = util.flask.AppSessionManager()
@ -199,7 +200,7 @@ class Server():
printer=printer,
app_session_manager=appSessionManager,
plugin_lifecycle_manager=pluginLifecycleManager,
data_folder=os.path.join(settings().getBaseFolder("data"), name)
data_folder=os.path.join(self._settings.getBaseFolder("data"), name)
)
def settings_plugin_inject_factory(name, implementation):
@ -276,8 +277,8 @@ class Server():
events.DebugEventListener()
# setup access control
if s.getBoolean(["accessControl", "enabled"]):
userManagerName = s.get(["accessControl", "userManager"])
if self._settings.getBoolean(["accessControl", "enabled"]):
userManagerName = self._settings.get(["accessControl", "userManager"])
try:
clazz = octoprint.util.get_class(userManagerName)
userManager = clazz()
@ -286,22 +287,22 @@ class Server():
app.wsgi_app = util.ReverseProxied(
app.wsgi_app,
s.get(["server", "reverseProxy", "prefixHeader"]),
s.get(["server", "reverseProxy", "schemeHeader"]),
s.get(["server", "reverseProxy", "hostHeader"]),
s.get(["server", "reverseProxy", "prefixFallback"]),
s.get(["server", "reverseProxy", "schemeFallback"]),
s.get(["server", "reverseProxy", "hostFallback"])
self._settings.get(["server", "reverseProxy", "prefixHeader"]),
self._settings.get(["server", "reverseProxy", "schemeHeader"]),
self._settings.get(["server", "reverseProxy", "hostHeader"]),
self._settings.get(["server", "reverseProxy", "prefixFallback"]),
self._settings.get(["server", "reverseProxy", "schemeFallback"]),
self._settings.get(["server", "reverseProxy", "hostFallback"])
)
secret_key = s.get(["server", "secretKey"])
secret_key = self._settings.get(["server", "secretKey"])
if not secret_key:
import string
from random import choice
chars = string.ascii_lowercase + string.ascii_uppercase + string.digits
secret_key = "".join(choice(chars) for _ in xrange(32))
s.set(["server", "secretKey"], secret_key)
s.save()
self._settings.set(["server", "secretKey"], secret_key)
self._settings.save()
app.secret_key = secret_key
loginManager = LoginManager()
loginManager.session_protection = "strong"
@ -312,9 +313,9 @@ class Server():
loginManager.init_app(app)
if self._host is None:
self._host = s.get(["server", "host"])
self._host = self._settings.get(["server", "host"])
if self._port is None:
self._port = s.getInt(["server", "port"])
self._port = self._settings.getInt(["server", "port"])
app.debug = self._debug
@ -328,17 +329,17 @@ class Server():
self._router = SockJSRouter(self._create_socket_connection, "/sockjs")
upload_suffixes = dict(name=s.get(["server", "uploads", "nameSuffix"]), path=s.get(["server", "uploads", "pathSuffix"]))
upload_suffixes = dict(name=self._settings.get(["server", "uploads", "nameSuffix"]), path=self._settings.get(["server", "uploads", "pathSuffix"]))
server_routes = self._router.urls + [
# various downloads
(r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("timelapse"), as_attachment=True)),
(r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("uploads"), as_attachment=True, path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))),
(r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("logs"), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))),
(r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, dict(path=self._settings.getBaseFolder("timelapse"), as_attachment=True)),
(r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, dict(path=self._settings.getBaseFolder("uploads"), as_attachment=True, path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))),
(r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, dict(path=self._settings.getBaseFolder("logs"), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))),
# camera snapshot
(r"/downloads/camera/current", util.tornado.UrlForwardHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))),
(r"/downloads/camera/current", util.tornado.UrlForwardHandler, dict(url=self._settings.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))),
# generated webassets
(r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=os.path.join(s.getBaseFolder("generated"), "webassets")))
(r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=os.path.join(self._settings.getBaseFolder("generated"), "webassets")))
]
for name, hook in pluginManager.get_hooks("octoprint.server.http.routes").items():
try:
@ -365,7 +366,7 @@ class Server():
self._tornado_app = Application(server_routes)
max_body_sizes = [
("POST", r"/api/files/([^/]*)", s.getInt(["server", "uploads", "maxSize"])),
("POST", r"/api/files/([^/]*)", self._settings.getInt(["server", "uploads", "maxSize"])),
("POST", r"/api/languages", 5 * 1024 * 1024)
]
@ -391,25 +392,25 @@ class Server():
self._logger.debug("Adding maximum body size of {size}B for {method} requests to {route})".format(**locals()))
max_body_sizes.append((method, route, size))
self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=s.getInt(["server", "maxSize"]))
self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=self._settings.getInt(["server", "maxSize"]))
self._server.listen(self._port, address=self._host)
eventManager.fire(events.Events.STARTUP)
if s.getBoolean(["serial", "autoconnect"]):
(port, baudrate) = s.get(["serial", "port"]), s.getInt(["serial", "baudrate"])
if self._settings.getBoolean(["serial", "autoconnect"]):
(port, baudrate) = self._settings.get(["serial", "port"]), self._settings.getInt(["serial", "baudrate"])
printer_profile = printerProfileManager.get_default()
connectionOptions = get_connection_options()
if port in connectionOptions["ports"]:
printer.connect(port=port, baudrate=baudrate, profile=printer_profile["id"] if "id" in printer_profile else "_default")
# start up watchdogs
if s.getBoolean(["feature", "pollWatched"]):
if self._settings.getBoolean(["feature", "pollWatched"]):
# use less performant polling observer if explicitely configured
observer = PollingObserver()
else:
# use os default
observer = Observer()
observer.schedule(util.watchdog.GcodeWatchdogHandler(fileManager, printer), s.getBaseFolder("watched"))
observer.schedule(util.watchdog.GcodeWatchdogHandler(fileManager, printer), self._settings.getBaseFolder("watched"))
observer.start()
# run our startup plugins
@ -499,89 +500,12 @@ class Server():
except octoprint.users.UnknownUser:
pass
default_language = settings().get(["appearance", "defaultLanguage"])
default_language = self._settings.get(["appearance", "defaultLanguage"])
if default_language is not None and not default_language == "_default" and default_language in LANGUAGES:
return Locale.negotiate([default_language], LANGUAGES)
return request.accept_languages.best_match(LANGUAGES)
def _setup_logging(self, debug, logConf=None):
defaultConfig = {
"version": 1,
"formatters": {
"simple": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"file": {
"class": "logging.handlers.TimedRotatingFileHandler",
"level": "DEBUG",
"formatter": "simple",
"when": "D",
"backupCount": "1",
"filename": os.path.join(settings().getBaseFolder("logs"), "octoprint.log")
},
"serialFile": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "simple",
"maxBytes": 2 * 1024 * 1024, # let's limit the serial log to 2MB in size
"filename": os.path.join(settings().getBaseFolder("logs"), "serial.log")
}
},
"loggers": {
"SERIAL": {
"level": "CRITICAL",
"handlers": ["serialFile"],
"propagate": False
},
"tornado.application": {
"level": "INFO"
},
"tornado.general": {
"level": "INFO"
},
"octoprint.server.util.flask": {
"level": "WARN"
}
},
"root": {
"level": "INFO",
"handlers": ["console", "file"]
}
}
if debug:
defaultConfig["root"]["level"] = "DEBUG"
if logConf is None:
logConf = os.path.join(settings().getBaseFolder("base"), "logging.yaml")
configFromFile = {}
if os.path.exists(logConf) and os.path.isfile(logConf):
import yaml
with open(logConf, "r") as f:
configFromFile = yaml.safe_load(f)
config = octoprint.util.dict_merge(defaultConfig, configFromFile)
logging.config.dictConfig(config)
logging.captureWarnings(True)
import warnings
warnings.simplefilter("always")
if settings().getBoolean(["serial", "log"]):
# enable debug logging to serial.log
logging.getLogger("SERIAL").setLevel(logging.DEBUG)
logging.getLogger("SERIAL").debug("Enabling serial logging")
def _setup_app(self):
@app.before_request
def before_request():
@ -731,10 +655,10 @@ class Server():
util.flask.fix_webassets_cache()
util.flask.fix_webassets_filtertool()
base_folder = settings().getBaseFolder("generated")
base_folder = self._settings.getBaseFolder("generated")
# clean the folder
if settings().getBoolean(["devel", "webassets", "clean_on_startup"]):
if self._settings.getBoolean(["devel", "webassets", "clean_on_startup"]):
import shutil
for entry in ("webassets", ".webassets-cache"):
path = os.path.join(base_folder, entry)
@ -756,16 +680,16 @@ class Server():
return base_folder
assets = CustomDirectoryEnvironment(app)
assets.debug = not settings().getBoolean(["devel", "webassets", "bundle"])
assets.debug = not self._settings.getBoolean(["devel", "webassets", "bundle"])
UpdaterType = type(util.flask.SettingsCheckUpdater)(util.flask.SettingsCheckUpdater.__name__, (util.flask.SettingsCheckUpdater,), dict(
updater=assets.updater
))
assets.updater = UpdaterType
enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"])
enable_timelapse = (settings().get(["webcam", "snapshot"]) and settings().get(["webcam", "ffmpeg"]))
preferred_stylesheet = settings().get(["devel", "stylesheet"])
enable_gcodeviewer = self._settings.getBoolean(["gcodeViewer", "enabled"])
enable_timelapse = (self._settings.get(["webcam", "snapshot"]) and self._settings.get(["webcam", "ffmpeg"]))
preferred_stylesheet = self._settings.get(["devel", "stylesheet"])
dynamic_assets = util.flask.collect_plugin_assets(
enable_gcodeviewer=enable_gcodeviewer,
@ -857,7 +781,7 @@ class Server():
register_filter(JsDelimiterBundle)
js_libs_bundle = Bundle(*js_libs, output="webassets/packed_libs.js", filters="js_delimiter_bundler")
if settings().getBoolean(["devel", "webassets", "minify"]):
if self._settings.getBoolean(["devel", "webassets", "minify"]):
js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="rjsmin, js_delimiter_bundler")
else:
js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="js_delimiter_bundler")