mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-12 01:00:29 +08:00
log interface
This commit is contained in:
parent
1f54776ce0
commit
ab342111d0
1
PKGBUILD
1
PKGBUILD
@ -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")
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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(),
|
||||||
|
}
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -5,3 +5,4 @@ pyudev
|
|||||||
pyyaml
|
pyyaml
|
||||||
pyserial
|
pyserial
|
||||||
setproctitle
|
setproctitle
|
||||||
|
systemd-python
|
||||||
|
|||||||
@ -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 \
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -5,5 +5,6 @@ pyudev
|
|||||||
pyyaml
|
pyyaml
|
||||||
pyserial
|
pyserial
|
||||||
setproctitle
|
setproctitle
|
||||||
|
systemd-python
|
||||||
bumpversion
|
bumpversion
|
||||||
tox
|
tox
|
||||||
|
|||||||
@ -112,6 +112,10 @@
|
|||||||
<button disabled data-force-hide-menu id="hid-reset-button">• Reset keyboard & mouse</button>
|
<button disabled data-force-hide-menu id="hid-reset-button">• Reset keyboard & mouse</button>
|
||||||
<button disabled data-force-hide-menu id="msd-reset-button">• Reset mass storage</button>
|
<button disabled data-force-hide-menu id="msd-reset-button">• 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');">• View log</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user