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:
parent
547dcdd725
commit
ef876cfd35
3 changed files with 323 additions and 1 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
214
src/octoprint/static/intermediary.html
Normal file
214
src/octoprint/static/intermediary.html
Normal 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>
|
||||
Loading…
Reference in a new issue