-
-
Save porst17/1b2076e6d1baf4375500 to your computer and use it in GitHub Desktop.
Heartbeat server / client and libraries
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
</head> | |
<body> | |
<h2>Open your JavaScript console!</h2> | |
<div> | |
<form name="my_form"> | |
App ID: <input type="text" name="app_id" value="browser_app"/><br/> | |
Interval: <input type="text" name="interval" value="2"/> seconds | |
</form> | |
</div> | |
<div> | |
<button type="button" onclick="heartbeart( document.my_form.app_id.value, 'hb_init', document.my_form.interval.value );">Init HB</button> | |
<button type="button" onclick="heartbeart( document.my_form.app_id.value, 'hb_ping', document.my_form.interval.value );">Ping HB</button> | |
<button type="button" onclick="heartbeart( document.my_form.app_id.value, 'hb_done', document.my_form.interval.value );">Done HB</button> | |
</div> | |
<div> | |
<textarea id="console" style="width:100%; height: 400px;"></textarea> | |
</div> | |
<script> | |
var HB_URL = "http://localhost:8888"; | |
var text_console = document.getElementById( "console" ); | |
text_console.value = ""; | |
function heartbeart( APP_ID, HB_COMMAND, HB_INTERVAL ) | |
{ | |
var xhttp = new XMLHttpRequest(); | |
xhttp.onreadystatechange = function() { | |
if (xhttp.readyState == 4 && xhttp.status == 200) { | |
text_console.value += "RESPONSE: " + xhttp.responseText + "\n"; | |
} | |
}; | |
var url = HB_URL + "/" + HB_COMMAND + "?" + HB_INTERVAL + "&appid=" + APP_ID + "&cache_buster=" + new Date().getTime() | |
text_console.value += " REQUEST: " + url + "\n"; | |
xhttp.open("GET", url, true); | |
xhttp.send(); | |
} | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2016 Christian Stussak | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
(function() { | |
//begin private closure | |
// Libraries and classes in JS: http://frugalcoder.us/post/2010/02/11/js-classes.aspx | |
// Static members are denoted by a leading '_'. | |
// The documentation is supposed to be JSDOC 3. | |
// Online JSDOC generator: http://jsdoc.sanstv.ru/ | |
// BEGIN - private static members | |
var _initCommand = 'hb_init'; | |
var _pingCommand = 'hb_ping'; | |
var _doneCommand = 'hb_done'; | |
var _onLoadCalled = false; | |
window.addEventListener("load", function() { | |
_onLoadCalled = true; | |
}, false); | |
/** | |
* Sends a HTTP request to the heartbeat server. | |
* @private | |
* @static | |
*/ | |
function _heartbeat(appId, url, command, interval, async, debugLog) { | |
var xhttp = new XMLHttpRequest(); | |
xhttp.onreadystatechange = function() { | |
if (xhttp.readyState == 4 && xhttp.status == 200) { | |
debugLog("RESPONSE: " + xhttp.responseText); | |
} | |
}; | |
var fullUrl = url + "/" + command + "?" + interval + "&appid=" + appId + "&cache_buster=" + new Date().getTime() | |
debugLog("REQUEST: " + fullUrl); | |
xhttp.open("GET", fullUrl, async); | |
xhttp.send(); | |
return xhttp; | |
} | |
var _defaultUrl = 'http://localhost:8888'; | |
var _defaultAppId = 'browser_app'; | |
/** | |
* Get URL parameter. | |
* @private | |
* @static | |
* @see https://stackoverflow.com/questions/11582512/how-to-get-url-parameters-with-javascript/11582513 | |
*/ | |
function _getUrlParameter(name) { | |
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [null, ''])[1].replace(/\+/g, '%20')) || null; | |
} | |
var _passedUrl = _getUrlParameter('HB_URL'); | |
var _passedAppId = _getUrlParameter('HB_APP_ID'); | |
// END - private static members | |
/** | |
* This callback is used to log debug messages. | |
* @callback Heartbeat~debugLog | |
* @param {String} message | |
*/ | |
// BEGIN - constructor | |
/** | |
* Creates an instance of the Heartbeat class. | |
* @public | |
* @class Heartbeat | |
* @classdesc This class provides functionality to send regular heartbeat | |
* signals to a HTTP server. Once initialized, there is usually | |
* no need to take further action except for changing the | |
* interval between subsequent heartbeats, see {@link Heartbeat#setInterval}. | |
* @param {Object} options - JSON object containing initial heartbeat settings. | |
* @param {String} [option.url] - The base URL to use when sending the heartbeat. | |
* @param {String} [option.appId] - The application id that is send with each heartbeat. | |
* @param {Number} [option.interval=1000] - The interval between two heartbeat pings in milliseconds. | |
* @param {Boolean} [option.sendInitCommand=false] - If the heartbeat init command should be send. If true, it will be send on the {@link window.onload} event or immediately if {@link window.onload} has already been triggered. | |
* @param {Boolean} [option.sendDoneCommand=false] - If the heartbeat done command should be send. If true, it will be send on the {@link window.onunload} event. | |
* @param {Heartbeat~debugLog} [option.debugLog=function(msg) {}] Callback function for debug messages. | |
* @example | |
* // Init and use internal defaults. | |
* var heartbeat = new Heartbeat({}); | |
* @example | |
* // Init and properly set base URL and application id with partial fallback. | |
* var heartbeat = new Heartbeat({ | |
* url: Heartbeat.getPassedUrl(), | |
* appId: Heartbeat.getPassedAppId('test_app') | |
* }); | |
* @example | |
* // Full initialization. | |
* var heartbeat = new Heartbeat({ | |
* url: Heartbeat.getPassedUrl('http://localhost:8881'), | |
* appId: Heartbeat.getPassedAppId('test_app'), | |
* interval: 5000, | |
* sendInitCommand: true, | |
* sendDoneCommand: true, | |
* debugLog: function(msg) { | |
* console.log(msg); | |
* } | |
* }); | |
*/ | |
this.Heartbeat = function(options) { | |
//if the function is called directly, return an instance of Heartbeat | |
if (!(this instanceof Heartbeat)) | |
return new Heartbeat(options); | |
var that = this; | |
this.url = _defaultUrl; | |
this.appId = _defaultAppId; | |
this.interval = 5000; | |
this.currentHbXhr = new XMLHttpRequest(); | |
this.debug = false; | |
this.timeout = null; | |
this.ping = null; | |
this.debugLog = function() {}; | |
//handle the options initialization here | |
if (options.hasOwnProperty("debugLog")) { | |
this.debugLog = options.debugLog; | |
} | |
if (options.hasOwnProperty("url")) { | |
this.url = options.url; | |
} | |
if (options.hasOwnProperty("appId")) { | |
this.appId = options.appId; | |
} | |
if (options.hasOwnProperty("interval")) { | |
this.interval = options.interval; | |
} | |
if (options.hasOwnProperty("sendDoneCommand") && options.sendDoneCommand) { | |
var done = function() { | |
that.debugLog("send heartbeat done"); | |
window.clearInterval(that.timeout); | |
// Abort current asynchronous heartbeat, if threre is one. | |
// Otherwise the currently running heartbeat request may be | |
// received by server *after* the synchronous `hb_done` heartbeat, | |
// i.e. the heartbeat events seem to be out of order. | |
that.currentHbXhr.abort(); | |
that.currentHbXhr = _heartbeat(that.appId, that.url, _doneCommand, 0, false, that.debugLog); | |
}; | |
window.addEventListener("unload", done, false); | |
} | |
this.ping = function() { | |
that.debugLog("send heartbeat ping"); | |
that.currentHbXhr = _heartbeat(that.appId, that.url, _pingCommand, that.interval, true, that.debugLog); | |
} | |
var init = function() { | |
that.debugLog("init heartbeat library"); | |
if (options.hasOwnProperty("sendInitCommand") && options.sendInitCommand) { | |
that.debugLog("send heartbeat init"); | |
that.currentHbXhr = _heartbeat(that.appId, that.url, _initCommand, that.interval, true, that.debugLog); | |
} | |
that.setInterval(that.interval); | |
} | |
if (_onLoadCalled) { | |
init(); | |
} else { | |
window.addEventListener("load", init, false); | |
} | |
} | |
// END - constructor | |
// BEGIN - public members | |
/** | |
* Get the current interval between two heartbeat pings in milliseconds. | |
* @public | |
* @returns {Number} | |
*/ | |
this.Heartbeat.prototype.getInterval = function() { | |
return this.interval; | |
} | |
/** | |
* Change the interval between two heartbeat pings. | |
* This function sends out a heartbeat ping immediately to satisfy the | |
* promise of the previous heartbeat. The next heartbeat will then be send | |
* out after the specified number of milliseconds. | |
* @public | |
* @param {Number} newInterval - Interval between future heartbeat pings. | |
*/ | |
this.Heartbeat.prototype.setInterval = function(newInterval) { | |
this.debugLog("set heartbeat interval to " + newInterval + "ms"); | |
window.clearInterval(this.timeout); | |
this.interval = newInterval; | |
this.ping(); | |
this.timeout = setInterval(this.ping, this.interval); | |
} | |
/** | |
* Get the base URL that is used for each heartbeat command. | |
* @public | |
* @returns {Number} | |
*/ | |
this.Heartbeat.prototype.getUrl = function() { | |
return this.url; | |
} | |
/** | |
* Get the application identifier that is send with each heartbet command. | |
* @public | |
* @returns {String} | |
*/ | |
this.Heartbeat.prototype.getAppId = function() { | |
return this.appId; | |
} | |
/** | |
* Get the function that is used for debug logging. | |
* @public | |
* @returns {Heartbeat~debugLog} | |
*/ | |
this.Heartbeat.prototype.getDebugLog = function() { | |
return this.debugLog; | |
} | |
/** | |
* Set the function that is used for debug logging. | |
* @public | |
* @param {Heartbeat~debugLog} debugLog | |
*/ | |
this.Heartbeat.prototype.setDebugLog = function(debugLog) { | |
this.debugLog == debugLog; | |
} | |
// END - public members | |
// BEGIN - public static members | |
/** | |
* Get the base URL that is passed as URL parameter HB_URL to this site. | |
* If HB_URL is not set, {@link fallbackUrl} is used. If {@link fallbackUrl} | |
* is not provided, an internal default will be returned. | |
* @public | |
* @param {String} [fallbackUrl] - Fallback that is used if HB_URL is not set. | |
* @returns {String} | |
*/ | |
this.Heartbeat.getPassedUrl = function(fallbackUrl) { | |
return (_passedUrl == null) ? (typeof fallbackUrl === 'undefined' ? _defaultUrl : fallbackUrl) : _passedUrl; | |
} | |
/** | |
* Get the application id that is passed as URL parameter HB_APP_ID to this site. | |
* If HB_APP_ID is not set, {@link fallbackAppId} is used. If {@link fallbackAppId} | |
* is not provided, an internal default will be returned. | |
* @public | |
* @param {String} [fallbackAppId] - Fallback that is used if HB_APP_ID is not set. | |
* @returns {String} | |
*/ | |
this.Heartbeat.getPassedAppId = function(fallbackAppId) { | |
return (_passedAppId == null) ? (typeof fallbackAppId === 'undefined' ? _defaultAppId : fallbackAppId) : _passedAppId; | |
} | |
// END - public static members | |
// end private closure then run the closure, localized to window | |
// (this allows to easily encapsulate this class to a namespace if necessary) | |
}).call(window); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
# import sched, time | |
from time import time | |
from time import sleep | |
from random import randint | |
from threading import Timer | |
from urllib2 import urlopen | |
import sys # what? | |
import urllib # what? why? | |
from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer | |
# https://docs.python.org/2/library/sched.html | |
#### SH = sched.scheduler(time, sleep) | |
PORT_NUMBER = 8888 | |
HOST_NAME = 'localhost' | |
# localhost:8888/hb_init?48&appid=test_client_python => | |
# /hb_init | |
# [*] | |
# ['48', 'appid=test_client_python'] | |
# Accept-Encoding: identity | |
# Host: localhost:8080 | |
# Connection: close | |
# User-Agent: Python-urllib/2.7 | |
# | |
# ID: Python-urllib/2.7@127.0.0.1 | |
visits = {} | |
# overdue = 0 # Just for now... TODO: FIXME?: should be a part of visit data, right?! | |
def toolate(ID): | |
ts = time() | |
print "[", ts, "] [", ID, "]: ", visits[ID] | |
d = visits[ID] | |
# Collect statistics about ID | |
if d[3] > 5: | |
print("TOO overdue!") # TODO: early detection of overdue clients!!??? | |
del visits[ID] | |
else: | |
visits[ID] = (d[0], d[1], Timer(d[1], toolate, [ID]), d[3] + 1) | |
visits[ID][2].start() # Another Chance??? | |
class MyHandler(BaseHTTPRequestHandler): | |
# s.headers, s.client_address, s.command, s.path, s.request_version, s.raw_requestline | |
def status(s, ID): | |
d = visits[ID] | |
t = d[0] | |
dd = d[1] | |
od = d[3] | |
## STATUS CODE??? | |
app = "application [" + str(ID) + "]| od="+str(od)+";1;3;0;10 delay="+str(dd)+";;; " | |
if od == 0: | |
s.wfile.write("OK - fine " + app) | |
if od > 0: | |
s.wfile.write("WARNING - somewhat late " + app) | |
if od > 2: | |
s.wfile.write("CRITICAL - somewhat late " + app) | |
def do_GET(s): | |
global visits | |
# global overdue | |
s.send_response(200) | |
s.send_header('Content-type','text/html') | |
s.send_header('Access-Control-Allow-Origin','*') | |
s.end_headers() | |
### TODO: FIXME: the following url parsing is neither failsafe nor secure! :-( | |
path, _, tail = s.path.partition('?') | |
path = urllib.unquote(path) | |
if path == "/list": | |
for k, v in visits.iteritems(): | |
s.wfile.write(str(k) + "\n") | |
return | |
query = tail.split('&') | |
if path == "/status": | |
if tail != "": | |
ID = query[0].split('=')[1] # + " @ " + s.client_address[0] # " || " + s.headers['User-Agent'] | |
if ID in visits: | |
s.status(ID) | |
else: | |
s.wfile.write("CRITICAL - no application record for " + str(ID)) | |
else: | |
if len(visits) == 1: | |
ID = visits.iterkeys().next() | |
s.status(ID) | |
elif len(visits) > 1: | |
s.wfile.write("WARNING - multiple (" + str(len(visits)) + ") applications" ) | |
else: | |
s.wfile.write("UNKNOWN - no heartbeats yes!" ) | |
return | |
# PARSING: s.path -->>> path ? T & appid = ID | |
T = int(query[0]) | |
ID = query[1].split('=')[1] + " @ " + s.client_address[0] # " || " + s.headers['User-Agent'] | |
if ID in visits: | |
print "PREVIOUS STATE", visits[ID] | |
visits[ID][2].cancel() # ! | |
ts = time() | |
if path == "/hb_init": | |
# Hello little brother! Big Brother is watching you! | |
print "Creation from scratch : ", ID, " at ", ts | |
T = T + 1 #max(10, (T*17)/16) | |
visits[ID] = (ts, T, Timer(T, toolate, [ID]), 0) | |
s.wfile.write(T) # ? | |
visits[ID][2].start() | |
if path == "/hb_done": | |
print "Destruction: ", ID, " at ", ts | |
del visits[ID] | |
s.wfile.write("So Long, and Thanks for All the Fish!") | |
if path == "/hb_ping": # | |
# TODO: make sure visits[ID] exists! | |
print "HEART-BEAT for: ", ID, " at ", ts # Here i come again... | |
lastts = visits[ID][0] | |
lastt = visits[ID][1] | |
overdue = visits[ID][3] | |
# if (ts - lastts) > lastt: # Sorry Sir, but you are too late :-( | |
# overdue += 1 | |
if overdue > 3: | |
print("overdue!") # TODO: early detection of overdue clients!!??? | |
s.wfile.write("dead") # ? | |
# del visits[ID] #?? | |
else: | |
T = T + 1 # max(3, (T*11)/8) | |
visits[ID] = (ts, T, Timer(T, toolate, [ID]), overdue) | |
s.wfile.write(T) # ? | |
visits[ID][2].start() | |
# WHAT ELSE???? | |
return | |
def test_server(HandlerClass = MyHandler, ServerClass = HTTPServer, protocol="HTTP/1.0"): | |
"""Test the HTTP request handler class. | |
""" | |
port = PORT_NUMBER | |
server_address = (HOST_NAME, port) | |
HandlerClass.protocol_version = protocol | |
httpd = ServerClass(server_address, HandlerClass) | |
sa = httpd.socket.getsockname() | |
print "Serving HTTP on", sa[0], "port", sa[1], "..." | |
httpd.serve_forever() | |
def test_client(): | |
t = randint(2, 5) | |
APP_ID = "test_client_python%" + str(randint(99999999, 9999999999)) # TODO: get unique ID from server? | |
HB_SERVER_URL = "http://" + HOST_NAME + ":" + str(PORT_NUMBER) | |
print "List HB apps: " + urlopen(HB_SERVER_URL + "/list" ).read() | |
print "APP HB Status: " + urlopen(HB_SERVER_URL + "/status" ).read() | |
tt = urlopen(HB_SERVER_URL + "/hb_init?" + str(t) + "&appid="+ APP_ID ).read() | |
print "Initial response: ", tt | |
print "List HB apps: " + urlopen(HB_SERVER_URL + "/list" ).read() | |
print "APP HB Status: " + urlopen(HB_SERVER_URL + "/status" ).read() | |
overdue = 0 | |
i = 0 | |
for i in xrange(1, 25): | |
# while True: | |
i = i + 1 | |
d = randint(0, (int(t) * 2)/1) | |
try: | |
print d, " > ", tt, "?" | |
if d > int(tt): | |
overdue += 1 | |
except: | |
pass | |
print "heart-beat: ", i, "! Promise: ", t, ", Max: ", tt, ", Delay: ", d, " sec........ overdues?: ", overdue | |
sleep(d) | |
# heartbeat: | |
t = randint(0, 5) | |
print "List HB apps: " + urlopen(HB_SERVER_URL + "/list" ).read() | |
print "APP HB Status: " + urlopen(HB_SERVER_URL + "/status" ).read() | |
print "Ping: ", t | |
tt = urlopen(HB_SERVER_URL + "/hb_ping?" + str(t) + "&appid="+ APP_ID ).read() | |
print "Pong: ", tt | |
print "List HB apps: " + urlopen(HB_SERVER_URL + "/list" ).read() | |
print "APP HB Status: " + urlopen(HB_SERVER_URL + "/status" ).read() | |
if tt == "dead": # Just for testing... | |
print "Ups: we run out of time..." | |
tt = urlopen(HB_SERVER_URL + "/hb_done?0"+ "&appid="+ APP_ID ).read() | |
print "Goodbye message: ", tt | |
print "List HB apps: " + urlopen(HB_SERVER_URL + "/list" ).read() | |
print "APP HB Status: " + urlopen(HB_SERVER_URL + "/status" ).read() | |
break | |
# port = int(sys.argv[1]) | |
if __name__ == '__main__': | |
print(sys.argv) | |
if (len(sys.argv) == 1): | |
test_client() | |
else: # Any Arguments? => Start HB Server | |
# if (sys.argv[1] == "-server"): | |
test_server() | |
##################################################################### | |
### Initial HB design: | |
### | |
### Heartbeat: | |
### webgl: add to render loop (initiate) | |
### multithread / asynchron sending (should not block the application) | |
### non-blocking i/o | |
### how often: 1 beat per second (or less)? | |
### TCPIP connection: leave open? | |
### | |
### GET request | |
### sending: add into URL "ID exhibit, heartbeat expectation" (selbstverpflichtung) | |
### The server knows the IP (computer). | |
### | |
### initial setup for monitoring software (also list of programs which run on which host) | |
### startup via Nagios? | |
### | |
### ---- | |
### maintenance mode could be added (you put station on maintenance), if a station is not expected to be running (in order to avoid automatic restart during maintenance) | |
### Heartbeat protocol: | |
### * pass protocol parameters into container via ENVIRONMENT VARIABLES, e.g. | |
### | |
### - HEARTBEART_HTTP=http//IP:PORT/heartbeat.html?container=pong&next=%n | |
### = application substitutes %n with minimal time until next heartbeart (milliseconds) and sends GET request | |
### = server answers with maximal time for next heartbeart (>minimal heartbeat time) (otherwise it will take some action) | |
### | |
### - HEARTBEART_TCP=IP:PORT | |
### = CLIENT: send minimal time until next heartbeat (ms) | |
### = SERVER: send maximal time until next heartbeat (ms) | |
### | |
### * ENVIRONMENT PARAMETERS are passed by url parameters into browser based applications some API exposed by electron for kiosk applications? | |
### | |
### * when container is starting, the management system is waiting for some predefined time (15 seconds? same as regular waiting time when the app is running properly) before expecting the first heartbeat; afterwards the protocol is self tuning | |
### Heartbeat protocol implementation: | |
### | |
### * asynchronous HTTP requests in JS | |
### * provide a JS library the ??? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
<script src="heartbeat.js"></script> | |
</head> | |
<body> | |
<script> | |
var heartbeat = new Heartbeat({ | |
url: Heartbeat.getPassedUrl('http://localhost:8881'), | |
appId: Heartbeat.getPassedAppId('test_app'), | |
interval: 5000, | |
sendInitCommand: true, | |
sendDoneCommand: true, | |
debugLog: function(msg) { | |
console.log(msg); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Besides, what about following https://www.nczonline.net/blog/2009/07/28/the-best-way-to-load-external-javascript/ to load this JS library?
Unfortunately the current python heartbeat server expects interval to be in seconds :-(
Could you please change https://gist.github.com/porst17/1b2076e6d1baf4375500#file-heartbeat-js-L46
interval
to (interval/1000.0)
before this JS lib gets used?
Any chance for getPassedAppId
to ignore run-time argument HB_APP_ID
???
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A few notes:
in https://gist.github.com/porst17/1b2076e6d1baf4375500#file-heartbeat-js-L152 : replace "_initCommand" with "_pingCommand"
in https://gist.github.com/porst17/1b2076e6d1baf4375500#file-heartbeat-js-L145 : replace "this" with "that"