mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-30 17:41:54 +08:00
重构更新
This commit is contained in:
BIN
patches/Boot_SkipUSBBurning.gz
Normal file
BIN
patches/Boot_SkipUSBBurning.gz
Normal file
Binary file not shown.
769
patches/__init__.py
Normal file
769
patches/__init__.py
Normal file
@@ -0,0 +1,769 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
import functools
|
||||
import argparse
|
||||
import logging
|
||||
import logging.config
|
||||
|
||||
import pygments
|
||||
import pygments.lexers.data
|
||||
import pygments.formatters
|
||||
|
||||
from .. import tools
|
||||
|
||||
from ..mouse import MouseRange
|
||||
|
||||
from ..plugins import UnknownPluginError
|
||||
from ..plugins.auth import get_auth_service_class
|
||||
from ..plugins.hid import get_hid_class
|
||||
from ..plugins.atx import get_atx_class
|
||||
from ..plugins.msd import get_msd_class
|
||||
|
||||
from ..plugins.ugpio import UserGpioModes
|
||||
from ..plugins.ugpio import BaseUserGpioDriver
|
||||
from ..plugins.ugpio import get_ugpio_driver_class
|
||||
|
||||
from ..yamlconf import ConfigError
|
||||
from ..yamlconf import manual_validated
|
||||
from ..yamlconf import make_config
|
||||
from ..yamlconf import Section
|
||||
from ..yamlconf import Option
|
||||
from ..yamlconf import build_raw_from_options
|
||||
from ..yamlconf.dumper import make_config_dump
|
||||
from ..yamlconf.loader import load_yaml_file
|
||||
from ..yamlconf.merger import yaml_merge
|
||||
|
||||
from ..validators.basic import valid_stripped_string
|
||||
from ..validators.basic import valid_stripped_string_not_empty
|
||||
from ..validators.basic import valid_bool
|
||||
from ..validators.basic import valid_number
|
||||
from ..validators.basic import valid_int_f0
|
||||
from ..validators.basic import valid_int_f1
|
||||
from ..validators.basic import valid_float_f0
|
||||
from ..validators.basic import valid_float_f01
|
||||
from ..validators.basic import valid_string_list
|
||||
|
||||
from ..validators.auth import valid_user
|
||||
from ..validators.auth import valid_users_list
|
||||
|
||||
from ..validators.os import valid_abs_path
|
||||
from ..validators.os import valid_abs_file
|
||||
from ..validators.os import valid_abs_dir
|
||||
from ..validators.os import valid_unix_mode
|
||||
from ..validators.os import valid_options
|
||||
from ..validators.os import valid_command
|
||||
|
||||
from ..validators.net import valid_ip_or_host
|
||||
from ..validators.net import valid_net
|
||||
from ..validators.net import valid_port
|
||||
from ..validators.net import valid_ports_list
|
||||
from ..validators.net import valid_mac
|
||||
from ..validators.net import valid_ssl_ciphers
|
||||
|
||||
from ..validators.hid import valid_hid_key
|
||||
from ..validators.hid import valid_hid_mouse_output
|
||||
from ..validators.hid import valid_hid_mouse_move
|
||||
|
||||
from ..validators.kvm import valid_stream_quality
|
||||
from ..validators.kvm import valid_stream_fps
|
||||
from ..validators.kvm import valid_stream_resolution
|
||||
from ..validators.kvm import valid_stream_h264_bitrate
|
||||
from ..validators.kvm import valid_stream_h264_gop
|
||||
|
||||
from ..validators.ugpio import valid_ugpio_driver
|
||||
from ..validators.ugpio import valid_ugpio_channel
|
||||
from ..validators.ugpio import valid_ugpio_mode
|
||||
from ..validators.ugpio import valid_ugpio_view_title
|
||||
from ..validators.ugpio import valid_ugpio_view_table
|
||||
|
||||
from ..validators.hw import valid_tty_speed
|
||||
from ..validators.hw import valid_otg_gadget
|
||||
from ..validators.hw import valid_otg_id
|
||||
from ..validators.hw import valid_otg_ethernet
|
||||
|
||||
|
||||
# =====
|
||||
def init(
|
||||
prog: (str | None)=None,
|
||||
description: (str | None)=None,
|
||||
add_help: bool=True,
|
||||
check_run: bool=False,
|
||||
cli_logging: bool=False,
|
||||
argv: (list[str] | None)=None,
|
||||
**load: bool,
|
||||
) -> tuple[argparse.ArgumentParser, list[str], Section]:
|
||||
|
||||
argv = (argv or sys.argv)
|
||||
assert len(argv) > 0
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog=(prog or argv[0]),
|
||||
description=description,
|
||||
add_help=add_help,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument("-c", "--config", default="/etc/kvmd/main.yaml", type=valid_abs_file,
|
||||
help="Set config file path", metavar="<file>")
|
||||
parser.add_argument("-o", "--set-options", default=[], nargs="+",
|
||||
help="Override config options list (like sec/sub/opt=value)", metavar="<k=v>",)
|
||||
parser.add_argument("-m", "--dump-config", action="store_true",
|
||||
help="View current configuration (include all overrides)")
|
||||
if check_run:
|
||||
parser.add_argument("--run", dest="run", action="store_true",
|
||||
help="Run the service")
|
||||
(options, remaining) = parser.parse_known_args(argv)
|
||||
|
||||
if options.dump_config:
|
||||
_dump_config(_init_config(
|
||||
config_path=options.config,
|
||||
override_options=options.set_options,
|
||||
load_auth=True,
|
||||
load_hid=True,
|
||||
load_atx=True,
|
||||
load_msd=True,
|
||||
load_gpio=True,
|
||||
))
|
||||
raise SystemExit()
|
||||
config = _init_config(options.config, options.set_options, **load)
|
||||
|
||||
logging.captureWarnings(True)
|
||||
logging.config.dictConfig(config.logging)
|
||||
if cli_logging:
|
||||
logging.getLogger().handlers[0].setFormatter(logging.Formatter(
|
||||
"-- {levelname:>7} -- {message}",
|
||||
style="{",
|
||||
))
|
||||
|
||||
if check_run and not options.run:
|
||||
raise SystemExit(
|
||||
"To prevent accidental startup, you must specify the --run option to start.\n"
|
||||
"Try the --help option to find out what this service does.\n"
|
||||
"Make sure you understand exactly what you are doing!"
|
||||
)
|
||||
|
||||
return (parser, remaining, config)
|
||||
|
||||
|
||||
# =====
|
||||
def _init_config(config_path: str, override_options: list[str], **load_flags: bool) -> Section:
|
||||
config_path = os.path.expanduser(config_path)
|
||||
try:
|
||||
raw_config: dict = load_yaml_file(config_path)
|
||||
except Exception as err:
|
||||
raise SystemExit(f"ConfigError: Can't read config file {config_path!r}:\n{tools.efmt(err)}")
|
||||
if not isinstance(raw_config, dict):
|
||||
raise SystemExit(f"ConfigError: Top-level of the file {config_path!r} must be a dictionary")
|
||||
|
||||
scheme = _get_config_scheme()
|
||||
try:
|
||||
yaml_merge(raw_config, (raw_config.pop("override", {}) or {}))
|
||||
yaml_merge(raw_config, build_raw_from_options(override_options), "raw CLI options")
|
||||
_patch_raw(raw_config)
|
||||
config = make_config(raw_config, scheme)
|
||||
|
||||
if _patch_dynamic(raw_config, config, scheme, **load_flags):
|
||||
config = make_config(raw_config, scheme)
|
||||
|
||||
return config
|
||||
except (ConfigError, UnknownPluginError) as err:
|
||||
raise SystemExit(f"ConfigError: {err}")
|
||||
|
||||
|
||||
def _patch_raw(raw_config: dict) -> None: # pylint: disable=too-many-branches
|
||||
if isinstance(raw_config.get("otg"), dict):
|
||||
for (old, new) in [
|
||||
("msd", "msd"),
|
||||
("acm", "serial"),
|
||||
("drives", "drives"),
|
||||
]:
|
||||
if old in raw_config["otg"]:
|
||||
if not isinstance(raw_config["otg"].get("devices"), dict):
|
||||
raw_config["otg"]["devices"] = {}
|
||||
raw_config["otg"]["devices"][new] = raw_config["otg"].pop(old)
|
||||
|
||||
if isinstance(raw_config.get("kvmd"), dict) and isinstance(raw_config["kvmd"].get("wol"), dict):
|
||||
if not isinstance(raw_config["kvmd"].get("gpio"), dict):
|
||||
raw_config["kvmd"]["gpio"] = {}
|
||||
for section in ["drivers", "scheme"]:
|
||||
if not isinstance(raw_config["kvmd"]["gpio"].get(section), dict):
|
||||
raw_config["kvmd"]["gpio"][section] = {}
|
||||
raw_config["kvmd"]["gpio"]["drivers"]["__wol__"] = {
|
||||
"type": "wol",
|
||||
**raw_config["kvmd"].pop("wol"),
|
||||
}
|
||||
raw_config["kvmd"]["gpio"]["scheme"]["__wol__"] = {
|
||||
"driver": "__wol__",
|
||||
"pin": 0,
|
||||
"mode": "output",
|
||||
"switch": False,
|
||||
}
|
||||
|
||||
if isinstance(raw_config.get("kvmd"), dict) and isinstance(raw_config["kvmd"].get("streamer"), dict):
|
||||
streamer_config = raw_config["kvmd"]["streamer"]
|
||||
|
||||
desired_fps = streamer_config.get("desired_fps")
|
||||
if desired_fps is not None and not isinstance(desired_fps, dict):
|
||||
streamer_config["desired_fps"] = {"default": desired_fps}
|
||||
|
||||
max_fps = streamer_config.get("max_fps")
|
||||
if max_fps is not None:
|
||||
if not isinstance(streamer_config.get("desired_fps"), dict):
|
||||
streamer_config["desired_fps"] = {}
|
||||
streamer_config["desired_fps"]["max"] = max_fps
|
||||
del streamer_config["max_fps"]
|
||||
|
||||
resolution = streamer_config.get("resolution")
|
||||
if resolution is not None and not isinstance(resolution, dict):
|
||||
streamer_config["resolution"] = {"default": resolution}
|
||||
|
||||
available_resolutions = streamer_config.get("available_resolutions")
|
||||
if available_resolutions is not None:
|
||||
if not isinstance(streamer_config.get("resolution"), dict):
|
||||
streamer_config["resolution"] = {}
|
||||
streamer_config["resolution"]["available"] = available_resolutions
|
||||
del streamer_config["available_resolutions"]
|
||||
|
||||
|
||||
def _patch_dynamic( # pylint: disable=too-many-locals
|
||||
raw_config: dict,
|
||||
config: Section,
|
||||
scheme: dict,
|
||||
load_auth: bool=False,
|
||||
load_hid: bool=False,
|
||||
load_atx: bool=False,
|
||||
load_msd: bool=False,
|
||||
load_gpio: bool=False,
|
||||
) -> bool:
|
||||
|
||||
rebuild = False
|
||||
|
||||
if load_auth:
|
||||
scheme["kvmd"]["auth"]["internal"].update(get_auth_service_class(config.kvmd.auth.internal.type).get_plugin_options())
|
||||
if config.kvmd.auth.external.type:
|
||||
scheme["kvmd"]["auth"]["external"].update(get_auth_service_class(config.kvmd.auth.external.type).get_plugin_options())
|
||||
rebuild = True
|
||||
|
||||
for (load, section, get_class) in [
|
||||
(load_hid, "hid", get_hid_class),
|
||||
(load_atx, "atx", get_atx_class),
|
||||
(load_msd, "msd", get_msd_class),
|
||||
]:
|
||||
if load:
|
||||
scheme["kvmd"][section].update(get_class(getattr(config.kvmd, section).type).get_plugin_options())
|
||||
rebuild = True
|
||||
|
||||
if load_gpio:
|
||||
driver: str
|
||||
drivers: dict[str, type[BaseUserGpioDriver]] = {} # Name to drivers
|
||||
for (driver, params) in { # type: ignore
|
||||
"__gpio__": {},
|
||||
**tools.rget(raw_config, "kvmd", "gpio", "drivers"),
|
||||
}.items():
|
||||
with manual_validated(driver, "kvmd", "gpio", "drivers", "<key>"):
|
||||
driver = valid_ugpio_driver(driver)
|
||||
|
||||
driver_type = valid_stripped_string_not_empty(params.get("type", "gpio"))
|
||||
driver_class = get_ugpio_driver_class(driver_type)
|
||||
drivers[driver] = driver_class
|
||||
scheme["kvmd"]["gpio"]["drivers"][driver] = {
|
||||
"type": Option(driver_type, type=valid_stripped_string_not_empty),
|
||||
**driver_class.get_plugin_options()
|
||||
}
|
||||
|
||||
path = ("kvmd", "gpio", "scheme")
|
||||
for (channel, params) in tools.rget(raw_config, *path).items():
|
||||
with manual_validated(channel, *path, "<key>"):
|
||||
channel = valid_ugpio_channel(channel)
|
||||
|
||||
driver = params.get("driver", "__gpio__")
|
||||
with manual_validated(driver, *path, channel, "driver"):
|
||||
driver = valid_ugpio_driver(driver, set(drivers))
|
||||
|
||||
mode: str = params.get("mode", "")
|
||||
with manual_validated(mode, *path, channel, "mode"):
|
||||
mode = valid_ugpio_mode(mode, drivers[driver].get_modes())
|
||||
|
||||
if params.get("pulse") == False: # noqa: E712 # pylint: disable=singleton-comparison
|
||||
params["pulse"] = {"delay": 0}
|
||||
|
||||
scheme["kvmd"]["gpio"]["scheme"][channel] = {
|
||||
"driver": Option("__gpio__", type=functools.partial(valid_ugpio_driver, variants=set(drivers))),
|
||||
"pin": Option(None, type=drivers[driver].get_pin_validator()),
|
||||
"mode": Option("", type=functools.partial(valid_ugpio_mode, variants=drivers[driver].get_modes())),
|
||||
"inverted": Option(False, type=valid_bool),
|
||||
**({
|
||||
"busy_delay": Option(0.2, type=valid_float_f01),
|
||||
"initial": Option(False, type=(lambda arg: (valid_bool(arg) if arg is not None else None))),
|
||||
"switch": Option(True, type=valid_bool),
|
||||
"pulse": { # type: ignore
|
||||
"delay": Option(0.1, type=valid_float_f0),
|
||||
"min_delay": Option(0.1, type=valid_float_f01),
|
||||
"max_delay": Option(0.1, type=valid_float_f01),
|
||||
},
|
||||
} if mode == UserGpioModes.OUTPUT else { # input
|
||||
"debounce": Option(0.1, type=valid_float_f0),
|
||||
})
|
||||
}
|
||||
|
||||
rebuild = True
|
||||
|
||||
return rebuild
|
||||
|
||||
|
||||
def _dump_config(config: Section) -> None:
|
||||
dump = make_config_dump(config)
|
||||
if sys.stdout.isatty():
|
||||
dump = pygments.highlight(
|
||||
dump,
|
||||
pygments.lexers.data.YamlLexer(),
|
||||
pygments.formatters.TerminalFormatter(bg="dark"), # pylint: disable=no-member
|
||||
)
|
||||
print(dump)
|
||||
|
||||
|
||||
def _get_config_scheme() -> dict:
|
||||
return {
|
||||
"logging": Option({}),
|
||||
|
||||
"kvmd": {
|
||||
"server": {
|
||||
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||
"unix_rm": Option(True, type=valid_bool),
|
||||
"unix_mode": Option(0o660, type=valid_unix_mode),
|
||||
"heartbeat": Option(15.0, type=valid_float_f01),
|
||||
"access_log_format": Option("[%P / %{X-Real-IP}i] '%r' => %s; size=%b ---"
|
||||
" referer='%{Referer}i'; user_agent='%{User-Agent}i'"),
|
||||
},
|
||||
|
||||
"auth": {
|
||||
"enabled": Option(True, type=valid_bool),
|
||||
|
||||
"internal": {
|
||||
"type": Option("htpasswd"),
|
||||
"force_users": Option([], type=valid_users_list),
|
||||
# Dynamic content
|
||||
},
|
||||
|
||||
"external": {
|
||||
"type": Option("", type=valid_stripped_string),
|
||||
# Dynamic content
|
||||
},
|
||||
|
||||
"totp": {
|
||||
"secret": {
|
||||
"file": Option("/etc/kvmd/totp.secret", type=valid_abs_path, if_empty=""),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"info": { # Accessed via global config, see kvmd/info for details
|
||||
"meta": Option("/etc/kvmd/meta.yaml", type=valid_abs_file),
|
||||
"extras": Option("/usr/share/kvmd/extras", type=valid_abs_dir),
|
||||
"hw": {
|
||||
"vcgencmd_cmd": Option(["/opt/vc/bin/vcgencmd"], type=valid_command),
|
||||
"ignore_past": Option(False, type=valid_bool),
|
||||
"state_poll": Option(10.0, type=valid_float_f01),
|
||||
},
|
||||
"fan": {
|
||||
"daemon": Option("kvmd-fan", type=valid_stripped_string),
|
||||
"unix": Option("", type=valid_abs_path, if_empty="", unpack_as="unix_path"),
|
||||
"timeout": Option(5.0, type=valid_float_f01),
|
||||
"state_poll": Option(5.0, type=valid_float_f01),
|
||||
},
|
||||
},
|
||||
|
||||
"log_reader": {
|
||||
"enabled": Option(True, type=valid_bool),
|
||||
},
|
||||
|
||||
"prometheus": {
|
||||
"auth": {
|
||||
"enabled": Option(True, type=valid_bool),
|
||||
},
|
||||
},
|
||||
|
||||
"hid": {
|
||||
"type": Option("", type=valid_stripped_string_not_empty),
|
||||
|
||||
"keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file),
|
||||
"ignore_keys": Option([], type=functools.partial(valid_string_list, subval=valid_hid_key)),
|
||||
|
||||
"mouse_x_range": {
|
||||
"min": Option(MouseRange.MIN, type=valid_hid_mouse_move),
|
||||
"max": Option(MouseRange.MAX, type=valid_hid_mouse_move),
|
||||
},
|
||||
"mouse_y_range": {
|
||||
"min": Option(MouseRange.MIN, type=valid_hid_mouse_move),
|
||||
"max": Option(MouseRange.MAX, type=valid_hid_mouse_move),
|
||||
},
|
||||
|
||||
# Dynamic content
|
||||
},
|
||||
|
||||
"atx": {
|
||||
"type": Option("", type=valid_stripped_string_not_empty),
|
||||
# Dynamic content
|
||||
},
|
||||
|
||||
"msd": {
|
||||
"type": Option("", type=valid_stripped_string_not_empty),
|
||||
# Dynamic content
|
||||
},
|
||||
|
||||
"streamer": {
|
||||
"forever": Option(False, type=valid_bool),
|
||||
|
||||
"reset_delay": Option(1.0, type=valid_float_f0),
|
||||
"shutdown_delay": Option(10.0, type=valid_float_f01),
|
||||
"state_poll": Option(1.0, type=valid_float_f01),
|
||||
|
||||
"quality": Option(80, type=valid_stream_quality, if_empty=0),
|
||||
|
||||
"resolution": {
|
||||
"default": Option("", type=valid_stream_resolution, if_empty="", unpack_as="resolution"),
|
||||
"available": Option(
|
||||
[],
|
||||
type=functools.partial(valid_string_list, subval=valid_stream_resolution),
|
||||
unpack_as="available_resolutions",
|
||||
),
|
||||
},
|
||||
|
||||
"desired_fps": {
|
||||
"default": Option(40, type=valid_stream_fps, unpack_as="desired_fps"),
|
||||
"min": Option(0, type=valid_stream_fps, unpack_as="desired_fps_min"),
|
||||
"max": Option(70, type=valid_stream_fps, unpack_as="desired_fps_max"),
|
||||
},
|
||||
|
||||
"h264_bitrate": {
|
||||
"default": Option(0, type=valid_stream_h264_bitrate, if_empty=0, unpack_as="h264_bitrate"),
|
||||
"min": Option(25, type=valid_stream_h264_bitrate, unpack_as="h264_bitrate_min"),
|
||||
"max": Option(20000, type=valid_stream_h264_bitrate, unpack_as="h264_bitrate_max"),
|
||||
},
|
||||
|
||||
"h264_gop": {
|
||||
"default": Option(30, type=valid_stream_h264_gop, unpack_as="h264_gop"),
|
||||
"min": Option(0, type=valid_stream_h264_gop, unpack_as="h264_gop_min"),
|
||||
"max": Option(60, type=valid_stream_h264_gop, unpack_as="h264_gop_max"),
|
||||
},
|
||||
|
||||
"unix": Option("/run/kvmd/ustreamer.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||
"timeout": Option(2.0, type=valid_float_f01),
|
||||
|
||||
"process_name_prefix": Option("kvmd/streamer"),
|
||||
|
||||
"cmd": Option(["/bin/true"], type=valid_command),
|
||||
"cmd_remove": Option([], type=valid_options),
|
||||
"cmd_append": Option([], type=valid_options),
|
||||
},
|
||||
|
||||
"ocr": {
|
||||
"langs": Option(["eng"], type=valid_string_list, unpack_as="default_langs"),
|
||||
"tessdata": Option("/usr/share/tessdata", type=valid_stripped_string_not_empty, unpack_as="data_dir_path")
|
||||
},
|
||||
|
||||
"snapshot": {
|
||||
"idle_interval": Option(0.0, type=valid_float_f0),
|
||||
"live_interval": Option(0.0, type=valid_float_f0),
|
||||
|
||||
"wakeup_key": Option("", type=valid_hid_key, if_empty=""),
|
||||
"wakeup_move": Option(0, type=valid_hid_mouse_move),
|
||||
|
||||
"online_delay": Option(5.0, type=valid_float_f0),
|
||||
"retries": Option(10, type=valid_int_f1),
|
||||
"retries_delay": Option(3.0, type=valid_float_f01),
|
||||
},
|
||||
|
||||
"gpio": {
|
||||
"state_poll": Option(0.1, type=valid_float_f01),
|
||||
"drivers": {}, # Dynamic content
|
||||
"scheme": {}, # Dymanic content
|
||||
"view": {
|
||||
"header": {
|
||||
"title": Option("GPIO", type=valid_ugpio_view_title),
|
||||
},
|
||||
"table": Option([], type=valid_ugpio_view_table),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"pst": {
|
||||
"server": {
|
||||
"unix": Option("/run/kvmd/pst.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||
"unix_rm": Option(True, type=valid_bool),
|
||||
"unix_mode": Option(0o660, type=valid_unix_mode),
|
||||
"heartbeat": Option(15.0, type=valid_float_f01),
|
||||
"access_log_format": Option("[%P / %{X-Real-IP}i] '%r' => %s; size=%b ---"
|
||||
" referer='%{Referer}i'; user_agent='%{User-Agent}i'"),
|
||||
},
|
||||
|
||||
"ro_retries_delay": Option(10.0, type=valid_float_f01),
|
||||
"ro_cleanup_delay": Option(3.0, type=valid_float_f01),
|
||||
|
||||
"remount_cmd": Option([
|
||||
"/usr/bin/sudo", "--non-interactive",
|
||||
"/usr/bin/kvmd-helper-pst-remount", "{mode}",
|
||||
], type=valid_command),
|
||||
},
|
||||
|
||||
"otg": {
|
||||
"vendor_id": Option(0x1D6B, type=valid_otg_id), # Linux Foundation
|
||||
"product_id": Option(0x0104, type=valid_otg_id), # Multifunction Composite Gadget
|
||||
"manufacturer": Option("PiKVM", type=valid_stripped_string),
|
||||
"product": Option("Composite KVM Device", type=valid_stripped_string),
|
||||
"serial": Option("CAFEBABE", type=valid_stripped_string, if_none=None),
|
||||
"device_version": Option(-1, type=functools.partial(valid_number, min=-1, max=0xFFFF)),
|
||||
"usb_version": Option(0x0200, type=valid_otg_id),
|
||||
"max_power": Option(250, type=functools.partial(valid_number, min=50, max=500)),
|
||||
"remote_wakeup": Option(False, type=valid_bool),
|
||||
|
||||
"gadget": Option("kvmd", type=valid_otg_gadget),
|
||||
"config": Option("PiKVM device", type=valid_stripped_string_not_empty),
|
||||
"udc": Option("", type=valid_stripped_string),
|
||||
"init_delay": Option(3.0, type=valid_float_f01),
|
||||
|
||||
"user": Option("kvmd", type=valid_user),
|
||||
"meta": Option("/run/kvmd/otg", type=valid_abs_path),
|
||||
|
||||
"devices": {
|
||||
"hid": {
|
||||
"keyboard": {
|
||||
"start": Option(True, type=valid_bool),
|
||||
},
|
||||
"mouse": {
|
||||
"start": Option(True, type=valid_bool),
|
||||
},
|
||||
},
|
||||
|
||||
"msd": {
|
||||
"start": Option(True, type=valid_bool),
|
||||
"default": {
|
||||
"stall": Option(False, type=valid_bool),
|
||||
"cdrom": Option(True, type=valid_bool),
|
||||
"rw": Option(False, type=valid_bool),
|
||||
"removable": Option(True, type=valid_bool),
|
||||
"fua": Option(True, type=valid_bool),
|
||||
},
|
||||
},
|
||||
|
||||
"serial": {
|
||||
"enabled": Option(False, type=valid_bool),
|
||||
"start": Option(True, type=valid_bool),
|
||||
},
|
||||
|
||||
"ethernet": {
|
||||
"enabled": Option(False, type=valid_bool),
|
||||
"start": Option(True, type=valid_bool),
|
||||
"driver": Option("ecm", type=valid_otg_ethernet),
|
||||
"host_mac": Option("", type=valid_mac, if_empty=""),
|
||||
"kvm_mac": Option("", type=valid_mac, if_empty=""),
|
||||
},
|
||||
|
||||
"drives": {
|
||||
"enabled": Option(False, type=valid_bool),
|
||||
"start": Option(True, type=valid_bool),
|
||||
"count": Option(1, type=valid_int_f1),
|
||||
"default": {
|
||||
"stall": Option(False, type=valid_bool),
|
||||
"cdrom": Option(False, type=valid_bool),
|
||||
"rw": Option(True, type=valid_bool),
|
||||
"removable": Option(True, type=valid_bool),
|
||||
"fua": Option(True, type=valid_bool),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"otgnet": {
|
||||
"iface": {
|
||||
"net": Option("172.30.30.0/24", type=functools.partial(valid_net, v6=False)),
|
||||
"ip_cmd": Option(["/usr/bin/ip"], type=valid_command),
|
||||
},
|
||||
|
||||
"firewall": {
|
||||
"allow_icmp": Option(True, type=valid_bool),
|
||||
"allow_tcp": Option([], type=valid_ports_list),
|
||||
"allow_udp": Option([67], type=valid_ports_list),
|
||||
"forward_iface": Option("", type=valid_stripped_string),
|
||||
"iptables_cmd": Option(["/usr/sbin/iptables", "--wait=5"], type=valid_command),
|
||||
},
|
||||
|
||||
"commands": {
|
||||
"pre_start_cmd": Option(["/bin/true", "pre-start"], type=valid_command),
|
||||
"pre_start_cmd_remove": Option([], type=valid_options),
|
||||
"pre_start_cmd_append": Option([], type=valid_options),
|
||||
|
||||
"post_start_cmd": Option([
|
||||
"/usr/bin/systemd-run",
|
||||
"--unit=kvmd-otgnet-dnsmasq",
|
||||
"/usr/sbin/dnsmasq",
|
||||
"--conf-file=/dev/null",
|
||||
"--pid-file",
|
||||
"--user=dnsmasq",
|
||||
"--interface={iface}",
|
||||
"--port=0",
|
||||
"--dhcp-range={dhcp_ip_begin},{dhcp_ip_end},24h",
|
||||
"--dhcp-leasefile=/run/kvmd/dnsmasq.lease",
|
||||
"--dhcp-option={dhcp_option_3}",
|
||||
"--dhcp-option=6",
|
||||
"--keep-in-foreground",
|
||||
], type=valid_command),
|
||||
"post_start_cmd_remove": Option([], type=valid_options),
|
||||
"post_start_cmd_append": Option([], type=valid_options),
|
||||
|
||||
"pre_stop_cmd": Option([
|
||||
"/usr/bin/systemctl",
|
||||
"stop",
|
||||
"kvmd-otgnet-dnsmasq",
|
||||
], type=valid_command),
|
||||
"pre_stop_cmd_remove": Option([], type=valid_options),
|
||||
"pre_stop_cmd_append": Option([], type=valid_options),
|
||||
|
||||
"post_stop_cmd": Option(["/bin/true", "post-stop"], type=valid_command),
|
||||
"post_stop_cmd_remove": Option([], type=valid_options),
|
||||
"post_stop_cmd_append": Option([], type=valid_options),
|
||||
},
|
||||
},
|
||||
|
||||
"ipmi": {
|
||||
"server": {
|
||||
"host": Option("::", type=valid_ip_or_host),
|
||||
"port": Option(623, type=valid_port),
|
||||
"timeout": Option(10.0, type=valid_float_f01),
|
||||
},
|
||||
|
||||
"kvmd": {
|
||||
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||
"timeout": Option(5.0, type=valid_float_f01),
|
||||
},
|
||||
|
||||
"auth": {
|
||||
"file": Option("/etc/kvmd/ipmipasswd", type=valid_abs_file, unpack_as="path"),
|
||||
},
|
||||
|
||||
"sol": {
|
||||
"device": Option("", type=valid_abs_path, if_empty="", unpack_as="sol_device_path"),
|
||||
"speed": Option(115200, type=valid_tty_speed, unpack_as="sol_speed"),
|
||||
"select_timeout": Option(0.1, type=valid_float_f01, unpack_as="sol_select_timeout"),
|
||||
"proxy_port": Option(0, type=valid_port, unpack_as="sol_proxy_port"),
|
||||
},
|
||||
},
|
||||
|
||||
"vnc": {
|
||||
"desired_fps": Option(30, type=valid_stream_fps),
|
||||
"mouse_output": Option("usb", type=valid_hid_mouse_output),
|
||||
"keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file),
|
||||
|
||||
"server": {
|
||||
"host": Option("::", type=valid_ip_or_host),
|
||||
"port": Option(5900, type=valid_port),
|
||||
"max_clients": Option(10, type=valid_int_f1),
|
||||
|
||||
"no_delay": Option(True, type=valid_bool),
|
||||
"keepalive": {
|
||||
"enabled": Option(True, type=valid_bool, unpack_as="keepalive_enabled"),
|
||||
"idle": Option(10, type=functools.partial(valid_number, min=1, max=3600), unpack_as="keepalive_idle"),
|
||||
"interval": Option(3, type=functools.partial(valid_number, min=1, max=60), unpack_as="keepalive_interval"),
|
||||
"count": Option(3, type=functools.partial(valid_number, min=1, max=10), unpack_as="keepalive_count"),
|
||||
},
|
||||
|
||||
"tls": {
|
||||
"ciphers": Option("ALL:@SECLEVEL=0", type=valid_ssl_ciphers, if_empty=""),
|
||||
"timeout": Option(30.0, type=valid_float_f01),
|
||||
"x509": {
|
||||
"cert": Option("/etc/kvmd/vnc/ssl/server.crt", type=valid_abs_file, if_empty=""),
|
||||
"key": Option("/etc/kvmd/vnc/ssl/server.key", type=valid_abs_file, if_empty=""),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"kvmd": {
|
||||
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||
"timeout": Option(5.0, type=valid_float_f01),
|
||||
},
|
||||
|
||||
"streamer": {
|
||||
"unix": Option("/run/kvmd/ustreamer.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||
"timeout": Option(5.0, type=valid_float_f01),
|
||||
},
|
||||
|
||||
"memsink": {
|
||||
"jpeg": {
|
||||
"sink": Option("", unpack_as="obj"),
|
||||
"lock_timeout": Option(1.0, type=valid_float_f01),
|
||||
"wait_timeout": Option(1.0, type=valid_float_f01),
|
||||
"drop_same_frames": Option(1.0, type=valid_float_f0),
|
||||
},
|
||||
"h264": {
|
||||
"sink": Option("", unpack_as="obj"),
|
||||
"lock_timeout": Option(1.0, type=valid_float_f01),
|
||||
"wait_timeout": Option(1.0, type=valid_float_f01),
|
||||
"drop_same_frames": Option(0.0, type=valid_float_f0),
|
||||
},
|
||||
},
|
||||
|
||||
"auth": {
|
||||
"vncauth": {
|
||||
"enabled": Option(False, type=valid_bool),
|
||||
"file": Option("/etc/kvmd/vncpasswd", type=valid_abs_file, unpack_as="path"),
|
||||
},
|
||||
"vencrypt": {
|
||||
"enabled": Option(True, type=valid_bool, unpack_as="vencrypt_enabled"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"janus": {
|
||||
"stun": {
|
||||
"host": Option("stun.l.google.com", type=valid_ip_or_host, unpack_as="stun_host"),
|
||||
"port": Option(19302, type=valid_port, unpack_as="stun_port"),
|
||||
"timeout": Option(5.0, type=valid_float_f01, unpack_as="stun_timeout"),
|
||||
"retries": Option(5, type=valid_int_f1, unpack_as="stun_retries"),
|
||||
"retries_delay": Option(5.0, type=valid_float_f01, unpack_as="stun_retries_delay"),
|
||||
},
|
||||
|
||||
"check": {
|
||||
"interval": Option(10.0, type=valid_float_f01, unpack_as="check_interval"),
|
||||
"retries": Option(5, type=valid_int_f1, unpack_as="check_retries"),
|
||||
"retries_delay": Option(5.0, type=valid_float_f01, unpack_as="check_retries_delay"),
|
||||
},
|
||||
|
||||
"cmd": Option([
|
||||
"/usr/bin/janus",
|
||||
"--disable-colors",
|
||||
"--plugins-folder=/usr/lib/ustreamer/janus",
|
||||
"--configs-folder=/etc/kvmd/janus",
|
||||
"--interface={src_ip}",
|
||||
"{o_stun_server}",
|
||||
], type=valid_command),
|
||||
"cmd_remove": Option([], type=valid_options),
|
||||
"cmd_append": Option([], type=valid_options),
|
||||
},
|
||||
|
||||
"watchdog": {
|
||||
"rtc": Option(0, type=valid_int_f0),
|
||||
"timeout": Option(300, type=valid_int_f1),
|
||||
"interval": Option(30, type=valid_int_f1),
|
||||
},
|
||||
}
|
||||
777
patches/__init__.py.2
Normal file
777
patches/__init__.py.2
Normal file
@@ -0,0 +1,777 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
import functools
|
||||
import argparse
|
||||
import logging
|
||||
import logging.config
|
||||
|
||||
import pygments
|
||||
import pygments.lexers.data
|
||||
import pygments.formatters
|
||||
|
||||
from .. import tools
|
||||
|
||||
from ..mouse import MouseRange
|
||||
|
||||
from ..plugins import UnknownPluginError
|
||||
from ..plugins.auth import get_auth_service_class
|
||||
from ..plugins.hid import get_hid_class
|
||||
from ..plugins.atx import get_atx_class
|
||||
from ..plugins.msd import get_msd_class
|
||||
|
||||
from ..plugins.ugpio import UserGpioModes
|
||||
from ..plugins.ugpio import BaseUserGpioDriver
|
||||
from ..plugins.ugpio import get_ugpio_driver_class
|
||||
|
||||
from ..yamlconf import ConfigError
|
||||
from ..yamlconf import manual_validated
|
||||
from ..yamlconf import make_config
|
||||
from ..yamlconf import Section
|
||||
from ..yamlconf import Option
|
||||
from ..yamlconf import build_raw_from_options
|
||||
from ..yamlconf.dumper import make_config_dump
|
||||
from ..yamlconf.loader import load_yaml_file
|
||||
from ..yamlconf.merger import yaml_merge
|
||||
|
||||
from ..validators.basic import valid_stripped_string
|
||||
from ..validators.basic import valid_stripped_string_not_empty
|
||||
from ..validators.basic import valid_bool
|
||||
from ..validators.basic import valid_number
|
||||
from ..validators.basic import valid_int_f0
|
||||
from ..validators.basic import valid_int_f1
|
||||
from ..validators.basic import valid_float_f0
|
||||
from ..validators.basic import valid_float_f01
|
||||
from ..validators.basic import valid_string_list
|
||||
|
||||
from ..validators.auth import valid_user
|
||||
from ..validators.auth import valid_users_list
|
||||
|
||||
from ..validators.os import valid_abs_path
|
||||
from ..validators.os import valid_abs_file
|
||||
from ..validators.os import valid_abs_dir
|
||||
from ..validators.os import valid_unix_mode
|
||||
from ..validators.os import valid_options
|
||||
from ..validators.os import valid_command
|
||||
|
||||
from ..validators.net import valid_ip_or_host
|
||||
from ..validators.net import valid_net
|
||||
from ..validators.net import valid_port
|
||||
from ..validators.net import valid_ports_list
|
||||
from ..validators.net import valid_mac
|
||||
from ..validators.net import valid_ssl_ciphers
|
||||
|
||||
from ..validators.hid import valid_hid_key
|
||||
from ..validators.hid import valid_hid_mouse_output
|
||||
from ..validators.hid import valid_hid_mouse_move
|
||||
|
||||
from ..validators.kvm import valid_stream_quality
|
||||
from ..validators.kvm import valid_stream_fps
|
||||
from ..validators.kvm import valid_stream_resolution
|
||||
from ..validators.kvm import valid_stream_h264_bitrate
|
||||
from ..validators.kvm import valid_stream_h264_gop
|
||||
|
||||
from ..validators.ugpio import valid_ugpio_driver
|
||||
from ..validators.ugpio import valid_ugpio_channel
|
||||
from ..validators.ugpio import valid_ugpio_mode
|
||||
from ..validators.ugpio import valid_ugpio_view_title
|
||||
from ..validators.ugpio import valid_ugpio_view_table
|
||||
|
||||
from ..validators.hw import valid_tty_speed
|
||||
from ..validators.hw import valid_otg_gadget
|
||||
from ..validators.hw import valid_otg_id
|
||||
from ..validators.hw import valid_otg_ethernet
|
||||
|
||||
|
||||
# =====
|
||||
def init(
|
||||
prog: (str | None)=None,
|
||||
description: (str | None)=None,
|
||||
add_help: bool=True,
|
||||
check_run: bool=False,
|
||||
cli_logging: bool=False,
|
||||
argv: (list[str] | None)=None,
|
||||
**load: bool,
|
||||
) -> tuple[argparse.ArgumentParser, list[str], Section]:
|
||||
|
||||
argv = (argv or sys.argv)
|
||||
assert len(argv) > 0
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog=(prog or argv[0]),
|
||||
description=description,
|
||||
add_help=add_help,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument("-c", "--config", default="/etc/kvmd/main.yaml", type=valid_abs_file,
|
||||
help="Set config file path", metavar="<file>")
|
||||
parser.add_argument("-o", "--set-options", default=[], nargs="+",
|
||||
help="Override config options list (like sec/sub/opt=value)", metavar="<k=v>",)
|
||||
parser.add_argument("-m", "--dump-config", action="store_true",
|
||||
help="View current configuration (include all overrides)")
|
||||
if check_run:
|
||||
parser.add_argument("--run", dest="run", action="store_true",
|
||||
help="Run the service")
|
||||
(options, remaining) = parser.parse_known_args(argv)
|
||||
|
||||
if options.dump_config:
|
||||
_dump_config(_init_config(
|
||||
config_path=options.config,
|
||||
override_options=options.set_options,
|
||||
load_auth=True,
|
||||
load_hid=True,
|
||||
load_atx=True,
|
||||
load_msd=True,
|
||||
load_gpio=True,
|
||||
))
|
||||
raise SystemExit()
|
||||
config = _init_config(options.config, options.set_options, **load)
|
||||
|
||||
logging.captureWarnings(True)
|
||||
logging.config.dictConfig(config.logging)
|
||||
if cli_logging:
|
||||
logging.getLogger().handlers[0].setFormatter(logging.Formatter(
|
||||
"-- {levelname:>7} -- {message}",
|
||||
style="{",
|
||||
))
|
||||
|
||||
if check_run and not options.run:
|
||||
raise SystemExit(
|
||||
"To prevent accidental startup, you must specify the --run option to start.\n"
|
||||
"Try the --help option to find out what this service does.\n"
|
||||
"Make sure you understand exactly what you are doing!"
|
||||
)
|
||||
|
||||
return (parser, remaining, config)
|
||||
|
||||
|
||||
# =====
|
||||
def _init_config(config_path: str, override_options: list[str], **load_flags: bool) -> Section:
|
||||
config_path = os.path.expanduser(config_path)
|
||||
try:
|
||||
raw_config: dict = load_yaml_file(config_path)
|
||||
except Exception as err:
|
||||
raise SystemExit(f"ConfigError: Can't read config file {config_path!r}:\n{tools.efmt(err)}")
|
||||
if not isinstance(raw_config, dict):
|
||||
raise SystemExit(f"ConfigError: Top-level of the file {config_path!r} must be a dictionary")
|
||||
|
||||
scheme = _get_config_scheme()
|
||||
try:
|
||||
yaml_merge(raw_config, (raw_config.pop("override", {}) or {}))
|
||||
yaml_merge(raw_config, build_raw_from_options(override_options), "raw CLI options")
|
||||
_patch_raw(raw_config)
|
||||
config = make_config(raw_config, scheme)
|
||||
|
||||
if _patch_dynamic(raw_config, config, scheme, **load_flags):
|
||||
config = make_config(raw_config, scheme)
|
||||
|
||||
return config
|
||||
except (ConfigError, UnknownPluginError) as err:
|
||||
raise SystemExit(f"ConfigError: {err}")
|
||||
|
||||
|
||||
def _patch_raw(raw_config: dict) -> None: # pylint: disable=too-many-branches
|
||||
if isinstance(raw_config.get("otg"), dict):
|
||||
for (old, new) in [
|
||||
("msd", "msd"),
|
||||
("acm", "serial"),
|
||||
("drives", "drives"),
|
||||
]:
|
||||
if old in raw_config["otg"]:
|
||||
if not isinstance(raw_config["otg"].get("devices"), dict):
|
||||
raw_config["otg"]["devices"] = {}
|
||||
raw_config["otg"]["devices"][new] = raw_config["otg"].pop(old)
|
||||
|
||||
if isinstance(raw_config.get("kvmd"), dict) and isinstance(raw_config["kvmd"].get("wol"), dict):
|
||||
if not isinstance(raw_config["kvmd"].get("gpio"), dict):
|
||||
raw_config["kvmd"]["gpio"] = {}
|
||||
for section in ["drivers", "scheme"]:
|
||||
if not isinstance(raw_config["kvmd"]["gpio"].get(section), dict):
|
||||
raw_config["kvmd"]["gpio"][section] = {}
|
||||
raw_config["kvmd"]["gpio"]["drivers"]["__wol__"] = {
|
||||
"type": "wol",
|
||||
**raw_config["kvmd"].pop("wol"),
|
||||
}
|
||||
raw_config["kvmd"]["gpio"]["scheme"]["__wol__"] = {
|
||||
"driver": "__wol__",
|
||||
"pin": 0,
|
||||
"mode": "output",
|
||||
"switch": False,
|
||||
}
|
||||
|
||||
if isinstance(raw_config.get("kvmd"), dict) and isinstance(raw_config["kvmd"].get("streamer"), dict):
|
||||
streamer_config = raw_config["kvmd"]["streamer"]
|
||||
|
||||
desired_fps = streamer_config.get("desired_fps")
|
||||
if desired_fps is not None and not isinstance(desired_fps, dict):
|
||||
streamer_config["desired_fps"] = {"default": desired_fps}
|
||||
|
||||
max_fps = streamer_config.get("max_fps")
|
||||
if max_fps is not None:
|
||||
if not isinstance(streamer_config.get("desired_fps"), dict):
|
||||
streamer_config["desired_fps"] = {}
|
||||
streamer_config["desired_fps"]["max"] = max_fps
|
||||
del streamer_config["max_fps"]
|
||||
|
||||
resolution = streamer_config.get("resolution")
|
||||
if resolution is not None and not isinstance(resolution, dict):
|
||||
streamer_config["resolution"] = {"default": resolution}
|
||||
|
||||
available_resolutions = streamer_config.get("available_resolutions")
|
||||
if available_resolutions is not None:
|
||||
if not isinstance(streamer_config.get("resolution"), dict):
|
||||
streamer_config["resolution"] = {}
|
||||
streamer_config["resolution"]["available"] = available_resolutions
|
||||
del streamer_config["available_resolutions"]
|
||||
|
||||
|
||||
def _patch_dynamic( # pylint: disable=too-many-locals
|
||||
raw_config: dict,
|
||||
config: Section,
|
||||
scheme: dict,
|
||||
load_auth: bool=False,
|
||||
load_hid: bool=False,
|
||||
load_atx: bool=False,
|
||||
load_msd: bool=False,
|
||||
load_gpio: bool=False,
|
||||
) -> bool:
|
||||
|
||||
rebuild = False
|
||||
|
||||
if load_auth:
|
||||
scheme["kvmd"]["auth"]["internal"].update(get_auth_service_class(config.kvmd.auth.internal.type).get_plugin_options())
|
||||
if config.kvmd.auth.external.type:
|
||||
scheme["kvmd"]["auth"]["external"].update(get_auth_service_class(config.kvmd.auth.external.type).get_plugin_options())
|
||||
rebuild = True
|
||||
|
||||
for (load, section, get_class) in [
|
||||
(load_hid, "hid", get_hid_class),
|
||||
(load_atx, "atx", get_atx_class),
|
||||
(load_msd, "msd", get_msd_class),
|
||||
]:
|
||||
if load:
|
||||
scheme["kvmd"][section].update(get_class(getattr(config.kvmd, section).type).get_plugin_options())
|
||||
rebuild = True
|
||||
|
||||
if load_gpio:
|
||||
driver: str
|
||||
drivers: dict[str, type[BaseUserGpioDriver]] = {} # Name to drivers
|
||||
for (driver, params) in { # type: ignore
|
||||
"__gpio__": {},
|
||||
**tools.rget(raw_config, "kvmd", "gpio", "drivers"),
|
||||
}.items():
|
||||
with manual_validated(driver, "kvmd", "gpio", "drivers", "<key>"):
|
||||
driver = valid_ugpio_driver(driver)
|
||||
|
||||
driver_type = valid_stripped_string_not_empty(params.get("type", "gpio"))
|
||||
driver_class = get_ugpio_driver_class(driver_type)
|
||||
drivers[driver] = driver_class
|
||||
scheme["kvmd"]["gpio"]["drivers"][driver] = {
|
||||
"type": Option(driver_type, type=valid_stripped_string_not_empty),
|
||||
**driver_class.get_plugin_options()
|
||||
}
|
||||
|
||||
path = ("kvmd", "gpio", "scheme")
|
||||
for (channel, params) in tools.rget(raw_config, *path).items():
|
||||
with manual_validated(channel, *path, "<key>"):
|
||||
channel = valid_ugpio_channel(channel)
|
||||
|
||||
driver = params.get("driver", "__gpio__")
|
||||
with manual_validated(driver, *path, channel, "driver"):
|
||||
driver = valid_ugpio_driver(driver, set(drivers))
|
||||
|
||||
mode: str = params.get("mode", "")
|
||||
with manual_validated(mode, *path, channel, "mode"):
|
||||
mode = valid_ugpio_mode(mode, drivers[driver].get_modes())
|
||||
|
||||
if params.get("pulse") == False: # noqa: E712 # pylint: disable=singleton-comparison
|
||||
params["pulse"] = {"delay": 0}
|
||||
|
||||
scheme["kvmd"]["gpio"]["scheme"][channel] = {
|
||||
"driver": Option("__gpio__", type=functools.partial(valid_ugpio_driver, variants=set(drivers))),
|
||||
"pin": Option(None, type=drivers[driver].get_pin_validator()),
|
||||
"mode": Option("", type=functools.partial(valid_ugpio_mode, variants=drivers[driver].get_modes())),
|
||||
"inverted": Option(False, type=valid_bool),
|
||||
**({
|
||||
"busy_delay": Option(0.2, type=valid_float_f01),
|
||||
"initial": Option(False, type=(lambda arg: (valid_bool(arg) if arg is not None else None))),
|
||||
"switch": Option(True, type=valid_bool),
|
||||
"pulse": { # type: ignore
|
||||
"delay": Option(0.1, type=valid_float_f0),
|
||||
"min_delay": Option(0.1, type=valid_float_f01),
|
||||
"max_delay": Option(0.1, type=valid_float_f01),
|
||||
},
|
||||
} if mode == UserGpioModes.OUTPUT else { # input
|
||||
"debounce": Option(0.1, type=valid_float_f0),
|
||||
})
|
||||
}
|
||||
|
||||
rebuild = True
|
||||
|
||||
return rebuild
|
||||
|
||||
|
||||
def _dump_config(config: Section) -> None:
|
||||
dump = make_config_dump(config)
|
||||
if sys.stdout.isatty():
|
||||
dump = pygments.highlight(
|
||||
dump,
|
||||
pygments.lexers.data.YamlLexer(),
|
||||
pygments.formatters.TerminalFormatter(bg="dark"), # pylint: disable=no-member
|
||||
)
|
||||
print(dump)
|
||||
|
||||
|
||||
def _get_config_scheme() -> dict:
|
||||
return {
|
||||
"logging": Option({}),
|
||||
|
||||
"kvmd": {
|
||||
"server": {
|
||||
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||
"unix_rm": Option(True, type=valid_bool),
|
||||
"unix_mode": Option(0o660, type=valid_unix_mode),
|
||||
"heartbeat": Option(15.0, type=valid_float_f01),
|
||||
"access_log_format": Option("[%P / %{X-Real-IP}i] '%r' => %s; size=%b ---"
|
||||
" referer='%{Referer}i'; user_agent='%{User-Agent}i'"),
|
||||
},
|
||||
|
||||
"auth": {
|
||||
"enabled": Option(True, type=valid_bool),
|
||||
|
||||
"internal": {
|
||||
"type": Option("htpasswd"),
|
||||
"force_users": Option([], type=valid_users_list),
|
||||
# Dynamic content
|
||||
},
|
||||
|
||||
"external": {
|
||||
"type": Option("", type=valid_stripped_string),
|
||||
# Dynamic content
|
||||
},
|
||||
|
||||
"totp": {
|
||||
"secret": {
|
||||
"file": Option("/etc/kvmd/totp.secret", type=valid_abs_path, if_empty=""),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"info": { # Accessed via global config, see kvmd/info for details
|
||||
"meta": Option("/etc/kvmd/meta.yaml", type=valid_abs_file),
|
||||
"extras": Option("/usr/share/kvmd/extras", type=valid_abs_dir),
|
||||
"hw": {
|
||||
"vcgencmd_cmd": Option(["/usr/bin/vcgencmd"], type=valid_command),
|
||||
"ignore_past": Option(False, type=valid_bool),
|
||||
"state_poll": Option(10.0, type=valid_float_f01),
|
||||
},
|
||||
"fan": {
|
||||
"daemon": Option("kvmd-fan", type=valid_stripped_string),
|
||||
"unix": Option("", type=valid_abs_path, if_empty="", unpack_as="unix_path"),
|
||||
"timeout": Option(5.0, type=valid_float_f01),
|
||||
"state_poll": Option(5.0, type=valid_float_f01),
|
||||
},
|
||||
},
|
||||
|
||||
"log_reader": {
|
||||
"enabled": Option(True, type=valid_bool),
|
||||
},
|
||||
|
||||
"prometheus": {
|
||||
"auth": {
|
||||
"enabled": Option(True, type=valid_bool),
|
||||
},
|
||||
},
|
||||
|
||||
"hid": {
|
||||
"type": Option("", type=valid_stripped_string_not_empty),
|
||||
|
||||
"keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file),
|
||||
"ignore_keys": Option([], type=functools.partial(valid_string_list, subval=valid_hid_key)),
|
||||
|
||||
"mouse_x_range": {
|
||||
"min": Option(MouseRange.MIN, type=valid_hid_mouse_move),
|
||||
"max": Option(MouseRange.MAX, type=valid_hid_mouse_move),
|
||||
},
|
||||
"mouse_y_range": {
|
||||
"min": Option(MouseRange.MIN, type=valid_hid_mouse_move),
|
||||
"max": Option(MouseRange.MAX, type=valid_hid_mouse_move),
|
||||
},
|
||||
|
||||
# Dynamic content
|
||||
},
|
||||
|
||||
"atx": {
|
||||
"type": Option("", type=valid_stripped_string_not_empty),
|
||||
# Dynamic content
|
||||
},
|
||||
|
||||
"msd": {
|
||||
"type": Option("", type=valid_stripped_string_not_empty),
|
||||
# Dynamic content
|
||||
},
|
||||
|
||||
"streamer": {
|
||||
"forever": Option(False, type=valid_bool),
|
||||
|
||||
"reset_delay": Option(1.0, type=valid_float_f0),
|
||||
"shutdown_delay": Option(10.0, type=valid_float_f01),
|
||||
"state_poll": Option(1.0, type=valid_float_f01),
|
||||
|
||||
"quality": Option(80, type=valid_stream_quality, if_empty=0),
|
||||
|
||||
"resolution": {
|
||||
"default": Option("", type=valid_stream_resolution, if_empty="", unpack_as="resolution"),
|
||||
"available": Option(
|
||||
[],
|
||||
type=functools.partial(valid_string_list, subval=valid_stream_resolution),
|
||||
unpack_as="available_resolutions",
|
||||
),
|
||||
},
|
||||
|
||||
"desired_fps": {
|
||||
"default": Option(40, type=valid_stream_fps, unpack_as="desired_fps"),
|
||||
"min": Option(0, type=valid_stream_fps, unpack_as="desired_fps_min"),
|
||||
"max": Option(70, type=valid_stream_fps, unpack_as="desired_fps_max"),
|
||||
},
|
||||
|
||||
"h264_bitrate": {
|
||||
"default": Option(0, type=valid_stream_h264_bitrate, if_empty=0, unpack_as="h264_bitrate"),
|
||||
"min": Option(25, type=valid_stream_h264_bitrate, unpack_as="h264_bitrate_min"),
|
||||
"max": Option(20000, type=valid_stream_h264_bitrate, unpack_as="h264_bitrate_max"),
|
||||
},
|
||||
|
||||
"h264_gop": {
|
||||
"default": Option(30, type=valid_stream_h264_gop, unpack_as="h264_gop"),
|
||||
"min": Option(0, type=valid_stream_h264_gop, unpack_as="h264_gop_min"),
|
||||
"max": Option(60, type=valid_stream_h264_gop, unpack_as="h264_gop_max"),
|
||||
},
|
||||
|
||||
"unix": Option("/run/kvmd/ustreamer.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||
"timeout": Option(2.0, type=valid_float_f01),
|
||||
|
||||
"process_name_prefix": Option("kvmd/streamer"),
|
||||
|
||||
"pre_start_cmd": Option(["/bin/true", "pre-start"], type=valid_command),
|
||||
"pre_start_cmd_remove": Option([], type=valid_options),
|
||||
"pre_start_cmd_append": Option([], type=valid_options),
|
||||
|
||||
"cmd": Option(["/bin/true"], type=valid_command),
|
||||
"cmd_remove": Option([], type=valid_options),
|
||||
"cmd_append": Option([], type=valid_options),
|
||||
|
||||
"post_stop_cmd": Option(["/bin/true", "post-stop"], type=valid_command),
|
||||
"post_stop_cmd_remove": Option([], type=valid_options),
|
||||
"post_stop_cmd_append": Option([], type=valid_options),
|
||||
},
|
||||
|
||||
"ocr": {
|
||||
"langs": Option(["eng"], type=valid_string_list, unpack_as="default_langs"),
|
||||
"tessdata": Option("/usr/share/tessdata", type=valid_stripped_string_not_empty, unpack_as="data_dir_path")
|
||||
},
|
||||
|
||||
"snapshot": {
|
||||
"idle_interval": Option(0.0, type=valid_float_f0),
|
||||
"live_interval": Option(0.0, type=valid_float_f0),
|
||||
|
||||
"wakeup_key": Option("", type=valid_hid_key, if_empty=""),
|
||||
"wakeup_move": Option(0, type=valid_hid_mouse_move),
|
||||
|
||||
"online_delay": Option(5.0, type=valid_float_f0),
|
||||
"retries": Option(10, type=valid_int_f1),
|
||||
"retries_delay": Option(3.0, type=valid_float_f01),
|
||||
},
|
||||
|
||||
"gpio": {
|
||||
"state_poll": Option(0.1, type=valid_float_f01),
|
||||
"drivers": {}, # Dynamic content
|
||||
"scheme": {}, # Dymanic content
|
||||
"view": {
|
||||
"header": {
|
||||
"title": Option("GPIO", type=valid_ugpio_view_title),
|
||||
},
|
||||
"table": Option([], type=valid_ugpio_view_table),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"pst": {
|
||||
"server": {
|
||||
"unix": Option("/run/kvmd/pst.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||
"unix_rm": Option(True, type=valid_bool),
|
||||
"unix_mode": Option(0o660, type=valid_unix_mode),
|
||||
"heartbeat": Option(15.0, type=valid_float_f01),
|
||||
"access_log_format": Option("[%P / %{X-Real-IP}i] '%r' => %s; size=%b ---"
|
||||
" referer='%{Referer}i'; user_agent='%{User-Agent}i'"),
|
||||
},
|
||||
|
||||
"ro_retries_delay": Option(10.0, type=valid_float_f01),
|
||||
"ro_cleanup_delay": Option(3.0, type=valid_float_f01),
|
||||
|
||||
"remount_cmd": Option([
|
||||
"/usr/bin/sudo", "--non-interactive",
|
||||
"/usr/bin/kvmd-helper-pst-remount", "{mode}",
|
||||
], type=valid_command),
|
||||
},
|
||||
|
||||
"otg": {
|
||||
"vendor_id": Option(0x1D6B, type=valid_otg_id), # Linux Foundation
|
||||
"product_id": Option(0x0104, type=valid_otg_id), # Multifunction Composite Gadget
|
||||
"manufacturer": Option("PiKVM", type=valid_stripped_string),
|
||||
"product": Option("Composite KVM Device", type=valid_stripped_string),
|
||||
"serial": Option("CAFEBABE", type=valid_stripped_string, if_none=None),
|
||||
"device_version": Option(-1, type=functools.partial(valid_number, min=-1, max=0xFFFF)),
|
||||
"usb_version": Option(0x0200, type=valid_otg_id),
|
||||
"max_power": Option(250, type=functools.partial(valid_number, min=50, max=500)),
|
||||
"remote_wakeup": Option(False, type=valid_bool),
|
||||
|
||||
"gadget": Option("kvmd", type=valid_otg_gadget),
|
||||
"config": Option("PiKVM device", type=valid_stripped_string_not_empty),
|
||||
"udc": Option("", type=valid_stripped_string),
|
||||
"init_delay": Option(3.0, type=valid_float_f01),
|
||||
|
||||
"user": Option("kvmd", type=valid_user),
|
||||
"meta": Option("/run/kvmd/otg", type=valid_abs_path),
|
||||
|
||||
"devices": {
|
||||
"hid": {
|
||||
"keyboard": {
|
||||
"start": Option(True, type=valid_bool),
|
||||
},
|
||||
"mouse": {
|
||||
"start": Option(True, type=valid_bool),
|
||||
},
|
||||
},
|
||||
|
||||
"msd": {
|
||||
"start": Option(True, type=valid_bool),
|
||||
"default": {
|
||||
"stall": Option(False, type=valid_bool),
|
||||
"cdrom": Option(True, type=valid_bool),
|
||||
"rw": Option(False, type=valid_bool),
|
||||
"removable": Option(True, type=valid_bool),
|
||||
"fua": Option(True, type=valid_bool),
|
||||
},
|
||||
},
|
||||
|
||||
"serial": {
|
||||
"enabled": Option(False, type=valid_bool),
|
||||
"start": Option(True, type=valid_bool),
|
||||
},
|
||||
|
||||
"ethernet": {
|
||||
"enabled": Option(False, type=valid_bool),
|
||||
"start": Option(True, type=valid_bool),
|
||||
"driver": Option("ecm", type=valid_otg_ethernet),
|
||||
"host_mac": Option("", type=valid_mac, if_empty=""),
|
||||
"kvm_mac": Option("", type=valid_mac, if_empty=""),
|
||||
},
|
||||
|
||||
"drives": {
|
||||
"enabled": Option(False, type=valid_bool),
|
||||
"start": Option(True, type=valid_bool),
|
||||
"count": Option(1, type=valid_int_f1),
|
||||
"default": {
|
||||
"stall": Option(False, type=valid_bool),
|
||||
"cdrom": Option(False, type=valid_bool),
|
||||
"rw": Option(True, type=valid_bool),
|
||||
"removable": Option(True, type=valid_bool),
|
||||
"fua": Option(True, type=valid_bool),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"otgnet": {
|
||||
"iface": {
|
||||
"net": Option("172.30.30.0/24", type=functools.partial(valid_net, v6=False)),
|
||||
"ip_cmd": Option(["/usr/bin/ip"], type=valid_command),
|
||||
},
|
||||
|
||||
"firewall": {
|
||||
"allow_icmp": Option(True, type=valid_bool),
|
||||
"allow_tcp": Option([], type=valid_ports_list),
|
||||
"allow_udp": Option([67], type=valid_ports_list),
|
||||
"forward_iface": Option("", type=valid_stripped_string),
|
||||
"iptables_cmd": Option(["/usr/sbin/iptables", "--wait=5"], type=valid_command),
|
||||
},
|
||||
|
||||
"commands": {
|
||||
"pre_start_cmd": Option(["/bin/true", "pre-start"], type=valid_command),
|
||||
"pre_start_cmd_remove": Option([], type=valid_options),
|
||||
"pre_start_cmd_append": Option([], type=valid_options),
|
||||
|
||||
"post_start_cmd": Option([
|
||||
"/usr/bin/systemd-run",
|
||||
"--unit=kvmd-otgnet-dnsmasq",
|
||||
"/usr/sbin/dnsmasq",
|
||||
"--conf-file=/dev/null",
|
||||
"--pid-file",
|
||||
"--user=dnsmasq",
|
||||
"--interface={iface}",
|
||||
"--port=0",
|
||||
"--dhcp-range={dhcp_ip_begin},{dhcp_ip_end},24h",
|
||||
"--dhcp-leasefile=/run/kvmd/dnsmasq.lease",
|
||||
"--dhcp-option={dhcp_option_3}",
|
||||
"--dhcp-option=6",
|
||||
"--keep-in-foreground",
|
||||
], type=valid_command),
|
||||
"post_start_cmd_remove": Option([], type=valid_options),
|
||||
"post_start_cmd_append": Option([], type=valid_options),
|
||||
|
||||
"pre_stop_cmd": Option([
|
||||
"/usr/bin/systemctl",
|
||||
"stop",
|
||||
"kvmd-otgnet-dnsmasq",
|
||||
], type=valid_command),
|
||||
"pre_stop_cmd_remove": Option([], type=valid_options),
|
||||
"pre_stop_cmd_append": Option([], type=valid_options),
|
||||
|
||||
"post_stop_cmd": Option(["/bin/true", "post-stop"], type=valid_command),
|
||||
"post_stop_cmd_remove": Option([], type=valid_options),
|
||||
"post_stop_cmd_append": Option([], type=valid_options),
|
||||
},
|
||||
},
|
||||
|
||||
"ipmi": {
|
||||
"server": {
|
||||
"host": Option("::", type=valid_ip_or_host),
|
||||
"port": Option(623, type=valid_port),
|
||||
"timeout": Option(10.0, type=valid_float_f01),
|
||||
},
|
||||
|
||||
"kvmd": {
|
||||
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||
"timeout": Option(5.0, type=valid_float_f01),
|
||||
},
|
||||
|
||||
"auth": {
|
||||
"file": Option("/etc/kvmd/ipmipasswd", type=valid_abs_file, unpack_as="path"),
|
||||
},
|
||||
|
||||
"sol": {
|
||||
"device": Option("", type=valid_abs_path, if_empty="", unpack_as="sol_device_path"),
|
||||
"speed": Option(115200, type=valid_tty_speed, unpack_as="sol_speed"),
|
||||
"select_timeout": Option(0.1, type=valid_float_f01, unpack_as="sol_select_timeout"),
|
||||
"proxy_port": Option(0, type=valid_port, unpack_as="sol_proxy_port"),
|
||||
},
|
||||
},
|
||||
|
||||
"vnc": {
|
||||
"desired_fps": Option(30, type=valid_stream_fps),
|
||||
"mouse_output": Option("usb", type=valid_hid_mouse_output),
|
||||
"keymap": Option("/usr/share/kvmd/keymaps/en-us", type=valid_abs_file),
|
||||
|
||||
"server": {
|
||||
"host": Option("::", type=valid_ip_or_host),
|
||||
"port": Option(5900, type=valid_port),
|
||||
"max_clients": Option(10, type=valid_int_f1),
|
||||
|
||||
"no_delay": Option(True, type=valid_bool),
|
||||
"keepalive": {
|
||||
"enabled": Option(True, type=valid_bool, unpack_as="keepalive_enabled"),
|
||||
"idle": Option(10, type=functools.partial(valid_number, min=1, max=3600), unpack_as="keepalive_idle"),
|
||||
"interval": Option(3, type=functools.partial(valid_number, min=1, max=60), unpack_as="keepalive_interval"),
|
||||
"count": Option(3, type=functools.partial(valid_number, min=1, max=10), unpack_as="keepalive_count"),
|
||||
},
|
||||
|
||||
"tls": {
|
||||
"ciphers": Option("ALL:@SECLEVEL=0", type=valid_ssl_ciphers, if_empty=""),
|
||||
"timeout": Option(30.0, type=valid_float_f01),
|
||||
"x509": {
|
||||
"cert": Option("/etc/kvmd/vnc/ssl/server.crt", type=valid_abs_file, if_empty=""),
|
||||
"key": Option("/etc/kvmd/vnc/ssl/server.key", type=valid_abs_file, if_empty=""),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"kvmd": {
|
||||
"unix": Option("/run/kvmd/kvmd.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||
"timeout": Option(5.0, type=valid_float_f01),
|
||||
},
|
||||
|
||||
"streamer": {
|
||||
"unix": Option("/run/kvmd/ustreamer.sock", type=valid_abs_path, unpack_as="unix_path"),
|
||||
"timeout": Option(5.0, type=valid_float_f01),
|
||||
},
|
||||
|
||||
"memsink": {
|
||||
"jpeg": {
|
||||
"sink": Option("", unpack_as="obj"),
|
||||
"lock_timeout": Option(1.0, type=valid_float_f01),
|
||||
"wait_timeout": Option(1.0, type=valid_float_f01),
|
||||
"drop_same_frames": Option(1.0, type=valid_float_f0),
|
||||
},
|
||||
"h264": {
|
||||
"sink": Option("", unpack_as="obj"),
|
||||
"lock_timeout": Option(1.0, type=valid_float_f01),
|
||||
"wait_timeout": Option(1.0, type=valid_float_f01),
|
||||
"drop_same_frames": Option(0.0, type=valid_float_f0),
|
||||
},
|
||||
},
|
||||
|
||||
"auth": {
|
||||
"vncauth": {
|
||||
"enabled": Option(False, type=valid_bool),
|
||||
"file": Option("/etc/kvmd/vncpasswd", type=valid_abs_file, unpack_as="path"),
|
||||
},
|
||||
"vencrypt": {
|
||||
"enabled": Option(True, type=valid_bool, unpack_as="vencrypt_enabled"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"janus": {
|
||||
"stun": {
|
||||
"host": Option("stun.l.google.com", type=valid_ip_or_host, unpack_as="stun_host"),
|
||||
"port": Option(19302, type=valid_port, unpack_as="stun_port"),
|
||||
"timeout": Option(5.0, type=valid_float_f01, unpack_as="stun_timeout"),
|
||||
"retries": Option(5, type=valid_int_f1, unpack_as="stun_retries"),
|
||||
"retries_delay": Option(5.0, type=valid_float_f01, unpack_as="stun_retries_delay"),
|
||||
},
|
||||
|
||||
"check": {
|
||||
"interval": Option(10.0, type=valid_float_f01, unpack_as="check_interval"),
|
||||
"retries": Option(5, type=valid_int_f1, unpack_as="check_retries"),
|
||||
"retries_delay": Option(5.0, type=valid_float_f01, unpack_as="check_retries_delay"),
|
||||
},
|
||||
|
||||
"cmd": Option([
|
||||
"/usr/bin/janus",
|
||||
"--disable-colors",
|
||||
"--plugins-folder=/usr/lib/ustreamer/janus",
|
||||
"--configs-folder=/etc/kvmd/janus",
|
||||
"--interface={src_ip}",
|
||||
"{o_stun_server}",
|
||||
], type=valid_command),
|
||||
"cmd_remove": Option([], type=valid_options),
|
||||
"cmd_append": Option([], type=valid_options),
|
||||
},
|
||||
|
||||
"watchdog": {
|
||||
"rtc": Option(0, type=valid_int_f0),
|
||||
"timeout": Option(300, type=valid_int_f1),
|
||||
"interval": Option(30, type=valid_int_f1),
|
||||
},
|
||||
}
|
||||
3364
patches/adapter.js
Normal file
3364
patches/adapter.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,330 @@
|
||||
From 3d137882ac38ac046b7d09cada1883b304b04319 Mon Sep 17 00:00:00 2001
|
||||
From: xe5700 <9338143+xe5700@users.noreply.github.com>
|
||||
Date: Fri, 20 May 2022 18:34:21 +0800
|
||||
Subject: [PATCH] Revert force eject feature to unlock helper
|
||||
|
||||
---
|
||||
kvmd/aiohelpers.py | 31 ++++++++++++-----
|
||||
kvmd/apps/otg/__init__.py | 3 +-
|
||||
kvmd/apps/otgmsd/__init__.py | 25 +++++++++++++-
|
||||
kvmd/helpers/unlock/__init__.py | 58 ++++++++++++++++++++++++++++++++
|
||||
kvmd/helpers/unlock/__main__.py | 24 +++++++++++++
|
||||
kvmd/plugins/msd/otg/__init__.py | 19 ++++++++---
|
||||
kvmd/plugins/msd/otg/drive.py | 5 +--
|
||||
7 files changed, 145 insertions(+), 20 deletions(-)
|
||||
create mode 100644 kvmd/helpers/unlock/__init__.py
|
||||
create mode 100644 kvmd/helpers/unlock/__main__.py
|
||||
|
||||
diff --git a/kvmd/aiohelpers.py b/kvmd/aiohelpers.py
|
||||
index 6357764c..37a5d4b9 100644
|
||||
--- a/kvmd/aiohelpers.py
|
||||
+++ b/kvmd/aiohelpers.py
|
||||
@@ -40,11 +40,26 @@ async def remount(name: str, base_cmd: List[str], rw: bool) -> bool:
|
||||
]
|
||||
logger.info("Remounting %s storage to %s: %s ...", name, mode.upper(), cmd)
|
||||
try:
|
||||
- proc = await aioproc.log_process(cmd, logger)
|
||||
- if proc.returncode != 0:
|
||||
- assert proc.returncode is not None
|
||||
- raise subprocess.CalledProcessError(proc.returncode, cmd)
|
||||
- except Exception as err:
|
||||
- logger.error("Can't remount %s storage: %s", name, tools.efmt(err))
|
||||
- return False
|
||||
- return True
|
||||
+ await _run_helper(cmd)
|
||||
+ except Exception:
|
||||
+ logger.error("Can't remount internal storage")
|
||||
+ raise
|
||||
+
|
||||
+
|
||||
+async def unlock_drive(base_cmd: List[str]) -> None:
|
||||
+ logger = get_logger(0)
|
||||
+ logger.info("Unlocking the drive ...")
|
||||
+ try:
|
||||
+ await _run_helper(base_cmd)
|
||||
+ except Exception:
|
||||
+ logger.error("Can't unlock the drive")
|
||||
+ raise
|
||||
+
|
||||
+
|
||||
+# =====
|
||||
+async def _run_helper(cmd: List[str]) -> None:
|
||||
+ logger = get_logger(0)
|
||||
+ logger.info("Executing helper %s ...", cmd)
|
||||
+ proc = await aioproc.log_process(cmd, logger)
|
||||
+ if proc.returncode != 0:
|
||||
+ raise MsdError(f"Error while helper execution: pid={proc.pid}; retcode={proc.returncode}")
|
||||
diff --git a/kvmd/apps/otg/__init__.py b/kvmd/apps/otg/__init__.py
|
||||
index cbf7a197..d0ed0554 100644
|
||||
--- a/kvmd/apps/otg/__init__.py
|
||||
+++ b/kvmd/apps/otg/__init__.py
|
||||
@@ -182,7 +182,6 @@ class _GadgetConfig:
|
||||
_chown(join(func_path, "lun.0/cdrom"), user)
|
||||
_chown(join(func_path, "lun.0/ro"), user)
|
||||
_chown(join(func_path, "lun.0/file"), user)
|
||||
- _chown(join(func_path, "lun.0/forced_eject"), user)
|
||||
_symlink(func_path, join(self.__profile_path, func))
|
||||
name = ("Mass Storage Drive" if self.__msd_instance == 0 else f"Extra Drive #{self.__msd_instance}")
|
||||
self.__create_meta(func, name)
|
||||
@@ -291,7 +290,7 @@ def _cmd_stop(config: Section) -> None:
|
||||
logger.info("Disabling gadget %r ...", config.otg.gadget)
|
||||
_write(join(gadget_path, "UDC"), "\n")
|
||||
|
||||
- _unlink(join(gadget_path, "os_desc", usb.G_PROFILE_NAME), optional=True)
|
||||
+ _unlink(join(gadget_path, "os_desc", usb.G_PROFILE_NAME), True)
|
||||
|
||||
profile_path = join(gadget_path, usb.G_PROFILE)
|
||||
for func in os.listdir(profile_path):
|
||||
diff --git a/kvmd/apps/otgmsd/__init__.py b/kvmd/apps/otgmsd/__init__.py
|
||||
index f57b3107..78f8e3c7 100644
|
||||
--- a/kvmd/apps/otgmsd/__init__.py
|
||||
+++ b/kvmd/apps/otgmsd/__init__.py
|
||||
@@ -21,12 +21,15 @@
|
||||
|
||||
|
||||
import os
|
||||
+import signal
|
||||
import errno
|
||||
import argparse
|
||||
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
+import psutil
|
||||
+
|
||||
from ...validators.basic import valid_bool
|
||||
from ...validators.basic import valid_int_f0
|
||||
from ...validators.os import valid_abs_file
|
||||
@@ -56,6 +59,21 @@ def _set_param(gadget: str, instance: int, param: str, value: str) -> None:
|
||||
raise
|
||||
|
||||
|
||||
+def _unlock() -> None:
|
||||
+ # https://github.com/torvalds/linux/blob/3039fad/drivers/usb/gadget/function/f_mass_storage.c#L2924
|
||||
+ found = False
|
||||
+ for proc in psutil.process_iter():
|
||||
+ attrs = proc.as_dict(attrs=["name", "exe", "pid"])
|
||||
+ if attrs.get("name") == "file-storage" and not attrs.get("exe"):
|
||||
+ try:
|
||||
+ proc.send_signal(signal.SIGUSR1)
|
||||
+ found = True
|
||||
+ except Exception as err:
|
||||
+ raise SystemExit(f"Can't send SIGUSR1 to MSD kernel thread with pid={attrs['pid']}: {err}")
|
||||
+ if not found:
|
||||
+ raise SystemExit("Can't find MSD kernel thread")
|
||||
+
|
||||
+
|
||||
# =====
|
||||
def main(argv: Optional[List[str]]=None) -> None:
|
||||
(parent_parser, argv, config) = init(
|
||||
@@ -70,6 +88,8 @@ def main(argv: Optional[List[str]]=None) -> None:
|
||||
)
|
||||
parser.add_argument("-i", "--instance", default=0, type=valid_int_f0,
|
||||
metavar="<N>", help="Drive instance (0 for KVMD drive)")
|
||||
+ parser.add_argument("--unlock", action="store_true",
|
||||
+ help="Send SIGUSR1 to MSD kernel thread")
|
||||
parser.add_argument("--set-cdrom", default=None, type=valid_bool,
|
||||
metavar="<1|0|yes|no>", help="Set CD-ROM flag")
|
||||
parser.add_argument("--set-rw", default=None, type=valid_bool,
|
||||
@@ -89,8 +109,11 @@ def main(argv: Optional[List[str]]=None) -> None:
|
||||
set_param = (lambda param, value: _set_param(config.otg.gadget, options.instance, param, value))
|
||||
get_param = (lambda param: _get_param(config.otg.gadget, options.instance, param))
|
||||
|
||||
+ if options.unlock:
|
||||
+ _unlock()
|
||||
+
|
||||
if options.eject:
|
||||
- set_param("forced_eject", "")
|
||||
+ set_param("file", "")
|
||||
|
||||
if options.set_cdrom is not None:
|
||||
set_param("cdrom", str(int(options.set_cdrom)))
|
||||
diff --git a/kvmd/helpers/unlock/__init__.py b/kvmd/helpers/unlock/__init__.py
|
||||
new file mode 100644
|
||||
index 00000000..140e0e7c
|
||||
--- /dev/null
|
||||
+++ b/kvmd/helpers/unlock/__init__.py
|
||||
@@ -0,0 +1,58 @@
|
||||
+# ========================================================================== #
|
||||
+# #
|
||||
+# KVMD - The main PiKVM daemon. #
|
||||
+# #
|
||||
+# Copyright (C) 2018-2022 Maxim Devaev <mdevaev@gmail.com> #
|
||||
+# #
|
||||
+# This program is free software: you can redistribute it and/or modify #
|
||||
+# it under the terms of the GNU General Public License as published by #
|
||||
+# the Free Software Foundation, either version 3 of the License, or #
|
||||
+# (at your option) any later version. #
|
||||
+# #
|
||||
+# This program is distributed in the hope that it will be useful, #
|
||||
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
+# GNU General Public License for more details. #
|
||||
+# #
|
||||
+# You should have received a copy of the GNU General Public License #
|
||||
+# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
+# #
|
||||
+# ========================================================================== #
|
||||
+
|
||||
+
|
||||
+import sys
|
||||
+import signal
|
||||
+
|
||||
+import psutil
|
||||
+
|
||||
+
|
||||
+# =====
|
||||
+_PROCESS_NAME = "file-storage"
|
||||
+
|
||||
+
|
||||
+# =====
|
||||
+def _log(msg: str) -> None:
|
||||
+ print(msg, file=sys.stderr)
|
||||
+
|
||||
+
|
||||
+def _unlock() -> None:
|
||||
+ # https://github.com/torvalds/linux/blob/3039fad/drivers/usb/gadget/function/f_mass_storage.c#L2924
|
||||
+ found = False
|
||||
+ for proc in psutil.process_iter():
|
||||
+ attrs = proc.as_dict(attrs=["name", "exe", "pid"])
|
||||
+ if attrs.get("name") == _PROCESS_NAME and not attrs.get("exe"):
|
||||
+ _log(f"Sending SIGUSR1 to MSD {_PROCESS_NAME!r} kernel thread with pid={attrs['pid']} ...")
|
||||
+ try:
|
||||
+ proc.send_signal(signal.SIGUSR1)
|
||||
+ found = True
|
||||
+ except Exception as err:
|
||||
+ raise SystemExit(f"Can't send SIGUSR1 to MSD kernel thread with pid={attrs['pid']}: {err}")
|
||||
+ if not found:
|
||||
+ raise SystemExit(f"Can't find MSD kernel thread {_PROCESS_NAME!r}")
|
||||
+
|
||||
+
|
||||
+# =====
|
||||
+def main() -> None:
|
||||
+ if len(sys.argv) != 2 or sys.argv[1] != "unlock":
|
||||
+ raise SystemExit(f"Usage: {sys.argv[0]} [unlock]")
|
||||
+ _unlock()
|
||||
diff --git a/kvmd/helpers/unlock/__main__.py b/kvmd/helpers/unlock/__main__.py
|
||||
new file mode 100644
|
||||
index 00000000..3849d1b9
|
||||
--- /dev/null
|
||||
+++ b/kvmd/helpers/unlock/__main__.py
|
||||
@@ -0,0 +1,24 @@
|
||||
+# ========================================================================== #
|
||||
+# #
|
||||
+# KVMD - The main PiKVM daemon. #
|
||||
+# #
|
||||
+# Copyright (C) 2018-2022 Maxim Devaev <mdevaev@gmail.com> #
|
||||
+# #
|
||||
+# This program is free software: you can redistribute it and/or modify #
|
||||
+# it under the terms of the GNU General Public License as published by #
|
||||
+# the Free Software Foundation, either version 3 of the License, or #
|
||||
+# (at your option) any later version. #
|
||||
+# #
|
||||
+# This program is distributed in the hope that it will be useful, #
|
||||
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
+# GNU General Public License for more details. #
|
||||
+# #
|
||||
+# You should have received a copy of the GNU General Public License #
|
||||
+# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
+# #
|
||||
+# ========================================================================== #
|
||||
+
|
||||
+
|
||||
+from . import main
|
||||
+main()
|
||||
diff --git a/kvmd/plugins/msd/otg/__init__.py b/kvmd/plugins/msd/otg/__init__.py
|
||||
index 409b899a..1342c6b4 100644
|
||||
--- a/kvmd/plugins/msd/otg/__init__.py
|
||||
+++ b/kvmd/plugins/msd/otg/__init__.py
|
||||
@@ -140,6 +140,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
storage_path: str,
|
||||
|
||||
remount_cmd: List[str],
|
||||
+ unlock_cmd: List[str],
|
||||
|
||||
initial: Dict,
|
||||
|
||||
@@ -154,6 +155,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
self.__meta_path = os.path.join(self.__storage_path, "meta")
|
||||
|
||||
self.__remount_cmd = remount_cmd
|
||||
+ self.__unlock_cmd = unlock_cmd
|
||||
|
||||
self.__initial_image: str = initial["image"]
|
||||
self.__initial_cdrom: bool = initial["cdrom"]
|
||||
@@ -178,10 +180,8 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
"storage": Option("/var/lib/kvmd/msd", type=valid_abs_dir, unpack_as="storage_path"),
|
||||
|
||||
- "remount_cmd": Option([
|
||||
- "/usr/bin/sudo", "--non-interactive",
|
||||
- "/usr/bin/kvmd-helper-otgmsd-remount", "{mode}",
|
||||
- ], type=valid_command),
|
||||
+ "remount_cmd": Option([*sudo, "/usr/bin/kvmd-helper-otgmsd-remount", "{mode}"], type=valid_command),
|
||||
+ "unlock_cmd": Option([*sudo, "/usr/bin/kvmd-helper-otgmsd-unlock", "unlock"], type=valid_command),
|
||||
|
||||
"initial": {
|
||||
"image": Option("", type=valid_printable_filename, if_empty=""),
|
||||
@@ -241,6 +241,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
async def reset(self) -> None:
|
||||
async with self.__state.busy(check_online=False):
|
||||
try:
|
||||
+ await self.__unlock_drive()
|
||||
self.__drive.set_image_path("")
|
||||
self.__drive.set_rw_flag(False)
|
||||
self.__drive.set_cdrom_flag(False)
|
||||
@@ -290,12 +291,15 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
if not os.path.exists(self.__state.vd.image.path):
|
||||
raise MsdUnknownImageError()
|
||||
|
||||
+ await self.__unlock_drive()
|
||||
self.__drive.set_cdrom_flag(self.__state.vd.cdrom)
|
||||
self.__drive.set_image_path(self.__state.vd.image.path)
|
||||
|
||||
else:
|
||||
if not (self.__state.vd.connected or self.__drive.get_image_path()):
|
||||
raise MsdDisconnectedError()
|
||||
+
|
||||
+ await self.__unlock_drive()
|
||||
self.__drive.set_image_path("")
|
||||
|
||||
self.__state.vd.connected = connected
|
||||
@@ -474,6 +478,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
if os.path.exists(path):
|
||||
logger.info("Setting up initial image %r ...", self.__initial_image)
|
||||
try:
|
||||
+ await self.__unlock_drive()
|
||||
self.__drive.set_cdrom_flag(self.__initial_cdrom)
|
||||
self.__drive.set_image_path(path)
|
||||
except Exception:
|
||||
@@ -541,4 +546,8 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
async def __remount_storage(self, rw: bool) -> None:
|
||||
if not (await aiohelpers.remount("MSD", self.__remount_cmd, rw)):
|
||||
- raise MsdError("Can't execute remount helper")
|
||||
+ pass
|
||||
+ #raise MsdError("Can't execute remount helper")
|
||||
+
|
||||
+ async def __unlock_drive(self) -> None:
|
||||
+ await helpers.unlock_drive(self.__unlock_cmd)
|
||||
\ No newline at end of file
|
||||
diff --git a/kvmd/plugins/msd/otg/drive.py b/kvmd/plugins/msd/otg/drive.py
|
||||
index 11af7f81..ee54e5e9 100644
|
||||
--- a/kvmd/plugins/msd/otg/drive.py
|
||||
+++ b/kvmd/plugins/msd/otg/drive.py
|
||||
@@ -53,10 +53,7 @@ class Drive:
|
||||
# =====
|
||||
|
||||
def set_image_path(self, path: str) -> None:
|
||||
- if path:
|
||||
- self.__set_param("file", path)
|
||||
- else:
|
||||
- self.__set_param("forced_eject", "")
|
||||
+ self.__set_param("file", path)
|
||||
|
||||
def get_image_path(self) -> str:
|
||||
return self.__get_param("file")
|
||||
--
|
||||
2.34.1.windows.1
|
||||
|
||||
11
patches/custom/old-kernel-msd/apply.sh
Normal file
11
patches/custom/old-kernel-msd/apply.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
APP_PATH=$(readlink -f $(dirname $0))
|
||||
echo "-> Apply patches"
|
||||
cd /usr/lib/python3.10/site-packages/
|
||||
git apply ${APP_PATH}/*.patch
|
||||
cd ${APP_PATH}
|
||||
echo "-> Add otgmsd unlock link"
|
||||
cp kvmd-helper-otgmsd-unlock /usr/bin/
|
||||
echo "-> Add sudoer"
|
||||
echo "kvmd ALL=(ALL) NOPASSWD: /usr/bin/kvmd-helper-otgmsd-unlock" >> /etc/sudoers.d/99_kvmd
|
||||
echo "-> Apply old kernel msd patch done."
|
||||
7
patches/custom/old-kernel-msd/kvmd-helper-otgmsd-unlock
Normal file
7
patches/custom/old-kernel-msd/kvmd-helper-otgmsd-unlock
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/sbin/python
|
||||
# KVMD-ARMBIAN
|
||||
|
||||
from kvmd.helpers.unlock import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
10
patches/display.sh
Normal file
10
patches/display.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
#tty清屏
|
||||
echo -e "\033c" > /dev/tty1
|
||||
|
||||
#隐藏光标
|
||||
echo -e "\033[?25l" > /dev/tty1
|
||||
|
||||
#在HDMI显示器输出采集卡画面
|
||||
ustreamer-dump --sink=kvmd::ustreamer::jpeg --output - | ffmpeg -use_wallclock_as_timestamps 1 -i pipe:c:v -an -pix_fmt bgr24 -f fbdev /dev/fb0
|
||||
147
patches/hw.py
Normal file
147
patches/hw.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from typing import Callable
|
||||
from typing import AsyncGenerator
|
||||
from typing import TypeVar
|
||||
|
||||
from ....logging import get_logger
|
||||
|
||||
from .... import env
|
||||
from .... import tools
|
||||
from .... import aiotools
|
||||
from .... import aioproc
|
||||
|
||||
from .base import BaseInfoSubmanager
|
||||
|
||||
|
||||
# =====
|
||||
_RetvalT = TypeVar("_RetvalT")
|
||||
|
||||
|
||||
# =====
|
||||
class HwInfoSubmanager(BaseInfoSubmanager):
|
||||
def __init__(
|
||||
self,
|
||||
vcgencmd_cmd: list[str],
|
||||
ignore_past: bool,
|
||||
state_poll: float,
|
||||
) -> None:
|
||||
|
||||
self.__vcgencmd_cmd = vcgencmd_cmd
|
||||
self.__ignore_past = ignore_past
|
||||
self.__state_poll = state_poll
|
||||
|
||||
self.__dt_cache: dict[str, str] = {}
|
||||
|
||||
#async def get_state(self) -> dict:
|
||||
# (model, serial, cpu_temp, throttling) = await asyncio.gather(
|
||||
# self.__read_dt_file("model"),
|
||||
# self.__read_dt_file("serial-number"),
|
||||
# self.__get_cpu_temp(),
|
||||
# self.__get_throttling(),
|
||||
# )
|
||||
# return {
|
||||
# "platform": {
|
||||
# "type": "rpi",
|
||||
# "base": model,
|
||||
# "serial": serial,
|
||||
# },
|
||||
# "health": {
|
||||
# "temp": {
|
||||
# "cpu": cpu_temp,
|
||||
# },
|
||||
# "throttling": throttling,
|
||||
# },
|
||||
# }
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
prev_state: dict = {}
|
||||
while True:
|
||||
state = await self.get_state()
|
||||
if state != prev_state:
|
||||
yield state
|
||||
prev_state = state
|
||||
await asyncio.sleep(self.__state_poll)
|
||||
|
||||
# =====
|
||||
|
||||
async def __read_dt_file(self, name: str) -> (str | None):
|
||||
if name not in self.__dt_cache:
|
||||
path = os.path.join(f"{env.PROCFS_PREFIX}/proc/device-tree", name)
|
||||
try:
|
||||
self.__dt_cache[name] = (await aiotools.read_file(path)).strip(" \t\r\n\0")
|
||||
except Exception as err:
|
||||
get_logger(0).error("Can't read DT %s from %s: %s", name, path, err)
|
||||
return None
|
||||
return self.__dt_cache[name]
|
||||
|
||||
async def __get_cpu_temp(self) -> (float | None):
|
||||
temp_path = f"{env.SYSFS_PREFIX}/sys/class/thermal/thermal_zone0/temp"
|
||||
try:
|
||||
return int((await aiotools.read_file(temp_path)).strip()) / 1000
|
||||
except Exception as err:
|
||||
get_logger(0).error("Can't read CPU temp from %s: %s", temp_path, err)
|
||||
return None
|
||||
|
||||
async def __get_throttling(self) -> (dict | None):
|
||||
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=147781&start=50#p972790
|
||||
flags = await self.__parse_vcgencmd(
|
||||
arg="get_throttled",
|
||||
parser=(lambda text: int(text.split("=")[-1].strip(), 16)),
|
||||
)
|
||||
if flags is not None:
|
||||
return {
|
||||
"raw_flags": flags,
|
||||
"parsed_flags": {
|
||||
"undervoltage": {
|
||||
"now": bool(flags & (1 << 0)),
|
||||
"past": bool(flags & (1 << 16)),
|
||||
},
|
||||
"freq_capped": {
|
||||
"now": bool(flags & (1 << 1)),
|
||||
"past": bool(flags & (1 << 17)),
|
||||
},
|
||||
"throttled": {
|
||||
"now": bool(flags & (1 << 2)),
|
||||
"past": bool(flags & (1 << 18)),
|
||||
},
|
||||
},
|
||||
"ignore_past": self.__ignore_past,
|
||||
}
|
||||
return None
|
||||
|
||||
async def __parse_vcgencmd(self, arg: str, parser: Callable[[str], _RetvalT]) -> (_RetvalT | None):
|
||||
cmd = [*self.__vcgencmd_cmd, arg]
|
||||
try:
|
||||
text = (await aioproc.read_process(cmd, err_to_null=True))[1]
|
||||
except Exception:
|
||||
get_logger(0).exception("Error while executing: %s", tools.cmdfmt(cmd))
|
||||
return None
|
||||
try:
|
||||
return parser(text)
|
||||
except Exception as err:
|
||||
get_logger(0).error("Can't parse [ %s ] output: %r: %s", tools.cmdfmt(cmd), text, tools.efmt(err))
|
||||
return None
|
||||
3651
patches/janus.js
Normal file
3651
patches/janus.js
Normal file
File diff suppressed because it is too large
Load Diff
13
patches/onecloud_gpio.sh
Normal file
13
patches/onecloud_gpio.sh
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
case $1 in
|
||||
short)
|
||||
gpioset -m time -s 1 gpiochip1 7=0
|
||||
gpioset gpiochip1 7=1
|
||||
;;
|
||||
long)
|
||||
gpioset -m time -s 5 gpiochip1 7=0
|
||||
gpioset gpiochip1 7=1
|
||||
;;
|
||||
*)
|
||||
echo "No thing."
|
||||
esac
|
||||
417
patches/session.js
Normal file
417
patches/session.js
Normal file
@@ -0,0 +1,417 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
|
||||
import {tools, $} from "../tools.js";
|
||||
import {wm} from "../wm.js";
|
||||
|
||||
import {Recorder} from "./recorder.js";
|
||||
import {Hid} from "./hid.js";
|
||||
import {Atx} from "./atx.js";
|
||||
import {Msd} from "./msd.js";
|
||||
import {Streamer} from "./stream.js";
|
||||
import {Gpio} from "./gpio.js";
|
||||
import {Ocr} from "./ocr.js";
|
||||
|
||||
|
||||
export function Session() {
|
||||
// var self = this;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
var __ws = null;
|
||||
|
||||
var __ping_timer = null;
|
||||
var __missed_heartbeats = 0;
|
||||
|
||||
var __streamer = new Streamer();
|
||||
var __recorder = new Recorder();
|
||||
var __hid = new Hid(__streamer.getGeometry, __recorder);
|
||||
var __atx = new Atx(__recorder);
|
||||
var __msd = new Msd();
|
||||
var __gpio = new Gpio(__recorder);
|
||||
var __ocr = new Ocr(__streamer.getGeometry);
|
||||
|
||||
var __info_hw_state = null;
|
||||
var __info_fan_state = null;
|
||||
|
||||
var __init__ = function() {
|
||||
__startSession();
|
||||
};
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
var __setAboutInfoMeta = function(state) {
|
||||
if (state !== null) {
|
||||
let text = JSON.stringify(state, undefined, 4).replace(/ /g, " ").replace(/\n/g, "<br>");
|
||||
$("about-meta").innerHTML = `
|
||||
<span class="code-comment">// The PiKVM metadata.<br>
|
||||
// You can get this JSON using handle <a target="_blank" href="/api/info?fields=meta">/api/info?fields=meta</a>.<br>
|
||||
// In the standard configuration this data<br>
|
||||
// is specified in the file /etc/kvmd/meta.yaml.</span><br>
|
||||
<br>
|
||||
${text}
|
||||
`;
|
||||
if (state.server && state.server.host) {
|
||||
$("kvmd-meta-server-host").innerHTML = `Server: ${state.server.host}`;
|
||||
document.title = `PiKVM Session: ${state.server.host}`;
|
||||
} else {
|
||||
$("kvmd-meta-server-host").innerHTML = "";
|
||||
document.title = "PiKVM Session";
|
||||
}
|
||||
|
||||
// Don't use this option, it may be removed in any time
|
||||
if (state.web && state.web.confirm_session_exit === false) {
|
||||
window.onbeforeunload = null; // See main.js
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var __setAboutInfoHw = function(state) {
|
||||
if (state.health.throttling !== null) {
|
||||
let flags = state.health.throttling.parsed_flags;
|
||||
let ignore_past = state.health.throttling.ignore_past;
|
||||
let undervoltage = (flags.undervoltage.now || (flags.undervoltage.past && !ignore_past));
|
||||
let freq_capped = (flags.freq_capped.now || (flags.freq_capped.past && !ignore_past));
|
||||
|
||||
tools.hidden.setVisible($("hw-health-dropdown"), (undervoltage || freq_capped));
|
||||
$("hw-health-undervoltage-led").className = (undervoltage ? (flags.undervoltage.now ? "led-red" : "led-yellow") : "hidden");
|
||||
$("hw-health-overheating-led").className = (freq_capped ? (flags.freq_capped.now ? "led-red" : "led-yellow") : "hidden");
|
||||
tools.hidden.setVisible($("hw-health-message-undervoltage"), undervoltage);
|
||||
tools.hidden.setVisible($("hw-health-message-overheating"), freq_capped);
|
||||
}
|
||||
__info_hw_state = state;
|
||||
__renderAboutInfoHardware();
|
||||
};
|
||||
|
||||
var __setAboutInfoFan = function(state) {
|
||||
let failed = false;
|
||||
let failed_past = false;
|
||||
if (state.monitored) {
|
||||
if (state.state === null) {
|
||||
failed = true;
|
||||
} else {
|
||||
if (!state.state.fan.ok) {
|
||||
failed = true;
|
||||
} else if (state.state.fan.last_fail_ts >= 0) {
|
||||
failed = true;
|
||||
failed_past = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
tools.hidden.setVisible($("fan-health-dropdown"), failed);
|
||||
$("fan-health-led").className = (failed ? (failed_past ? "led-yellow" : "led-red") : "hidden");
|
||||
|
||||
__info_fan_state = state;
|
||||
__renderAboutInfoHardware();
|
||||
};
|
||||
|
||||
var __renderAboutInfoHardware = function() {
|
||||
let html = "";
|
||||
if (__info_hw_state !== null) {
|
||||
html += `
|
||||
Platform:
|
||||
${__formatPlatform(__info_hw_state.platform)}
|
||||
<hr>
|
||||
Temperature:
|
||||
${__formatTemp(__info_hw_state.health.temp)}
|
||||
<hr>
|
||||
Throttling:
|
||||
${__formatThrottling(__info_hw_state.health.throttling)}
|
||||
`;
|
||||
}
|
||||
if (__info_fan_state !== null) {
|
||||
if (html.length > 0) {
|
||||
html += "<hr>";
|
||||
}
|
||||
html += `
|
||||
Fan:
|
||||
${__formatFan(__info_fan_state)}
|
||||
`;
|
||||
}
|
||||
$("about-hardware").innerHTML = html;
|
||||
};
|
||||
|
||||
var __formatPlatform = function(state) {
|
||||
return __formatUl([["Base", state.base], ["Serial", state.serial]]);
|
||||
};
|
||||
|
||||
var __formatFan = function(state) {
|
||||
if (!state.monitored) {
|
||||
return __formatUl([["Status", "Not monitored"]]);
|
||||
} else if (state.state === null) {
|
||||
return __formatUl([["Status", __colored("red", "Not available")]]);
|
||||
} else {
|
||||
state = state.state;
|
||||
let pairs = [
|
||||
["Status", (state.fan.ok ? __colored("green", "Ok") : __colored("red", "Failed"))],
|
||||
["Desired speed", `${state.fan.speed}%`],
|
||||
["PWM", `${state.fan.pwm}`],
|
||||
];
|
||||
if (state.hall.available) {
|
||||
pairs.push(["RPM", __colored((state.fan.ok ? "green" : "red"), state.hall.rpm)]);
|
||||
}
|
||||
return __formatUl(pairs);
|
||||
}
|
||||
};
|
||||
|
||||
var __formatTemp = function(temp) {
|
||||
let pairs = [];
|
||||
for (let field of Object.keys(temp).sort()) {
|
||||
pairs.push([field.toUpperCase(), `${temp[field]}°C`]);
|
||||
}
|
||||
return __formatUl(pairs);
|
||||
};
|
||||
|
||||
var __formatThrottling = function(throttling) {
|
||||
if (throttling !== null) {
|
||||
let pairs = [];
|
||||
for (let field of Object.keys(throttling.parsed_flags).sort()) {
|
||||
let flags = throttling.parsed_flags[field];
|
||||
let key = tools.upperFirst(field).replace("_", " ");
|
||||
let value = (flags["now"] ? __colored("red", "RIGHT NOW") : __colored("green", "No"));
|
||||
if (!throttling.ignore_past) {
|
||||
value += "; " + (flags["past"] ? __colored("red", "In the past") : __colored("green", "Never"));
|
||||
}
|
||||
pairs.push([key, value]);
|
||||
}
|
||||
return __formatUl(pairs);
|
||||
} else {
|
||||
return "NO DATA";
|
||||
}
|
||||
};
|
||||
|
||||
var __colored = function(color, text) {
|
||||
return `<font color="${color}">${text}</font>`;
|
||||
};
|
||||
|
||||
var __setAboutInfoSystem = function(state) {
|
||||
$("about-version").innerHTML = `
|
||||
KVMD: <span class="code-comment">${state.kvmd.version}</span><br>
|
||||
<hr>
|
||||
Streamer: <span class="code-comment">${state.streamer.version} (${state.streamer.app})</span>
|
||||
${__formatStreamerFeatures(state.streamer.features)}
|
||||
<hr>
|
||||
${state.kernel.system} kernel:
|
||||
${__formatUname(state.kernel)}
|
||||
`;
|
||||
$("kvmd-version-kvmd").innerHTML = state.kvmd.version;
|
||||
$("kvmd-version-streamer").innerHTML = state.streamer.version;
|
||||
};
|
||||
|
||||
var __formatStreamerFeatures = function(features) {
|
||||
let pairs = [];
|
||||
for (let field of Object.keys(features).sort()) {
|
||||
pairs.push([field, (features[field] ? "Yes" : "No")]);
|
||||
}
|
||||
return __formatUl(pairs);
|
||||
};
|
||||
|
||||
var __formatUname = function(kernel) {
|
||||
let pairs = [];
|
||||
for (let field of Object.keys(kernel).sort()) {
|
||||
if (field !== "system") {
|
||||
pairs.push([tools.upperFirst(field), kernel[field]]);
|
||||
}
|
||||
}
|
||||
return __formatUl(pairs);
|
||||
};
|
||||
|
||||
var __formatUl = function(pairs) {
|
||||
let text = "<ul>";
|
||||
for (let pair of pairs) {
|
||||
text += `<li>${pair[0]}: <span class="code-comment">${pair[1]}</span></li>`;
|
||||
}
|
||||
return text + "</ul>";
|
||||
};
|
||||
|
||||
var __setExtras = function(state) {
|
||||
let show_hook = null;
|
||||
let close_hook = null;
|
||||
let has_webterm = (state.webterm && (state.webterm.enabled || state.webterm.started));
|
||||
if (has_webterm) {
|
||||
let path = "/" + state.webterm.path;
|
||||
show_hook = function() {
|
||||
tools.info("Terminal opened: ", path);
|
||||
$("webterm-iframe").src = path;
|
||||
};
|
||||
close_hook = function() {
|
||||
tools.info("Terminal closed");
|
||||
$("webterm-iframe").src = "";
|
||||
};
|
||||
}
|
||||
tools.feature.setEnabled($("system-tool-webterm"), has_webterm);
|
||||
$("webterm-window").show_hook = show_hook;
|
||||
$("webterm-window").close_hook = close_hook;
|
||||
|
||||
__streamer.setJanusEnabled(
|
||||
(state.janus && (state.janus.enabled || state.janus.started))
|
||||
|| (state.janus_static && (state.janus_static.enabled || state.janus_static.started))
|
||||
);
|
||||
};
|
||||
|
||||
var __startSession = function() {
|
||||
$("link-led").className = "led-yellow";
|
||||
$("link-led").title = "Connecting...";
|
||||
|
||||
let http = tools.makeRequest("GET", "/api/auth/check", function() {
|
||||
if (http.readyState === 4) {
|
||||
if (http.status === 200) {
|
||||
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws`);
|
||||
__ws.sendHidEvent = (event) => __sendHidEvent(__ws, event.event_type, event.event);
|
||||
__ws.onopen = __wsOpenHandler;
|
||||
__ws.onmessage = __wsMessageHandler;
|
||||
__ws.onerror = __wsErrorHandler;
|
||||
__ws.onclose = __wsCloseHandler;
|
||||
} else if (http.status === 401 || http.status === 403) {
|
||||
window.onbeforeunload = () => null;
|
||||
wm.error("Unexpected logout occured, please login again").then(function() {
|
||||
document.location.href = "/login";
|
||||
});
|
||||
} else {
|
||||
__wsCloseHandler(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var __ascii_encoder = new TextEncoder("ascii");
|
||||
|
||||
var __sendHidEvent = function(ws, event_type, event) {
|
||||
if (event_type == "key") {
|
||||
let data = __ascii_encoder.encode("\x01\x00" + event.key);
|
||||
data[1] = (event.state ? 1 : 0);
|
||||
ws.send(data);
|
||||
|
||||
} else if (event_type == "mouse_button") {
|
||||
let data = __ascii_encoder.encode("\x02\x00" + event.button);
|
||||
data[1] = (event.state ? 1 : 0);
|
||||
ws.send(data);
|
||||
|
||||
} else if (event_type == "mouse_move") {
|
||||
let data = new Uint8Array([
|
||||
3,
|
||||
(event.to.x >> 8) & 0xFF, event.to.x & 0xFF,
|
||||
(event.to.y >> 8) & 0xFF, event.to.y & 0xFF,
|
||||
]);
|
||||
ws.send(data);
|
||||
|
||||
} else if (event_type == "mouse_relative" || event_type == "mouse_wheel") {
|
||||
let data;
|
||||
if (Array.isArray(event.delta)) {
|
||||
data = new Int8Array(2 + event.delta.length * 2);
|
||||
let index = 0;
|
||||
for (let delta of event.delta) {
|
||||
data[index + 2] = delta["x"];
|
||||
data[index + 3] = delta["y"];
|
||||
index += 2;
|
||||
}
|
||||
} else {
|
||||
data = new Int8Array([0, 0, event.delta.x, event.delta.y]);
|
||||
}
|
||||
data[0] = (event_type == "mouse_relative" ? 4 : 5);
|
||||
data[1] = (event.squash ? 1 : 0);
|
||||
ws.send(data);
|
||||
}
|
||||
};
|
||||
|
||||
var __wsOpenHandler = function(event) {
|
||||
tools.debug("Session: socket opened:", event);
|
||||
$("link-led").className = "led-green";
|
||||
$("link-led").title = "Connected";
|
||||
__recorder.setSocket(__ws);
|
||||
__hid.setSocket(__ws);
|
||||
__missed_heartbeats = 0;
|
||||
__ping_timer = setInterval(__pingServer, 1000);
|
||||
};
|
||||
|
||||
var __wsMessageHandler = function(event) {
|
||||
// tools.debug("Session: received socket data:", event.data);
|
||||
let data = JSON.parse(event.data);
|
||||
switch (data.event_type) {
|
||||
case "pong": __missed_heartbeats = 0; break;
|
||||
case "info_meta_state": __setAboutInfoMeta(data.event); break;
|
||||
case "info_hw_state": __setAboutInfoHw(data.event); break;
|
||||
case "info_fan_state": __setAboutInfoFan(data.event); break;
|
||||
case "info_system_state": __setAboutInfoSystem(data.event); break;
|
||||
case "info_extras_state": __setExtras(data.event); break;
|
||||
case "gpio_model_state": __gpio.setModel(data.event); break;
|
||||
case "gpio_state": __gpio.setState(data.event); break;
|
||||
case "hid_keymaps_state": __hid.setKeymaps(data.event); break;
|
||||
case "hid_state": __hid.setState(data.event); break;
|
||||
case "atx_state": __atx.setState(data.event); break;
|
||||
case "msd_state": __msd.setState(data.event); break;
|
||||
case "streamer_state": __streamer.setState(data.event); break;
|
||||
case "streamer_ocr_state": __ocr.setState(data.event); break;
|
||||
}
|
||||
};
|
||||
|
||||
var __wsErrorHandler = function(event) {
|
||||
tools.error("Session: socket error:", event);
|
||||
if (__ws) {
|
||||
__ws.onclose = null;
|
||||
__ws.close();
|
||||
__wsCloseHandler(null);
|
||||
}
|
||||
};
|
||||
|
||||
var __wsCloseHandler = function(event) {
|
||||
tools.debug("Session: socket closed:", event);
|
||||
|
||||
$("link-led").className = "led-gray";
|
||||
|
||||
if (__ping_timer) {
|
||||
clearInterval(__ping_timer);
|
||||
__ping_timer = null;
|
||||
}
|
||||
|
||||
__ocr.setState(null);
|
||||
__gpio.setState(null);
|
||||
__hid.setSocket(null);
|
||||
__recorder.setSocket(null);
|
||||
__atx.setState(null);
|
||||
__msd.setState(null);
|
||||
__streamer.setState(null);
|
||||
__ws = null;
|
||||
|
||||
setTimeout(function() {
|
||||
$("link-led").className = "led-yellow";
|
||||
setTimeout(__startSession, 500);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
var __pingServer = function() {
|
||||
try {
|
||||
__missed_heartbeats += 1;
|
||||
if (__missed_heartbeats >= 15) {
|
||||
throw new Error("Too many missed heartbeats");
|
||||
}
|
||||
__ws.send("{\"event_type\": \"ping\", \"event\": {}}");
|
||||
} catch (err) {
|
||||
__wsErrorHandler(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
__init__();
|
||||
}
|
||||
3
patches/stream.sh
Normal file
3
patches/stream.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
/usr/bin/ustreamer-dump --sink kvmd::ustreamer::jpeg --output - | /usr/bin/ffmpeg -re -use_wallclock_as_timestamps 1 -i pipe: -rtbufsize 10M -c:v libx264 -pix_fmt yuv420p -preset:v ultrafast -tune:v zerolatency -profile:v baseline -bf 0 -b:v 3M -maxrate:v 5M -r 10 -g 10 -an -f rtp rtp://127.0.0.1:5004
|
||||
43
patches/stream_when_ustream_exists.sh
Normal file
43
patches/stream_when_ustream_exists.sh
Normal file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 存储 stream.sh 的进程 ID
|
||||
b_pid=""
|
||||
|
||||
|
||||
cleanup() {
|
||||
echo "Received SIGINT. Terminating the process group..."
|
||||
[ -n "$b_pid" ] && pkill -9 -g $b_pid # 终止整个进程组
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 捕获 SIGINT 信号
|
||||
trap cleanup SIGINT
|
||||
|
||||
|
||||
while true; do
|
||||
# 检测是否有包含 "ustreamer" 的进程
|
||||
if pgrep -f "/usr/bin/ustreamer " > /dev/null; then
|
||||
# 如果存在,但是 stream.sh 进程不存在,执行 stream.sh 并记录其进程 ID
|
||||
if [ -z "$b_pid" ]; then
|
||||
echo "Found a process with 'ustreamer' in the command. Executing stream.sh in the background..."
|
||||
setsid /usr/share/kvmd/stream.sh &
|
||||
b_pid=$(ps -o pgid= $!)
|
||||
echo "stream.sh started with PID: $b_pid"
|
||||
else
|
||||
echo "Process with 'ustreamer' is already running. Skipping..."
|
||||
fi
|
||||
else
|
||||
# 如果不存在 "ustreamer" 进程,但是 stream.sh 进程存在,终止 stream.sh 并清除进程 ID
|
||||
if [ -n "$b_pid" ]; then
|
||||
echo "No process with 'ustreamer' found. Terminating stream.sh (PID: $b_pid)..."
|
||||
pkill -9 -g $b_pid
|
||||
b_pid=""
|
||||
else
|
||||
echo "No process with 'ustreamer' found. Waiting for the next check..."
|
||||
fi
|
||||
fi
|
||||
|
||||
# 等待一段时间,可以根据需要调整等待的时间间隔
|
||||
sleep 1
|
||||
|
||||
done
|
||||
482
patches/streamer.py.1
Normal file
482
patches/streamer.py.1
Normal file
@@ -0,0 +1,482 @@
|
||||
# ========================================================================== #
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2023 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
import io
|
||||
import signal
|
||||
import asyncio
|
||||
import asyncio.subprocess
|
||||
import dataclasses
|
||||
import functools
|
||||
|
||||
from typing import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
from PIL import Image as PilImage
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ... import tools
|
||||
from ... import aiotools
|
||||
from ... import aioproc
|
||||
from ... import htclient
|
||||
|
||||
|
||||
# =====
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class StreamerSnapshot:
|
||||
online: bool
|
||||
width: int
|
||||
height: int
|
||||
headers: tuple[tuple[str, str], ...]
|
||||
data: bytes
|
||||
|
||||
async def make_preview(self, max_width: int, max_height: int, quality: int) -> bytes:
|
||||
assert max_width >= 0
|
||||
assert max_height >= 0
|
||||
assert quality > 0
|
||||
|
||||
if max_width == 0 and max_height == 0:
|
||||
max_width = self.width // 5
|
||||
max_height = self.height // 5
|
||||
else:
|
||||
max_width = min((max_width or self.width), self.width)
|
||||
max_height = min((max_height or self.height), self.height)
|
||||
|
||||
if (max_width, max_height) == (self.width, self.height):
|
||||
return self.data
|
||||
return (await aiotools.run_async(self.__inner_make_preview, max_width, max_height, quality))
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def __inner_make_preview(self, max_width: int, max_height: int, quality: int) -> bytes:
|
||||
with io.BytesIO(self.data) as snapshot_bio:
|
||||
with io.BytesIO() as preview_bio:
|
||||
with PilImage.open(snapshot_bio) as image:
|
||||
image.thumbnail((max_width, max_height), PilImage.Resampling.LANCZOS)
|
||||
image.save(preview_bio, format="jpeg", quality=quality)
|
||||
return preview_bio.getvalue()
|
||||
|
||||
|
||||
class _StreamerParams:
|
||||
__DESIRED_FPS = "desired_fps"
|
||||
|
||||
__QUALITY = "quality"
|
||||
|
||||
__RESOLUTION = "resolution"
|
||||
__AVAILABLE_RESOLUTIONS = "available_resolutions"
|
||||
|
||||
__H264_BITRATE = "h264_bitrate"
|
||||
__H264_GOP = "h264_gop"
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
quality: int,
|
||||
|
||||
resolution: str,
|
||||
available_resolutions: list[str],
|
||||
|
||||
desired_fps: int,
|
||||
desired_fps_min: int,
|
||||
desired_fps_max: int,
|
||||
|
||||
h264_bitrate: int,
|
||||
h264_bitrate_min: int,
|
||||
h264_bitrate_max: int,
|
||||
|
||||
h264_gop: int,
|
||||
h264_gop_min: int,
|
||||
h264_gop_max: int,
|
||||
) -> None:
|
||||
|
||||
self.__has_quality = bool(quality)
|
||||
self.__has_resolution = bool(resolution)
|
||||
self.__has_h264 = bool(h264_bitrate)
|
||||
|
||||
self.__params: dict = {self.__DESIRED_FPS: min(max(desired_fps, desired_fps_min), desired_fps_max)}
|
||||
self.__limits: dict = {self.__DESIRED_FPS: {"min": desired_fps_min, "max": desired_fps_max}}
|
||||
|
||||
if self.__has_quality:
|
||||
self.__params[self.__QUALITY] = quality
|
||||
|
||||
if self.__has_resolution:
|
||||
self.__params[self.__RESOLUTION] = resolution
|
||||
self.__limits[self.__AVAILABLE_RESOLUTIONS] = available_resolutions
|
||||
|
||||
if self.__has_h264:
|
||||
self.__params[self.__H264_BITRATE] = min(max(h264_bitrate, h264_bitrate_min), h264_bitrate_max)
|
||||
self.__limits[self.__H264_BITRATE] = {"min": h264_bitrate_min, "max": h264_bitrate_max}
|
||||
self.__params[self.__H264_GOP] = min(max(h264_gop, h264_gop_min), h264_gop_max)
|
||||
self.__limits[self.__H264_GOP] = {"min": h264_gop_min, "max": h264_gop_max}
|
||||
|
||||
def get_features(self) -> dict:
|
||||
return {
|
||||
self.__QUALITY: self.__has_quality,
|
||||
self.__RESOLUTION: self.__has_resolution,
|
||||
"h264": self.__has_h264,
|
||||
}
|
||||
|
||||
def get_limits(self) -> dict:
|
||||
limits = dict(self.__limits)
|
||||
if self.__has_resolution:
|
||||
limits[self.__AVAILABLE_RESOLUTIONS] = list(limits[self.__AVAILABLE_RESOLUTIONS])
|
||||
return limits
|
||||
|
||||
def get_params(self) -> dict:
|
||||
return dict(self.__params)
|
||||
|
||||
def set_params(self, params: dict) -> None:
|
||||
new_params = dict(self.__params)
|
||||
|
||||
if self.__QUALITY in params and self.__has_quality:
|
||||
new_params[self.__QUALITY] = min(max(params[self.__QUALITY], 1), 100)
|
||||
|
||||
if self.__RESOLUTION in params and self.__has_resolution:
|
||||
if params[self.__RESOLUTION] in self.__limits[self.__AVAILABLE_RESOLUTIONS]:
|
||||
new_params[self.__RESOLUTION] = params[self.__RESOLUTION]
|
||||
|
||||
for (key, enabled) in [
|
||||
(self.__DESIRED_FPS, True),
|
||||
(self.__H264_BITRATE, self.__has_h264),
|
||||
(self.__H264_GOP, self.__has_h264),
|
||||
]:
|
||||
if key in params and enabled:
|
||||
if self.__check_limits_min_max(key, params[key]):
|
||||
new_params[key] = params[key]
|
||||
|
||||
self.__params = new_params
|
||||
|
||||
def __check_limits_min_max(self, key: str, value: int) -> bool:
|
||||
return (self.__limits[key]["min"] <= value <= self.__limits[key]["max"])
|
||||
|
||||
|
||||
class Streamer: # pylint: disable=too-many-instance-attributes
|
||||
def __init__( # pylint: disable=too-many-arguments,too-many-locals
|
||||
self,
|
||||
|
||||
reset_delay: float,
|
||||
shutdown_delay: float,
|
||||
state_poll: float,
|
||||
|
||||
unix_path: str,
|
||||
timeout: float,
|
||||
|
||||
process_name_prefix: str,
|
||||
|
||||
pre_start_cmd: list[str],
|
||||
pre_start_cmd_remove: list[str],
|
||||
pre_start_cmd_append: list[str],
|
||||
|
||||
cmd: list[str],
|
||||
cmd_remove: list[str],
|
||||
cmd_append: list[str],
|
||||
|
||||
post_stop_cmd: list[str],
|
||||
post_stop_cmd_remove: list[str],
|
||||
post_stop_cmd_append: list[str],
|
||||
|
||||
**params_kwargs: Any,
|
||||
) -> None:
|
||||
|
||||
self.__reset_delay = reset_delay
|
||||
self.__shutdown_delay = shutdown_delay
|
||||
self.__state_poll = state_poll
|
||||
|
||||
self.__unix_path = unix_path
|
||||
self.__timeout = timeout
|
||||
|
||||
self.__process_name_prefix = process_name_prefix
|
||||
|
||||
self.__pre_start_cmd = tools.build_cmd(pre_start_cmd, pre_start_cmd_remove, pre_start_cmd_append)
|
||||
self.__cmd = tools.build_cmd(cmd, cmd_remove, cmd_append)
|
||||
self.__post_stop_cmd = tools.build_cmd(post_stop_cmd, post_stop_cmd_remove, post_stop_cmd_append)
|
||||
|
||||
self.__params = _StreamerParams(**params_kwargs)
|
||||
|
||||
self.__stop_task: (asyncio.Task | None) = None
|
||||
self.__stop_wip = False
|
||||
|
||||
self.__streamer_task: (asyncio.Task | None) = None
|
||||
self.__streamer_proc: (asyncio.subprocess.Process | None) = None # pylint: disable=no-member
|
||||
|
||||
self.__http_session: (aiohttp.ClientSession | None) = None
|
||||
|
||||
self.__snapshot: (StreamerSnapshot | None) = None
|
||||
|
||||
self.__notifier = aiotools.AioNotifier()
|
||||
|
||||
# =====
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def ensure_start(self, reset: bool) -> None:
|
||||
if not self.__streamer_task or self.__stop_task:
|
||||
logger = get_logger(0)
|
||||
|
||||
if self.__stop_task:
|
||||
if not self.__stop_wip:
|
||||
self.__stop_task.cancel()
|
||||
await asyncio.gather(self.__stop_task, return_exceptions=True)
|
||||
logger.info("Streamer stop cancelled")
|
||||
return
|
||||
else:
|
||||
await asyncio.gather(self.__stop_task, return_exceptions=True)
|
||||
|
||||
if reset and self.__reset_delay > 0:
|
||||
logger.info("Waiting %.2f seconds for reset delay ...", self.__reset_delay)
|
||||
await asyncio.sleep(self.__reset_delay)
|
||||
logger.info("Starting streamer ...")
|
||||
await self.__inner_start()
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def ensure_stop(self, immediately: bool) -> None:
|
||||
if self.__streamer_task:
|
||||
logger = get_logger(0)
|
||||
|
||||
if immediately:
|
||||
if self.__stop_task:
|
||||
if not self.__stop_wip:
|
||||
self.__stop_task.cancel()
|
||||
await asyncio.gather(self.__stop_task, return_exceptions=True)
|
||||
logger.info("Stopping streamer immediately ...")
|
||||
await self.__inner_stop()
|
||||
else:
|
||||
await asyncio.gather(self.__stop_task, return_exceptions=True)
|
||||
else:
|
||||
logger.info("Stopping streamer immediately ...")
|
||||
await self.__inner_stop()
|
||||
|
||||
elif not self.__stop_task:
|
||||
|
||||
async def delayed_stop() -> None:
|
||||
try:
|
||||
await asyncio.sleep(self.__shutdown_delay)
|
||||
self.__stop_wip = True
|
||||
logger.info("Stopping streamer after delay ...")
|
||||
await self.__inner_stop()
|
||||
finally:
|
||||
self.__stop_task = None
|
||||
self.__stop_wip = False
|
||||
|
||||
logger.info("Planning to stop streamer in %.2f seconds ...", self.__shutdown_delay)
|
||||
self.__stop_task = asyncio.create_task(delayed_stop())
|
||||
|
||||
def is_working(self) -> bool:
|
||||
# Запущено и не планирует останавливаться
|
||||
return bool(self.__streamer_task and not self.__stop_task)
|
||||
|
||||
# =====
|
||||
|
||||
def set_params(self, params: dict) -> None:
|
||||
assert not self.__streamer_task
|
||||
return self.__params.set_params(params)
|
||||
|
||||
def get_params(self) -> dict:
|
||||
return self.__params.get_params()
|
||||
|
||||
# =====
|
||||
|
||||
async def get_state(self) -> dict:
|
||||
streamer_state = None
|
||||
if self.__streamer_task:
|
||||
session = self.__ensure_http_session()
|
||||
try:
|
||||
async with session.get(self.__make_url("state")) as response:
|
||||
htclient.raise_not_200(response)
|
||||
streamer_state = (await response.json())["result"]
|
||||
except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError):
|
||||
pass
|
||||
except Exception:
|
||||
get_logger().exception("Invalid streamer response from /state")
|
||||
|
||||
snapshot: (dict | None) = None
|
||||
if self.__snapshot:
|
||||
snapshot = dataclasses.asdict(self.__snapshot)
|
||||
del snapshot["headers"]
|
||||
del snapshot["data"]
|
||||
|
||||
return {
|
||||
"limits": self.__params.get_limits(),
|
||||
"params": self.__params.get_params(),
|
||||
"snapshot": {"saved": snapshot},
|
||||
"streamer": streamer_state,
|
||||
"features": self.__params.get_features(),
|
||||
}
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
def signal_handler(*_: Any) -> None:
|
||||
get_logger(0).info("Got SIGUSR2, checking the stream state ...")
|
||||
self.__notifier.notify()
|
||||
|
||||
get_logger(0).info("Installing SIGUSR2 streamer handler ...")
|
||||
asyncio.get_event_loop().add_signal_handler(signal.SIGUSR2, signal_handler)
|
||||
|
||||
waiter_task: (asyncio.Task | None) = None
|
||||
prev_state: dict = {}
|
||||
while True:
|
||||
state = await self.get_state()
|
||||
if state != prev_state:
|
||||
yield state
|
||||
prev_state = state
|
||||
|
||||
if waiter_task is None:
|
||||
waiter_task = asyncio.create_task(self.__notifier.wait())
|
||||
if waiter_task in (await aiotools.wait_first(
|
||||
asyncio.ensure_future(asyncio.sleep(self.__state_poll)),
|
||||
waiter_task,
|
||||
))[0]:
|
||||
waiter_task = None
|
||||
|
||||
# =====
|
||||
|
||||
async def take_snapshot(self, save: bool, load: bool, allow_offline: bool) -> (StreamerSnapshot | None):
|
||||
if load:
|
||||
return self.__snapshot
|
||||
else:
|
||||
logger = get_logger()
|
||||
session = self.__ensure_http_session()
|
||||
try:
|
||||
async with session.get(self.__make_url("snapshot")) as response:
|
||||
htclient.raise_not_200(response)
|
||||
online = (response.headers["X-UStreamer-Online"] == "true")
|
||||
if online or allow_offline:
|
||||
snapshot = StreamerSnapshot(
|
||||
online=online,
|
||||
width=int(response.headers["X-UStreamer-Width"]),
|
||||
height=int(response.headers["X-UStreamer-Height"]),
|
||||
headers=tuple(
|
||||
(key, value)
|
||||
for (key, value) in tools.sorted_kvs(dict(response.headers))
|
||||
if key.lower().startswith("x-ustreamer-") or key.lower() in [
|
||||
"x-timestamp",
|
||||
"access-control-allow-origin",
|
||||
"cache-control",
|
||||
"pragma",
|
||||
"expires",
|
||||
]
|
||||
),
|
||||
data=bytes(await response.read()),
|
||||
)
|
||||
if save:
|
||||
self.__snapshot = snapshot
|
||||
self.__notifier.notify()
|
||||
return snapshot
|
||||
logger.error("Stream is offline, no signal or so")
|
||||
except (aiohttp.ClientConnectionError, aiohttp.ServerConnectionError) as err:
|
||||
logger.error("Can't connect to streamer: %s", tools.efmt(err))
|
||||
except Exception:
|
||||
logger.exception("Invalid streamer response from /snapshot")
|
||||
return None
|
||||
|
||||
def remove_snapshot(self) -> None:
|
||||
self.__snapshot = None
|
||||
|
||||
# =====
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def cleanup(self) -> None:
|
||||
await self.ensure_stop(immediately=True)
|
||||
if self.__http_session:
|
||||
await self.__http_session.close()
|
||||
self.__http_session = None
|
||||
|
||||
# =====
|
||||
|
||||
def __ensure_http_session(self) -> aiohttp.ClientSession:
|
||||
if not self.__http_session:
|
||||
kwargs: dict = {
|
||||
"headers": {"User-Agent": htclient.make_user_agent("KVMD")},
|
||||
"connector": aiohttp.UnixConnector(path=self.__unix_path),
|
||||
"timeout": aiohttp.ClientTimeout(total=self.__timeout),
|
||||
}
|
||||
self.__http_session = aiohttp.ClientSession(**kwargs)
|
||||
return self.__http_session
|
||||
|
||||
def __make_url(self, handle: str) -> str:
|
||||
assert not handle.startswith("/"), handle
|
||||
return f"http://localhost:0/{handle}"
|
||||
|
||||
# =====
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def __inner_start(self) -> None:
|
||||
assert not self.__streamer_task
|
||||
await self.__run_hook("PRE-START-CMD", self.__pre_start_cmd)
|
||||
self.__streamer_task = asyncio.create_task(self.__streamer_task_loop())
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def __inner_stop(self) -> None:
|
||||
assert self.__streamer_task
|
||||
self.__streamer_task.cancel()
|
||||
await asyncio.gather(self.__streamer_task, return_exceptions=True)
|
||||
await self.__kill_streamer_proc()
|
||||
await self.__run_hook("POST-STOP-CMD", self.__post_stop_cmd)
|
||||
self.__streamer_task = None
|
||||
|
||||
# =====
|
||||
|
||||
async def __streamer_task_loop(self) -> None: # pylint: disable=too-many-branches
|
||||
logger = get_logger(0)
|
||||
while True: # pylint: disable=too-many-nested-blocks
|
||||
try:
|
||||
await self.__start_streamer_proc()
|
||||
assert self.__streamer_proc is not None
|
||||
await aioproc.log_stdout_infinite(self.__streamer_proc, logger)
|
||||
raise RuntimeError("Streamer unexpectedly died")
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception:
|
||||
if self.__streamer_proc:
|
||||
logger.exception("Unexpected streamer error: pid=%d", self.__streamer_proc.pid)
|
||||
else:
|
||||
logger.exception("Can't start streamer")
|
||||
await self.__kill_streamer_proc()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
def __make_cmd(self, cmd: list[str]) -> list[str]:
|
||||
return [
|
||||
part.format(
|
||||
unix=self.__unix_path,
|
||||
process_name_prefix=self.__process_name_prefix,
|
||||
**self.__params.get_params(),
|
||||
)
|
||||
for part in cmd
|
||||
]
|
||||
|
||||
async def __run_hook(self, name: str, cmd: list[str]) -> None:
|
||||
logger = get_logger()
|
||||
cmd = self.__make_cmd(cmd)
|
||||
logger.info("%s: %s", name, tools.cmdfmt(cmd))
|
||||
try:
|
||||
await aioproc.log_process(cmd, logger, prefix=name)
|
||||
except Exception as err:
|
||||
logger.exception("Can't execute command: %s", err)
|
||||
|
||||
async def __start_streamer_proc(self) -> None:
|
||||
assert self.__streamer_proc is None
|
||||
cmd = self.__make_cmd(self.__cmd)
|
||||
self.__streamer_proc = await aioproc.run_process(cmd)
|
||||
get_logger(0).info("Started streamer pid=%d: %s", self.__streamer_proc.pid, tools.cmdfmt(cmd))
|
||||
|
||||
async def __kill_streamer_proc(self) -> None:
|
||||
if self.__streamer_proc:
|
||||
await aioproc.kill_process(self.__streamer_proc, 1, get_logger(0))
|
||||
self.__streamer_proc = None
|
||||
Reference in New Issue
Block a user