From 7226c1edae75d0cdf4ef433844cc49a3edb38efd Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Sun, 10 Mar 2013 13:14:37 +0000 Subject: [PATCH] Lots of work to daemonise - logging still not working --- octoprint/daemon.py | 126 +++++++++++++++++++++++++++++++++ octoprint/server.py | 166 ++++++++++++++++++++++++-------------------- run-as-daemon | 29 ++++++++ 3 files changed, 244 insertions(+), 77 deletions(-) create mode 100644 octoprint/daemon.py create mode 100755 run-as-daemon diff --git a/octoprint/daemon.py b/octoprint/daemon.py new file mode 100644 index 0000000..919ae9c --- /dev/null +++ b/octoprint/daemon.py @@ -0,0 +1,126 @@ +"""Generic linux daemon base class for python 3.x.""" + +import sys, os, time, atexit, signal + +class Daemon: + """A generic daemon class. + + Usage: subclass the daemon class and override the run() method.""" + + def __init__(self, pidfile): self.pidfile = pidfile + + def daemonize(self): + """Deamonize class. UNIX double fork mechanism.""" + + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError as err: + sys.stderr.write('fork #1 failed: {0}\n'.format(err)) + sys.exit(1) + + # decouple from parent environment + os.chdir('/') + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + + # exit from second parent + sys.exit(0) + except OSError as err: + sys.stderr.write('fork #2 failed: {0}\n'.format(err)) + sys.exit(1) + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = open(os.devnull, 'r') + so = open(os.devnull, 'a+') + se = open(os.devnull, 'a+') + + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + # write pidfile + atexit.register(self.delpid) + + pid = str(os.getpid()) + with open(self.pidfile,'w+') as f: + f.write(pid + '\n') + + def delpid(self): + os.remove(self.pidfile) + + def start(self): + """Start the daemon.""" + + # Check for a pidfile to see if the daemon already runs + try: + with open(self.pidfile,'r') as pf: + + pid = int(pf.read().strip()) + except IOError: + pid = None + + if pid: + message = "pidfile {0} already exist. " + \ + "Daemon already running?\n" + sys.stderr.write(message.format(self.pidfile)) + sys.exit(1) + + # Start the daemon + self.daemonize() + self.run() + + # Should the daemon terminate ensure pid removal + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + + def stop(self): + """Stop the daemon.""" + + # Get the pid from the pidfile + try: + with open(self.pidfile,'r') as pf: + pid = int(pf.read().strip()) + except IOError: + pid = None + + if not pid: + message = "pidfile {0} does not exist. " + \ + "Daemon not running?\n" + sys.stderr.write(message.format(self.pidfile)) + return # not an error in a restart + + # Try killing the daemon process + try: + while 1: + os.kill(pid, signal.SIGTERM) + time.sleep(0.1) + except OSError as err: + e = str(err.args) + if e.find("No such process") > 0: + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + print (str(err.args)) + sys.exit(1) + + def restart(self): + """Restart the daemon.""" + self.stop() + self.start() + + def run(self): + """You should override this method when you subclass Daemon. + + It will be called after the process has been daemonized by + start() or restart().""" + diff --git a/octoprint/server.py b/octoprint/server.py index 80dacbb..285997e 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -16,23 +16,13 @@ import octoprint.timelapse as timelapse import octoprint.gcodefiles as gcodefiles import octoprint.util as util -BASEURL = "/ajax/" SUCCESS = {} - UPLOAD_FOLDER = settings().getBaseFolder("uploads") - +BASEURL = "/ajax/" app = Flask("octoprint") -gcodeManager = gcodefiles.GcodeManager() -printer = Printer(gcodeManager) - -@app.route("/") -def index(): - return render_template( - "index.html", - webcamStream=settings().get(["webcam", "stream"]), - enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None), - enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]) - ) +# Only instantiated by the app runner +printer = None +gcodeManager = None #~~ Printer state @@ -51,11 +41,13 @@ class PrinterStateConnection(tornadio2.SocketConnection): def on_open(self, info): self._logger.info("New connection from client") + # Use of global here is smelly printer.registerCallback(self) gcodeManager.registerCallback(self) def on_close(self): self._logger.info("Closed client connection") + # Use of global here is smelly printer.unregisterCallback(self) gcodeManager.unregisterCallback(self) @@ -101,6 +93,17 @@ class PrinterStateConnection(tornadio2.SocketConnection): with self._temperatureBacklogMutex: self._temperatureBacklog.append(data) +# Did attempt to make webserver an encapsulated class but ended up with __call__ failures + +@app.route("/") +def index(): + return render_template( + "index.html", + webcamStream=settings().get(["webcam", "stream"]), + enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None), + enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]) + ) + #~~ Printer control @app.route(BASEURL + "control/connectionOptions", methods=["GET"]) @@ -171,7 +174,7 @@ def setTargetTemperature(): return jsonify(SUCCESS) elif request.values.has_key("temp"): - # set target temperature + # set target temperature temp = request.values["temp"] printer.command("M104 S" + temp) @@ -381,7 +384,7 @@ def setSettings(): if "movementSpeedX" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "x"], data["printer"]["movementSpeedX"]) if "movementSpeedY" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "y"], data["printer"]["movementSpeedY"]) if "movementSpeedZ" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "z"], data["printer"]["movementSpeedZ"]) - if "movementSpeedE" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "e"], data["printer"]["movementSpeedE"]) + if "movementSpeedE" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "e"], data["printer"]["movementSpeedE"]) if "webcam" in data.keys(): if "streamUrl" in data["webcam"].keys(): s.set(["webcam", "stream"], data["webcam"]["streamUrl"]) @@ -408,77 +411,86 @@ def setSettings(): return getSettings() #~~ startup code +class Server(): + def run(self, host = "0.0.0.0", port = 5000, debug = False): + # Global as I can't work out a way to get it into PrinterStateConnection + global printer + global gcodeManager -def run(host = "0.0.0.0", port = 5000, debug = False): - from tornado.wsgi import WSGIContainer - from tornado.httpserver import HTTPServer - from tornado.ioloop import IOLoop - from tornado.web import Application, FallbackHandler + from tornado.wsgi import WSGIContainer + from tornado.httpserver import HTTPServer + from tornado.ioloop import IOLoop + from tornado.web import Application, FallbackHandler - logging.getLogger(__name__).info("Listening on http://%s:%d" % (host, port)) - app.debug = debug + gcodeManager = gcodefiles.GcodeManager() + printer = Printer(gcodeManager) - router = tornadio2.TornadioRouter(PrinterStateConnection) - tornado_app = Application(router.urls + [ - (".*", FallbackHandler, {"fallback": WSGIContainer(app)}) - ]) - server = HTTPServer(tornado_app) - server.listen(port, address=host) - IOLoop.instance().start() + logging.getLogger(__name__).info("Listening on http://%s:%d" % (host, port)) + app.debug = debug -def initLogging(): - 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" + self._router = tornadio2.TornadioRouter(PrinterStateConnection) + + self._tornado_app = Application(self._router.urls + [ + (".*", FallbackHandler, {"fallback": WSGIContainer(app)}) + ]) + self._server = HTTPServer(self._tornado_app) + self._server.listen(port, address=host) + IOLoop.instance().start() + + def initLogging(self): + self._config = { + "version": 1, + "formatters": { + "simple": { + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + } }, - "file": { - "class": "logging.handlers.TimedRotatingFileHandler", - "level": "DEBUG", - "formatter": "simple", - "when": "D", - "backupCount": "1", - "filename": os.path.join(settings().getBaseFolder("logs"), "octoprint.log") + "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") + } + }, + "loggers": { + "octoprint.gcodefiles": { + "level": "DEBUG" + } + }, + "root": { + "level": "INFO", + "handlers": ["console", "file"] } - }, - "loggers": { - "octoprint.gcodefiles": { - "level": "DEBUG" - } - }, - "root": { - "level": "INFO", - "handlers": ["console", "file"] } - } - logging.config.dictConfig(config) + logging.config.dictConfig(self._config) -def main(): - from optparse import OptionParser + def start(self): + from optparse import OptionParser - defaultHost = settings().get(["server", "host"]) - defaultPort = settings().get(["server", "port"]) + self._defaultHost = settings().get(["server", "host"]) + self._defaultPort = settings().get(["server", "port"]) - parser = OptionParser(usage="usage: %prog [options]") - parser.add_option("-d", "--debug", action="store_true", dest="debug", - help="Enable debug mode") - parser.add_option("--host", action="store", type="string", default=defaultHost, dest="host", - help="Specify the host on which to bind the server, defaults to %s if not set" % (defaultHost)) - parser.add_option("--port", action="store", type="int", default=defaultPort, dest="port", - help="Specify the port on which to bind the server, defaults to %s if not set" % (defaultPort)) - (options, args) = parser.parse_args() + 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() - initLogging() - run(host=options.host, port=options.port, debug=options.debug) + self.initLogging() + self.run(host=options.host, port=options.port, debug=options.debug) if __name__ == "__main__": - main() + octoprint = Server() + octoprint.start() diff --git a/run-as-daemon b/run-as-daemon new file mode 100755 index 0000000..cb43a91 --- /dev/null +++ b/run-as-daemon @@ -0,0 +1,29 @@ +#!/usr/bin/python +import sys, time +from octoprint.daemon import Daemon +from octoprint.server import Server + +class Main(Daemon): + def run(self): + octoprint = Server() + octoprint.run() + +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()