commit 75b3296f2dab433b518e6e93bf304a2054c3438e Author: Mikko Ahlroth Date: Thu Jul 18 11:53:26 2013 +0300 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..83bf814 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2013 Mikko Ahlroth + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..671e835 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +Sysmon-ws is a simple Python WebSocket server which broadcasts system +information to all listening clients. It also includes a small HTML5 UI which +displays the information in a somewhat sensible manner. I pieced this together +one night when inspiration hit, so the code is written in a quick and dirty +manner. + +Sysmon-ws uses 3rd party code: +* AutoBahnPython WebSocketServer from https://github.com/tavendo/AutobahnPython/blob/master/examples/websocket/broadcast/server.py +* jquery-graceful-websocket from https://code.google.com/p/jquery-graceful-websocket/ + + +### Installation + +Clone the files to a directory, create a virtualenv if you like and then +install libs with pip: + + pip install -r requirements.txt + + +### Usage + +Usage example from my server (I use a virtualenv for it): + + [nicd@nytsoi ~]$ cd /srv/sysmon-ws + [nicd@nytsoi sysmon-ws]$ . venv-sysmon-ws/bin/activate + (venv-sysmon-ws)[nicd@nytsoi sysmon-ws]$ python server.py >/dev/null + + +### License + +* Original AutoBahnPython code in server.py is licensed with Apache 2.0, read + server.py for more details. +* jquery-graceful-websocket (ws.js) is licensed with the MIT License. +* My code (index.html and modifications in server.py) is licensed with the + MIT License. See LICENSE for more details. diff --git a/htmlui/index.html b/htmlui/index.html new file mode 100644 index 0000000..347ec11 --- /dev/null +++ b/htmlui/index.html @@ -0,0 +1,411 @@ + + + + + nytsoi.net system monitor + + + + + +
+

nytsoi.net system monitor + + | Uptime: ? + | ? users, ? processes +

+ +
+ +
+ +
+
+
+
+
+

CPU times                                                   

+
user +system +idle +nice +iowait +irq +softirq +steal +guest +guest_nice
+
+
+
+ +
+

I/O                                                         

+
Network + Bytes in + Bytes out + +Disk + Reads + Writes
+
+
+
+ + + + + + + diff --git a/htmlui/ws.js b/htmlui/ws.js new file mode 100644 index 0000000..cfd39aa --- /dev/null +++ b/htmlui/ws.js @@ -0,0 +1,165 @@ +/** + * WebSocket with graceful degradation - jQuery plugin + * @author David Lindkvist + * @version 0.1 + * + * Returns an object implementing the WebSocket API. + * + * If browser supports WebSockets a native WebSocket instance is returned. + * If not, a simulated half-duplex implementation is returned which uses polling + * over HTTP to retrieve new messages + * + * OPTIONS + * ----------------------------------------------------------------------------- + * + * {Number} fallbackOpenDelay number of ms to delay simulated open + * event for fallback + * {Number} fallbackPollInterval number of ms between requests for + * fallback polling + * {Object} fallbackPollParams optional params to pass with each poll + * requests + * + * EXAMPLES + * ----------------------------------------------------------------------------- + * + * var websocket = $.gracefulWebSocket("ws://127.0.0.1:8080/"); + * + * var websocket = $.gracefulWebSocket({ + * fallbackPollParams: { + * "latestMessageID": function () { + * return latestMessageID; + * } + * } + * }); + * + */ + +(function ($) { + + $.extend({ + + gracefulWebSocket: function (url, options) { + + // Default properties + this.defaults = { + keepAlive: false, // not implemented - should ping server to keep socket open + autoReconnect: false, // not implemented - should try to reconnect silently if socket is closed + fallback: true, // not implemented - always use HTTP fallback if native browser support is missing + fallbackSendURL: url.replace('ws:', 'http:').replace('wss:', 'https:'), + fallbackSendMethod: 'POST', + fallbackPollURL: url.replace('ws:', 'http:').replace('wss:', 'https:'), + fallbackPollMethod: 'GET', + fallbackOpenDelay: 100, // number of ms to delay simulated open event + fallbackPollInterval: 3000, // number of ms between poll requests + fallbackPollParams: {} // optional params to pass with poll requests + }; + + // Override defaults with user properties + var opts = $.extend({}, this.defaults, options); + + /** + * Creates a fallback object implementing the WebSocket interface + */ + function FallbackSocket() { + + // WebSocket interface constants + const CONNECTING = 0; + const OPEN = 1; + const CLOSING = 2; + const CLOSED = 3; + + var pollInterval; + var openTimout; + + // create WebSocket object + var fws = { + // ready state + readyState: CONNECTING, + bufferedAmount: 0, + send: function (data) { + var success = true; + $.ajax({ + async: false, // send synchronously + type: opts.fallbackSendMethod, + url: opts.fallbackSendURL + '?' + $.param( getFallbackParams() ), + data: data, + dataType: 'text', + contentType : "application/x-www-form-urlencoded; charset=utf-8", + success: pollSuccess, + error: function (xhr) { + success = false; + $(fws).triggerHandler('error'); + } + }); + return success; + }, + close: function () { + clearTimeout(openTimout); + clearInterval(pollInterval); + this.readyState = CLOSED; + $(fws).triggerHandler('close'); + }, + onopen: function () {}, + onmessage: function () {}, + onerror: function () {}, + onclose: function () {}, + previousRequest: null, + currentRequest: null + }; + + function getFallbackParams() { + + // update timestamp of previous and current poll request + fws.previousRequest = fws.currentRequest; + fws.currentRequest = new Date().getTime(); + + // extend default params with plugin options + return $.extend({"previousRequest": fws.previousRequest, "currentRequest": fws.currentRequest}, opts.fallbackPollParams); + } + + /** + * @param {Object} data + */ + function pollSuccess(data) { + + // trigger onmessage + var messageEvent = {"data" : data}; + fws.onmessage(messageEvent); + } + + function poll() { + + $.ajax({ + type: opts.fallbackPollMethod, + url: opts.fallbackPollURL, + dataType: 'text', + data: getFallbackParams(), + success: pollSuccess, + error: function (xhr) { + $(fws).triggerHandler('error'); + } + }); + } + + // simulate open event and start polling + openTimout = setTimeout(function () { + fws.readyState = OPEN; + //fws.currentRequest = new Date().getTime(); + $(fws).triggerHandler('open'); + poll(); + pollInterval = setInterval(poll, opts.fallbackPollInterval); + + }, opts.fallbackOpenDelay); + + // return socket impl + return fws; + } + + // create a new websocket or fallback + var ws = window.WebSocket ? new WebSocket(url) : new FallbackSocket(); + $(window).unload(function () { ws.close(); ws = null }); + return ws; + } + }); + +})(jQuery); \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..56d53a5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +autobahn==0.5.14 +psutil==0.7.1 + diff --git a/server.py b/server.py new file mode 100644 index 0000000..57d52ae --- /dev/null +++ b/server.py @@ -0,0 +1,167 @@ +############################################################################### +## +## Copyright 2011,2012 Tavendo GmbH +## +## 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. +## +## +## Modified by Mikko Ahlroth, 2013 +## Original is part of AutobahnPython and can be found at +## https://github.com/tavendo/AutobahnPython/blob/master/examples/websocket/broadcast/server.py +## Modifications are licensed with the MIT License, see LICENSE for details. +## +############################################################################### + +import sys +import json +import psutil + +from twisted.internet import reactor +from twisted.python import log + +from autobahn.websocket import WebSocketServerFactory, \ + WebSocketServerProtocol, \ + listenWS + +TICK_INTERVAL = 2 +CPU_INTERVAL = 1 +WEBSOCKET = 'ws://localhost:8081/' +NET_INTERFACE = 'eth0' + +INITIAL_INFO = { + 'cpus': psutil.NUM_CPUS, + 'boot': psutil.get_boot_time(), + 'version': '0.1', +} + + +def get_sys_status(): + cpu = psutil.cpu_percent(interval=CPU_INTERVAL, percpu=True) + mem = psutil.virtual_memory().percent + swap = psutil.swap_memory().percent + procs = len(psutil.get_pid_list()) + ct = psutil.cpu_times_percent() + cpu_times = { + 'user': ct[0], + 'system': ct[1], + 'idle': ct[2], + 'nice': ct[3], + 'iowait': ct[4], + 'irq': ct[5], + 'softirq': ct[6], + 'steal': ct[7], + 'guest': ct[8], + 'guest_nice': ct[9], + } + net = psutil.network_io_counters(pernic=True)[NET_INTERFACE] + net_out = { + 'bytes_out': net.bytes_sent, + 'bytes_in': net.bytes_recv, + } + disk = psutil.disk_io_counters() + disk_out = { + 'read_count': disk.read_count, + 'write_count': disk.write_count, + } + + users = len(psutil.get_users()) + + return { + 'cpu': cpu, + 'mem': mem, + 'swap': swap, + 'procs': procs, + 'cpu_times': cpu_times, + 'net': net_out, + 'disk': disk_out, + 'users': users, + } + + + +class BroadcastServerProtocol(WebSocketServerProtocol): + + def onOpen(self): + self.factory.register(self) + + def onMessage(self, msg, binary): + pass + + def connectionLost(self, reason): + WebSocketServerProtocol.connectionLost(self, reason) + self.factory.unregister(self) + + +class BroadcastServerFactory(WebSocketServerFactory): + + def __init__(self, url, debug=False, debugCodePaths=False): + WebSocketServerFactory.__init__(self, url, debug=debug, debugCodePaths=debugCodePaths) + self.clients = [] + self.tick() + + def tick(self): + self.broadcast(json.dumps(get_sys_status())) + reactor.callLater(TICK_INTERVAL - CPU_INTERVAL, self.tick) + + def register(self, client): + if not client in self.clients: + print "registered client " + client.peerstr + self.clients.append(client) + client.sendMessage(json.dumps(INITIAL_INFO)) + + def unregister(self, client): + if client in self.clients: + print "unregistered client " + client.peerstr + self.clients.remove(client) + + def broadcast(self, msg): + print "broadcasting message '%s' .." % msg + for c in self.clients: + c.sendMessage(msg) + print "message sent to " + c.peerstr + + +class BroadcastPreparedServerFactory(BroadcastServerFactory): + """ + Functionally same as above, but optimized broadcast using + prepareMessage and sendPreparedMessage. + """ + + def broadcast(self, msg): + print "broadcasting prepared message '%s' .." % msg + preparedMsg = self.prepareMessage(msg) + for c in self.clients: + c.sendPreparedMessage(preparedMsg) + print "prepared message sent to " + c.peerstr + + +if __name__ == '__main__': + + if len(sys.argv) > 1 and sys.argv[1] == 'debug': + log.startLogging(sys.stdout) + debug = True + else: + debug = False + + #ServerFactory = BroadcastServerFactory + ServerFactory = BroadcastPreparedServerFactory + + factory = ServerFactory(WEBSOCKET, + debug=debug, + debugCodePaths=debug) + + factory.protocol = BroadcastServerProtocol + factory.setProtocolOptions(allowHixie76=True) + listenWS(factory) + + reactor.run()