From 363f00775b18074dca689f41105a76ee57976fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 11 Mar 2013 21:00:43 +0100 Subject: [PATCH] New config options for daemonization, configfile location and basedir location Using --daemon {start|stop|restart} OctoPrint can now be daemonized/controlled in daemon mode. Via --pidfile it's possible to set the pidfile to use, --configfile allows specification of the config.yaml to use, --basedir specifies the location of the basedir from which to base off the upload, timelapse and log folders. I also updated the README to include some config file settings which were previously undocumented. --- README.md | 69 +++++++++++++++++++++++-------- octoprint/server.py | 54 +++++++++++++------------ octoprint/settings.py | 94 +++++++++++++++++-------------------------- run | 70 +++++++++++++++++++++++++++----- run-as-daemon | 29 ------------- 5 files changed, 177 insertions(+), 139 deletions(-) delete mode 100755 run-as-daemon diff --git a/README.md b/README.md index d9e9e72..11c49ba 100644 --- a/README.md +++ b/README.md @@ -39,39 +39,39 @@ Usage Just start the server via - python -m octoprint.server - -or alternatively - ./run By default it binds to all interfaces on port 5000 (so pointing your browser to `http://127.0.0.1:5000` will do the trick). If you want to change that, use the additional command line parameters `host` and `port`, -which accept the host ip to bind to and the numeric port number respectively. If for example you want to the server +which accept the host ip to bind to and the numeric port number respectively. If for example you want the server to only listen on the local interface on port 8080, the command line would be - python -m octoprint.server --host=127.0.0.1 --port=8080 - -or - ./run --host=127.0.0.1 --port=8080 Alternatively, the host and port on which to bind can be defined via the configuration. -If you want to run OctoPrint as a daemon, there's another script for that: +If you want to run OctoPrint as a daemon (only supported on Linux), use - ./run-as-daemon [start|stop|restart] + ./run --daemon {start|stop|restart} [--pidfile PIDFILE] -It will create a pid file at `/tmp/octoprint.pid` for now. Further commandline arguments will not be evaluated, -so you'll need to define host and port in the configuration file if you want something different there than the default. +If you do not supply a custom pidfile location via `--pidfile PIDFILE`, it will be created at `/tmp/octoprint.pid`. + +You can also specify the configfile or the base directory (for basing off the `uploads`, `timelapse` and `logs` folders), +e.g.: + + ./run --config /path/to/another/config.yaml --basedir /path/to/my/basedir + +See `run --help` for further information. Configuration ------------- -The config-file `config.yaml` for OctoPrint is expected in its settings folder, which is located at `~/.octoprint` -on Linux, at `%APPDATA%/OctoPrint` on Windows and at `~/Library/Application Support/OctoPrint` on MacOS. +If not specified via the commandline, the configfile `config.yaml` for OctoPrint is expected in its settings folder, +which is located at `~/.octoprint` on Linux, at `%APPDATA%/OctoPrint` on Windows and at +`~/Library/Application Support/OctoPrint` on MacOS. -The following example config should explain the available options: +The following example config should explain the available options, most of which can also be configured via the +settings dialog within OctoPrint: # Use the following settings to configure the serial connection to the printer serial: @@ -111,6 +111,10 @@ The following example config should explain the available options: # Whether to enable the gcode viewer in the UI or not gCodeVisualizer: true + # Specified whether OctoPrint should wait for the start response from the printer before trying to send commands + # during connect + waitForStartOnConnect: false + # Use the following settings to set custom paths for folders used by OctoPrint folder: # Absolute path where to store gcode uploads. Defaults to the uploads folder in the OctoPrint settings folder @@ -122,7 +126,38 @@ The following example config should explain the available options: # Absolute path where to store temporary timelapse files. Defaults to the timelapse/tmp folder in the OctoPrint # settings dir - timelapse_tmp: /path/timelapse/tmp/folder + timelapse_tmp: /path/to/timelapse/tmp/folder + + # Absolute path where to store log files. Defaults to the logs folder in the OctoPrint settings dir + logs: /path/to/logs/folder + + # Use the following settings to configure temperature profiles which will be displayed in the temperature tab. + temperature: + profiles: + - name: ABS + extruder: 210 + bed: 100 + - name: PLA + extruder: 180 + bed: 60 + + # Use the following settings to configure printer parameters + printerParameters: + # Use this to define the movement speed on X, Y, Z and E to use for the controls on the controls tab + movementSpeed: + x: 6000 + y: 6000 + z: 200 + e: 300 + + # Use the following settings to tweak OctoPrint's appearance a bit to better distinguish multiple instances/printers + appearance: + # Use this to give your printer a name. It will be displayed in the title bar (as " [OctoPrint]") and in the + # navigation bar (as "OctoPrint: ") + name: My Printer Model + + # Use this to color the navigation bar. Supported colors are red, orange, yellow, green, blue, violet and default. + color: blue # Use the following settings to add custom controls to the "Controls" tab within OctoPrint # diff --git a/octoprint/server.py b/octoprint/server.py index 6147706..9a342da 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -18,7 +18,6 @@ import octoprint.gcodefiles as gcodefiles import octoprint.util as util SUCCESS = {} -UPLOAD_FOLDER = settings().getBaseFolder("uploads") BASEURL = "/ajax/" app = Flask("octoprint") # Only instantiated by the Server().run() method @@ -249,7 +248,7 @@ def readGcodeFiles(): @app.route(BASEURL + "gcodefiles/", methods=["GET"]) def readGcodeFile(filename): - return send_from_directory(UPLOAD_FOLDER, filename, as_attachment=True) + return send_from_directory(settings().getBaseFolder("uploads"), filename, as_attachment=True) @app.route(BASEURL + "gcodefiles/upload", methods=["POST"]) def uploadGcodeFile(): @@ -442,7 +441,14 @@ def performSystemAction(): #~~ startup code class Server(): - def run(self, host = "0.0.0.0", port = 5000, debug = False): + def __init__(self, configfile=None, basedir=None, host="0.0.0.0", port=5000, debug=False): + self._configfile = configfile + self._basedir = basedir + self._host = host + self._port = port + self._debug = debug + + def run(self): # Global as I can't work out a way to get it into PrinterStateConnection global printer global gcodeManager @@ -452,11 +458,22 @@ class Server(): from tornado.ioloop import IOLoop from tornado.web import Application, FallbackHandler + # 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) + gcodeManager = gcodefiles.GcodeManager() printer = Printer(gcodeManager) - logging.getLogger(__name__).info("Listening on http://%s:%d" % (host, port)) - app.debug = debug + if self._host is None: + self._host = settings().get(["server", "host"]) + if self._port is None: + self._port = settings().getInt(["server", "port"]) + + logging.getLogger(__name__).info("Listening on http://%s:%d" % (self._host, self._port)) + app.debug = self._debug self._router = tornadio2.TornadioRouter(PrinterStateConnection) @@ -464,10 +481,13 @@ class Server(): (".*", FallbackHandler, {"fallback": WSGIContainer(app)}) ]) self._server = HTTPServer(self._tornado_app) - self._server.listen(port, address=host) + self._server.listen(self._port, address=self._host) IOLoop.instance().start() - def initLogging(self): + def _initSettings(self, configfile, basedir): + s = settings(init=True, basedir=basedir, configfile=configfile) + + def _initLogging(self, debug): self._config = { "version": 1, "formatters": { @@ -503,24 +523,6 @@ class Server(): } logging.config.dictConfig(self._config) - def start(self): - from optparse import OptionParser - - self._defaultHost = settings().get(["server", "host"]) - self._defaultPort = settings().get(["server", "port"]) - - self._parser = OptionParser(usage="usage: %prog [options]") - self._parser.add_option("-d", "--debug", action="store_true", dest="debug", - help="Enable debug mode") - self._parser.add_option("--host", action="store", type="string", default=self._defaultHost, dest="host", - help="Specify the host on which to bind the server, defaults to %s if not set" % (self._defaultHost)) - self._parser.add_option("--port", action="store", type="int", default=self._defaultPort, dest="port", - help="Specify the port on which to bind the server, defaults to %s if not set" % (self._defaultPort)) - (options, args) = self._parser.parse_args() - - self.initLogging() - self.run(host=options.host, port=options.port, debug=options.debug) - if __name__ == "__main__": octoprint = Server() - octoprint.start() + octoprint.run() diff --git a/octoprint/settings.py b/octoprint/settings.py index 86eb988..7e037fa 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -9,17 +9,19 @@ import yaml import logging APPNAME="OctoPrint" -OLD_APPNAME="PrinterWebUI" instance = None -def settings(): +def settings(init=False, configfile=None, basedir=None): global instance if instance is None: - instance = Settings() + if init: + instance = Settings(configfile, basedir) + else: + raise ValueError("Settings not initialized yet") return instance -old_default_settings = { +default_settings = { "serial": { "port": None, "baudrate": None @@ -35,33 +37,16 @@ old_default_settings = { "bitrate": "5000k", "watermark": True }, + "feature": { + "gCodeVisualizer": True, + "waitForStartOnConnect": False + }, "folder": { "uploads": None, "timelapse": None, "timelapse_tmp": None, "logs": None }, - "feature": { - "gCodeVisualizer": True, - "waitForStartOnConnect": False - }, -} - -default_settings = old_default_settings.copy() -default_settings.update({ - "appearance": { - "name": "", - "color": "default" - }, - "controls": [], - "printerParameters": { - "movementSpeed": { - "x": 6000, - "y": 6000, - "z": 200, - "e": 300 - } - }, "temperature": { "profiles": [ @@ -69,16 +54,29 @@ default_settings.update({ {"name": "PLA", "extruder" : 180, "bed" : 60 } ] }, + "printerParameters": { + "movementSpeed": { + "x": 6000, + "y": 6000, + "z": 200, + "e": 300 + } + }, + "appearance": { + "name": "", + "color": "default" + }, + "controls": [], "system": { "actions": [] } -}) +} valid_boolean_trues = ["true", "yes", "y", "1"] class Settings(object): - def __init__(self): + def __init__(self, configfile=None, basedir=None): self._logger = logging.getLogger(__name__) self.settings_dir = None @@ -86,16 +84,14 @@ class Settings(object): self._config = None self._dirty = False - self._init_settings_dir() - self.load() + self._init_settings_dir(basedir) + self.load(configfile) - def _init_settings_dir(self): - self.settings_dir = _resolveSettingsDir(APPNAME) - - # migration due to rename - old_settings_dir = _resolveSettingsDir(OLD_APPNAME) - if os.path.exists(old_settings_dir) and os.path.isdir(old_settings_dir) and not os.path.exists(self.settings_dir): - os.rename(old_settings_dir, self.settings_dir) + def _init_settings_dir(self, basedir): + if basedir is not None: + self.settings_dir = basedir + else: + self.settings_dir = _resolveSettingsDir(APPNAME) def _getDefaultFolder(self, type): folder = default_settings["folder"][type] @@ -105,29 +101,15 @@ class Settings(object): #~~ load and save - def load(self): - filename = os.path.join(self.settings_dir, "config.yaml") - oldFilename = os.path.join(self.settings_dir, "config.ini") + def load(self, configfile): + if configfile is not None: + filename = configfile + else: + filename = os.path.join(self.settings_dir, "config.yaml") + if os.path.exists(filename) and os.path.isfile(filename): with open(filename, "r") as f: self._config = yaml.safe_load(f) - elif os.path.exists(oldFilename) and os.path.isfile(oldFilename): - config = ConfigParser.ConfigParser(allow_no_value=True) - config.read(oldFilename) - self._config = {} - for section in old_default_settings.keys(): - if not config.has_section(section): - continue - - self._config[section] = {} - for option in old_default_settings[section].keys(): - if not config.has_option(section, option): - continue - - self._config[section][option] = config.get(section, option) - self._dirty = True - self.save(force=True) - os.rename(oldFilename, oldFilename + ".bck") else: self._config = {} diff --git a/run b/run index fd9f698..e43344c 100755 --- a/run +++ b/run @@ -1,14 +1,62 @@ -#!/bin/bash +#!/usr/bin/python +import sys +from octoprint.daemon import Daemon +from octoprint.server import Server -PYTHON=`which python` +class Main(Daemon): + def __init__(self, pidfile, configfile, basedir, host, port, debug): + Daemon.__init__(self, pidfile) -SOURCE="${BASH_SOURCE[0]}" -while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink - DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" - SOURCE="$(readlink "$SOURCE")" - [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located -done -DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + self._configfile = configfile + self._basedir = basedir + self._host = host + self._port = port + self._debug = debug -cd $DIR -$PYTHON -m octoprint.server $@ + def run(self): + octoprint = Server(self._configfile, self._basedir, self._host, self._port, self._debug) + octoprint.run() + +def main(): + import argparse + + parser = argparse.ArgumentParser(prog="run") + + 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("--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") + args = parser.parse_args() + + 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) + if args.command is not None: + if "start" == args.command: + daemon.start() + elif "stop" == args.command: + daemon.stop() + elif "restart" == args.command: + daemon.restart() + else: + octoprint = Server(args.config, args.basedir, args.host, args.port, args.debug) + octoprint.run() + +if __name__ == "__main__": + main() diff --git a/run-as-daemon b/run-as-daemon deleted file mode 100755 index 0b4ce11..0000000 --- a/run-as-daemon +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/python -import sys -from octoprint.daemon import Daemon -from octoprint.server import Server - -class Main(Daemon): - def run(self): - octoprint = Server() - octoprint.start() - -def main(): - daemon = Main('/tmp/octoprint.pid') - if len(sys.argv) == 2: - if 'start' == sys.argv[1]: - daemon.start() - elif 'stop' == sys.argv[1]: - daemon.stop() - elif 'restart' == sys.argv[1]: - daemon.restart() - else: - print "Unknown command" - sys.exit(2) - sys.exit(0) - else: - print "usage: %s start|stop|restart" % sys.argv[0] - sys.exit(2) - -if __name__ == "__main__": - main()