OctoPrint/octoprint/server.py

888 lines
28 KiB
Python
Raw Normal View History

2012-12-25 10:55:00 +00:00
# coding=utf-8
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
2012-12-25 10:55:00 +00:00
from werkzeug.utils import secure_filename
import tornadio2
from flask import Flask, request, render_template, jsonify, send_from_directory, url_for, current_app, session, abort
from flask.ext.login import LoginManager, login_user, logout_user, login_required, current_user
from flask.ext.principal import Principal, Permission, RoleNeed, Identity, identity_changed, AnonymousIdentity, identity_loaded, UserNeed
2012-12-25 10:55:00 +00:00
import os
import threading
import logging, logging.config
import subprocess
from octoprint.printer import Printer, getConnectionOptions
from octoprint.settings import settings, valid_boolean_trues
import octoprint.timelapse
import octoprint.gcodefiles as gcodefiles
import octoprint.util as util
import octoprint.users as users
import octoprint.events as events
SUCCESS = {}
BASEURL = "/ajax/"
APIBASEURL = "/api/"
app = Flask("octoprint")
2013-03-10 13:40:53 +00:00
# Only instantiated by the Server().run() method
# In order that threads don't start too early when running as a Daemon
printer = None
timelapse = None
gcodeManager = None
userManager = None
2013-05-26 22:56:57 +00:00
eventManager = None
2012-12-25 10:55:00 +00:00
principals = Principal(app)
admin_permission = Permission(RoleNeed("admin"))
user_permission = Permission(RoleNeed("user"))
#~~ Printer state
2012-12-25 10:55:00 +00:00
class PrinterStateConnection(tornadio2.SocketConnection):
2013-05-26 22:56:57 +00:00
def __init__(self, printer, gcodeManager, userManager, eventManager, session, endpoint=None):
tornadio2.SocketConnection.__init__(self, session, endpoint)
self._logger = logging.getLogger(__name__)
self._temperatureBacklog = []
self._temperatureBacklogMutex = threading.Lock()
self._logBacklog = []
self._logBacklogMutex = threading.Lock()
self._messageBacklog = []
self._messageBacklogMutex = threading.Lock()
self._printer = printer
self._gcodeManager = gcodeManager
self._userManager = userManager
2013-05-26 22:56:57 +00:00
self._eventManager = eventManager
def on_open(self, info):
self._logger.info("New connection from client")
# Use of global here is smelly
self._printer.registerCallback(self)
self._gcodeManager.registerCallback(self)
2013-05-26 22:56:57 +00:00
self._eventManager.fire("ClientOpened")
self._eventManager.subscribe("MovieDone", self._onMovieDone)
2013-05-26 22:56:57 +00:00
def on_close(self):
self._logger.info("Closed client connection")
# Use of global here is smelly
self._printer.unregisterCallback(self)
self._gcodeManager.unregisterCallback(self)
2013-05-26 22:56:57 +00:00
self._eventManager.fire("ClientClosed")
self._eventManager.unsubscribe("MovieDone", self._onMovieDone)
2013-05-26 22:56:57 +00:00
def on_message(self, message):
pass
def sendCurrentData(self, data):
# add current temperature, log and message backlogs to sent data
with self._temperatureBacklogMutex:
temperatures = self._temperatureBacklog
self._temperatureBacklog = []
with self._logBacklogMutex:
logs = self._logBacklog
self._logBacklog = []
with self._messageBacklogMutex:
messages = self._messageBacklog
self._messageBacklog = []
data.update({
"temperatures": temperatures,
"logs": logs,
"messages": messages
})
self.emit("current", data)
def sendHistoryData(self, data):
self.emit("history", data)
def sendUpdateTrigger(self, type):
self.emit("updateTrigger", type)
def sendFeedbackCommandOutput(self, name, output):
self.emit("feedbackCommandOutput", {"name": name, "output": output})
def addLog(self, data):
with self._logBacklogMutex:
self._logBacklog.append(data)
def addMessage(self, data):
with self._messageBacklogMutex:
self._messageBacklog.append(data)
def addTemperature(self, data):
with self._temperatureBacklogMutex:
self._temperatureBacklog.append(data)
def _onMovieDone(self, event, payload):
self.sendUpdateTrigger("timelapseFiles")
# Did attempt to make webserver an encapsulated class but ended up with __call__ failures
@app.route("/")
def index():
return render_template(
"index.jinja2",
ajaxBaseUrl=BASEURL,
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"]),
enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0,
2013-05-20 17:18:03 +00:00
enableAccessControl=userManager is not None,
enableSdSupport=settings().get(["feature", "sdSupport"])
)
#~~ Printer control
2013-04-12 21:08:14 +00:00
@app.route(BASEURL + "control/connection/options", methods=["GET"])
def connectionOptions():
return jsonify(getConnectionOptions())
2012-12-25 10:55:00 +00:00
2013-04-12 21:08:14 +00:00
@app.route(BASEURL + "control/connection", methods=["POST"])
@login_required
def connect():
2013-04-12 21:08:14 +00:00
if "command" in request.values.keys() and request.values["command"] == "connect":
port = None
baudrate = None
if "port" in request.values.keys():
port = request.values["port"]
if "baudrate" in request.values.keys():
baudrate = request.values["baudrate"]
if "save" in request.values.keys():
settings().set(["serial", "port"], port)
settings().setInt(["serial", "baudrate"], baudrate)
settings().save()
printer.connect(port=port, baudrate=baudrate)
elif "command" in request.values.keys() and request.values["command"] == "disconnect":
printer.disconnect()
return jsonify(SUCCESS)
@app.route(BASEURL + "control/command", methods=["POST"])
@login_required
def printerCommand():
if "application/json" in request.headers["Content-Type"]:
data = request.json
parameters = {}
if "parameters" in data.keys(): parameters = data["parameters"]
commands = []
if "command" in data.keys(): commands = [data["command"]]
elif "commands" in data.keys(): commands = data["commands"]
commandsToSend = []
for command in commands:
commandToSend = command
if len(parameters) > 0:
commandToSend = command % parameters
commandsToSend.append(commandToSend)
printer.commands(commandsToSend)
return jsonify(SUCCESS)
2013-04-12 21:08:14 +00:00
@app.route(BASEURL + "control/job", methods=["POST"])
@login_required
2013-04-12 21:08:14 +00:00
def printJobControl():
if "command" in request.values.keys():
if request.values["command"] == "start":
printer.startPrint()
elif request.values["command"] == "pause":
printer.togglePausePrint()
elif request.values["command"] == "cancel":
printer.cancelPrint()
return jsonify(SUCCESS)
@app.route(BASEURL + "control/temperature", methods=["POST"])
@login_required
def setTargetTemperature():
2013-04-12 21:08:14 +00:00
if "temp" in request.values.keys():
# set target temperature
temp = request.values["temp"]
printer.command("M104 S" + temp)
2013-04-12 21:08:14 +00:00
if "bedTemp" in request.values.keys():
# set target bed temperature
bedTemp = request.values["bedTemp"]
printer.command("M140 S" + bedTemp)
return jsonify(SUCCESS)
@app.route(BASEURL + "control/jog", methods=["POST"])
@login_required
def jog():
if not printer.isOperational() or printer.isPrinting():
# do not jog when a print job is running or we don't have a connection
return jsonify(SUCCESS)
(movementSpeedX, movementSpeedY, movementSpeedZ, movementSpeedE) = settings().get(["printerParameters", "movementSpeed", ["x", "y", "z", "e"]])
if "x" in request.values.keys():
# jog x
x = request.values["x"]
printer.commands(["G91", "G1 X%s F%d" % (x, movementSpeedX), "G90"])
if "y" in request.values.keys():
# jog y
y = request.values["y"]
printer.commands(["G91", "G1 Y%s F%d" % (y, movementSpeedY), "G90"])
if "z" in request.values.keys():
# jog z
z = request.values["z"]
printer.commands(["G91", "G1 Z%s F%d" % (z, movementSpeedZ), "G90"])
if "homeXY" in request.values.keys():
# home x/y
printer.command("G28 X0 Y0")
if "homeZ" in request.values.keys():
# home z
printer.command("G28 Z0")
if "extrude" in request.values.keys():
# extrude/retract
length = request.values["extrude"]
printer.commands(["G91", "G1 E%s F%d" % (length, movementSpeedE), "G90"])
return jsonify(SUCCESS)
@app.route(BASEURL + "control/custom", methods=["GET"])
def getCustomControls():
customControls = settings().get(["controls"])
return jsonify(controls=customControls)
@app.route(BASEURL + "control/sd", methods=["POST"])
@login_required
def sdCommand():
if not settings().getBoolean(["feature", "sdSupport"]) or not printer.isOperational() or printer.isPrinting():
return jsonify(SUCCESS)
if "command" in request.values.keys():
command = request.values["command"]
if command == "init":
printer.initSdCard()
elif command == "refresh":
printer.refreshSdFiles()
elif command == "release":
printer.releaseSdCard()
return jsonify(SUCCESS)
#~~ Printer State
@app.route(BASEURL + "state", methods=["GET"])
@login_required
def getPrinterState():
currentData = printer.getCurrentData()
currentData.update({
"temperatures": printer.getCurrentTemperatures()
})
return jsonify(currentData)
#~~ GCODE file handling
@app.route(BASEURL + "gcodefiles", methods=["GET"])
def readGcodeFiles():
2013-05-20 17:18:03 +00:00
files = gcodeManager.getAllFileData()
sdFileList = printer.getSdFiles()
if sdFileList is not None:
for sdFile in sdFileList:
files.append({
"name": sdFile,
"size": "n/a",
"bytes": 0,
"date": "n/a",
"origin": "sd"
})
return jsonify(files=files)
@app.route(BASEURL + "gcodefiles/<path:filename>", methods=["GET"])
def readGcodeFile(filename):
return send_from_directory(settings().getBaseFolder("uploads"), filename, as_attachment=True)
@app.route(BASEURL + "gcodefiles/upload", methods=["POST"])
@login_required
def uploadGcodeFile():
filename = None
if "gcode_file" in request.files.keys():
2013-01-04 12:11:00 +00:00
file = request.files["gcode_file"]
filename = gcodeManager.addFile(file)
2013-05-20 17:18:03 +00:00
if filename and "target" in request.values.keys() and request.values["target"] == "sd":
printer.addSdFile(filename, gcodeManager.getAbsolutePath(filename))
2013-05-26 22:56:57 +00:00
global eventManager
2013-05-26 22:56:57 +00:00
eventManager.fire("Upload", filename)
return jsonify(files=gcodeManager.getAllFileData(), filename=filename)
@app.route(BASEURL + "gcodefiles/load", methods=["POST"])
@login_required
def loadGcodeFile():
if "filename" in request.values.keys():
printAfterLoading = False
if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
printAfterLoading = True
2013-05-26 22:56:57 +00:00
sd = False
filename = None
2013-05-20 17:18:03 +00:00
if "target" in request.values.keys() and request.values["target"] == "sd":
filename = request.values["filename"]
sd = True
2013-05-20 17:18:03 +00:00
else:
filename = gcodeManager.getAbsolutePath(request.values["filename"])
printer.selectFile(filename, sd, printAfterLoading)
return jsonify(SUCCESS)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
@login_required
def deleteGcodeFile():
if "filename" in request.values.keys():
filename = request.values["filename"]
2013-05-20 17:18:03 +00:00
if "target" in request.values.keys() and request.values["target"] == "sd":
printer.deleteSdFile(filename)
else:
gcodeManager.removeFile(filename)
return readGcodeFiles()
@app.route(BASEURL + "gcodefiles/refresh", methods=["POST"])
@login_required
def refreshFiles():
printer.updateSdFiles()
return jsonify(SUCCESS)
#-- very simple api routines
@app.route(APIBASEURL + "load", methods=["POST"])
def apiLoad():
logger = logging.getLogger(__name__)
if not settings().get(["api", "allow"]):
abort(401)
if not "apikey" in request.values.keys():
abort(401)
if request.values["apikey"] != settings().get(["api", "key"]):
abort(403)
if not "file" in request.files.keys():
abort(400)
# Perform an upload
file = request.files["file"]
filename = gcodeManager.addFile(file)
if filename is None:
logger.warn("Upload via API failed")
abort(500)
# Immediately perform a file select and possibly print too
printAfterSelect = False
if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
printAfterSelect = True
filepath = gcodeManager.getAbsolutePath(filename)
if filepath is not None:
printer.selectFile(filepath, False, printAfterSelect)
return jsonify(SUCCESS)
2013-01-04 12:11:00 +00:00
#~~ timelapse handling
@app.route(BASEURL + "timelapse", methods=["GET"])
2013-01-04 12:11:00 +00:00
def getTimelapseData():
global timelapse
type = "off"
additionalConfig = {}
if timelapse is not None and isinstance(timelapse, octoprint.timelapse.ZTimelapse):
type = "zchange"
elif timelapse is not None and isinstance(timelapse, octoprint.timelapse.TimedTimelapse):
type = "timed"
additionalConfig = {
"interval": timelapse.interval()
}
files = octoprint.timelapse.getFinishedTimelapses()
2013-01-04 12:11:00 +00:00
for file in files:
file["url"] = url_for("downloadTimelapse", filename=file["name"])
return jsonify({
"type": type,
"config": additionalConfig,
"files": files
})
2013-01-04 12:11:00 +00:00
@app.route(BASEURL + "timelapse/<filename>", methods=["GET"])
def downloadTimelapse(filename):
if util.isAllowedFile(filename, set(["mpg"])):
2013-01-04 12:11:00 +00:00
return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True)
@app.route(BASEURL + "timelapse/<filename>", methods=["DELETE"])
@login_required
2013-01-04 12:11:00 +00:00
def deleteTimelapse(filename):
if util.isAllowedFile(filename, set(["mpg"])):
2013-01-04 12:11:00 +00:00
secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename))
if os.path.exists(secure):
os.remove(secure)
return getTimelapseData()
2013-04-12 21:08:14 +00:00
@app.route(BASEURL + "timelapse", methods=["POST"])
@login_required
def setTimelapseConfig():
global timelapse
2013-01-04 12:11:00 +00:00
if request.values.has_key("type"):
type = request.values["type"]
if type in ["zchange", "timed"]:
# valid timelapse type, check if there is an old one we need to stop first
if timelapse is not None:
timelapse.unload()
timelapse = None
2013-01-04 12:11:00 +00:00
if "zchange" == type:
timelapse = octoprint.timelapse.ZTimelapse()
2013-01-04 12:11:00 +00:00
elif "timed" == type:
interval = 10
if request.values.has_key("interval"):
try:
interval = int(request.values["interval"])
except ValueError:
pass
timelapse = octoprint.timelapse.TimedTimelapse(interval)
2013-01-04 12:11:00 +00:00
return getTimelapseData()
#~~ settings
@app.route(BASEURL + "settings", methods=["GET"])
def getSettings():
s = settings()
[movementSpeedX, movementSpeedY, movementSpeedZ, movementSpeedE] = s.get(["printerParameters", "movementSpeed", ["x", "y", "z", "e"]])
return jsonify({
"api": {
"allow": s.getBoolean(["api", "allow"]),
"key": s.get(["api", "key"])
},
"appearance": {
"name": s.get(["appearance", "name"]),
"color": s.get(["appearance", "color"])
},
"printer": {
"movementSpeedX": movementSpeedX,
"movementSpeedY": movementSpeedY,
"movementSpeedZ": movementSpeedZ,
"movementSpeedE": movementSpeedE,
},
"webcam": {
"streamUrl": s.get(["webcam", "stream"]),
"snapshotUrl": s.get(["webcam", "snapshot"]),
"ffmpegPath": s.get(["webcam", "ffmpeg"]),
"bitrate": s.get(["webcam", "bitrate"]),
"watermark": s.getBoolean(["webcam", "watermark"]),
"flipH": s.getBoolean(["webcam", "flipH"]),
"flipV": s.getBoolean(["webcam", "flipV"])
},
"feature": {
"gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]),
2013-05-20 17:18:03 +00:00
"waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]),
"alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]),
"resetLineNumbersWithPrefixedN": s.getBoolean(["feature", "resetLineNumbersWithPrefixedN"]),
2013-05-20 17:18:03 +00:00
"sdSupport": s.getBoolean(["feature", "sdSupport"])
},
"folder": {
"uploads": s.getBaseFolder("uploads"),
"timelapse": s.getBaseFolder("timelapse"),
"timelapseTmp": s.getBaseFolder("timelapse_tmp"),
"logs": s.getBaseFolder("logs")
},
"temperature": {
"profiles": s.get(["temperature", "profiles"])
},
"system": {
"actions": s.get(["system", "actions"]),
"events": s.get(["system", "events"])
}
})
@app.route(BASEURL + "settings", methods=["POST"])
@login_required
@admin_permission.require(403)
def setSettings():
if "application/json" in request.headers["Content-Type"]:
data = request.json
s = settings()
if "api" in data.keys():
if "allow" in data["api"].keys(): s.set(["api", "allow"], data["api"]["allow"])
if "key" in data["api"].keys(): s.set(["api", "key"], data["api"]["key"], True)
if "appearance" in data.keys():
if "name" in data["appearance"].keys(): s.set(["appearance", "name"], data["appearance"]["name"])
if "color" in data["appearance"].keys(): s.set(["appearance", "color"], data["appearance"]["color"])
if "printer" in data.keys():
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 "webcam" in data.keys():
if "streamUrl" in data["webcam"].keys(): s.set(["webcam", "stream"], data["webcam"]["streamUrl"])
2013-03-08 22:24:30 +00:00
if "snapshotUrl" in data["webcam"].keys(): s.set(["webcam", "snapshot"], data["webcam"]["snapshotUrl"])
2013-03-08 23:29:07 +00:00
if "ffmpegPath" in data["webcam"].keys(): s.set(["webcam", "ffmpeg"], data["webcam"]["ffmpegPath"])
if "bitrate" in data["webcam"].keys(): s.set(["webcam", "bitrate"], data["webcam"]["bitrate"])
if "watermark" in data["webcam"].keys(): s.setBoolean(["webcam", "watermark"], data["webcam"]["watermark"])
if "flipH" in data["webcam"].keys(): s.setBoolean(["webcam", "flipH"], data["webcam"]["flipH"])
if "flipV" in data["webcam"].keys(): s.setBoolean(["webcam", "flipV"], data["webcam"]["flipV"])
if "feature" in data.keys():
if "gcodeViewer" in data["feature"].keys(): s.setBoolean(["feature", "gCodeVisualizer"], data["feature"]["gcodeViewer"])
if "waitForStart" in data["feature"].keys(): s.setBoolean(["feature", "waitForStartOnConnect"], data["feature"]["waitForStart"])
if "alwaysSendChecksum" in data["feature"].keys(): s.setBoolean(["feature", "alwaysSendChecksum"], data["feature"]["alwaysSendChecksum"])
if "resetLineNumbersWithPrefixedN" in data["feature"].keys(): s.setBoolean(["feature", "resetLineNumbersWithPrefixedN"], data["feature"]["resetLineNumbersWithPrefixedN"])
2013-05-20 17:18:03 +00:00
if "sdSupport" in data["feature"].keys(): s.setBoolean(["feature", "sdSupport"], data["feature"]["sdSupport"])
if "folder" in data.keys():
if "uploads" in data["folder"].keys(): s.setBaseFolder("uploads", data["folder"]["uploads"])
if "timelapse" in data["folder"].keys(): s.setBaseFolder("timelapse", data["folder"]["timelapse"])
if "timelapseTmp" in data["folder"].keys(): s.setBaseFolder("timelapse_tmp", data["folder"]["timelapseTmp"])
if "logs" in data["folder"].keys(): s.setBaseFolder("logs", data["folder"]["logs"])
if "temperature" in data.keys():
if "profiles" in data["temperature"].keys(): s.set(["temperature", "profiles"], data["temperature"]["profiles"])
if "system" in data.keys():
if "actions" in data["system"].keys(): s.set(["system", "actions"], data["system"]["actions"])
if "events" in data["system"].keys(): s.set(["system", "events"], data["system"]["events"])
s.save()
return getSettings()
2013-04-12 21:08:14 +00:00
#~~ user settings
@app.route(BASEURL + "users", methods=["GET"])
@login_required
@admin_permission.require(403)
2013-04-12 21:08:14 +00:00
def getUsers():
if userManager is None:
return jsonify(SUCCESS)
2013-04-12 21:08:14 +00:00
return jsonify({"users": userManager.getAllUsers()})
@app.route(BASEURL + "users", methods=["POST"])
@login_required
@admin_permission.require(403)
2013-04-12 21:08:14 +00:00
def addUser():
if userManager is None:
return jsonify(SUCCESS)
2013-04-12 21:08:14 +00:00
if "application/json" in request.headers["Content-Type"]:
data = request.json
name = data["name"]
password = data["password"]
active = data["active"]
roles = ["user"]
if "admin" in data.keys() and data["admin"]:
roles.append("admin")
try:
userManager.addUser(name, password, active, roles)
except users.UserAlreadyExists:
abort(409)
2013-04-12 21:08:14 +00:00
return getUsers()
@app.route(BASEURL + "users/<username>", methods=["GET"])
@login_required
def getUser(username):
if userManager is None:
return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
user = userManager.findUser(username)
if user is not None:
return jsonify(user.asDict())
else:
abort(404)
2013-04-12 21:08:14 +00:00
else:
abort(403)
2013-04-12 21:08:14 +00:00
@app.route(BASEURL + "users/<username>", methods=["PUT"])
@login_required
@admin_permission.require(403)
2013-04-12 21:08:14 +00:00
def updateUser(username):
if userManager is None:
return jsonify(SUCCESS)
2013-04-12 21:08:14 +00:00
user = userManager.findUser(username)
if user is not None:
if "application/json" in request.headers["Content-Type"]:
data = request.json
# change roles
roles = ["user"]
if "admin" in data.keys() and data["admin"]:
roles.append("admin")
userManager.changeUserRoles(username, roles)
# change activation
if "active" in data.keys():
userManager.changeUserActivation(username, data["active"])
return getUsers()
else:
abort(404)
2013-04-12 21:08:14 +00:00
@app.route(BASEURL + "users/<username>", methods=["DELETE"])
@login_required
@admin_permission.require(http_exception=403)
2013-04-12 21:08:14 +00:00
def removeUser(username):
if userManager is None:
return jsonify(SUCCESS)
2013-04-12 21:08:14 +00:00
try:
userManager.removeUser(username)
return getUsers()
except users.UnknownUser:
abort(404)
2013-04-12 21:08:14 +00:00
@app.route(BASEURL + "users/<username>/password", methods=["PUT"])
@login_required
def changePasswordForUser(username):
if userManager is None:
return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
if "application/json" in request.headers["Content-Type"]:
data = request.json
if "password" in data.keys() and data["password"]:
try:
userManager.changeUserPassword(username, data["password"])
except users.UnknownUser:
return app.make_response(("Unknown user: %s" % username, 404, []))
return jsonify(SUCCESS)
else:
return app.make_response(("Forbidden", 403, []))
2013-04-12 21:08:14 +00:00
#~~ system control
@app.route(BASEURL + "system", methods=["POST"])
@login_required
@admin_permission.require(403)
def performSystemAction():
logger = logging.getLogger(__name__)
if request.values.has_key("action"):
action = request.values["action"]
availableActions = settings().get(["system", "actions"])
for availableAction in availableActions:
if availableAction["action"] == action:
logger.info("Performing command: %s" % availableAction["command"])
try:
subprocess.check_output(availableAction["command"], shell=True)
except subprocess.CalledProcessError, e:
logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message))
return app.make_response(("Command failed with return code %i: %s" % (e.returncode, e.message), 500, []))
except Exception, ex:
logger.exception("Command failed")
return app.make_response(("Command failed: %r" % ex, 500, []))
return jsonify(SUCCESS)
#~~ Login/user handling
@app.route(BASEURL + "login", methods=["POST"])
def login():
if userManager is not None and "user" in request.values.keys() and "pass" in request.values.keys():
username = request.values["user"]
password = request.values["pass"]
if "remember" in request.values.keys() and request.values["remember"]:
remember = True
else:
remember = False
user = userManager.findUser(username)
if user is not None:
if user.check_password(users.UserManager.createPasswordHash(password)):
login_user(user, remember=remember)
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
return jsonify(user.asDict())
return app.make_response(("User unknown or password incorrect", 401, []))
elif "passive" in request.values.keys():
user = current_user
if user is not None and not user.is_anonymous():
return jsonify(user.asDict())
return jsonify(SUCCESS)
@app.route(BASEURL + "logout", methods=["POST"])
@login_required
def logout():
# Remove session keys set by Flask-Principal
for key in ('identity.id', 'identity.auth_type'):
del session[key]
identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity())
logout_user()
return jsonify(SUCCESS)
@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):
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
global userManager
global eventManager
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
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)
logger = logging.getLogger(__name__)
2013-05-26 22:56:57 +00:00
eventManager = events.eventManager()
gcodeManager = gcodefiles.GcodeManager()
2013-05-26 22:56:57 +00:00
printer = Printer(gcodeManager)
# setup system and gcode command triggers
events.SystemCommandTrigger(printer)
events.GcodeCommandTrigger(printer)
if self._debug:
events.DebugEventListener()
if settings().getBoolean(["accessControl", "enabled"]):
userManagerName = settings().get(["accessControl", "userManager"])
try:
clazz = util.getClass(userManagerName)
userManager = clazz()
except AttributeError, e:
logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName)
app.secret_key = "k3PuVYgtxNm8DXKKTw2nWmFQQun9qceV"
login_manager = LoginManager()
login_manager.session_protection = "strong"
login_manager.user_callback = load_user
if userManager is None:
login_manager.anonymous_user = users.DummyUser
principals.identity_loaders.appendleft(users.dummy_identity_loader)
login_manager.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"])
logger.info("Listening on http://%s:%d" % (self._host, self._port))
app.debug = self._debug
self._router = tornadio2.TornadioRouter(self._createSocketConnection)
self._tornado_app = Application(self._router.urls + [
(".*", FallbackHandler, {"fallback": WSGIContainer(app)})
])
self._server = HTTPServer(self._tornado_app)
self._server.listen(self._port, address=self._host)
eventManager.fire("Startup")
IOLoop.instance().start()
def _createSocketConnection(self, session, endpoint=None):
2013-05-26 22:56:57 +00:00
global printer, gcodeManager, userManager, eventManager
return PrinterStateConnection(printer, gcodeManager, userManager, eventManager, session, endpoint)
def _initSettings(self, configfile, basedir):
s = settings(init=True, basedir=basedir, configfile=configfile)
def _initLogging(self, debug):
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": {
#"octoprint.timelapse": {
# "level": "DEBUG"
#},
#"octoprint.events": {
# "level": "DEBUG"
#}
},
"root": {
"level": "INFO",
"handlers": ["console", "file"]
}
}
if debug:
config["loggers"]["SERIAL"] = {
"level": "DEBUG",
"handlers": ["serialFile"],
"propagate": False
}
logging.config.dictConfig(config)
if __name__ == "__main__":
octoprint = Server()
octoprint.run()