# coding=utf-8 from __future__ import absolute_import __author__ = "Gina Häußge " __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" import uuid from sockjs.tornado import SockJSRouter from flask import Flask, render_template, send_from_directory, g, request, make_response from flask.ext.login import LoginManager from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed from flask.ext.babel import Babel from babel import Locale from watchdog.observers import Observer import os import logging import logging.config SUCCESS = {} NO_CONTENT = ("", 204) app = Flask("octoprint") babel = Babel(app) debug = False printer = None gcodeManager = None userManager = None eventManager = None loginManager = None pluginManager = None principals = Principal(app) admin_permission = Permission(RoleNeed("admin")) user_permission = Permission(RoleNeed("user")) # only import the octoprint stuff down here, as it might depend on things defined above to be initialized already from octoprint.printer import Printer, getConnectionOptions from octoprint.settings import settings import octoprint.gcodefiles as gcodefiles import octoprint.users as users import octoprint.events as events import octoprint.plugin import octoprint.timelapse import octoprint._version import octoprint.util from . import util UI_API_KEY = ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes) versions = octoprint._version.get_versions() VERSION = versions['version'] BRANCH = versions['branch'] if 'branch' in versions else None DISPLAY_VERSION = "%s (%s branch)" % (VERSION, BRANCH) if BRANCH else VERSION del versions def get_available_locale_identifiers(locales): result = set() # add available translations for locale in locales: result.add(locale.language) if locale.territory: # if a territory is specified, add that too result.add("%s_%s" % (locale.language, locale.territory)) return result LOCALES = [Locale.parse("en")] + babel.list_translations() LANGUAGES = get_available_locale_identifiers(LOCALES) @app.before_request def before_request(): g.locale = get_locale() @babel.localeselector def get_locale(): if "l10n" in request.values: return Locale.negotiate([request.values["l10n"]], LANGUAGES) return request.accept_languages.best_match(LANGUAGES) @app.route("/") def index(): settings_plugins = pluginManager.get_implementations(octoprint.plugin.SettingsPlugin) settings_plugin_template_vars = dict() for name, implementation in settings_plugins.items(): settings_plugin_template_vars[name] = implementation.get_template_vars() asset_plugins = pluginManager.get_implementations(octoprint.plugin.AssetPlugin) asset_plugin_urls = dict() for name, implementation in asset_plugins.items(): asset_plugin_urls[name] = implementation.get_assets() return render_template( "index.jinja2", webcamStream=settings().get(["webcam", "stream"]), enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None), enableGCodeVisualizer=settings().get(["gcodeViewer", "enabled"]), enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]), enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0, enableAccessControl=userManager is not None, enableSdSupport=settings().get(["feature", "sdSupport"]), firstRun=settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()), debug=debug, version=VERSION, display_version=DISPLAY_VERSION, stylesheet=settings().get(["devel", "stylesheet"]), gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]), gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]), uiApiKey=UI_API_KEY, settingsPlugins=settings_plugin_template_vars, assetPlugins=asset_plugin_urls ) @app.route("/robots.txt") def robotsTxt(): return send_from_directory(app.static_folder, "robots.txt") @app.route("/plugin_assets//") def plugin_assets(name, filename): asset_plugins = pluginManager.get_implementations(octoprint.plugin.AssetPlugin) if not name in asset_plugins: return make_response(404) asset_plugin = asset_plugins[name] asset_folder = asset_plugin.get_asset_folder() if asset_folder is None: make_response(404) return send_from_directory(asset_folder, filename) @identity_loaded.connect_via(app) def on_identity_loaded(sender, identity): user = load_user(identity.id) if user is None: return identity.provides.add(UserNeed(user.get_name())) if user.is_user(): identity.provides.add(RoleNeed("user")) if user.is_admin(): identity.provides.add(RoleNeed("admin")) def load_user(id): if userManager is not None: return userManager.findUser(id) return users.DummyUser() #~~ startup code 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 self._host = host self._port = port self._debug = debug self._allowRoot = allowRoot self._logConf = logConf self._server = None def run(self): if not self._allowRoot: self._checkForRoot() global printer global gcodeManager global userManager global eventManager global loginManager global pluginManager global debug from tornado.ioloop import IOLoop from tornado.web import Application debug = self._debug # first initialize the settings singleton and make sure it uses given configfile and basedir if available self._initSettings(self._configfile, self._basedir) # then initialize logging self._initLogging(self._debug, self._logConf) logger = logging.getLogger(__name__) logger.info("Starting OctoPrint %s" % DISPLAY_VERSION) eventManager = events.eventManager() gcodeManager = gcodefiles.GcodeManager() printer = Printer(gcodeManager) pluginManager = octoprint.plugin.plugin_manager(init=True) # configure additional template folders for jinja2 template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin) additional_template_folders = [] for plugin in template_plugins.values(): folder = plugin.get_template_folder() if folder is not None: additional_template_folders.append(plugin.get_template_folder()) import jinja2 jinja_loader = jinja2.ChoiceLoader([ app.jinja_loader, jinja2.FileSystemLoader(additional_template_folders) ]) app.jinja_loader = jinja_loader del jinja2 # configure timelapse octoprint.timelapse.configureTimelapse() # setup command triggers events.CommandTrigger(printer) if self._debug: events.DebugEventListener() if settings().getBoolean(["accessControl", "enabled"]): userManagerName = settings().get(["accessControl", "userManager"]) try: clazz = octoprint.util.getClass(userManagerName) userManager = clazz() except AttributeError, e: logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName) app.wsgi_app = util.ReverseProxied( app.wsgi_app, settings().get(["server", "reverseProxy", "prefixHeader"]), settings().get(["server", "reverseProxy", "schemeHeader"]), settings().get(["server", "reverseProxy", "prefixFallback"]), settings().get(["server", "reverseProxy", "prefixScheme"]) ) app.secret_key = "k3PuVYgtxNm8DXKKTw2nWmFQQun9qceV" loginManager = LoginManager() loginManager.session_protection = "strong" loginManager.user_callback = load_user if userManager is None: loginManager.anonymous_user = users.DummyUser principals.identity_loaders.appendleft(users.dummy_identity_loader) loginManager.init_app(app) if self._host is None: self._host = settings().get(["server", "host"]) if self._port is None: self._port = settings().getInt(["server", "port"]) app.debug = self._debug from octoprint.server.api import api # register API blueprint app.register_blueprint(api, url_prefix="/api") # also register any blueprints defined in BlueprintPlugins octoprint.plugin.call_plugin(octoprint.plugin.types.BlueprintPlugin, "get_blueprint", callback=lambda name, _, blueprint: app.register_blueprint(blueprint, url_prefix="/plugin/{name}".format(name=name))) self._router = SockJSRouter(self._createSocketConnection, "/sockjs") upload_suffixes = dict(name=settings().get(["server", "uploads", "nameSuffix"]), path=settings().get(["server", "uploads", "pathSuffix"])) self._tornado_app = Application(self._router.urls + [ (r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, dict(path=settings().getBaseFolder("timelapse"), as_attachment=True)), (r"/downloads/files/local/([^/]*\.(gco|gcode))", util.tornado.LargeResponseHandler, dict(path=settings().getBaseFolder("uploads"), as_attachment=True)), (r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, dict(path=settings().getBaseFolder("logs"), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))), (r"/downloads/camera/current", util.tornado.UrlForwardHandler, dict(url=settings().get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))), (r".*", util.tornado.UploadStorageFallbackHandler, dict(fallback=util.tornado.WsgiInputContainer(app.wsgi_app), file_prefix="octoprint-file-upload-", file_suffix=".tmp", suffixes=upload_suffixes)) ]) max_body_sizes = [ ("POST", r"/api/files/([^/]*)", settings().getInt(["server", "uploads", "maxSize"])) ] self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=settings().getInt(["server", "maxSize"])) self._server.listen(self._port, address=self._host) eventManager.fire(events.Events.STARTUP) if settings().getBoolean(["serial", "autoconnect"]): (port, baudrate) = settings().get(["serial", "port"]), settings().getInt(["serial", "baudrate"]) connectionOptions = getConnectionOptions() if port in connectionOptions["ports"]: printer.connect(port, baudrate) # start up watchdogs observer = Observer() observer.schedule(util.watchdog.GcodeWatchdogHandler(gcodeManager, printer), settings().getBaseFolder("watched")) observer.schedule(util.watchdog.UploadCleanupWatchdogHandler(gcodeManager), settings().getBaseFolder("uploads")) observer.start() # now it's the turn of the startup plugins octoprint.plugin.call_plugin(octoprint.plugin.StartupPlugin, "on_startup", args=(self._host, self._port)) logger.info("Listening on http://%s:%d" % (self._host, self._port)) try: IOLoop.instance().start() except KeyboardInterrupt: logger.info("Goodbye!") except: logger.fatal("Now that is embarrassing... Something really really went wrong here. Please report this including the stacktrace below in OctoPrint's bugtracker. Thanks!") logger.exception("Stacktrace follows:") finally: observer.stop() observer.join() def _createSocketConnection(self, session): global printer, gcodeManager, userManager, eventManager return util.sockjs.PrinterStateConnection(printer, gcodeManager, userManager, eventManager, session) def _checkForRoot(self): if "geteuid" in dir(os) and os.geteuid() == 0: exit("You should not run OctoPrint as root!") def _initSettings(self, configfile, basedir): settings(init=True, basedir=basedir, configfile=configfile) def _initLogging(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 } }, "root": { "level": "INFO", "handlers": ["console", "file"] } } if debug: defaultConfig["root"]["level"] = "DEBUG" if logConf is None: logConf = os.path.join(settings().settings_dir, "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) if settings().getBoolean(["serial", "log"]): # enable debug logging to serial.log logging.getLogger("SERIAL").setLevel(logging.DEBUG) logging.getLogger("SERIAL").debug("Enabling serial logging") if __name__ == "__main__": server = Server() server.run()