forked from Jibo-Revival-Group/JiboOs
Version 3.1 InDev
This commit is contained in:
55
V3.1/build/opt/jibo/Jibo/Skills/tools/robot/logd/README.md
Normal file
55
V3.1/build/opt/jibo/Jibo/Skills/tools/robot/logd/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# jibo-skills-logd
|
||||
|
||||
A tiny UDP logging daemon intended for **very old BusyBox + Python 2.7** robot environments.
|
||||
|
||||
## What it gives you
|
||||
|
||||
- A single place where all your scripts can log (JS, Python, shell)
|
||||
- A single file you can `tail -f` on robot
|
||||
- Minimal moving parts (no external deps)
|
||||
|
||||
## Files
|
||||
|
||||
- `tools/robot/logd/jibo_logd.py` — Python daemon (UDP → append to file)
|
||||
- `tools/robot/init.d/jibo-skills-logd` — init.d service template
|
||||
|
||||
## Quick test (on robot)
|
||||
|
||||
Start the daemon in foreground:
|
||||
|
||||
- `python /opt/jibo/Jibo/Skills/tools/robot/logd/jibo_logd.py --host 127.0.0.1 --port 15140 --logfile /tmp/jibo-skills.log`
|
||||
|
||||
Send a message:
|
||||
|
||||
- `echo '{"tag":"test","level":"info","msg":"hello"}' | nc -u -w1 127.0.0.1 15140`
|
||||
|
||||
View:
|
||||
|
||||
- `tail -f /tmp/jibo-skills.log`
|
||||
|
||||
## Using from Node
|
||||
|
||||
In your skill code:
|
||||
|
||||
- `const rlog = require('@be/be/be/robot-logger');`
|
||||
- `rlog.info('menu', 'injected entries', {count: 12});`
|
||||
|
||||
Env vars (optional):
|
||||
|
||||
- `JIBO_LOGD_HOST` (default `127.0.0.1`)
|
||||
- `JIBO_LOGD_PORT` (default `15140`)
|
||||
|
||||
## Live web panel (optional)
|
||||
|
||||
There is also a tiny HTTP panel that streams the same logfile in real time (SSE).
|
||||
|
||||
- Script: `tools/robot/logpanel/jibo_logpanel.py`
|
||||
- Init script: `/init.d/jibo-skills-logpanel`
|
||||
|
||||
Run (foreground):
|
||||
|
||||
- `python /opt/jibo/Jibo/Skills/tools/robot/logpanel/jibo_logpanel.py --bind 0.0.0.0 --port 15150 --logfile /tmp/jibo-skills.log`
|
||||
|
||||
Open in a browser:
|
||||
|
||||
- `http://<robot-ip>:15150/`
|
||||
252
V3.1/build/opt/jibo/Jibo/Skills/tools/robot/logd/jibo_logd.py
Normal file
252
V3.1/build/opt/jibo/Jibo/Skills/tools/robot/logd/jibo_logd.py
Normal file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""jibo_logd.py
|
||||
|
||||
Tiny logging daemon for old BusyBox environments.
|
||||
- Python 2.7 compatible
|
||||
- Listens on UDP (default 127.0.0.1:15140)
|
||||
- Appends newline-delimited logs to a file (default /tmp/jibo-skills.log)
|
||||
- Optional daemonize + pidfile
|
||||
|
||||
Protocol:
|
||||
- UDP payload may be plain text (written as-is)
|
||||
- OR JSON object with optional fields: tag, level, msg, data
|
||||
|
||||
Examples:
|
||||
echo '{"tag":"menu","level":"info","msg":"hello"}' | nc -u -w1 127.0.0.1 15140
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import errno
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
DEFAULT_HOST = '127.0.0.1'
|
||||
DEFAULT_PORT = 15140
|
||||
DEFAULT_LOGFILE = '/tmp/jibo-skills.log'
|
||||
DEFAULT_MAX_BYTES = 2 * 1024 * 1024
|
||||
DEFAULT_BACKUPS = 3
|
||||
|
||||
|
||||
class Logd(object):
|
||||
def __init__(self, host, port, logfile, max_bytes, backups, flush_every):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.logfile = logfile
|
||||
self.max_bytes = max_bytes
|
||||
self.backups = backups
|
||||
self.flush_every = flush_every
|
||||
|
||||
self._sock = None
|
||||
self._stop = False
|
||||
self._line_count = 0
|
||||
|
||||
def stop(self, *_args):
|
||||
self._stop = True
|
||||
|
||||
def _ensure_parent_dir(self):
|
||||
parent = os.path.dirname(self.logfile)
|
||||
if parent and not os.path.isdir(parent):
|
||||
try:
|
||||
os.makedirs(parent)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
def _rotate_if_needed(self):
|
||||
if self.max_bytes <= 0:
|
||||
return
|
||||
try:
|
||||
st = os.stat(self.logfile)
|
||||
except OSError:
|
||||
return
|
||||
if st.st_size < self.max_bytes:
|
||||
return
|
||||
|
||||
# Rotate: logfile -> logfile.1 -> logfile.2 ...
|
||||
for i in range(self.backups, 0, -1):
|
||||
src = self.logfile + ('' if i == 0 else '.%d' % i)
|
||||
dst = self.logfile + '.%d' % (i + 1)
|
||||
if i == self.backups:
|
||||
# drop oldest
|
||||
try:
|
||||
os.unlink(self.logfile + '.%d' % (i + 1))
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
if os.path.exists(src):
|
||||
os.rename(src, dst)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
try:
|
||||
os.rename(self.logfile, self.logfile + '.1')
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _format_line(self, payload, addr):
|
||||
ts = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ')
|
||||
src = '%s:%s' % (addr[0], addr[1])
|
||||
|
||||
txt = payload.strip()
|
||||
if not txt:
|
||||
return None
|
||||
|
||||
if txt.startswith('{') and txt.endswith('}'):
|
||||
try:
|
||||
obj = json.loads(txt)
|
||||
tag = obj.get('tag', 'skill')
|
||||
level = obj.get('level', 'info')
|
||||
msg = obj.get('msg', '')
|
||||
data = obj.get('data', None)
|
||||
if data is not None:
|
||||
try:
|
||||
data_txt = json.dumps(data, separators=(',', ':'), sort_keys=True)
|
||||
except Exception:
|
||||
data_txt = repr(data)
|
||||
return '%s [%s] %s %s %s\n' % (ts, level, tag, msg, data_txt)
|
||||
return '%s [%s] %s %s\n' % (ts, level, tag, msg)
|
||||
except Exception:
|
||||
# fall back to raw
|
||||
pass
|
||||
|
||||
return '%s [info] raw %s %s\n' % (ts, src, txt)
|
||||
|
||||
def serve_forever(self):
|
||||
self._ensure_parent_dir()
|
||||
|
||||
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self._sock.bind((self.host, self.port))
|
||||
self._sock.settimeout(0.5)
|
||||
|
||||
f = None
|
||||
try:
|
||||
f = open(self.logfile, 'a')
|
||||
while not self._stop:
|
||||
try:
|
||||
data, addr = self._sock.recvfrom(65535)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except socket.error:
|
||||
continue
|
||||
|
||||
try:
|
||||
if isinstance(data, bytes):
|
||||
try:
|
||||
payload = data.decode('utf-8', 'replace')
|
||||
except Exception:
|
||||
payload = str(data)
|
||||
else:
|
||||
payload = str(data)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
line = self._format_line(payload, addr)
|
||||
if not line:
|
||||
continue
|
||||
|
||||
self._rotate_if_needed()
|
||||
try:
|
||||
f.write(line)
|
||||
self._line_count += 1
|
||||
if self.flush_every > 0 and (self._line_count % self.flush_every) == 0:
|
||||
try:
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
# best effort: reopen file if it moved/rotated
|
||||
try:
|
||||
f.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
f = open(self.logfile, 'a')
|
||||
except Exception:
|
||||
time.sleep(0.25)
|
||||
finally:
|
||||
try:
|
||||
if f:
|
||||
f.flush()
|
||||
f.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if self._sock:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def daemonize(pidfile):
|
||||
# Double-fork daemonize (POSIX)
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
os._exit(0)
|
||||
except OSError:
|
||||
raise
|
||||
|
||||
os.setsid()
|
||||
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
os._exit(0)
|
||||
except OSError:
|
||||
raise
|
||||
|
||||
# Redirect stdio
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
si = open('/dev/null', 'r')
|
||||
so = open('/dev/null', 'a+')
|
||||
se = open('/dev/null', 'a+')
|
||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||
|
||||
if pidfile:
|
||||
try:
|
||||
with open(pidfile, 'w') as pf:
|
||||
pf.write(str(os.getpid()))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main(argv):
|
||||
ap = argparse.ArgumentParser(description='Jibo Skills UDP log daemon')
|
||||
ap.add_argument('--host', default=os.environ.get('JIBO_LOGD_HOST', DEFAULT_HOST))
|
||||
ap.add_argument('--port', type=int, default=int(os.environ.get('JIBO_LOGD_PORT', str(DEFAULT_PORT))))
|
||||
ap.add_argument('--logfile', default=os.environ.get('JIBO_LOGD_FILE', DEFAULT_LOGFILE))
|
||||
ap.add_argument('--max-bytes', type=int, default=int(os.environ.get('JIBO_LOGD_MAX_BYTES', str(DEFAULT_MAX_BYTES))))
|
||||
ap.add_argument('--backups', type=int, default=int(os.environ.get('JIBO_LOGD_BACKUPS', str(DEFAULT_BACKUPS))))
|
||||
ap.add_argument('--flush-every', type=int, default=int(os.environ.get('JIBO_LOGD_FLUSH_EVERY', '1')))
|
||||
ap.add_argument('--daemonize', action='store_true')
|
||||
ap.add_argument('--pidfile', default=os.environ.get('JIBO_LOGD_PIDFILE', '/tmp/jibo-skills-logd.pid'))
|
||||
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
logd = Logd(args.host, args.port, args.logfile, args.max_bytes, args.backups, args.flush_every)
|
||||
|
||||
signal.signal(signal.SIGTERM, logd.stop)
|
||||
signal.signal(signal.SIGINT, logd.stop)
|
||||
|
||||
if args.daemonize:
|
||||
daemonize(args.pidfile)
|
||||
|
||||
logd.serve_forever()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Binary file not shown.
@@ -0,0 +1,503 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""jibo_logpanel.py
|
||||
|
||||
Minimal HTTP log panel for old BusyBox + Python 2.7 environments.
|
||||
|
||||
- Serves a tiny HTML page at `/`
|
||||
- Streams log lines via Server-Sent Events (SSE) at `/events`
|
||||
- Tails a logfile (default: /tmp/jibo-skills.log)
|
||||
|
||||
This is intentionally dependency-free (stdlib only).
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import json # stdlib
|
||||
except Exception:
|
||||
json = None
|
||||
|
||||
try:
|
||||
import urlparse # Py2
|
||||
except ImportError:
|
||||
from urllib import parse as urlparse # Py3
|
||||
|
||||
try:
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
except ImportError:
|
||||
# Py3 fallback (dev hosts)
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
try:
|
||||
from SocketServer import ThreadingMixIn
|
||||
except ImportError:
|
||||
from socketserver import ThreadingMixIn
|
||||
|
||||
|
||||
INDEX_HTML = """<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=\"utf-8\" />
|
||||
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
|
||||
<title>Jibo Skills Logs</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 0; }
|
||||
header { padding: 10px 12px; background: #111; color: #eee; position: sticky; top: 0; }
|
||||
#status { opacity: 0.8; font-size: 12px; }
|
||||
#controls { margin-top: 6px; font-size: 12px; }
|
||||
#log { white-space: pre-wrap; font-family: monospace; font-size: 12px; padding: 10px 12px; }
|
||||
.dim { opacity: 0.65; }
|
||||
button { font-size: 12px; padding: 4px 8px; }
|
||||
input { font-size: 12px; padding: 4px 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div><strong>Jibo Skills Logs</strong> <span id=\"status\" class=\"dim\">(connecting)</span></div>
|
||||
<div id=\"controls\">
|
||||
<button id=\"pause\">Pause</button>
|
||||
<button id=\"clear\">Clear</button>
|
||||
<label class=\"dim\">Filter:</label>
|
||||
<input id=\"filter\" placeholder=\"e.g. MENU-PATCH or [error]\" size=\"28\" />
|
||||
</div>
|
||||
</header>
|
||||
<div id=\"log\"></div>
|
||||
|
||||
<script>
|
||||
var logEl = document.getElementById('log');
|
||||
var statusEl = document.getElementById('status');
|
||||
var pauseBtn = document.getElementById('pause');
|
||||
var clearBtn = document.getElementById('clear');
|
||||
var filterEl = document.getElementById('filter');
|
||||
|
||||
var paused = false;
|
||||
var filter = '';
|
||||
var es = null;
|
||||
var pollTimer = null;
|
||||
var pollPos = 0;
|
||||
|
||||
function requestJSON(url, onOk, onErr) {
|
||||
// Older embedded browsers may not have fetch(); use XHR.
|
||||
try {
|
||||
if (typeof fetch === 'function') {
|
||||
fetch(url).then(function(r){ return r.json(); }).then(onOk).catch(onErr);
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* fall through */ }
|
||||
|
||||
try {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, true);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState !== 4) return;
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
onOk(JSON.parse(xhr.responseText));
|
||||
} catch (e) {
|
||||
onErr(e);
|
||||
}
|
||||
} else {
|
||||
onErr(new Error('HTTP ' + xhr.status));
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
} catch (e) {
|
||||
onErr(e);
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(s) { statusEl.textContent = s; }
|
||||
|
||||
function appendLine(line) {
|
||||
if (paused) return;
|
||||
if (filter && line.indexOf(filter) === -1) return;
|
||||
logEl.textContent += line + "\\n";
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (pollTimer) return;
|
||||
setStatus('(polling)');
|
||||
function tick() {
|
||||
var url = 'tail?pos=' + encodeURIComponent(String(pollPos)) + '&max=8192';
|
||||
requestJSON(url, function(obj){
|
||||
if (!obj) return;
|
||||
if (typeof obj.pos === 'number') pollPos = obj.pos;
|
||||
if (obj.lines && obj.lines.length) {
|
||||
for (var i=0; i<obj.lines.length; i++) appendLine(obj.lines[i]);
|
||||
}
|
||||
}, function(){ /* ignore */ });
|
||||
}
|
||||
tick();
|
||||
pollTimer = setInterval(tick, 500);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (es) { try { es.close(); } catch(e) {} }
|
||||
stopPolling();
|
||||
setStatus('(connecting)');
|
||||
var opened = false;
|
||||
|
||||
// If EventSource isn't supported, go straight to polling.
|
||||
if (typeof EventSource !== 'function') {
|
||||
startPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
es = new EventSource('events?lines=200');
|
||||
} catch (e) {
|
||||
startPolling();
|
||||
return;
|
||||
}
|
||||
var openTimeout = setTimeout(function(){
|
||||
if (!opened) startPolling();
|
||||
}, 1500);
|
||||
|
||||
es.onopen = function() {
|
||||
opened = true;
|
||||
clearTimeout(openTimeout);
|
||||
setStatus('(live)');
|
||||
stopPolling();
|
||||
};
|
||||
es.onerror = function() {
|
||||
setStatus('(disconnected; retrying)');
|
||||
startPolling();
|
||||
};
|
||||
es.onmessage = function(ev) { appendLine(ev.data); };
|
||||
}
|
||||
|
||||
pauseBtn.onclick = function() {
|
||||
paused = !paused;
|
||||
pauseBtn.textContent = paused ? 'Resume' : 'Pause';
|
||||
};
|
||||
|
||||
clearBtn.onclick = function() {
|
||||
logEl.textContent = '';
|
||||
};
|
||||
|
||||
filterEl.oninput = function() { filter = filterEl.value || ''; };
|
||||
|
||||
connect();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def _tail_last_lines(path, max_lines):
|
||||
if max_lines <= 0:
|
||||
return []
|
||||
|
||||
try:
|
||||
f = open(path, 'rb')
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
try:
|
||||
# naive but safe: read entire file if small, else chunk backwards
|
||||
try:
|
||||
size = os.path.getsize(path)
|
||||
except Exception:
|
||||
size = 0
|
||||
|
||||
if size <= 0:
|
||||
return []
|
||||
|
||||
# read up to ~256KB from end
|
||||
read_size = min(size, 256 * 1024)
|
||||
f.seek(-read_size, os.SEEK_END)
|
||||
data = f.read(read_size)
|
||||
|
||||
try:
|
||||
txt = data.decode('utf-8', 'replace')
|
||||
except Exception:
|
||||
try:
|
||||
txt = data.decode('latin-1', 'replace')
|
||||
except Exception:
|
||||
txt = str(data)
|
||||
|
||||
lines = txt.splitlines()
|
||||
if len(lines) > max_lines:
|
||||
lines = lines[-max_lines:]
|
||||
return lines
|
||||
finally:
|
||||
try:
|
||||
f.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "JiboLogPanel/0.1"
|
||||
# Use HTTP/1.0 semantics to avoid needing chunked encoding.
|
||||
protocol_version = "HTTP/1.0"
|
||||
|
||||
try:
|
||||
_text_type = unicode # Py2
|
||||
except NameError:
|
||||
_text_type = str # Py3
|
||||
|
||||
def _send(self, code, content_type, body):
|
||||
if isinstance(body, self._text_type):
|
||||
body = body.encode('utf-8')
|
||||
self.send_response(code)
|
||||
self.send_header('Content-Type', content_type)
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.send_header('Cache-Control', 'no-cache')
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urlparse.urlparse(self.path)
|
||||
path = parsed.path
|
||||
qs = urlparse.parse_qs(parsed.query)
|
||||
|
||||
if path == '/' or path == '/index.html':
|
||||
return self._send(200, 'text/html; charset=utf-8', INDEX_HTML)
|
||||
|
||||
if path == '/health':
|
||||
return self._send(200, 'text/plain; charset=utf-8', 'ok')
|
||||
|
||||
if path == '/events':
|
||||
return self._handle_events(qs)
|
||||
|
||||
if path == '/tail':
|
||||
return self._handle_tail(qs)
|
||||
|
||||
return self._send(404, 'text/plain; charset=utf-8', 'not found')
|
||||
|
||||
def _handle_tail(self, qs):
|
||||
logfile = getattr(self.server, 'logfile', '/tmp/jibo-skills.log')
|
||||
try:
|
||||
pos = int((qs.get('pos') or ['0'])[0])
|
||||
except Exception:
|
||||
pos = 0
|
||||
try:
|
||||
max_bytes = int((qs.get('max') or ['8192'])[0])
|
||||
except Exception:
|
||||
max_bytes = 8192
|
||||
if max_bytes <= 0:
|
||||
max_bytes = 8192
|
||||
if max_bytes > 65536:
|
||||
max_bytes = 65536
|
||||
|
||||
out = { 'pos': 0, 'lines': [] }
|
||||
|
||||
try:
|
||||
st = os.stat(logfile)
|
||||
size = st.st_size
|
||||
if pos < 0 or pos > size:
|
||||
pos = 0
|
||||
with open(logfile, 'rb') as f:
|
||||
f.seek(pos)
|
||||
data = f.read(max_bytes)
|
||||
new_pos = f.tell()
|
||||
except Exception:
|
||||
out['pos'] = 0
|
||||
out['lines'] = []
|
||||
body = json.dumps(out) if json else '{"pos":0,"lines":[]}'
|
||||
return self._send(200, 'application/json; charset=utf-8', body)
|
||||
|
||||
try:
|
||||
try:
|
||||
txt = data.decode('utf-8', 'replace')
|
||||
except Exception:
|
||||
txt = data.decode('latin-1', 'replace')
|
||||
# splitlines() drops trailing newline; that's fine for display
|
||||
lines = txt.replace('\r', '').splitlines()
|
||||
except Exception:
|
||||
lines = []
|
||||
|
||||
out['pos'] = new_pos
|
||||
out['lines'] = lines
|
||||
body = json.dumps(out) if json else '{"pos":%d,"lines":[]}' % new_pos
|
||||
return self._send(200, 'application/json; charset=utf-8', body)
|
||||
|
||||
def _handle_events(self, qs):
|
||||
logfile = getattr(self.server, 'logfile', '/tmp/jibo-skills.log')
|
||||
try:
|
||||
lines = int((qs.get('lines') or ['0'])[0])
|
||||
except Exception:
|
||||
lines = 0
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/event-stream; charset=utf-8')
|
||||
self.send_header('Cache-Control', 'no-cache')
|
||||
self.send_header('X-Accel-Buffering', 'no')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Connection', 'keep-alive')
|
||||
self.end_headers()
|
||||
|
||||
# Ensure the connection stays open.
|
||||
try:
|
||||
self.close_connection = False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _b(s):
|
||||
try:
|
||||
if isinstance(s, bytes):
|
||||
return s
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if isinstance(s, self._text_type):
|
||||
return s.encode('utf-8')
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return str(s).encode('utf-8')
|
||||
except Exception:
|
||||
return b''
|
||||
|
||||
def _write(data_bytes):
|
||||
if not data_bytes:
|
||||
return
|
||||
try:
|
||||
self.wfile.write(data_bytes)
|
||||
try:
|
||||
self.wfile.flush()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
# Advise client to retry quickly.
|
||||
try:
|
||||
_write(_b('retry: 1000\n\n'))
|
||||
except Exception:
|
||||
return
|
||||
|
||||
# initial payload: last N lines
|
||||
try:
|
||||
for line in _tail_last_lines(logfile, lines):
|
||||
_write(_b('data: ' + line.replace('\r', '') + '\n\n'))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# follow
|
||||
f = None
|
||||
|
||||
last_inode = None
|
||||
try:
|
||||
try:
|
||||
st = os.stat(logfile)
|
||||
last_inode = st.st_ino
|
||||
except Exception:
|
||||
last_inode = None
|
||||
|
||||
last_ping = time.time()
|
||||
|
||||
def _try_open_follow():
|
||||
try:
|
||||
fh = open(logfile, 'rb')
|
||||
fh.seek(0, os.SEEK_END)
|
||||
return fh
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
f = _try_open_follow()
|
||||
|
||||
while True:
|
||||
# handle rotation: reopen if inode changes
|
||||
try:
|
||||
st2 = os.stat(logfile)
|
||||
if last_inode is not None and st2.st_ino != last_inode:
|
||||
try:
|
||||
if f:
|
||||
f.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
f = open(logfile, 'rb')
|
||||
f.seek(0, os.SEEK_END)
|
||||
last_inode = st2.st_ino
|
||||
except Exception:
|
||||
f = None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not f:
|
||||
# keep-alive heartbeat even if file missing
|
||||
now = time.time()
|
||||
if now - last_ping >= 5.0:
|
||||
last_ping = now
|
||||
try:
|
||||
_write(_b(': ping\n\n'))
|
||||
except Exception:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
|
||||
pos = f.tell()
|
||||
chunk = f.readline()
|
||||
if not chunk:
|
||||
f.seek(pos)
|
||||
now = time.time()
|
||||
if now - last_ping >= 5.0:
|
||||
last_ping = now
|
||||
try:
|
||||
_write(_b(': ping\n\n'))
|
||||
except Exception:
|
||||
break
|
||||
time.sleep(0.25)
|
||||
continue
|
||||
|
||||
try:
|
||||
try:
|
||||
line = chunk.decode('utf-8', 'replace')
|
||||
except Exception:
|
||||
line = chunk.decode('latin-1', 'replace')
|
||||
line = line.rstrip('\n').rstrip('\r')
|
||||
_write(_b('data: ' + line + '\n\n'))
|
||||
except Exception:
|
||||
# client disconnected
|
||||
break
|
||||
finally:
|
||||
try:
|
||||
if f:
|
||||
f.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
# keep quiet (panel should not spam stdout)
|
||||
return
|
||||
|
||||
|
||||
def main(argv):
|
||||
ap = argparse.ArgumentParser(description='Jibo Skills Log Panel (SSE)')
|
||||
ap.add_argument('--bind', default=os.environ.get('JIBO_LOGPANEL_BIND', '0.0.0.0'))
|
||||
ap.add_argument('--port', type=int, default=int(os.environ.get('JIBO_LOGPANEL_PORT', '15150')))
|
||||
ap.add_argument('--logfile', default=os.environ.get('JIBO_LOGD_FILE', '/tmp/jibo-skills.log'))
|
||||
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
httpd = ThreadedHTTPServer((args.bind, args.port), Handler)
|
||||
httpd.logfile = args.logfile
|
||||
|
||||
print('logpanel listening on %s:%d, logfile=%s' % (args.bind, args.port, args.logfile))
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Reference in New Issue
Block a user