Fire up intermediary server on host and port

That way people will not see connection failed messages while the server is
still starting up.

Served intermediary page als "pings" an image on the backend to detect if
a) the backend is still responding at all ("intermediary.gif") and b) whether the
server has fully started up ("online.gif").

If the backend stops responding for 5s, a message is output that tells the user
that something went really wrong and to please check the log file.

Once the server becomes online, the intermediary page reloads/switches to the
actual UI.
This commit is contained in:
Gina Häußge 2015-12-08 13:28:12 +01:00
parent 547dcdd725
commit ef876cfd35
3 changed files with 323 additions and 1 deletions

View file

@ -22,6 +22,7 @@ import logging
import logging.config
import atexit
import signal
import base64
SUCCESS = {}
NO_CONTENT = ("", 204)
@ -124,6 +125,8 @@ class Server():
self._template_searchpaths = []
self._intermediary_server = None
def run(self):
if not self._allowRoot:
self._check_for_root()
@ -172,6 +175,9 @@ class Server():
sys.excepthook = exception_logger
self._logger.info("Starting OctoPrint %s" % DISPLAY_VERSION)
# start the intermediary server
self._start_intermediary_server(s)
# then initialize the plugin manager
pluginManager = octoprint.plugin.plugin_manager(init=True)
@ -361,7 +367,10 @@ class Server():
# camera snapshot
(r"/downloads/camera/current", util.tornado.UrlProxyHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))),
# generated webassets
(r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=os.path.join(s.getBaseFolder("generated"), "webassets")))
(r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=os.path.join(s.getBaseFolder("generated"), "webassets"))),
# online indicators - text file with "online" as content and a transparent gif
(r"/online.txt", util.tornado.StaticDataHandler, dict(data="online\n")),
(r"/online.gif", util.tornado.StaticDataHandler, dict(data=bytes(base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")), content_type="image/gif"))
]
for name, hook in pluginManager.get_hooks("octoprint.server.http.routes").items():
try:
@ -414,6 +423,8 @@ class Server():
self._logger.debug("Adding maximum body size of {size}B for {method} requests to {route})".format(**locals()))
max_body_sizes.append((method, route, size))
self._stop_intermediary_server()
self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=s.getInt(["server", "maxSize"]))
self._server.listen(self._port, address=self._host)
@ -938,6 +949,90 @@ class Server():
assets.register("css_app", css_app_bundle)
assets.register("less_app", all_less_bundle)
def _start_intermediary_server(self, s):
import BaseHTTPServer
import SimpleHTTPServer
import threading
host = self._host
port = self._port
if host is None:
host = s.get(["server", "host"])
if port is None:
port = s.getInt(["server", "port"])
self._logger.debug("Starting intermediary server on {}:{}".format(host, port))
class IntermediaryServerHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def __init__(self, rules=None, *args, **kwargs):
if rules is None:
rules = []
self.rules = rules
SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, *args, **kwargs)
def do_GET(self):
request_path = self.path
if "?" in request_path:
request_path = request_path[0:request_path.find("?")]
for rule in self.rules:
path, data, content_type = rule
if request_path == path:
self.send_response(200)
if content_type:
self.send_header("Content-Type", content_type)
self.end_headers()
self.wfile.write(data)
break
else:
self.send_response(404)
self.wfile.write("Not found")
base_path = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "static"))
rules = [
("/", ["intermediary.html",], "text/html"),
("/favicon.ico", ["img", "tentacle-20x20.png"], "image/png"),
("/intermediary.gif", bytes(base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")), "image/gif")
]
def contents(args):
path = os.path.join(base_path, *args)
if not os.path.isfile(path):
return ""
with open(path, "rb") as f:
data = f.read()
return data
def process(rule):
if len(rule) == 2:
path, data = rule
content_type = None
else:
path, data, content_type = rule
if isinstance(data, (list, tuple)):
data = contents(data)
return path, data, content_type
rules = map(process, filter(lambda rule: len(rule) == 2 or len(rule) == 3, rules))
self._intermediary_server = BaseHTTPServer.HTTPServer((host, port), lambda *args, **kwargs: IntermediaryServerHandler(rules, *args, **kwargs))
thread = threading.Thread(target=self._intermediary_server.serve_forever)
thread.daemon = True
thread.start()
self._logger.debug("Intermediary server started")
def _stop_intermediary_server(self):
if self._intermediary_server is None:
return
self._logger.debug("Shutting down intermediary server...")
self._intermediary_server.shutdown()
self._intermediary_server.server_close()
self._logger.debug("Intermediary server shut down")
class LifecycleManager(object):
def __init__(self, plugin_manager):

View file

@ -927,6 +927,19 @@ class UrlProxyHandler(tornado.web.RequestHandler):
return "%s%s" % (self._basename, extension)
class StaticDataHandler(tornado.web.RequestHandler):
def initialize(self, data="", content_type="text/plain"):
self.data = data
self.content_type = content_type
def get(self, *args, **kwargs):
self.set_status(200)
self.set_header("Content-Type", self.content_type)
self.write(self.data)
self.flush()
self.finish()
#~~ Factory method for creating Flask access validation wrappers from the Tornado request context

View file

@ -0,0 +1,214 @@
<html>
<head>
<title>OctoPrint is still starting</title>
<style>
body {
margin: 0;
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #333333;
background-color: #ffffff;
}
#startup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
display: block;
}
.background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.wrapper {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.wrapper .outer {
display: table;
width: 100%;
height: 100%;
}
.wrapper .outer .inner {
display: table-cell;
vertical-align: middle;
}
.wrapper .outer .inner .content {
text-align: center;
}
.green {
color: #169300;
}
.red {
color: #990000;
}
.pulsate3 {
-webkit-animation: pulsate 3s ease-out;
-webkit-animation-iteration-count: infinite;
opacity: 0.5;
}
.pulsate1 {
-webkit-animation: pulsate 1s ease-out;
-webkit-animation-iteration-count: infinite;
opacity: 0.5;
}
@-webkit-keyframes pulsate {
0% {
opacity: 0.5;
}
50% {
opacity: 1.0;
}
100% {
opacity: 0.5;
}
}
</style>
<script>
if (!String.prototype.endsWith) {
String.prototype.endsWith = function(searchString, position) {
var subjectString = this.toString();
if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
position = subjectString.length;
}
position -= searchString.length;
var lastIndex = subjectString.indexOf(searchString, position);
return lastIndex !== -1 && lastIndex === position;
};
}
function ping(url, timeout, callback) {
var img = new Image();
var calledBack = false;
var urlToUse = url;
var postfix = "_=" + Date.now();
if (url.indexOf("?") > -1) {
urlToUse += "&" + postfix;
} else {
urlToUse += "?" + postfix;
}
img.onload = function() {
callback("load");
calledBack = true;
};
img.onerror = function() {
if (!calledBack) {
callback("error");
calledBack = true;
}
};
img.src = urlToUse;
setTimeout(function() {
if (!calledBack) {
callback("timeout");
calledBack = true;
}
}, timeout);
}
window.onload = function() {
var intervals = [1, 1, 2, 3, 5, 8];
var timeout = 1500;
var baseUrl = window.location.href;
if (baseUrl.indexOf("/static") > -1) {
baseUrl = baseUrl.substring(0, baseUrl.indexOf("/static")) + "/";
}
var serverOnlineUrl = baseUrl + "online.gif";
var backendOnlineUrl = baseUrl + "intermediary.gif";
var serverTimeout;
var message = window.document.getElementById("message");
var serverIsOnline = false;
var serverOnlineCallback = function(result) {
if (result == "load") {
// our online.gif loaded, so the server is up, let's reload
serverIsOnline = true;
message.className = "pulsate1 green";
message.innerText = "OctoPrint server online, reloading page...";
window.location = baseUrl;
} else {
// online.gif still not available, let's look at
var interval = 15;
if (intervals.length) {
interval = intervals.shift();
}
serverTimeout = setTimeout(function() {
console.log("Pinging " + serverOnlineUrl);
ping(serverOnlineUrl, timeout, serverOnlineCallback);
}, interval * 1000)
}
};
var backendOfflineCounter = 0;
var backendOnlineCallback = function(result) {
if (serverIsOnline) {
return;
}
if (result == "load") {
setTimeout(function() {
ping(backendOnlineUrl, timeout, backendOnlineCallback);
}, 1000);
return;
}
backendOfflineCounter++;
if (backendOfflineCounter > 5) {
if (serverTimeout) {
window.clearTimeout(serverTimeout);
}
message.className = "red";
message.innerHTML = "Looks like something went wrong during startup, the server is gone again. You should check <code>octoprint.log</code>.";
} else {
setTimeout(function() {
ping(backendOnlineUrl, timeout, backendOnlineCallback);
}, 1000);
}
};
ping(backendOnlineUrl, timeout, backendOnlineCallback);
ping(serverOnlineUrl, timeout, serverOnlineCallback);
}
</script>
</head>
<body>
<div id="startup-overlay">
<div class="background"></div>
<div class="wrapper">
<div class="outer">
<div class="inner">
<div class="content">
<h1 id="message" class="pulsate3">OctoPrint is still starting up, please wait...</h1>
</div>
</div>
</div>
</div>
</div>
</body>
</html>