Merge branch 'devel' into customTerminalFilters

master
Gina Häußge 2013-08-26 17:46:36 +02:00
commit 757294d2e2
15 changed files with 198 additions and 3941 deletions

View File

@ -15,6 +15,8 @@ from octoprint.settings import settings
from werkzeug.utils import secure_filename
SUPPORTED_EXTENSIONS=["gcode", "gco"]
class GcodeManager:
def __init__(self):
self._logger = logging.getLogger(__name__)
@ -171,7 +173,7 @@ class GcodeManager:
"""
filename = self._getBasicFilename(filename)
if not util.isAllowedFile(filename, set(["gcode"])):
if not util.isAllowedFile(filename.lower(), set(SUPPORTED_EXTENSIONS)):
return None
secure = os.path.join(self._uploadFolder, secure_filename(self._getBasicFilename(filename)))

View File

@ -3,7 +3,7 @@ __author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
from werkzeug.utils import secure_filename
import tornadio2
from sockjs.tornado import SockJSRouter, SockJSConnection
from flask import Flask, request, render_template, jsonify, send_from_directory, url_for, current_app, session, abort, make_response
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
@ -34,6 +34,7 @@ app = Flask("octoprint")
# In order that threads don't start too early when running as a Daemon
printer = None
timelapse = None
debug = False
gcodeManager = None
userManager = None
@ -46,9 +47,9 @@ user_permission = Permission(RoleNeed("user"))
#~~ Printer state
class PrinterStateConnection(tornadio2.SocketConnection):
def __init__(self, printer, gcodeManager, userManager, eventManager, session, endpoint=None):
tornadio2.SocketConnection.__init__(self, session, endpoint)
class PrinterStateConnection(SockJSConnection):
def __init__(self, printer, gcodeManager, userManager, eventManager, session):
SockJSConnection.__init__(self, session)
self._logger = logging.getLogger(__name__)
@ -64,8 +65,14 @@ class PrinterStateConnection(tornadio2.SocketConnection):
self._userManager = userManager
self._eventManager = eventManager
def _getRemoteAddress(self, info):
forwardedFor = info.headers.get("X-Forwarded-For")
if forwardedFor is not None:
return forwardedFor.split(",")[0]
return info.ip
def on_open(self, info):
self._logger.info("New connection from client: %s" % info.ip)
self._logger.info("New connection from client: %s" % self._getRemoteAddress(info))
self._printer.registerCallback(self)
self._gcodeManager.registerCallback(self)
@ -102,16 +109,16 @@ class PrinterStateConnection(tornadio2.SocketConnection):
"logs": logs,
"messages": messages
})
self.emit("current", data)
self._emit("current", data)
def sendHistoryData(self, data):
self.emit("history", data)
self._emit("history", data)
def sendUpdateTrigger(self, type):
self.emit("updateTrigger", type)
self._emit("updateTrigger", type)
def sendFeedbackCommandOutput(self, name, output):
self.emit("feedbackCommandOutput", {"name": name, "output": output})
self._emit("feedbackCommandOutput", {"name": name, "output": output})
def addLog(self, data):
with self._logBacklogMutex:
@ -128,6 +135,9 @@ class PrinterStateConnection(tornadio2.SocketConnection):
def _onMovieDone(self, event, payload):
self.sendUpdateTrigger("timelapseFiles")
def _emit(self, type, payload):
self.send({type: payload})
def restricted_access(func):
"""
If you decorate a view with this, it will ensure that first setup has been
@ -159,6 +169,8 @@ def index():
except:
pass
global debug
return render_template(
"index.jinja2",
ajaxBaseUrl=BASEURL,
@ -169,6 +181,7 @@ def index():
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,
gitBranch=branch,
gitCommit=commit
)
@ -858,12 +871,16 @@ def login():
return jsonify(user.asDict())
elif settings().getBoolean(["accessControl", "autologinLocal"]) \
and settings().get(["accessControl", "autologinAs"]) is not None \
and settings().get(["accessControl", "localNetwork"]) is not None:
and settings().get(["accessControl", "localNetworks"]) is not None:
autologinAs = settings().get(["accessControl", "autologinAs"])
localNetwork = settings().get(["accessControl", "localNetwork"])
localNetworks = netaddr.IPSet([])
for ip in settings().get(["accessControl", "localNetworks"]):
localNetworks.add(ip)
try:
remoteAddr = util.getRemoteAddress(request)
if netaddr.IPAddress(remoteAddr) in netaddr.IPNetwork(localNetwork):
if netaddr.IPAddress(remoteAddr) in localNetworks:
user = userManager.findUser(autologinAs)
if user is not None:
login_user(user)
@ -871,7 +888,7 @@ def login():
return jsonify(user.asDict())
except:
logger = logging.getLogger(__name__)
logger.exception("Could not autologin user %s for network %s" % (autologinAs, localNetwork))
logger.exception("Could not autologin user %s for networks %r" % (autologinAs, localNetworks))
return jsonify(SUCCESS)
@app.route(BASEURL + "logout", methods=["POST"])
@ -924,12 +941,15 @@ class Server():
global userManager
global eventManager
global loginManager
global debug
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.web import Application, FallbackHandler
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)
@ -972,7 +992,7 @@ class Server():
logger.info("Listening on http://%s:%d" % (self._host, self._port))
app.debug = self._debug
self._router = tornadio2.TornadioRouter(self._createSocketConnection)
self._router = SockJSRouter(self._createSocketConnection, "/sockjs")
self._tornado_app = Application(self._router.urls + [
(".*", FallbackHandler, {"fallback": WSGIContainer(app)})
@ -986,11 +1006,15 @@ class Server():
connectionOptions = getConnectionOptions()
if port in connectionOptions["ports"]:
printer.connect(port, baudrate)
IOLoop.instance().start()
try:
IOLoop.instance().start()
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:")
def _createSocketConnection(self, session, endpoint=None):
def _createSocketConnection(self, session):
global printer, gcodeManager, userManager, eventManager
return PrinterStateConnection(printer, gcodeManager, userManager, eventManager, session, endpoint)
return PrinterStateConnection(printer, gcodeManager, userManager, eventManager, session)
def _checkForRoot(self):
if "geteuid" in dir(os) and os.geteuid() == 0:

View File

@ -90,7 +90,7 @@ default_settings = {
"userManager": "octoprint.users.FilebasedUserManager",
"userfile": None,
"autologinLocal": False,
"localNetwork": "127.0.0.1",
"localNetworks": ["127.0.0.0/8"],
"autologinAs": None
},
"events": {
@ -248,7 +248,7 @@ class Settings(object):
return feedbackControls
def _getFeedbackControls(self, control=None):
if control["type"] == "feedback_command":
if control["type"] == "feedback_command" or control["type"] == "feedback":
pattern = control["regex"]
try:
matcher = re.compile(pattern)

View File

@ -11,8 +11,32 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
self.timelapseViewModel = timelapseViewModel;
self.gcodeViewModel = gcodeViewModel;
self._socket = io.connect();
self._socket.on("connect", function() {
self._socket = undefined;
self._autoReconnecting = false;
self._autoReconnectTrial = 0;
self._autoReconnectTimeouts = [1, 1, 2, 3, 5, 8, 13, 20, 40, 100];
self.connect = function() {
var options = {};
if (SOCKJS_DEBUG) {
options["debug"] = true;
}
self._socket = new SockJS(SOCKJS_URI, undefined, options);
self._socket.onopen = self._onconnect;
self._socket.onclose = self._onclose;
self._socket.onmessage = self._onmessage;
}
self.reconnect = function() {
delete self._socket;
self.connect();
}
self._onconnect = function() {
self._autoReconnecting = false;
self._autoReconnectTrial = 0;
if ($("#offline_overlay").is(":visible")) {
$("#offline_overlay").hide();
self.timelapseViewModel.requestData();
@ -20,8 +44,9 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
self.loginStateViewModel.requestData();
self.gcodeFilesViewModel.requestData();
}
});
self._socket.on("disconnect", function() {
}
self._onclose = function() {
$("#offline_overlay_message").html(
"The server appears to be offline, at least I'm not getting any response from it. I'll try to reconnect " +
"automatically <strong>over the next couple of minutes</strong>, however you are welcome to try a manual reconnect " +
@ -29,45 +54,66 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
);
if (!$("#offline_overlay").is(":visible"))
$("#offline_overlay").show();
});
self._socket.on("reconnect_failed", function() {
if (self._autoReconnectTrial < self._autoReconnectTimeouts.length) {
var timeout = self._autoReconnectTimeouts[self._autoReconnectTrial];
console.log("Reconnect trial #" + self._autoReconnectTrial + ", waiting " + timeout + "s");
setTimeout(self.reconnect, timeout * 1000);
self._autoReconnectTrial++;
} else {
self._onreconnectfailed();
}
}
self._onreconnectfailed = function() {
$("#offline_overlay_message").html(
"The server appears to be offline, at least I'm not getting any response from it. I <strong>could not reconnect automatically</strong>, " +
"but you may try a manual reconnect using the button below."
);
});
self._socket.on("history", function(data) {
self.connectionViewModel.fromHistoryData(data);
self.printerStateViewModel.fromHistoryData(data);
self.temperatureViewModel.fromHistoryData(data);
self.controlViewModel.fromHistoryData(data);
self.terminalViewModel.fromHistoryData(data);
self.timelapseViewModel.fromHistoryData(data);
self.gcodeViewModel.fromHistoryData(data);
self.gcodeFilesViewModel.fromCurrentData(data);
});
self._socket.on("current", function(data) {
self.connectionViewModel.fromCurrentData(data);
self.printerStateViewModel.fromCurrentData(data);
self.temperatureViewModel.fromCurrentData(data);
self.controlViewModel.fromCurrentData(data);
self.terminalViewModel.fromCurrentData(data);
self.timelapseViewModel.fromCurrentData(data);
self.gcodeViewModel.fromCurrentData(data);
self.gcodeFilesViewModel.fromCurrentData(data);
});
self._socket.on("updateTrigger", function(type) {
if (type == "gcodeFiles") {
gcodeFilesViewModel.requestData();
} else if (type == "timelapseFiles") {
timelapseViewModel.requestData();
}
});
self._socket.on("feedbackCommandOutput", function(data) {
self.controlViewModel.fromFeedbackCommandData(data);
});
self.reconnect = function() {
self._socket.socket.connect();
}
self._onmessage = function(e) {
for (var prop in e.data) {
var payload = e.data[prop];
switch (prop) {
case "history": {
self.connectionViewModel.fromHistoryData(payload);
self.printerStateViewModel.fromHistoryData(payload);
self.temperatureViewModel.fromHistoryData(payload);
self.controlViewModel.fromHistoryData(payload);
self.terminalViewModel.fromHistoryData(payload);
self.timelapseViewModel.fromHistoryData(payload);
self.gcodeViewModel.fromHistoryData(payload);
self.gcodeFilesViewModel.fromCurrentData(payload);
break;
}
case "current": {
self.connectionViewModel.fromCurrentData(payload);
self.printerStateViewModel.fromCurrentData(payload);
self.temperatureViewModel.fromCurrentData(payload);
self.controlViewModel.fromCurrentData(payload);
self.terminalViewModel.fromCurrentData(payload);
self.timelapseViewModel.fromCurrentData(payload);
self.gcodeViewModel.fromCurrentData(payload);
self.gcodeFilesViewModel.fromCurrentData(payload);
break;
}
case "updateTrigger": {
if (payload == "gcodeFiles") {
gcodeFilesViewModel.requestData();
} else if (payload == "timelapseFiles") {
timelapseViewModel.requestData();
}
break;
}
case "feedbackCommandOutput": {
self.controlViewModel.fromFeedbackCommandData(payload);
break
}
}
}
}
self.connect();
}

View File

@ -60,6 +60,18 @@ $(function() {
$("#gcode_upload_progress .bar").text("");
}
function gcode_upload_fail(e, data) {
$.pnotify({
title: "Upload failed",
text: "<p>Could not upload the file. Make sure it is a GCODE file and has one of the following extensions: .gcode, .gco</p><p>Server reported: <pre>" + data.jqXHR.responseText + "</pre></p>",
type: "error",
hide: false
});
$("#gcode_upload_progress .bar").css("width", "0%");
$("#gcode_upload_progress").removeClass("progress-striped").removeClass("active");
$("#gcode_upload_progress .bar").text("");
}
function gcode_upload_progress(e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
$("#gcode_upload_progress .bar").css("width", progress + "%");
@ -82,6 +94,7 @@ $(function() {
dropZone: localTarget,
formData: {target: "local"},
done: gcode_upload_done,
fail: gcode_upload_fail,
progressall: gcode_upload_progress
});
@ -91,6 +104,7 @@ $(function() {
dropZone: $("#drop_sd"),
formData: {target: "sd"},
done: gcode_upload_done,
fail: gcode_upload_fail,
progressall: gcode_upload_progress
});
}

View File

@ -68,7 +68,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
for (var i = 0; i < control.input.length; i++) {
control.input[i].value = control.input[i].default;
}
} else if (control.type == "feedback_command") {
} else if (control.type == "feedback_command" || control.type == "feedback") {
control.output = ko.observable("");
self.feedbackControlLookup[control.name] = control.output;
} else if (control.type == "section") {
@ -162,6 +162,8 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
return "customControls_parametricCommandTemplate";
case "feedback_command":
return "customControls_feedbackCommandTemplate";
case "feedback":
return "customControls_feedbackTemplate";
default:
return "customControls_emptyTemplate";
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -27,8 +27,8 @@
var CONFIG_SD_SUPPORT = {% if enableSdSupport -%} true; {% else %} false; {%- endif %}
var CONFIG_FIRST_RUN = {% if firstRun -%} true; {% else %} false; {%- endif %}
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/socket.io/WebSocketMain.swf') }}";
var WEB_SOCKET_DEBUG = true;
var SOCKJS_URI = window.location.protocol.slice(0, -1) + "://" + (window.document ? window.document.domain : window.location.hostname) + ":" + window.location.port + "/sockjs";
var SOCKJS_DEBUG = {% if debug -%} true; {% else %} false; {%- endif %}
</script>
<script src="{{ url_for('static', filename='js/lib/less-1.3.3.min.js') }}" type="text/javascript"></script>
</head>
@ -402,6 +402,11 @@
<button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button> <span data-bind="text: output"></span>
</form>
</script>
<script type="text/html" id="customControls_feedbackTemplate">
<div>
<strong data-bind="text: name"></strong>: <span data-bind="text: output"></span>
</div>
</script>
<script type="text/html" id="customControls_parametricCommandTemplate">
<form class="form-inline">
<!-- ko foreach: input -->
@ -614,7 +619,7 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.flot.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.iframe-transport.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.fileupload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/socket.io/socket.io.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/sockjs-0.3.4.min.js') }}"></script>
<!-- Include OctoPrint files -->
<!-- TODO: merge/minimize in the future -->

View File

@ -448,6 +448,7 @@ class MachineCom(object):
def _monitor(self):
feedbackControls = settings().getFeedbackControls()
pauseTriggers = settings().getPauseTriggers()
feedbackErrors = []
#Open the serial port.
if self._port == 'AUTO':
@ -606,9 +607,18 @@ class MachineCom(object):
try:
match = matcher.search(line)
if match is not None:
self._callback.mcReceivedRegisteredMessage(name, str.format(template, *(match.groups("n/a"))))
format = None
if isinstance(template, str):
format = str.format
elif isinstance(template, unicode):
format = unicode.format
if format is not None:
self._callback.mcReceivedRegisteredMessage(name, format(template, *(match.groups("n/a"))))
except:
# ignored on purpose
if not name in feedbackErrors:
self._logger.info("Something went wrong with feedbackControl \"%s\": " % name, exc_info=True)
feedbackErrors.append(name)
pass
##~~ Parsing for pause triggers

View File

@ -1,7 +1,7 @@
flask==0.9
werkzeug==0.8.3
tornado==3.0.2
tornadio2==0.0.4
sockjs-tornado>=1.0.0
PyYAML==3.10
Flask-Login==0.2.2
Flask-Principal==0.3.5

View File

@ -1,7 +1,7 @@
flask==0.9
werkzeug==0.8.3
tornado==3.0.2
tornadio2==0.0.4
sockjs-tornado>=1.0.0
PyYAML==3.10
Flask-Login==0.2.2
Flask-Principal==0.3.5