log interface

This commit is contained in:
Devaev Maxim 2018-10-28 06:51:51 +03:00
parent 1f54776ce0
commit ab342111d0
12 changed files with 133 additions and 16 deletions

View File

@ -18,6 +18,7 @@ depends=(
python-raspberry-gpio python-raspberry-gpio
python-pyserial python-pyserial
python-setproctitle python-setproctitle
python-systemd
) )
makedepends=(python-setuptools) makedepends=(python-setuptools)
source=("$url/archive/v$pkgver.tar.gz") source=("$url/archive/v$pkgver.tar.gz")

View File

@ -4,6 +4,11 @@ kvmd:
port: 8081 port: 8081
heartbeat: 3.0 heartbeat: 3.0
log:
services:
- kvmd.service
- kvmd-tc358743.service
hid: hid:
pinout: pinout:
reset: 4 reset: 4
@ -66,8 +71,7 @@ logging:
console: console:
(): logging.Formatter (): logging.Formatter
style: "{" style: "{"
datefmt: "%H:%M:%S" format: "{name:20.20} {levelname:>7} --- {message}"
format: "[{asctime}] {name:20.20} {levelname:>7} --- {message}"
handlers: handlers:
console: console:

View File

@ -4,6 +4,11 @@ kvmd:
port: 8081 port: 8081
heartbeat: 3.0 heartbeat: 3.0
log:
services:
- kvmd.service
- kvmd-tc358743.service
hid: hid:
pinout: pinout:
reset: 4 reset: 4
@ -69,8 +74,7 @@ logging:
console: console:
(): logging.Formatter (): logging.Formatter
style: "{" style: "{"
datefmt: "%H:%M:%S" format: "{name:20.20} {levelname:>7} --- {message}"
format: "[{asctime}] {name:20.20} {levelname:>7} --- {message}"
handlers: handlers:
console: console:

View File

@ -109,6 +109,16 @@ http {
proxy_request_buffering off; proxy_request_buffering off;
} }
location /kvmd/log {
rewrite /kvmd/log /log break;
proxy_pass http://kvmd;
include /etc/nginx/proxy-params.conf;
proxy_read_timeout 7d;
postpone_output 0;
proxy_buffering off;
proxy_ignore_headers X-Accel-Buffering;
}
location /kvmd { location /kvmd {
rewrite /kvmd/?(.*) /$1 break; rewrite /kvmd/?(.*) /$1 break;
proxy_pass http://kvmd; proxy_pass http://kvmd;

View File

@ -2,6 +2,7 @@ import asyncio
from .application import init from .application import init
from .logging import get_logger from .logging import get_logger
from .logging import Log
from .hid import Hid from .hid import Hid
from .atx import Atx from .atx import Atx
@ -22,6 +23,11 @@ def main() -> None:
with gpio.bcm(): with gpio.bcm():
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
log = Log(
services=list(config["log"]["services"]),
loop=loop,
)
hid = Hid( hid = Hid(
reset=int(config["hid"]["pinout"]["reset"]), reset=int(config["hid"]["pinout"]["reset"]),
device_path=str(config["hid"]["device"]), device_path=str(config["hid"]["device"]),
@ -60,6 +66,7 @@ def main() -> None:
) )
Server( Server(
log=log,
hid=hid, hid=hid,
atx=atx, atx=atx,
msd=msd, msd=msd,

View File

@ -1,5 +1,13 @@
import sys import sys
import asyncio
import logging import logging
import time
from typing import List
from typing import Dict
from typing import AsyncGenerator
import systemd.journal
# ===== # =====
@ -13,3 +21,41 @@ def get_logger(depth: int=1) -> logging.Logger:
break break
name = frames[depth].f_globals["__name__"] name = frames[depth].f_globals["__name__"]
return logging.getLogger(name) return logging.getLogger(name)
class Log:
def __init__(
self,
services: List[str],
loop: asyncio.AbstractEventLoop,
) -> None:
self.__services = services
self.__loop = loop
async def log(self, seek: int, follow: bool) -> AsyncGenerator[Dict, None]:
reader = systemd.journal.Reader()
reader.this_boot()
reader.this_machine()
reader.log_level(systemd.journal.LOG_DEBUG)
for service in self.__services:
reader.add_match(_SYSTEMD_UNIT=service)
if seek > 0:
reader.seek_realtime(float(time.time() - seek))
for entry in reader:
yield self.__entry_to_record(entry)
while follow:
entry = reader.get_next()
if entry:
yield self.__entry_to_record(entry)
else:
await asyncio.sleep(1)
def __entry_to_record(self, entry: Dict) -> Dict[str, Dict]:
return {
"dt": entry["__REALTIME_TIMESTAMP"],
"service": entry["_SYSTEMD_UNIT"],
"msg": entry["MESSAGE"].rstrip(),
}

View File

@ -25,6 +25,7 @@ from .msd import MassStorageDevice
from .streamer import Streamer from .streamer import Streamer
from .logging import get_logger from .logging import get_logger
from .logging import Log
# ===== # =====
@ -68,6 +69,28 @@ class BadRequest(Exception):
pass pass
def _valid_bool(name: str, flag: Optional[str]) -> bool:
flag = str(flag).strip().lower()
if flag in ["1", "true", "yes"]:
return True
elif flag in ["0", "false", "no"]:
return False
raise BadRequest("Invalid param '%s'" % (name))
def _valid_int(name: str, value: Optional[str], min_value: Optional[int]=None, max_value: Optional[int]=None) -> int:
try:
value_int = int(value) # type: ignore
if (
(min_value is not None and value_int < min_value)
or (max_value is not None and value_int > max_value)
):
raise ValueError()
return value_int
except Exception:
raise BadRequest("Invalid param %r" % (name))
def _wrap_exceptions_for_web(msg: str) -> Callable: def _wrap_exceptions_for_web(msg: str) -> Callable:
def make_wrapper(method: Callable) -> Callable: def make_wrapper(method: Callable) -> Callable:
async def wrap(self: "Server", request: aiohttp.web.Request) -> aiohttp.web.Response: async def wrap(self: "Server", request: aiohttp.web.Request) -> aiohttp.web.Response:
@ -82,8 +105,9 @@ def _wrap_exceptions_for_web(msg: str) -> Callable:
class Server: # pylint: disable=too-many-instance-attributes class Server: # pylint: disable=too-many-instance-attributes
def __init__( def __init__( # pylint: disable=too-many-arguments
self, self,
log: Log,
hid: Hid, hid: Hid,
atx: Atx, atx: Atx,
msd: MassStorageDevice, msd: MassStorageDevice,
@ -97,6 +121,7 @@ class Server: # pylint: disable=too-many-instance-attributes
loop: asyncio.AbstractEventLoop, loop: asyncio.AbstractEventLoop,
) -> None: ) -> None:
self.__log = log
self.__hid = hid self.__hid = hid
self.__atx = atx self.__atx = atx
self.__msd = msd self.__msd = msd
@ -125,6 +150,7 @@ class Server: # pylint: disable=too-many-instance-attributes
app = aiohttp.web.Application(loop=self.__loop) app = aiohttp.web.Application(loop=self.__loop)
app.router.add_get("/info", self.__info_handler) app.router.add_get("/info", self.__info_handler)
app.router.add_get("/log", self.__log_handler)
app.router.add_get("/ws", self.__ws_handler) app.router.add_get("/ws", self.__ws_handler)
@ -154,7 +180,7 @@ class Server: # pylint: disable=too-many-instance-attributes
aiohttp.web.run_app(app, host=host, port=port, print=self.__run_app_print) aiohttp.web.run_app(app, host=host, port=port, print=self.__run_app_print)
# ===== INFO # ===== SYSTEM
async def __info_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response: async def __info_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
return _json({ return _json({
@ -165,6 +191,20 @@ class Server: # pylint: disable=too-many-instance-attributes
"streamer": self.__streamer.get_app(), "streamer": self.__streamer.get_app(),
}) })
@_wrap_exceptions_for_web("Log error")
async def __log_handler(self, request: aiohttp.web.Request) -> aiohttp.web.StreamResponse:
seek = _valid_int("seek", request.query.get("seek", "0"), 0)
follow = _valid_bool("follow", request.query.get("follow", "false"))
response = aiohttp.web.StreamResponse(status=200, reason="OK", headers={"Content-Type": "text/plain"})
await response.prepare(request)
async for record in self.__log.log(seek, follow):
await response.write(("[%s %s] --- %s" % (
record["dt"].strftime("%Y-%m-%d %H:%M:%S"),
record["service"],
record["msg"],
)).encode("utf-8") + b"\r\n")
return response
# ===== WEBSOCKET # ===== WEBSOCKET
async def __ws_handler(self, request: aiohttp.web.Request) -> aiohttp.web.WebSocketResponse: async def __ws_handler(self, request: aiohttp.web.Request) -> aiohttp.web.WebSocketResponse:
@ -243,7 +283,7 @@ class Server: # pylint: disable=too-many-instance-attributes
"reset": self.__atx.click_reset, "reset": self.__atx.click_reset,
}.get(button) }.get(button)
if not clicker: if not clicker:
raise BadRequest("Missing or invalid 'button=%s'" % (button)) raise BadRequest("Invalid param 'button'")
await self.__broadcast_event("atx_click", button=button) # type: ignore await self.__broadcast_event("atx_click", button=button) # type: ignore
await clicker() await clicker()
await self.__broadcast_event("atx_click", button=None) # type: ignore await self.__broadcast_event("atx_click", button=None) # type: ignore
@ -266,7 +306,7 @@ class Server: # pylint: disable=too-many-instance-attributes
state = self.__msd.get_state() state = self.__msd.get_state()
await self.__broadcast_event("msd_state", **state) await self.__broadcast_event("msd_state", **state)
else: else:
raise BadRequest("Missing or invalid 'to=%s'" % (to)) raise BadRequest("Invalid param 'to'")
return _json(state) return _json(state)
@_wrap_exceptions_for_web("Can't write data to mass-storage device") @_wrap_exceptions_for_web("Can't write data to mass-storage device")
@ -314,13 +354,7 @@ class Server: # pylint: disable=too-many-instance-attributes
async def __streamer_set_params_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response: async def __streamer_set_params_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
quality = request.query.get("quality") quality = request.query.get("quality")
if quality: if quality:
try: self.__streamer_quality = _valid_int("quality", quality, 1, 100)
quality_int = int(quality)
if not (1 <= quality_int <= 100):
raise ValueError()
except Exception:
raise BadRequest("Invalid quality %r" % (quality))
self.__streamer_quality = quality_int
return _json() return _json()
async def __streamer_reset_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response: async def __streamer_reset_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:

View File

@ -5,3 +5,4 @@ pyudev
pyyaml pyyaml
pyserial pyserial
setproctitle setproctitle
systemd-python

View File

@ -36,6 +36,7 @@ RUN pacman -Syy \
&& user-packer -S --noconfirm \ && user-packer -S --noconfirm \
python \ python \
python-pip \ python-pip \
python-systemd \
nginx-mainline \ nginx-mainline \
ustreamer \ ustreamer \
socat \ socat \

View File

@ -4,11 +4,15 @@ kvmd:
port: 8081 port: 8081
heartbeat: 3.0 heartbeat: 3.0
log:
services:
- kvmd.service
hid: hid:
pinout: pinout:
reset: 4 reset: 4
device: /dev/ttyAMA0 device: /dev/ttyS10
speed: 115200 speed: 115200
reset_delay: 0.1 reset_delay: 0.1

View File

@ -5,5 +5,6 @@ pyudev
pyyaml pyyaml
pyserial pyserial
setproctitle setproctitle
systemd-python
bumpversion bumpversion
tox tox

View File

@ -112,6 +112,10 @@
<button disabled data-force-hide-menu id="hid-reset-button">&bull; Reset keyboard &amp; mouse</button> <button disabled data-force-hide-menu id="hid-reset-button">&bull; Reset keyboard &amp; mouse</button>
<button disabled data-force-hide-menu id="msd-reset-button">&bull; Reset mass storage</button> <button disabled data-force-hide-menu id="msd-reset-button">&bull; Reset mass storage</button>
</div> </div>
<hr>
<div class="ctl-dropdown-content-buttons">
<button data-force-hide-menu onclick="window.open('kvmd/log?seek=3600&follow=1', '_blank');">&bull; View log</button>
</div>
</div> </div>
</div> </div>
</li> </li>