import os import logging import threading from itertools import islice from time import sleep from datetime import datetime,timedelta from pwnagotchi import plugins from pwnagotchi.utils import StatusFile from flask import render_template_string from flask import jsonify from flask import abort from flask import Response TEMPLATE = """ {% extends "base.html" %} {% set active_page = "plugins" %} {% block title %} Logtail {% endblock %} {% block styles %} {{ super() }} <style> * { box-sizing: border-box; } #filter { width: 100%; font-size: 16px; padding: 12px 20px 12px 40px; border: 1px solid #ddd; margin-bottom: 12px; } table { border-collapse: collapse; width: 100%; border: 1px solid #ddd; } th, td { text-align: left; padding: 12px; width: 1px; white-space: nowrap; } td:nth-child(2) { text-align: center; } thead, tr:hover { background-color: #f1f1f1; } tr { border-bottom: 1px solid #ddd; } div.sticky { position: -webkit-sticky; position: sticky; top: 0; display: table; width: 100%; } div.sticky > * { display: table-cell; } div.sticky > span { width: 1%; } div.sticky > input { width: 100%; } tr.default { color: black; } tr.info { color: black; } tr.warning { color: darkorange; } tr.error { color: crimson; } tr.debug { color: blueviolet; } .ui-mobile .ui-page-active { overflow: visible; overflow-x: visible; } </style> {% endblock %} {% block script %} var table = document.getElementById('table'); var filter = document.getElementById('filter'); var filterVal = filter.value.toUpperCase(); var xhr = new XMLHttpRequest(); xhr.open('GET', '{{ url_for('plugins') }}/logtail/stream'); xhr.send(); var position = 0; var data; var time; var level; var msg; var colorClass; function handleNewData() { var messages = xhr.responseText.split('\\n'); filterVal = filter.value.toUpperCase(); messages.slice(position, -1).forEach(function(value) { if (value.charAt(0) != '[') { msg = value; time = ''; level = ''; } else { data = value.split(']'); time = data.shift() + ']'; level = data.shift() + ']'; msg = data.join(']'); switch(level) { case ' [INFO]': colorClass = 'info'; break; case ' [WARNING]': colorClass = 'warning'; break; case ' [ERROR]': colorClass = 'error'; break; case ' [DEBUG]': colorClass = 'debug'; break; default: colorClass = 'default'; break; } } var tr = document.createElement('tr'); var td1 = document.createElement('td'); var td2 = document.createElement('td'); var td3 = document.createElement('td'); td1.textContent = time; td2.textContent = level; td3.textContent = msg; tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3); tr.className = colorClass; if (filterVal.length > 0 && value.toUpperCase().indexOf(filterVal) == -1) { tr.style.display = "none"; } table.appendChild(tr); }); position = messages.length - 1; } var scrollingElement = (document.scrollingElement || document.body) function scrollToBottom () { scrollingElement.scrollTop = scrollingElement.scrollHeight; } var timer; var scrollElm = document.getElementById('autoscroll'); timer = setInterval(function() { handleNewData(); if (scrollElm.checked) { scrollToBottom(); } if (xhr.readyState == XMLHttpRequest.DONE) { clearInterval(timer); } }, 1000); var typingTimer; var doneTypingInterval = 1000; filter.onkeyup = function() { clearTimeout(typingTimer); typingTimer = setTimeout(doneTyping, doneTypingInterval); } filter.onkeydown = function() { clearTimeout(typingTimer); } function doneTyping() { document.body.style.cursor = 'progress'; var tr, tds, td, i, txtValue; filterVal = filter.value.toUpperCase(); tr = table.getElementsByTagName("tr"); for (i = 1; i < tr.length; i++) { txtValue = tr[i].textContent || tr[i].innerText; if (txtValue.toUpperCase().indexOf(filterVal) > -1) { tr[i].style.display = "table-row"; } else { tr[i].style.display = "none"; } } document.body.style.cursor = 'default'; } {% endblock %} {% block content %} <div class="sticky"> <input type="text" id="filter" placeholder="Search for ..." title="Type in a filter"> <span><input checked type="checkbox" id="autoscroll"></span> <span><label for="autoscroll"> Autoscroll to bottom</label><br></span> </div> <table id="table"> <thead> <th> Time </th> <th> Level </th> <th> Message </th> </thead> </table> {% endblock %} """ class Logtail(plugins.Plugin): __author__ = '33197631+dadav@users.noreply.github.com' __version__ = '0.1.0' __license__ = 'GPL3' __description__ = 'This plugin tails the logfile.' def __init__(self): self.lock = threading.Lock() self.options = dict() self.ready = False def on_config_changed(self, config): self.config = config self.ready = True def on_loaded(self): """ Gets called when the plugin gets loaded """ logging.info("Logtail plugin loaded.") def on_webhook(self, path, request): if not self.ready: return "Plugin not ready" if not path or path == "/": return render_template_string(TEMPLATE) if path == 'stream': def generate(): with open(self.config['main']['log']['path']) as f: # https://stackoverflow.com/questions/39549426/read-multiple-lines-from-a-file-batch-by-batch/39549901#39549901 n = 1024 for n_lines in iter(lambda: ''.join(islice(f, n)), ''): yield n_lines while True: yield f.readline() return Response(generate(), mimetype='text/plain') abort(404)