mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +08:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@@ -48,7 +48,16 @@ class BaseAtx(BasePlugin):
|
||||
async def get_state(self) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
# ==== Granularity table ====
|
||||
# - enabled -- Full
|
||||
# - busy -- Partial
|
||||
# - leds -- Partial
|
||||
# ===========================
|
||||
|
||||
yield {}
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ class AtxDisabledError(AtxOperationError):
|
||||
|
||||
# =====
|
||||
class Plugin(BaseAtx):
|
||||
def __init__(self) -> None:
|
||||
self.__notifier = aiotools.AioNotifier()
|
||||
|
||||
async def get_state(self) -> dict:
|
||||
return {
|
||||
"enabled": False,
|
||||
@@ -46,10 +49,13 @@ class Plugin(BaseAtx):
|
||||
},
|
||||
}
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
self.__notifier.notify()
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
while True:
|
||||
await self.__notifier.wait()
|
||||
yield (await self.get_state())
|
||||
await aiotools.wait_infinite()
|
||||
|
||||
# =====
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
|
||||
from typing import AsyncGenerator
|
||||
|
||||
@@ -76,7 +77,7 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes
|
||||
self.__notifier = aiotools.AioNotifier()
|
||||
self.__region = aiotools.AioExclusiveRegion(AtxIsBusyError, self.__notifier)
|
||||
|
||||
self.__line_request: (gpiod.LineRequest | None) = None
|
||||
self.__line_req: (gpiod.LineRequest | None) = None
|
||||
|
||||
self.__reader = aiogp.AioReader(
|
||||
path=self.__device_path,
|
||||
@@ -108,8 +109,8 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes
|
||||
}
|
||||
|
||||
def sysprep(self) -> None:
|
||||
assert self.__line_request is None
|
||||
self.__line_request = gpiod.request_lines(
|
||||
assert self.__line_req is None
|
||||
self.__line_req = gpiod.request_lines(
|
||||
self.__device_path,
|
||||
consumer="kvmd::atx",
|
||||
config={
|
||||
@@ -130,22 +131,26 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes
|
||||
},
|
||||
}
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
self.__notifier.notify(1)
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
prev_state: dict = {}
|
||||
prev: dict = {}
|
||||
while True:
|
||||
state = await self.get_state()
|
||||
if state != prev_state:
|
||||
yield state
|
||||
prev_state = state
|
||||
await self.__notifier.wait()
|
||||
if (await self.__notifier.wait()) > 0:
|
||||
prev = {}
|
||||
new = await self.get_state()
|
||||
if new != prev:
|
||||
prev = copy.deepcopy(new)
|
||||
yield new
|
||||
|
||||
async def systask(self) -> None:
|
||||
await self.__reader.poll()
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
if self.__line_request:
|
||||
if self.__line_req:
|
||||
try:
|
||||
self.__line_request.release()
|
||||
self.__line_req.release()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -186,7 +191,7 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes
|
||||
@aiotools.atomic_fg
|
||||
async def __click(self, name: str, pin: int, delay: float, wait: bool) -> None:
|
||||
if wait:
|
||||
async with self.__region:
|
||||
with self.__region:
|
||||
await self.__inner_click(name, pin, delay)
|
||||
else:
|
||||
await aiotools.run_region_task(
|
||||
@@ -196,11 +201,11 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def __inner_click(self, name: str, pin: int, delay: float) -> None:
|
||||
assert self.__line_request
|
||||
assert self.__line_req
|
||||
try:
|
||||
self.__line_request.set_value(pin, gpiod.line.Value(True))
|
||||
self.__line_req.set_value(pin, gpiod.line.Value(True))
|
||||
await asyncio.sleep(delay)
|
||||
finally:
|
||||
self.__line_request.set_value(pin, gpiod.line.Value(False))
|
||||
self.__line_req.set_value(pin, gpiod.line.Value(False))
|
||||
await asyncio.sleep(1)
|
||||
get_logger(0).info("Clicked ATX button %r", name)
|
||||
|
||||
@@ -75,7 +75,7 @@ class Plugin(BaseAuthService):
|
||||
async with session.request(
|
||||
method="POST",
|
||||
url=self.__url,
|
||||
timeout=self.__timeout,
|
||||
timeout=aiohttp.ClientTimeout(total=self.__timeout),
|
||||
json={
|
||||
"user": user,
|
||||
"passwd": passwd,
|
||||
@@ -85,8 +85,8 @@ class Plugin(BaseAuthService):
|
||||
"User-Agent": htclient.make_user_agent("KVMD"),
|
||||
"X-KVMD-User": user,
|
||||
},
|
||||
) as response:
|
||||
htclient.raise_not_200(response)
|
||||
) as resp:
|
||||
htclient.raise_not_200(resp)
|
||||
return True
|
||||
except Exception:
|
||||
get_logger().exception("Failed HTTP auth request for user %r", user)
|
||||
|
||||
@@ -100,10 +100,10 @@ class Plugin(BaseAuthService):
|
||||
return True
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
pass
|
||||
except ldap.SERVER_DOWN as err:
|
||||
get_logger().error("LDAP server is down: %s", tools.efmt(err))
|
||||
except Exception as err:
|
||||
get_logger().error("Unexpected LDAP error: %s", tools.efmt(err))
|
||||
except ldap.SERVER_DOWN as ex:
|
||||
get_logger().error("LDAP server is down: %s", tools.efmt(ex))
|
||||
except Exception as ex:
|
||||
get_logger().error("Unexpected LDAP error: %s", tools.efmt(ex))
|
||||
finally:
|
||||
if conn is not None:
|
||||
try:
|
||||
|
||||
@@ -435,10 +435,10 @@ class Plugin(BaseAuthService):
|
||||
timeout=self.__timeout,
|
||||
dict=dct,
|
||||
)
|
||||
request = client.CreateAuthPacket(code=pyrad.packet.AccessRequest, User_Name=user)
|
||||
request["User-Password"] = request.PwCrypt(passwd)
|
||||
response = client.SendPacket(request)
|
||||
return (response.code == pyrad.packet.AccessAccept)
|
||||
req = client.CreateAuthPacket(code=pyrad.packet.AccessRequest, User_Name=user)
|
||||
req["User-Password"] = req.PwCrypt(passwd)
|
||||
resp = client.SendPacket(req)
|
||||
return (resp.code == pyrad.packet.AccessAccept)
|
||||
except Exception:
|
||||
get_logger().exception("Failed RADIUS auth request for user %r", user)
|
||||
return False
|
||||
|
||||
@@ -21,9 +21,11 @@
|
||||
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import time
|
||||
|
||||
from typing import Iterable
|
||||
from typing import Callable
|
||||
from typing import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
@@ -31,14 +33,37 @@ from ...yamlconf import Option
|
||||
|
||||
from ...validators.basic import valid_bool
|
||||
from ...validators.basic import valid_int_f1
|
||||
from ...validators.basic import valid_string_list
|
||||
from ...validators.hid import valid_hid_key
|
||||
from ...validators.hid import valid_hid_mouse_move
|
||||
|
||||
from ...mouse import MouseRange
|
||||
|
||||
from .. import BasePlugin
|
||||
from .. import get_plugin_class
|
||||
|
||||
|
||||
# =====
|
||||
class BaseHid(BasePlugin):
|
||||
def __init__(self, jiggler_enabled: bool, jiggler_active: bool, jiggler_interval: int) -> None:
|
||||
class BaseHid(BasePlugin): # pylint: disable=too-many-instance-attributes
|
||||
def __init__(
|
||||
self,
|
||||
ignore_keys: list[str],
|
||||
|
||||
mouse_x_min: int,
|
||||
mouse_x_max: int,
|
||||
mouse_y_min: int,
|
||||
mouse_y_max: int,
|
||||
|
||||
jiggler_enabled: bool,
|
||||
jiggler_active: bool,
|
||||
jiggler_interval: int,
|
||||
) -> None:
|
||||
|
||||
self.__ignore_keys = ignore_keys
|
||||
|
||||
self.__mouse_x_range = (mouse_x_min, mouse_x_max)
|
||||
self.__mouse_y_range = (mouse_y_min, mouse_y_max)
|
||||
|
||||
self.__jiggler_enabled = jiggler_enabled
|
||||
self.__jiggler_active = jiggler_active
|
||||
self.__jiggler_interval = jiggler_interval
|
||||
@@ -46,8 +71,17 @@ class BaseHid(BasePlugin):
|
||||
self.__activity_ts = 0
|
||||
|
||||
@classmethod
|
||||
def _get_jiggler_options(cls) -> dict[str, Any]:
|
||||
def _get_base_options(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"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, unpack_as="mouse_x_min"),
|
||||
"max": Option(MouseRange.MAX, type=valid_hid_mouse_move, unpack_as="mouse_x_max"),
|
||||
},
|
||||
"mouse_y_range": {
|
||||
"min": Option(MouseRange.MIN, type=valid_hid_mouse_move, unpack_as="mouse_y_min"),
|
||||
"max": Option(MouseRange.MAX, type=valid_hid_mouse_move, unpack_as="mouse_y_max"),
|
||||
},
|
||||
"jiggler": {
|
||||
"enabled": Option(False, type=valid_bool, unpack_as="jiggler_enabled"),
|
||||
"active": Option(False, type=valid_bool, unpack_as="jiggler_active"),
|
||||
@@ -63,7 +97,23 @@ class BaseHid(BasePlugin):
|
||||
async def get_state(self) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
# ==== Granularity table ====
|
||||
# - enabled -- Full
|
||||
# - online -- Partial
|
||||
# - busy -- Partial
|
||||
# - connected -- Partial, nullable
|
||||
# - keyboard.online -- Partial
|
||||
# - keyboard.outputs -- Partial
|
||||
# - keyboard.leds -- Partial
|
||||
# - mouse.online -- Partial
|
||||
# - mouse.outputs -- Partial, follows with absolute
|
||||
# - mouse.absolute -- Partial, follows with outputs
|
||||
# ===========================
|
||||
|
||||
yield {}
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -73,25 +123,6 @@ class BaseHid(BasePlugin):
|
||||
async def cleanup(self) -> None:
|
||||
pass
|
||||
|
||||
# =====
|
||||
|
||||
def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def send_mouse_button_event(self, button: str, state: bool) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
|
||||
_ = to_x
|
||||
_ = to_y
|
||||
|
||||
def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None:
|
||||
_ = delta_x
|
||||
_ = delta_y
|
||||
|
||||
def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def set_params(
|
||||
self,
|
||||
keyboard_output: (str | None)=None,
|
||||
@@ -104,25 +135,100 @@ class BaseHid(BasePlugin):
|
||||
def set_connected(self, connected: bool) -> None:
|
||||
_ = connected
|
||||
|
||||
def clear_events(self) -> None:
|
||||
# =====
|
||||
|
||||
def send_key_events(self, keys: Iterable[tuple[str, bool]], no_ignore_keys: bool=False) -> None:
|
||||
for (key, state) in keys:
|
||||
if no_ignore_keys or key not in self.__ignore_keys:
|
||||
self.send_key_event(key, state)
|
||||
|
||||
def send_key_event(self, key: str, state: bool) -> None:
|
||||
self._send_key_event(key, state)
|
||||
self.__bump_activity()
|
||||
|
||||
def _send_key_event(self, key: str, state: bool) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
# =====
|
||||
|
||||
async def systask(self) -> None:
|
||||
factor = 1
|
||||
while True:
|
||||
if self.__jiggler_active and (self.__activity_ts + self.__jiggler_interval < int(time.monotonic())):
|
||||
for _ in range(5):
|
||||
if self.__jiggler_absolute:
|
||||
self.send_mouse_move_event(100 * factor, 100 * factor)
|
||||
else:
|
||||
self.send_mouse_relative_event(10 * factor, 10 * factor)
|
||||
factor *= -1
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(1)
|
||||
def send_mouse_button_event(self, button: str, state: bool) -> None:
|
||||
self._send_mouse_button_event(button, state)
|
||||
self.__bump_activity()
|
||||
|
||||
def _bump_activity(self) -> None:
|
||||
def _send_mouse_button_event(self, button: str, state: bool) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
# =====
|
||||
|
||||
def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
|
||||
if self.__mouse_x_range != MouseRange.RANGE:
|
||||
to_x = MouseRange.remap(to_x, *self.__mouse_x_range)
|
||||
if self.__mouse_y_range != MouseRange.RANGE:
|
||||
to_y = MouseRange.remap(to_y, *self.__mouse_y_range)
|
||||
self._send_mouse_move_event(to_x, to_y)
|
||||
self.__bump_activity()
|
||||
|
||||
def _send_mouse_move_event(self, to_x: int, to_y: int) -> None:
|
||||
_ = to_x # XXX: NotImplementedError
|
||||
_ = to_y
|
||||
|
||||
# =====
|
||||
|
||||
def send_mouse_relative_events(self, deltas: Iterable[tuple[int, int]], squash: bool) -> None:
|
||||
self.__process_mouse_delta_event(deltas, squash, self.send_mouse_relative_event)
|
||||
|
||||
def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self._send_mouse_relative_event(delta_x, delta_y)
|
||||
self.__bump_activity()
|
||||
|
||||
def _send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None:
|
||||
_ = delta_x # XXX: NotImplementedError
|
||||
_ = delta_y
|
||||
|
||||
# =====
|
||||
|
||||
def send_mouse_wheel_events(self, deltas: Iterable[tuple[int, int]], squash: bool) -> None:
|
||||
self.__process_mouse_delta_event(deltas, squash, self.send_mouse_wheel_event)
|
||||
|
||||
def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self._send_mouse_wheel_event(delta_x, delta_y)
|
||||
self.__bump_activity()
|
||||
|
||||
def _send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
# =====
|
||||
|
||||
def clear_events(self) -> None:
|
||||
self._clear_events() # Don't bump activity here
|
||||
|
||||
def _clear_events(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
# =====
|
||||
|
||||
def __process_mouse_delta_event(
|
||||
self,
|
||||
deltas: Iterable[tuple[int, int]],
|
||||
squash: bool,
|
||||
handler: Callable[[int, int], None],
|
||||
) -> None:
|
||||
|
||||
if squash:
|
||||
prev = (0, 0)
|
||||
for cur in deltas:
|
||||
if abs(prev[0] + cur[0]) > 127 or abs(prev[1] + cur[1]) > 127:
|
||||
handler(*prev)
|
||||
prev = cur
|
||||
else:
|
||||
prev = (prev[0] + cur[0], prev[1] + cur[1])
|
||||
if prev[0] or prev[1]:
|
||||
handler(*prev)
|
||||
else:
|
||||
for xy in deltas:
|
||||
handler(*xy)
|
||||
|
||||
def __bump_activity(self) -> None:
|
||||
self.__activity_ts = int(time.monotonic())
|
||||
|
||||
def _set_jiggler_absolute(self, absolute: bool) -> None:
|
||||
@@ -141,6 +247,21 @@ class BaseHid(BasePlugin):
|
||||
},
|
||||
}
|
||||
|
||||
# =====
|
||||
|
||||
async def systask(self) -> None:
|
||||
factor = 1
|
||||
while True:
|
||||
if self.__jiggler_active and (self.__activity_ts + self.__jiggler_interval < int(time.monotonic())):
|
||||
for _ in range(5):
|
||||
if self.__jiggler_absolute:
|
||||
self.send_mouse_move_event(100 * factor, 100 * factor)
|
||||
else:
|
||||
self.send_mouse_relative_event(10 * factor, 10 * factor)
|
||||
factor *= -1
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
# =====
|
||||
def get_hid_class(name: str) -> type[BaseHid]:
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
import multiprocessing
|
||||
import contextlib
|
||||
import queue
|
||||
import copy
|
||||
import time
|
||||
|
||||
from typing import Iterable
|
||||
from typing import Generator
|
||||
from typing import AsyncGenerator
|
||||
from typing import Any
|
||||
@@ -91,7 +91,7 @@ class _TempRequestError(_RequestError):
|
||||
|
||||
# =====
|
||||
class BasePhyConnection:
|
||||
def send(self, request: bytes) -> bytes:
|
||||
def send(self, req: bytes) -> bytes:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -108,17 +108,22 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
|
||||
def __init__( # pylint: disable=too-many-arguments,super-init-not-called
|
||||
self,
|
||||
phy: BasePhy,
|
||||
|
||||
ignore_keys: list[str],
|
||||
mouse_x_range: dict[str, Any],
|
||||
mouse_y_range: dict[str, Any],
|
||||
jiggler: dict[str, Any],
|
||||
|
||||
reset_self: bool,
|
||||
read_retries: int,
|
||||
common_retries: int,
|
||||
retries_delay: float,
|
||||
errors_threshold: int,
|
||||
noop: bool,
|
||||
jiggler: dict[str, Any],
|
||||
**gpio_kwargs: Any,
|
||||
) -> None:
|
||||
|
||||
BaseHid.__init__(self, **jiggler)
|
||||
BaseHid.__init__(self, ignore_keys=ignore_keys, **mouse_x_range, **mouse_y_range, **jiggler)
|
||||
multiprocessing.Process.__init__(self, daemon=True)
|
||||
|
||||
self.__read_retries = read_retries
|
||||
@@ -163,7 +168,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
|
||||
"errors_threshold": Option(5, type=valid_int_f0),
|
||||
"noop": Option(False, type=valid_bool),
|
||||
|
||||
**cls._get_jiggler_options(),
|
||||
**cls._get_base_options(),
|
||||
}
|
||||
|
||||
def sysprep(self) -> None:
|
||||
@@ -212,6 +217,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
|
||||
mouse_outputs["active"] = active_mouse
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
"online": online,
|
||||
"busy": bool(state["busy"]),
|
||||
"connected": (bool(outputs2 & 0b01000000) if outputs2 & 0b10000000 else None),
|
||||
@@ -232,14 +238,18 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
|
||||
**self._get_jiggler_state(),
|
||||
}
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
self.__notifier.notify(1)
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
prev_state: dict = {}
|
||||
prev: dict = {}
|
||||
while True:
|
||||
state = await self.get_state()
|
||||
if state != prev_state:
|
||||
yield state
|
||||
prev_state = state
|
||||
await self.__notifier.wait()
|
||||
if (await self.__notifier.wait()) > 0:
|
||||
prev = {}
|
||||
new = await self.get_state()
|
||||
if new != prev:
|
||||
prev = copy.deepcopy(new)
|
||||
yield new
|
||||
|
||||
async def reset(self) -> None:
|
||||
self.__reset_required_event.set()
|
||||
@@ -254,27 +264,6 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
|
||||
|
||||
# =====
|
||||
|
||||
def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None:
|
||||
for (key, state) in keys:
|
||||
self.__queue_event(KeyEvent(key, state))
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_button_event(self, button: str, state: bool) -> None:
|
||||
self.__queue_event(MouseButtonEvent(button, state))
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
|
||||
self.__queue_event(MouseMoveEvent(to_x, to_y))
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__queue_event(MouseRelativeEvent(delta_x, delta_y))
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__queue_event(MouseWheelEvent(delta_x, delta_y))
|
||||
self._bump_activity()
|
||||
|
||||
def set_params(
|
||||
self,
|
||||
keyboard_output: (str | None)=None,
|
||||
@@ -296,9 +285,23 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
|
||||
def set_connected(self, connected: bool) -> None:
|
||||
self.__queue_event(SetConnectedEvent(connected), clear=True)
|
||||
|
||||
def clear_events(self) -> None:
|
||||
def _send_key_event(self, key: str, state: bool) -> None:
|
||||
self.__queue_event(KeyEvent(key, state))
|
||||
|
||||
def _send_mouse_button_event(self, button: str, state: bool) -> None:
|
||||
self.__queue_event(MouseButtonEvent(button, state))
|
||||
|
||||
def _send_mouse_move_event(self, to_x: int, to_y: int) -> None:
|
||||
self.__queue_event(MouseMoveEvent(to_x, to_y))
|
||||
|
||||
def _send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__queue_event(MouseRelativeEvent(delta_x, delta_y))
|
||||
|
||||
def _send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__queue_event(MouseWheelEvent(delta_x, delta_y))
|
||||
|
||||
def _clear_events(self) -> None:
|
||||
self.__queue_event(ClearEvent(), clear=True)
|
||||
self._bump_activity()
|
||||
|
||||
def __queue_event(self, event: BaseEvent, clear: bool=False) -> None:
|
||||
if not self.__stop_event.is_set():
|
||||
@@ -374,7 +377,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
|
||||
self.__set_state_online(False)
|
||||
return False
|
||||
|
||||
def __process_request(self, conn: BasePhyConnection, request: bytes) -> bool: # pylint: disable=too-many-branches
|
||||
def __process_request(self, conn: BasePhyConnection, req: bytes) -> bool: # pylint: disable=too-many-branches
|
||||
logger = get_logger()
|
||||
error_messages: list[str] = []
|
||||
live_log_errors = False
|
||||
@@ -384,47 +387,47 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
|
||||
error_retval = False
|
||||
|
||||
while self.__gpio.is_powered() and common_retries and read_retries:
|
||||
response = (RESPONSE_LEGACY_OK if self.__noop else conn.send(request))
|
||||
resp = (RESPONSE_LEGACY_OK if self.__noop else conn.send(req))
|
||||
try:
|
||||
if len(response) < 4:
|
||||
if len(resp) < 4:
|
||||
read_retries -= 1
|
||||
raise _TempRequestError(f"No response from HID: request={request!r}")
|
||||
raise _TempRequestError(f"No response from HID: request={req!r}")
|
||||
|
||||
if not check_response(response):
|
||||
request = REQUEST_REPEAT
|
||||
if not check_response(resp):
|
||||
req = REQUEST_REPEAT
|
||||
raise _TempRequestError("Invalid response CRC; requesting response again ...")
|
||||
|
||||
code = response[1]
|
||||
code = resp[1]
|
||||
if code == 0x48: # Request timeout # pylint: disable=no-else-raise
|
||||
raise _TempRequestError(f"Got request timeout from HID: request={request!r}")
|
||||
raise _TempRequestError(f"Got request timeout from HID: request={req!r}")
|
||||
elif code == 0x40: # CRC Error
|
||||
raise _TempRequestError(f"Got CRC error of request from HID: request={request!r}")
|
||||
raise _TempRequestError(f"Got CRC error of request from HID: request={req!r}")
|
||||
elif code == 0x45: # Unknown command
|
||||
raise _PermRequestError(f"HID did not recognize the request={request!r}")
|
||||
raise _PermRequestError(f"HID did not recognize the request={req!r}")
|
||||
elif code == 0x24: # Rebooted?
|
||||
raise _PermRequestError("No previous command state inside HID, seems it was rebooted")
|
||||
elif code == 0x20: # Legacy done
|
||||
self.__set_state_online(True)
|
||||
return True
|
||||
elif code & 0x80: # Pong/Done with state
|
||||
self.__set_state_pong(response)
|
||||
self.__set_state_pong(resp)
|
||||
return True
|
||||
raise _TempRequestError(f"Invalid response from HID: request={request!r}, response=0x{response!r}")
|
||||
raise _TempRequestError(f"Invalid response from HID: request={req!r}, response=0x{resp!r}")
|
||||
|
||||
except _RequestError as err:
|
||||
except _RequestError as ex:
|
||||
common_retries -= 1
|
||||
|
||||
if live_log_errors:
|
||||
logger.error(err.msg)
|
||||
logger.error(ex.msg)
|
||||
else:
|
||||
error_messages.append(err.msg)
|
||||
error_messages.append(ex.msg)
|
||||
if len(error_messages) > self.__errors_threshold:
|
||||
for msg in error_messages:
|
||||
logger.error(msg)
|
||||
error_messages = []
|
||||
live_log_errors = True
|
||||
|
||||
if isinstance(err, _PermRequestError):
|
||||
if isinstance(ex, _PermRequestError):
|
||||
error_retval = True
|
||||
break
|
||||
|
||||
@@ -440,7 +443,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
|
||||
for msg in error_messages:
|
||||
logger.error(msg)
|
||||
if not (common_retries and read_retries):
|
||||
logger.error("Can't process HID request due many errors: %r", request)
|
||||
logger.error("Can't process HID request due many errors: %r", req)
|
||||
return error_retval
|
||||
|
||||
def __set_state_online(self, online: bool) -> None:
|
||||
@@ -449,11 +452,11 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-
|
||||
def __set_state_busy(self, busy: bool) -> None:
|
||||
self.__state_flags.update(busy=int(busy))
|
||||
|
||||
def __set_state_pong(self, response: bytes) -> None:
|
||||
status = response[1] << 16
|
||||
if len(response) > 4:
|
||||
status |= (response[2] << 8) | response[3]
|
||||
reset_required = (1 if response[1] & 0b01000000 else 0)
|
||||
def __set_state_pong(self, resp: bytes) -> None:
|
||||
status = resp[1] << 16
|
||||
if len(resp) > 4:
|
||||
status |= (resp[2] << 8) | resp[3]
|
||||
reset_required = (1 if resp[1] & 0b01000000 else 0)
|
||||
self.__state_flags.update(online=1, busy=reset_required, status=status)
|
||||
if reset_required:
|
||||
if self.__reset_self:
|
||||
|
||||
@@ -47,12 +47,12 @@ class Gpio: # pylint: disable=too-many-instance-attributes
|
||||
self.__reset_inverted = reset_inverted
|
||||
self.__reset_delay = reset_delay
|
||||
|
||||
self.__line_request: (gpiod.LineRequest | None) = None
|
||||
self.__line_req: (gpiod.LineRequest | None) = None
|
||||
self.__last_power: (bool | None) = None
|
||||
|
||||
def __enter__(self) -> None:
|
||||
if self.__power_detect_pin >= 0 or self.__reset_pin >= 0:
|
||||
assert self.__line_request is None
|
||||
assert self.__line_req is None
|
||||
config: dict[int, gpiod.LineSettings] = {}
|
||||
if self.__power_detect_pin >= 0:
|
||||
config[self.__power_detect_pin] = gpiod.LineSettings(
|
||||
@@ -65,7 +65,7 @@ class Gpio: # pylint: disable=too-many-instance-attributes
|
||||
output_value=gpiod.line.Value(self.__reset_inverted),
|
||||
)
|
||||
assert len(config) > 0
|
||||
self.__line_request = gpiod.request_lines(
|
||||
self.__line_req = gpiod.request_lines(
|
||||
self.__device_path,
|
||||
consumer="kvmd::hid",
|
||||
config=config,
|
||||
@@ -78,18 +78,18 @@ class Gpio: # pylint: disable=too-many-instance-attributes
|
||||
_tb: types.TracebackType,
|
||||
) -> None:
|
||||
|
||||
if self.__line_request:
|
||||
if self.__line_req:
|
||||
try:
|
||||
self.__line_request.release()
|
||||
self.__line_req.release()
|
||||
except Exception:
|
||||
pass
|
||||
self.__last_power = None
|
||||
self.__line_request = None
|
||||
self.__line_req = None
|
||||
|
||||
def is_powered(self) -> bool:
|
||||
if self.__power_detect_pin >= 0:
|
||||
assert self.__line_request
|
||||
power = bool(self.__line_request.get_value(self.__power_detect_pin).value)
|
||||
assert self.__line_req
|
||||
power = bool(self.__line_req.get_value(self.__power_detect_pin).value)
|
||||
if power != self.__last_power:
|
||||
get_logger(0).info("HID power state changed: %s -> %s", self.__last_power, power)
|
||||
self.__last_power = power
|
||||
@@ -98,11 +98,11 @@ class Gpio: # pylint: disable=too-many-instance-attributes
|
||||
|
||||
def reset(self) -> None:
|
||||
if self.__reset_pin >= 0:
|
||||
assert self.__line_request
|
||||
assert self.__line_req
|
||||
try:
|
||||
self.__line_request.set_value(self.__reset_pin, gpiod.line.Value(not self.__reset_inverted))
|
||||
self.__line_req.set_value(self.__reset_pin, gpiod.line.Value(not self.__reset_inverted))
|
||||
time.sleep(self.__reset_delay)
|
||||
finally:
|
||||
self.__line_request.set_value(self.__reset_pin, gpiod.line.Value(self.__reset_inverted))
|
||||
self.__line_req.set_value(self.__reset_pin, gpiod.line.Value(self.__reset_inverted))
|
||||
time.sleep(1)
|
||||
get_logger(0).info("Reset HID performed")
|
||||
|
||||
@@ -184,17 +184,17 @@ class MouseWheelEvent(BaseEvent):
|
||||
|
||||
|
||||
# =====
|
||||
def check_response(response: bytes) -> bool:
|
||||
assert len(response) in (4, 8), response
|
||||
return (bitbang.make_crc16(response[:-2]) == struct.unpack(">H", response[-2:])[0])
|
||||
def check_response(resp: bytes) -> bool:
|
||||
assert len(resp) in (4, 8), resp
|
||||
return (bitbang.make_crc16(resp[:-2]) == struct.unpack(">H", resp[-2:])[0])
|
||||
|
||||
|
||||
def _make_request(command: bytes) -> bytes:
|
||||
assert len(command) == 5, command
|
||||
request = b"\x33" + command
|
||||
request += struct.pack(">H", bitbang.make_crc16(request))
|
||||
assert len(request) == 8, request
|
||||
return request
|
||||
def _make_request(cmd: bytes) -> bytes:
|
||||
assert len(cmd) == 5, cmd
|
||||
req = b"\x33" + cmd
|
||||
req += struct.pack(">H", bitbang.make_crc16(req))
|
||||
assert len(req) == 8, req
|
||||
return req
|
||||
|
||||
|
||||
# =====
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
|
||||
import multiprocessing
|
||||
import copy
|
||||
import time
|
||||
|
||||
from typing import Iterable
|
||||
from typing import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
@@ -63,6 +63,11 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
def __init__( # pylint: disable=too-many-arguments,too-many-locals
|
||||
self,
|
||||
|
||||
ignore_keys: list[str],
|
||||
mouse_x_range: dict[str, Any],
|
||||
mouse_y_range: dict[str, Any],
|
||||
jiggler: dict[str, Any],
|
||||
|
||||
manufacturer: str,
|
||||
product: str,
|
||||
description: str,
|
||||
@@ -78,11 +83,9 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
max_clients: int,
|
||||
socket_timeout: float,
|
||||
select_timeout: float,
|
||||
|
||||
jiggler: dict[str, Any],
|
||||
) -> None:
|
||||
|
||||
super().__init__(**jiggler)
|
||||
super().__init__(ignore_keys=ignore_keys, **mouse_x_range, **mouse_y_range, **jiggler)
|
||||
self._set_jiggler_absolute(False)
|
||||
|
||||
self.__proc: (multiprocessing.Process | None) = None
|
||||
@@ -126,7 +129,7 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
"socket_timeout": Option(5.0, type=valid_float_f01),
|
||||
"select_timeout": Option(1.0, type=valid_float_f01),
|
||||
|
||||
**cls._get_jiggler_options(),
|
||||
**cls._get_base_options(),
|
||||
}
|
||||
|
||||
def sysprep(self) -> None:
|
||||
@@ -138,6 +141,7 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
state = await self.__server.get_state()
|
||||
outputs: dict = {"available": [], "active": ""}
|
||||
return {
|
||||
"enabled": True,
|
||||
"online": True,
|
||||
"busy": False,
|
||||
"connected": None,
|
||||
@@ -158,14 +162,18 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
**self._get_jiggler_state(),
|
||||
}
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
self.__notifier.notify(1)
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
prev_state: dict = {}
|
||||
prev: dict = {}
|
||||
while True:
|
||||
state = await self.get_state()
|
||||
if state != prev_state:
|
||||
yield state
|
||||
prev_state = state
|
||||
await self.__notifier.wait()
|
||||
if (await self.__notifier.wait()) > 0:
|
||||
prev = {}
|
||||
new = await self.get_state()
|
||||
if new != prev:
|
||||
prev = copy.deepcopy(new)
|
||||
yield new
|
||||
|
||||
async def reset(self) -> None:
|
||||
self.clear_events()
|
||||
@@ -182,27 +190,6 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
# =====
|
||||
|
||||
def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None:
|
||||
for (key, state) in keys:
|
||||
self.__server.queue_event(make_keyboard_event(key, state))
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_button_event(self, button: str, state: bool) -> None:
|
||||
self.__server.queue_event(MouseButtonEvent(button, state))
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__server.queue_event(MouseRelativeEvent(delta_x, delta_y))
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__server.queue_event(MouseWheelEvent(delta_x, delta_y))
|
||||
self._bump_activity()
|
||||
|
||||
def clear_events(self) -> None:
|
||||
self.__server.clear_events()
|
||||
self._bump_activity()
|
||||
|
||||
def set_params(
|
||||
self,
|
||||
keyboard_output: (str | None)=None,
|
||||
@@ -216,6 +203,21 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
self._set_jiggler_active(jiggler)
|
||||
self.__notifier.notify()
|
||||
|
||||
def _send_key_event(self, key: str, state: bool) -> None:
|
||||
self.__server.queue_event(make_keyboard_event(key, state))
|
||||
|
||||
def _send_mouse_button_event(self, button: str, state: bool) -> None:
|
||||
self.__server.queue_event(MouseButtonEvent(button, state))
|
||||
|
||||
def _send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__server.queue_event(MouseRelativeEvent(delta_x, delta_y))
|
||||
|
||||
def _send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__server.queue_event(MouseWheelEvent(delta_x, delta_y))
|
||||
|
||||
def _clear_events(self) -> None:
|
||||
self.__server.clear_events()
|
||||
|
||||
# =====
|
||||
|
||||
def __server_worker(self) -> None: # pylint: disable=too-many-branches
|
||||
|
||||
@@ -182,8 +182,8 @@ class BtServer: # pylint: disable=too-many-instance-attributes
|
||||
self.__close_client("CTL", client, "ctl_sock")
|
||||
elif data == b"\x71":
|
||||
sock.send(b"\x00")
|
||||
except Exception as err:
|
||||
get_logger(0).exception("CTL socket error on %s: %s", client.addr, tools.efmt(err))
|
||||
except Exception as ex:
|
||||
get_logger(0).exception("CTL socket error on %s: %s", client.addr, tools.efmt(ex))
|
||||
self.__close_client("CTL", client, "ctl_sock")
|
||||
continue
|
||||
|
||||
@@ -196,8 +196,8 @@ class BtServer: # pylint: disable=too-many-instance-attributes
|
||||
self.__close_client("INT", client, "int_sock")
|
||||
elif data[:2] == b"\xA2\x01":
|
||||
self.__process_leds(data[2])
|
||||
except Exception as err:
|
||||
get_logger(0).exception("INT socket error on %s: %s", client.addr, tools.efmt(err))
|
||||
except Exception as ex:
|
||||
get_logger(0).exception("INT socket error on %s: %s", client.addr, tools.efmt(ex))
|
||||
self.__close_client("INT", client, "ctl_sock")
|
||||
|
||||
if qr in ready_read:
|
||||
@@ -279,8 +279,8 @@ class BtServer: # pylint: disable=too-many-instance-attributes
|
||||
assert client.int_sock is not None
|
||||
try:
|
||||
client.int_sock.send(report)
|
||||
except Exception as err:
|
||||
get_logger(0).info("Can't send %s report to %s: %s", name, client.addr, tools.efmt(err))
|
||||
except Exception as ex:
|
||||
get_logger(0).info("Can't send %s report to %s: %s", name, client.addr, tools.efmt(ex))
|
||||
self.__close_client_pair(client)
|
||||
|
||||
def __clear_modifiers(self) -> None:
|
||||
@@ -371,13 +371,13 @@ class BtServer: # pylint: disable=too-many-instance-attributes
|
||||
logger.info("Publishing ..." if public else "Unpublishing ...")
|
||||
try:
|
||||
self.__iface.set_public(public)
|
||||
except Exception as err:
|
||||
logger.error("Can't change public mode: %s", tools.efmt(err))
|
||||
except Exception as ex:
|
||||
logger.error("Can't change public mode: %s", tools.efmt(ex))
|
||||
|
||||
def __unpair_client(self, client: _BtClient) -> None:
|
||||
logger = get_logger(0)
|
||||
logger.info("Unpairing %s ...", client.addr)
|
||||
try:
|
||||
self.__iface.unpair(client.addr)
|
||||
except Exception as err:
|
||||
logger.error("Can't unpair %s: %s", client.addr, tools.efmt(err))
|
||||
except Exception as ex:
|
||||
logger.error("Can't unpair %s: %s", client.addr, tools.efmt(ex))
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
|
||||
import multiprocessing
|
||||
import queue
|
||||
import copy
|
||||
import time
|
||||
|
||||
from typing import Iterable
|
||||
from typing import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
@@ -54,13 +54,17 @@ from .keyboard import Keyboard
|
||||
class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-instance-attributes
|
||||
def __init__( # pylint: disable=too-many-arguments,super-init-not-called
|
||||
self,
|
||||
ignore_keys: list[str],
|
||||
mouse_x_range: dict[str, Any],
|
||||
mouse_y_range: dict[str, Any],
|
||||
jiggler: dict[str, Any],
|
||||
|
||||
device_path: str,
|
||||
speed: int,
|
||||
read_timeout: float,
|
||||
jiggler: dict[str, Any],
|
||||
) -> None:
|
||||
|
||||
BaseHid.__init__(self, **jiggler)
|
||||
BaseHid.__init__(self, ignore_keys=ignore_keys, **mouse_x_range, **mouse_y_range, **jiggler)
|
||||
multiprocessing.Process.__init__(self, daemon=True)
|
||||
|
||||
self.__device_path = device_path
|
||||
@@ -88,7 +92,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
|
||||
"device": Option("/dev/kvmd-hid", type=valid_abs_path, unpack_as="device_path"),
|
||||
"speed": Option(9600, type=valid_tty_speed),
|
||||
"read_timeout": Option(0.3, type=valid_float_f01),
|
||||
**cls._get_jiggler_options(),
|
||||
**cls._get_base_options(),
|
||||
}
|
||||
|
||||
def sysprep(self) -> None:
|
||||
@@ -100,6 +104,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
|
||||
absolute = self.__mouse.is_absolute()
|
||||
leds = await self.__keyboard.get_leds()
|
||||
return {
|
||||
"enabled": True,
|
||||
"online": state["online"],
|
||||
"busy": False,
|
||||
"connected": None,
|
||||
@@ -119,14 +124,18 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
|
||||
**self._get_jiggler_state(),
|
||||
}
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
self.__notifier.notify(1)
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
prev_state: dict = {}
|
||||
prev: dict = {}
|
||||
while True:
|
||||
state = await self.get_state()
|
||||
if state != prev_state:
|
||||
yield state
|
||||
prev_state = state
|
||||
await self.__notifier.wait()
|
||||
if (await self.__notifier.wait()) > 0:
|
||||
prev = {}
|
||||
new = await self.get_state()
|
||||
if new != prev:
|
||||
prev = copy.deepcopy(new)
|
||||
yield new
|
||||
|
||||
async def reset(self) -> None:
|
||||
self.__reset_required_event.set()
|
||||
@@ -141,27 +150,6 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
|
||||
|
||||
# =====
|
||||
|
||||
def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None:
|
||||
for (key, state) in keys:
|
||||
self.__queue_cmd(self.__keyboard.process_key(key, state))
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_button_event(self, button: str, state: bool) -> None:
|
||||
self.__queue_cmd(self.__mouse.process_button(button, state))
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
|
||||
self.__queue_cmd(self.__mouse.process_move(to_x, to_y))
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__queue_cmd(self.__mouse.process_wheel(delta_x, delta_y))
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__queue_cmd(self.__mouse.process_relative(delta_x, delta_y))
|
||||
self._bump_activity()
|
||||
|
||||
def set_params(
|
||||
self,
|
||||
keyboard_output: (str | None)=None,
|
||||
@@ -180,10 +168,22 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
|
||||
self._set_jiggler_active(jiggler)
|
||||
self.__notifier.notify()
|
||||
|
||||
def set_connected(self, connected: bool) -> None:
|
||||
pass
|
||||
def _send_key_event(self, key: str, state: bool) -> None:
|
||||
self.__queue_cmd(self.__keyboard.process_key(key, state))
|
||||
|
||||
def clear_events(self) -> None:
|
||||
def _send_mouse_button_event(self, button: str, state: bool) -> None:
|
||||
self.__queue_cmd(self.__mouse.process_button(button, state))
|
||||
|
||||
def _send_mouse_move_event(self, to_x: int, to_y: int) -> None:
|
||||
self.__queue_cmd(self.__mouse.process_move(to_x, to_y))
|
||||
|
||||
def _send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__queue_cmd(self.__mouse.process_wheel(delta_x, delta_y))
|
||||
|
||||
def _send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__queue_cmd(self.__mouse.process_relative(delta_x, delta_y))
|
||||
|
||||
def _clear_events(self) -> None:
|
||||
tools.clear_queue(self.__cmd_queue)
|
||||
|
||||
def __queue_cmd(self, cmd: bytes, clear: bool=False) -> None:
|
||||
@@ -230,9 +230,9 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
|
||||
def __process_cmd(self, conn: ChipConnection, cmd: bytes) -> bool: # pylint: disable=too-many-branches
|
||||
try:
|
||||
led_byte = conn.xfer(cmd)
|
||||
except ChipResponseError as err:
|
||||
except ChipResponseError as ex:
|
||||
self.__set_state_online(False)
|
||||
get_logger(0).info(err)
|
||||
get_logger(0).error("Invalid chip response: %s", tools.efmt(ex))
|
||||
time.sleep(2)
|
||||
else:
|
||||
if led_byte >= 0:
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import Iterable
|
||||
import copy
|
||||
|
||||
from typing import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
@@ -46,15 +47,20 @@ from .mouse import MouseProcess
|
||||
class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
def __init__(
|
||||
self,
|
||||
ignore_keys: list[str],
|
||||
mouse_x_range: dict[str, Any],
|
||||
mouse_y_range: dict[str, Any],
|
||||
jiggler: dict[str, Any],
|
||||
|
||||
keyboard: dict[str, Any],
|
||||
mouse: dict[str, Any],
|
||||
mouse_alt: dict[str, Any],
|
||||
jiggler: dict[str, Any],
|
||||
noop: bool,
|
||||
|
||||
udc: str, # XXX: Not from options, see /kvmd/apps/kvmd/__init__.py for details
|
||||
) -> None:
|
||||
|
||||
super().__init__(**jiggler)
|
||||
super().__init__(ignore_keys=ignore_keys, **mouse_x_range, **mouse_y_range, **jiggler)
|
||||
|
||||
self.__udc = udc
|
||||
|
||||
@@ -113,7 +119,7 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
"horizontal_wheel": Option(True, type=valid_bool),
|
||||
},
|
||||
"noop": Option(False, type=valid_bool),
|
||||
**cls._get_jiggler_options(),
|
||||
**cls._get_base_options(),
|
||||
}
|
||||
|
||||
def sysprep(self) -> None:
|
||||
@@ -128,6 +134,7 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
keyboard_state = await self.__keyboard_proc.get_state()
|
||||
mouse_state = await self.__mouse_current.get_state()
|
||||
return {
|
||||
"enabled": True,
|
||||
"online": True,
|
||||
"busy": False,
|
||||
"connected": None,
|
||||
@@ -150,14 +157,18 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
**self._get_jiggler_state(),
|
||||
}
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
self.__notifier.notify(1)
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
prev_state: dict = {}
|
||||
prev: dict = {}
|
||||
while True:
|
||||
state = await self.get_state()
|
||||
if state != prev_state:
|
||||
yield state
|
||||
prev_state = state
|
||||
await self.__notifier.wait()
|
||||
if (await self.__notifier.wait()) > 0:
|
||||
prev = {}
|
||||
new = await self.get_state()
|
||||
if new != prev:
|
||||
prev = copy.deepcopy(new)
|
||||
yield new
|
||||
|
||||
async def reset(self) -> None:
|
||||
self.__keyboard_proc.send_reset_event()
|
||||
@@ -177,26 +188,6 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
# =====
|
||||
|
||||
def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None:
|
||||
self.__keyboard_proc.send_key_events(keys)
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_button_event(self, button: str, state: bool) -> None:
|
||||
self.__mouse_current.send_button_event(button, state)
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
|
||||
self.__mouse_current.send_move_event(to_x, to_y)
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__mouse_current.send_relative_event(delta_x, delta_y)
|
||||
self._bump_activity()
|
||||
|
||||
def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__mouse_current.send_wheel_event(delta_x, delta_y)
|
||||
self._bump_activity()
|
||||
|
||||
def set_params(
|
||||
self,
|
||||
keyboard_output: (str | None)=None,
|
||||
@@ -215,12 +206,26 @@ class Plugin(BaseHid): # pylint: disable=too-many-instance-attributes
|
||||
self._set_jiggler_active(jiggler)
|
||||
self.__notifier.notify()
|
||||
|
||||
def clear_events(self) -> None:
|
||||
def _send_key_event(self, key: str, state: bool) -> None:
|
||||
self.__keyboard_proc.send_key_event(key, state)
|
||||
|
||||
def _send_mouse_button_event(self, button: str, state: bool) -> None:
|
||||
self.__mouse_current.send_button_event(button, state)
|
||||
|
||||
def _send_mouse_move_event(self, to_x: int, to_y: int) -> None:
|
||||
self.__mouse_current.send_move_event(to_x, to_y)
|
||||
|
||||
def _send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__mouse_current.send_relative_event(delta_x, delta_y)
|
||||
|
||||
def _send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
|
||||
self.__mouse_current.send_wheel_event(delta_x, delta_y)
|
||||
|
||||
def _clear_events(self) -> None:
|
||||
self.__keyboard_proc.send_clear_event()
|
||||
self.__mouse_proc.send_clear_event()
|
||||
if self.__mouse_alt_proc:
|
||||
self.__mouse_alt_proc.send_clear_event()
|
||||
self._bump_activity()
|
||||
|
||||
# =====
|
||||
|
||||
|
||||
@@ -192,13 +192,13 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
||||
else:
|
||||
logger.error("HID-%s write() error: written (%s) != report length (%d)",
|
||||
self.__name, written, len(report))
|
||||
except Exception as err:
|
||||
if isinstance(err, OSError) and (
|
||||
except Exception as ex:
|
||||
if isinstance(ex, OSError) and (
|
||||
# https://github.com/raspberrypi/linux/commit/61b7f805dc2fd364e0df682de89227e94ce88e25
|
||||
err.errno == errno.EAGAIN # pylint: disable=no-member
|
||||
or err.errno == errno.ESHUTDOWN # pylint: disable=no-member
|
||||
ex.errno == errno.EAGAIN # pylint: disable=no-member
|
||||
or ex.errno == errno.ESHUTDOWN # pylint: disable=no-member
|
||||
):
|
||||
logger.debug("HID-%s busy/unplugged (write): %s", self.__name, tools.efmt(err))
|
||||
logger.debug("HID-%s busy/unplugged (write): %s", self.__name, tools.efmt(ex))
|
||||
else:
|
||||
logger.exception("Can't write report to HID-%s", self.__name)
|
||||
|
||||
@@ -216,16 +216,16 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
||||
while read:
|
||||
try:
|
||||
read = bool(select.select([self.__fd], [], [], 0)[0])
|
||||
except Exception as err:
|
||||
logger.error("Can't select() for read HID-%s: %s", self.__name, tools.efmt(err))
|
||||
except Exception as ex:
|
||||
logger.error("Can't select() for read HID-%s: %s", self.__name, tools.efmt(ex))
|
||||
break
|
||||
|
||||
if read:
|
||||
try:
|
||||
report = os.read(self.__fd, self.__read_size)
|
||||
except Exception as err:
|
||||
if isinstance(err, OSError) and err.errno == errno.EAGAIN: # pylint: disable=no-member
|
||||
logger.debug("HID-%s busy/unplugged (read): %s", self.__name, tools.efmt(err))
|
||||
except Exception as ex:
|
||||
if isinstance(ex, OSError) and ex.errno == errno.EAGAIN: # pylint: disable=no-member
|
||||
logger.debug("HID-%s busy/unplugged (read): %s", self.__name, tools.efmt(ex))
|
||||
else:
|
||||
logger.exception("Can't read report from HID-%s", self.__name)
|
||||
else:
|
||||
@@ -255,9 +255,9 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
||||
flags = os.O_NONBLOCK
|
||||
flags |= (os.O_RDWR if self.__read_size else os.O_WRONLY)
|
||||
self.__fd = os.open(self.__device_path, flags)
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
logger.error("Can't open HID-%s device %s: %s",
|
||||
self.__name, self.__device_path, tools.efmt(err))
|
||||
self.__name, self.__device_path, tools.efmt(ex))
|
||||
|
||||
if self.__fd >= 0:
|
||||
try:
|
||||
@@ -268,8 +268,8 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
||||
else:
|
||||
# Если запись недоступна, то скорее всего устройство отключено
|
||||
logger.debug("HID-%s is busy/unplugged (write select)", self.__name)
|
||||
except Exception as err:
|
||||
logger.error("Can't select() for write HID-%s: %s", self.__name, tools.efmt(err))
|
||||
except Exception as ex:
|
||||
logger.error("Can't select() for write HID-%s: %s", self.__name, tools.efmt(ex))
|
||||
|
||||
self.__state_flags.update(online=False)
|
||||
return False
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
# ========================================================================== #
|
||||
|
||||
|
||||
from typing import Iterable
|
||||
from typing import Generator
|
||||
from typing import Any
|
||||
|
||||
@@ -68,9 +67,8 @@ class KeyboardProcess(BaseDeviceProcess):
|
||||
self._clear_queue()
|
||||
self._queue_event(ResetEvent())
|
||||
|
||||
def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None:
|
||||
for (key, state) in keys:
|
||||
self._queue_event(make_keyboard_event(key, state))
|
||||
def send_key_event(self, key: str, state: bool) -> None:
|
||||
self._queue_event(make_keyboard_event(key, state))
|
||||
|
||||
# =====
|
||||
|
||||
|
||||
@@ -44,12 +44,12 @@ class _SerialPhyConnection(BasePhyConnection):
|
||||
def __init__(self, tty: serial.Serial) -> None:
|
||||
self.__tty = tty
|
||||
|
||||
def send(self, request: bytes) -> bytes:
|
||||
assert len(request) == 8
|
||||
assert request[0] == 0x33
|
||||
def send(self, req: bytes) -> bytes:
|
||||
assert len(req) == 8
|
||||
assert req[0] == 0x33
|
||||
if self.__tty.in_waiting:
|
||||
self.__tty.read_all()
|
||||
assert self.__tty.write(request) == 8
|
||||
assert self.__tty.write(req) == 8
|
||||
data = self.__tty.read(4)
|
||||
if len(data) == 4:
|
||||
if data[0] == 0x34: # New response protocol
|
||||
|
||||
@@ -57,9 +57,9 @@ class _SpiPhyConnection(BasePhyConnection):
|
||||
self.__xfer = xfer
|
||||
self.__read_timeout = read_timeout
|
||||
|
||||
def send(self, request: bytes) -> bytes:
|
||||
assert len(request) == 8
|
||||
assert request[0] == 0x33
|
||||
def send(self, req: bytes) -> bytes:
|
||||
assert len(req) == 8
|
||||
assert req[0] == 0x33
|
||||
|
||||
deadline_ts = time.monotonic() + self.__read_timeout
|
||||
dummy = b"\x00" * 10
|
||||
@@ -70,26 +70,26 @@ class _SpiPhyConnection(BasePhyConnection):
|
||||
get_logger(0).error("SPI timeout reached while garbage reading")
|
||||
return b""
|
||||
|
||||
self.__xfer(request)
|
||||
self.__xfer(req)
|
||||
|
||||
response: list[int] = []
|
||||
resp: list[int] = []
|
||||
deadline_ts = time.monotonic() + self.__read_timeout
|
||||
found = False
|
||||
while time.monotonic() < deadline_ts:
|
||||
for byte in self.__xfer(b"\x00" * (9 - len(response))):
|
||||
for byte in self.__xfer(b"\x00" * (9 - len(resp))):
|
||||
if not found:
|
||||
if byte == 0:
|
||||
continue
|
||||
found = True
|
||||
response.append(byte)
|
||||
if len(response) == 8:
|
||||
resp.append(byte)
|
||||
if len(resp) == 8:
|
||||
break
|
||||
if len(response) == 8:
|
||||
if len(resp) == 8:
|
||||
break
|
||||
else:
|
||||
get_logger(0).error("SPI timeout reached while responce waiting")
|
||||
return b""
|
||||
return bytes(response)
|
||||
return bytes(resp)
|
||||
|
||||
|
||||
class _SpiPhy(BasePhy): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
@@ -117,7 +117,22 @@ class BaseMsd(BasePlugin):
|
||||
async def get_state(self) -> dict:
|
||||
raise NotImplementedError()
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
# ==== Granularity table ====
|
||||
# - enabled -- Full
|
||||
# - online -- Partial
|
||||
# - busy -- Partial
|
||||
# - drive -- Partial, nullable
|
||||
# - storage -- Partial, nullable
|
||||
# - storage.parts -- Partial
|
||||
# - storage.images -- Partial
|
||||
# - storage.downloading -- Partial, nullable
|
||||
# - storage.uploading -- Partial, nullable
|
||||
# ===========================
|
||||
|
||||
if self is not None: # XXX: Vulture and pylint hack
|
||||
raise NotImplementedError()
|
||||
yield
|
||||
@@ -263,16 +278,18 @@ class MsdFileWriter(BaseMsdWriter): # pylint: disable=too-many-instance-attribu
|
||||
|
||||
return self.__written
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
return (self.__written >= self.__file_size)
|
||||
|
||||
async def open(self) -> "MsdFileWriter":
|
||||
assert self.__file is None
|
||||
get_logger(1).info("Writing %r image (%d bytes) to MSD ...", self.__name, self.__file_size)
|
||||
await aiofiles.os.makedirs(os.path.dirname(self.__path), exist_ok=True)
|
||||
self.__file = await aiofiles.open(self.__path, mode="w+b", buffering=0) # type: ignore
|
||||
await aiotools.run_async(os.ftruncate, self.__file.fileno(), self.__file_size) # type: ignore
|
||||
return self
|
||||
|
||||
async def finish(self) -> bool:
|
||||
await self.__sync()
|
||||
return (self.__written >= self.__file_size)
|
||||
|
||||
async def close(self) -> None:
|
||||
assert self.__file is not None
|
||||
logger = get_logger()
|
||||
@@ -285,10 +302,7 @@ class MsdFileWriter(BaseMsdWriter): # pylint: disable=too-many-instance-attribu
|
||||
else: # written > size
|
||||
(log, result) = (logger.warning, "OVERFLOW")
|
||||
log("Written %d of %d bytes to MSD image %r: %s", self.__written, self.__file_size, self.__name, result)
|
||||
try:
|
||||
await self.__sync()
|
||||
finally:
|
||||
await self.__file.close() # type: ignore
|
||||
await self.__file.close() # type: ignore
|
||||
except Exception:
|
||||
logger.exception("Can't close image writer")
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ class MsdDisabledError(MsdOperationError):
|
||||
|
||||
# =====
|
||||
class Plugin(BaseMsd):
|
||||
def __init__(self) -> None:
|
||||
self.__notifier = aiotools.AioNotifier()
|
||||
|
||||
async def get_state(self) -> dict:
|
||||
return {
|
||||
"enabled": False,
|
||||
@@ -49,10 +52,13 @@ class Plugin(BaseMsd):
|
||||
"drive": None,
|
||||
}
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
self.__notifier.notify()
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
while True:
|
||||
await self.__notifier.wait()
|
||||
yield (await self.get_state())
|
||||
await aiotools.wait_infinite()
|
||||
|
||||
async def reset(self) -> None:
|
||||
raise MsdDisabledError()
|
||||
|
||||
@@ -26,12 +26,12 @@ import dataclasses
|
||||
import functools
|
||||
import time
|
||||
import os
|
||||
import copy
|
||||
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from ....logging import get_logger
|
||||
|
||||
from ....inotify import InotifyMask
|
||||
from ....inotify import Inotify
|
||||
|
||||
from ....yamlconf import Option
|
||||
@@ -97,15 +97,17 @@ class _State:
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def busy(self, check_online: bool=True) -> AsyncGenerator[None, None]:
|
||||
async with self._region:
|
||||
async with self._lock:
|
||||
self.__notifier.notify()
|
||||
if check_online:
|
||||
if self.vd is None:
|
||||
raise MsdOfflineError()
|
||||
assert self.storage
|
||||
yield
|
||||
self.__notifier.notify()
|
||||
try:
|
||||
with self._region:
|
||||
async with self._lock:
|
||||
self.__notifier.notify()
|
||||
if check_online:
|
||||
if self.vd is None:
|
||||
raise MsdOfflineError()
|
||||
assert self.storage
|
||||
yield
|
||||
finally:
|
||||
self.__notifier.notify()
|
||||
|
||||
def is_busy(self) -> bool:
|
||||
return self._region.is_busy()
|
||||
@@ -141,10 +143,11 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
self.__notifier = aiotools.AioNotifier()
|
||||
self.__state = _State(self.__notifier)
|
||||
self.__reset = False
|
||||
|
||||
logger = get_logger(0)
|
||||
logger.info("Using OTG gadget %r as MSD", gadget)
|
||||
aiotools.run_sync(self.__reload_state(notify=False))
|
||||
aiotools.run_sync(self.__unsafe_reload_state())
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
@@ -164,14 +167,13 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
},
|
||||
}
|
||||
|
||||
# =====
|
||||
|
||||
async def get_state(self) -> dict:
|
||||
async with self.__state._lock: # pylint: disable=protected-access
|
||||
storage: (dict | None) = None
|
||||
if self.__state.storage:
|
||||
if self.__writer:
|
||||
# При загрузке файла показываем актуальную статистику вручную
|
||||
await self.__storage.reload_parts_info()
|
||||
|
||||
assert self.__state.vd
|
||||
storage = dataclasses.asdict(self.__state.storage)
|
||||
for name in list(storage["images"]):
|
||||
del storage["images"][name]["name"]
|
||||
@@ -185,34 +187,50 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
vd: (dict | None) = None
|
||||
if self.__state.vd:
|
||||
assert self.__state.storage
|
||||
vd = dataclasses.asdict(self.__state.vd)
|
||||
if vd["image"]:
|
||||
del vd["image"]["path"]
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
"online": (bool(self.__state.vd) and self.__drive.is_enabled()),
|
||||
"online": (bool(vd) and self.__drive.is_enabled()),
|
||||
"busy": self.__state.is_busy(),
|
||||
"storage": storage,
|
||||
"drive": vd,
|
||||
}
|
||||
|
||||
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 self.__notifier.wait()
|
||||
async def trigger_state(self) -> None:
|
||||
self.__notifier.notify(1)
|
||||
|
||||
async def systask(self) -> None:
|
||||
await self.__watch_inotify()
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
prev: dict = {}
|
||||
while True:
|
||||
if (await self.__notifier.wait()) > 0:
|
||||
prev = {}
|
||||
new = await self.get_state()
|
||||
if not prev or (prev.get("online") != new["online"]):
|
||||
prev = copy.deepcopy(new)
|
||||
yield new
|
||||
else:
|
||||
diff: dict = {}
|
||||
for sub in ["busy", "drive"]:
|
||||
if prev.get(sub) != new[sub]:
|
||||
diff[sub] = new[sub]
|
||||
for sub in ["images", "parts", "downloading", "uploading"]:
|
||||
if (prev.get("storage") or {}).get(sub) != (new["storage"] or {}).get(sub):
|
||||
if "storage" not in diff:
|
||||
diff["storage"] = {}
|
||||
diff["storage"][sub] = new["storage"][sub]
|
||||
if diff:
|
||||
prev = copy.deepcopy(new)
|
||||
yield diff
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def reset(self) -> None:
|
||||
async with self.__state.busy(check_online=False):
|
||||
try:
|
||||
self.__reset = True
|
||||
self.__drive.set_image_path("")
|
||||
self.__drive.set_cdrom_flag(False)
|
||||
self.__drive.set_rw_flag(False)
|
||||
@@ -220,11 +238,6 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
except Exception:
|
||||
get_logger(0).exception("Can't reset MSD properly")
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def cleanup(self) -> None:
|
||||
await self.__close_reader()
|
||||
await self.__close_writer()
|
||||
|
||||
# =====
|
||||
|
||||
@aiotools.atomic_fg
|
||||
@@ -296,11 +309,12 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
@contextlib.asynccontextmanager
|
||||
async def read_image(self, name: str) -> AsyncGenerator[MsdFileReader, None]:
|
||||
try:
|
||||
async with self.__state._region: # pylint: disable=protected-access
|
||||
with self.__state._region: # pylint: disable=protected-access
|
||||
try:
|
||||
async with self.__state._lock: # pylint: disable=protected-access
|
||||
self.__notifier.notify()
|
||||
self.__STATE_check_disconnected()
|
||||
|
||||
image = await self.__STATE_get_storage_image(name)
|
||||
self.__reader = await MsdFileReader(
|
||||
notifier=self.__notifier,
|
||||
@@ -308,7 +322,10 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
path=image.path,
|
||||
chunk_size=self.__read_chunk_size,
|
||||
).open()
|
||||
|
||||
self.__notifier.notify()
|
||||
yield self.__reader
|
||||
|
||||
finally:
|
||||
await aiotools.shield_fg(self.__close_reader())
|
||||
finally:
|
||||
@@ -316,18 +333,40 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def write_image(self, name: str, size: int, remove_incomplete: (bool | None)) -> AsyncGenerator[MsdFileWriter, None]:
|
||||
image: (Image | None) = None
|
||||
complete = False
|
||||
|
||||
async def finish_writing() -> None:
|
||||
# Делаем под блокировкой, чтобы эвент айнотифи не был обработан
|
||||
# до того, как мы не закончим все процедуры.
|
||||
async with self.__state._lock: # pylint: disable=protected-access
|
||||
try:
|
||||
self.__notifier.notify()
|
||||
finally:
|
||||
try:
|
||||
if image:
|
||||
await image.set_complete(complete)
|
||||
finally:
|
||||
try:
|
||||
if image and remove_incomplete and not complete:
|
||||
await image.remove(fatal=False)
|
||||
finally:
|
||||
try:
|
||||
await self.__close_writer()
|
||||
finally:
|
||||
if image:
|
||||
await image.remount_rw(False, fatal=False)
|
||||
|
||||
try:
|
||||
async with self.__state._region: # pylint: disable=protected-access
|
||||
image: (Image | None) = None
|
||||
with self.__state._region: # pylint: disable=protected-access
|
||||
try:
|
||||
async with self.__state._lock: # pylint: disable=protected-access
|
||||
self.__notifier.notify()
|
||||
self.__STATE_check_disconnected()
|
||||
image = await self.__STORAGE_create_new_image(name)
|
||||
|
||||
image = await self.__STORAGE_create_new_image(name)
|
||||
await image.remount_rw(True)
|
||||
await image.set_complete(False)
|
||||
|
||||
self.__writer = await MsdFileWriter(
|
||||
notifier=self.__notifier,
|
||||
name=image.name,
|
||||
@@ -339,22 +378,12 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
self.__notifier.notify()
|
||||
yield self.__writer
|
||||
await image.set_complete(self.__writer.is_complete())
|
||||
complete = await self.__writer.finish()
|
||||
|
||||
finally:
|
||||
try:
|
||||
if image and remove_incomplete and self.__writer and not self.__writer.is_complete():
|
||||
await image.remove(fatal=False)
|
||||
finally:
|
||||
try:
|
||||
await aiotools.shield_fg(self.__close_writer())
|
||||
finally:
|
||||
if image:
|
||||
await aiotools.shield_fg(image.remount_rw(False, fatal=False))
|
||||
await aiotools.shield_fg(finish_writing())
|
||||
finally:
|
||||
# Между закрытием файла и эвентом айнотифи состояние может быть не обновлено,
|
||||
# так что форсим обновление вручную, чтобы получить актуальное состояние.
|
||||
await aiotools.shield_fg(self.__reload_state())
|
||||
self.__notifier.notify()
|
||||
|
||||
@aiotools.atomic_fg
|
||||
async def remove(self, name: str) -> None:
|
||||
@@ -404,17 +433,26 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
async def __close_reader(self) -> None:
|
||||
if self.__reader:
|
||||
await self.__reader.close()
|
||||
self.__reader = None
|
||||
try:
|
||||
await self.__reader.close()
|
||||
finally:
|
||||
self.__reader = None
|
||||
|
||||
async def __close_writer(self) -> None:
|
||||
if self.__writer:
|
||||
await self.__writer.close()
|
||||
self.__writer = None
|
||||
try:
|
||||
await self.__writer.close()
|
||||
finally:
|
||||
self.__writer = None
|
||||
|
||||
# =====
|
||||
|
||||
async def __watch_inotify(self) -> None:
|
||||
@aiotools.atomic_fg
|
||||
async def cleanup(self) -> None:
|
||||
await self.__close_reader()
|
||||
await self.__close_writer()
|
||||
|
||||
async def systask(self) -> None:
|
||||
logger = get_logger(0)
|
||||
while True:
|
||||
try:
|
||||
@@ -426,19 +464,25 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
await asyncio.sleep(5)
|
||||
|
||||
with Inotify() as inotify:
|
||||
await inotify.watch(InotifyMask.ALL_MODIFY_EVENTS, *self.__storage.get_watchable_paths())
|
||||
await inotify.watch(InotifyMask.ALL_MODIFY_EVENTS, *self.__drive.get_watchable_paths())
|
||||
# Из-за гонки между первым релоадом и установкой вотчеров,
|
||||
# мы можем потерять какие-то каталоги стораджа, но это допустимо,
|
||||
# так как всегда есть ручной перезапуск.
|
||||
await inotify.watch_all_changes(*self.__storage.get_watchable_paths())
|
||||
await inotify.watch_all_changes(*self.__drive.get_watchable_paths())
|
||||
|
||||
# После установки вотчеров еще раз проверяем стейт, чтобы ничего не потерять
|
||||
# После установки вотчеров еще раз проверяем стейт,
|
||||
# чтобы не потерять состояние привода.
|
||||
await self.__reload_state()
|
||||
|
||||
while self.__state.vd: # Если живы после предыдущей проверки
|
||||
need_restart = False
|
||||
need_restart = self.__reset
|
||||
self.__reset = False
|
||||
need_reload_state = False
|
||||
for event in (await inotify.get_series(timeout=1)):
|
||||
need_reload_state = True
|
||||
if event.mask & (InotifyMask.DELETE_SELF | InotifyMask.MOVE_SELF | InotifyMask.UNMOUNT | InotifyMask.ISDIR):
|
||||
# Если выгрузили OTG, изменили каталоги, что-то отмонтировали или делают еще какую-то странную фигню
|
||||
if event.restart:
|
||||
# Если выгрузили OTG, изменили каталоги, что-то отмонтировали или делают еще какую-то странную фигню.
|
||||
# Проверяется маска InotifyMask.ALL_RESTART_EVENTS
|
||||
logger.info("Got a big inotify event: %s; reinitializing MSD ...", event)
|
||||
need_restart = True
|
||||
break
|
||||
@@ -446,56 +490,71 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||
break
|
||||
if need_reload_state:
|
||||
await self.__reload_state()
|
||||
elif self.__writer:
|
||||
# При загрузке файла обновляем статистику раз в секунду (по таймауту).
|
||||
# Это не нужно при обычном релоаде, потому что там и так проверяются все разделы.
|
||||
await self.__reload_parts_info()
|
||||
|
||||
except Exception:
|
||||
logger.exception("Unexpected MSD watcher error")
|
||||
time.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def __reload_state(self, notify: bool=True) -> None:
|
||||
logger = get_logger(0)
|
||||
async def __reload_state(self) -> None:
|
||||
async with self.__state._lock: # pylint: disable=protected-access
|
||||
try:
|
||||
path = self.__drive.get_image_path()
|
||||
drive_state = _DriveState(
|
||||
image=((await self.__storage.make_image_by_path(path)) if path else None),
|
||||
cdrom=self.__drive.get_cdrom_flag(),
|
||||
rw=self.__drive.get_rw_flag(),
|
||||
)
|
||||
await self.__unsafe_reload_state()
|
||||
self.__notifier.notify()
|
||||
|
||||
await self.__storage.reload()
|
||||
async def __reload_parts_info(self) -> None:
|
||||
assert self.__writer # Использовать только при записи образа
|
||||
async with self.__state._lock: # pylint: disable=protected-access
|
||||
await self.__storage.reload_parts_info()
|
||||
self.__notifier.notify()
|
||||
|
||||
if self.__state.vd is None and drive_state.image is None:
|
||||
# Если только что включились и образ не подключен - попробовать
|
||||
# перемонтировать хранилище (и создать images и meta).
|
||||
logger.info("Probing to remount storage ...")
|
||||
await self.__storage.remount_rw(True)
|
||||
await self.__storage.remount_rw(False)
|
||||
await self.__setup_initial()
|
||||
# ===== Don't call this directly ====
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error while reloading MSD state; switching to offline")
|
||||
self.__state.storage = None
|
||||
self.__state.vd = None
|
||||
async def __unsafe_reload_state(self) -> None:
|
||||
logger = get_logger(0)
|
||||
try:
|
||||
path = self.__drive.get_image_path()
|
||||
drive_state = _DriveState(
|
||||
image=((await self.__storage.make_image_by_path(path)) if path else None),
|
||||
cdrom=self.__drive.get_cdrom_flag(),
|
||||
rw=self.__drive.get_rw_flag(),
|
||||
)
|
||||
|
||||
await self.__storage.reload()
|
||||
|
||||
if self.__state.vd is None and drive_state.image is None:
|
||||
# Если только что включились и образ не подключен - попробовать
|
||||
# перемонтировать хранилище (и создать images и meta).
|
||||
logger.info("Probing to remount storage ...")
|
||||
await self.__storage.remount_rw(True)
|
||||
await self.__storage.remount_rw(False)
|
||||
await self.__unsafe_setup_initial()
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error while reloading MSD state; switching to offline")
|
||||
self.__state.storage = None
|
||||
self.__state.vd = None
|
||||
|
||||
else:
|
||||
self.__state.storage = self.__storage
|
||||
if drive_state.image:
|
||||
# При подключенном образе виртуальный стейт заменяется реальным
|
||||
self.__state.vd = _VirtualDriveState.from_drive_state(drive_state)
|
||||
else:
|
||||
self.__state.storage = self.__storage
|
||||
if drive_state.image:
|
||||
# При подключенном образе виртуальный стейт заменяется реальным
|
||||
if self.__state.vd is None:
|
||||
# Если раньше MSD был отключен
|
||||
self.__state.vd = _VirtualDriveState.from_drive_state(drive_state)
|
||||
else:
|
||||
if self.__state.vd is None:
|
||||
# Если раньше MSD был отключен
|
||||
self.__state.vd = _VirtualDriveState.from_drive_state(drive_state)
|
||||
|
||||
image = self.__state.vd.image
|
||||
if image and (not image.in_storage or not (await image.exists())):
|
||||
# Если только что отключили ручной образ вне хранилища или ранее выбранный образ был удален
|
||||
self.__state.vd.image = None
|
||||
image = self.__state.vd.image
|
||||
if image and (not image.in_storage or not (await image.exists())):
|
||||
# Если только что отключили ручной образ вне хранилища или ранее выбранный образ был удален
|
||||
self.__state.vd.image = None
|
||||
|
||||
self.__state.vd.connected = False
|
||||
if notify:
|
||||
self.__notifier.notify()
|
||||
self.__state.vd.connected = False
|
||||
|
||||
async def __setup_initial(self) -> None:
|
||||
async def __unsafe_setup_initial(self) -> None:
|
||||
if self.__initial_image:
|
||||
logger = get_logger(0)
|
||||
image = await self.__storage.make_image_by_name(self.__initial_image)
|
||||
|
||||
@@ -88,7 +88,7 @@ class Drive:
|
||||
try:
|
||||
with open(os.path.join(self.__lun_path, param), "w") as file:
|
||||
file.write(value + "\n")
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBUSY:
|
||||
except OSError as ex:
|
||||
if ex.errno == errno.EBUSY:
|
||||
raise MsdDriveLockedError()
|
||||
raise
|
||||
|
||||
@@ -169,8 +169,6 @@ class _Part(_PartDc):
|
||||
# =====
|
||||
@dataclasses.dataclass(frozen=True, eq=False)
|
||||
class _StorageDc:
|
||||
size: int = dataclasses.field(init=False)
|
||||
free: int = dataclasses.field(init=False)
|
||||
images: dict[str, Image] = dataclasses.field(init=False)
|
||||
parts: dict[str, _Part] = dataclasses.field(init=False)
|
||||
|
||||
@@ -185,25 +183,15 @@ class Storage(_StorageDc):
|
||||
self.__images: (dict[str, Image] | None) = None
|
||||
self.__parts: (dict[str, _Part] | None) = None
|
||||
|
||||
@property
|
||||
def size(self) -> int: # API Legacy
|
||||
assert self.__parts is not None
|
||||
return self.__parts[""].size
|
||||
|
||||
@property
|
||||
def free(self) -> int: # API Legacy
|
||||
assert self.__parts is not None
|
||||
return self.__parts[""].free
|
||||
|
||||
@property
|
||||
def images(self) -> dict[str, Image]:
|
||||
assert self.__images is not None
|
||||
return self.__images
|
||||
return dict(self.__images)
|
||||
|
||||
@property
|
||||
def parts(self) -> dict[str, _Part]:
|
||||
assert self.__parts is not None
|
||||
return self.__parts
|
||||
return dict(self.__parts)
|
||||
|
||||
async def reload(self) -> None:
|
||||
self.__watchable_paths = None
|
||||
@@ -222,6 +210,7 @@ class Storage(_StorageDc):
|
||||
part = _Part(name, root_path)
|
||||
await part._reload() # pylint: disable=protected-access
|
||||
parts[name] = part
|
||||
assert "" in parts, parts
|
||||
|
||||
self.__watchable_paths = watchable_paths
|
||||
self.__images = images
|
||||
|
||||
@@ -113,13 +113,13 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
while True:
|
||||
session = self.__ensure_http_session()
|
||||
try:
|
||||
async with session.get(f"{self.__url}/strg.cfg") as response:
|
||||
htclient.raise_not_200(response)
|
||||
parts = (await response.text()).split(";")
|
||||
async with session.get(f"{self.__url}/strg.cfg") as resp:
|
||||
htclient.raise_not_200(resp)
|
||||
parts = (await resp.text()).split(";")
|
||||
for pin in self.__state:
|
||||
self.__state[pin] = (parts[1 + int(pin) * 5] == "1")
|
||||
except Exception as err:
|
||||
get_logger().error("Failed ANELPWR bulk GET request: %s", tools.efmt(err))
|
||||
except Exception as ex:
|
||||
get_logger().error("Failed ANELPWR bulk GET request: %s", tools.efmt(ex))
|
||||
self.__state = dict.fromkeys(self.__state, None)
|
||||
if self.__state != prev_state:
|
||||
self._notifier.notify()
|
||||
@@ -143,10 +143,10 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
url=f"{self.__url}/ctrl.htm",
|
||||
data=f"F{pin}={int(state)}",
|
||||
headers={"Content-Type": "text/plain"},
|
||||
) as response:
|
||||
htclient.raise_not_200(response)
|
||||
except Exception as err:
|
||||
get_logger().error("Failed ANELPWR POST request to pin %s: %s", pin, tools.efmt(err))
|
||||
) as resp:
|
||||
htclient.raise_not_200(resp)
|
||||
except Exception as ex:
|
||||
get_logger().error("Failed ANELPWR POST request to pin %s: %s", pin, tools.efmt(ex))
|
||||
raise GpioDriverOfflineError(self)
|
||||
self.__update_notifier.notify()
|
||||
|
||||
|
||||
@@ -78,9 +78,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
proc = await aioproc.log_process(self.__cmd, logger=get_logger(0), prefix=str(self))
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"Custom command error: retcode={proc.returncode}")
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Can't run custom command [ %s ]: %s",
|
||||
tools.cmdfmt(self.__cmd), tools.efmt(err))
|
||||
tools.cmdfmt(self.__cmd), tools.efmt(ex))
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@@ -71,9 +71,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
try:
|
||||
proc = await aioproc.log_process(self.__cmd, logger=get_logger(0), prefix=str(self))
|
||||
return (proc.returncode == 0)
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Can't run custom command [ %s ]: %s",
|
||||
tools.cmdfmt(self.__cmd), tools.efmt(err))
|
||||
tools.cmdfmt(self.__cmd), tools.efmt(ex))
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
|
||||
@@ -150,9 +150,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
assert channel is not None
|
||||
self.__send_channel(tty, channel)
|
||||
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
self.__channel_queue.put_nowait(None)
|
||||
if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member
|
||||
if isinstance(ex, serial.SerialException) and ex.errno == errno.ENOENT: # pylint: disable=no-member
|
||||
logger.error("Missing %s serial device: %s", self, self.__device_path)
|
||||
else:
|
||||
logger.exception("Unexpected %s error", self)
|
||||
|
||||
@@ -150,9 +150,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
assert channel is not None
|
||||
self.__send_channel(tty, channel)
|
||||
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
self.__channel_queue.put_nowait(None)
|
||||
if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member
|
||||
if isinstance(ex, serial.SerialException) and ex.errno == errno.ENOENT: # pylint: disable=no-member
|
||||
logger.error("Missing %s serial device: %s", self, self.__device_path)
|
||||
else:
|
||||
logger.exception("Unexpected %s error", self)
|
||||
|
||||
@@ -54,7 +54,7 @@ class Plugin(BaseUserGpioDriver):
|
||||
self.__output_pins: dict[int, (bool | None)] = {}
|
||||
|
||||
self.__reader: (aiogp.AioReader | None) = None
|
||||
self.__outputs_request: (gpiod.LineRequest | None) = None
|
||||
self.__outputs_req: (gpiod.LineRequest | None) = None
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
@@ -74,7 +74,7 @@ class Plugin(BaseUserGpioDriver):
|
||||
|
||||
def prepare(self) -> None:
|
||||
assert self.__reader is None
|
||||
assert self.__outputs_request is None
|
||||
assert self.__outputs_req is None
|
||||
self.__reader = aiogp.AioReader(
|
||||
path=self.__device_path,
|
||||
consumer="kvmd::gpio::inputs",
|
||||
@@ -82,7 +82,7 @@ class Plugin(BaseUserGpioDriver):
|
||||
notifier=self._notifier,
|
||||
)
|
||||
if self.__output_pins:
|
||||
self.__outputs_request = gpiod.request_lines(
|
||||
self.__outputs_req = gpiod.request_lines(
|
||||
self.__device_path,
|
||||
consumer="kvmd::gpiod::outputs",
|
||||
config={
|
||||
@@ -99,9 +99,9 @@ class Plugin(BaseUserGpioDriver):
|
||||
await self.__reader.poll()
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
if self.__outputs_request:
|
||||
if self.__outputs_req:
|
||||
try:
|
||||
self.__outputs_request.release()
|
||||
self.__outputs_req.release()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -110,15 +110,15 @@ class Plugin(BaseUserGpioDriver):
|
||||
pin_int = int(pin)
|
||||
if pin_int in self.__input_pins:
|
||||
return self.__reader.get(pin_int)
|
||||
assert self.__outputs_request
|
||||
assert self.__outputs_req
|
||||
assert pin_int in self.__output_pins
|
||||
return bool(self.__outputs_request.get_value(pin_int).value)
|
||||
return bool(self.__outputs_req.get_value(pin_int).value)
|
||||
|
||||
async def write(self, pin: str, state: bool) -> None:
|
||||
assert self.__outputs_request
|
||||
assert self.__outputs_req
|
||||
pin_int = int(pin)
|
||||
assert pin_int in self.__output_pins
|
||||
self.__outputs_request.set_value(pin_int, gpiod.line.Value(state))
|
||||
self.__outputs_req.set_value(pin_int, gpiod.line.Value(state))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"GPIO({self._instance_name})"
|
||||
|
||||
@@ -93,9 +93,9 @@ class Plugin(BaseUserGpioDriver):
|
||||
try:
|
||||
with self.__ensure_device("probing"):
|
||||
pass
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
logger.error("Can't probe %s on %s: %s",
|
||||
self, self.__device_path, tools.efmt(err))
|
||||
self, self.__device_path, tools.efmt(ex))
|
||||
self.__reset_pins()
|
||||
|
||||
async def run(self) -> None:
|
||||
@@ -137,9 +137,9 @@ class Plugin(BaseUserGpioDriver):
|
||||
pin, state, self, self.__device_path)
|
||||
try:
|
||||
self.__inner_write(pin, state)
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
logger.error("Can't reset pin=%d of %s on %s: %s",
|
||||
pin, self, self.__device_path, tools.efmt(err))
|
||||
pin, self, self.__device_path, tools.efmt(ex))
|
||||
|
||||
def __inner_read(self, pin: int) -> bool:
|
||||
assert 0 <= pin <= 7
|
||||
@@ -168,9 +168,9 @@ class Plugin(BaseUserGpioDriver):
|
||||
get_logger(0).info("Opened %s on %s while %s", self, self.__device_path, context)
|
||||
try:
|
||||
yield self.__device
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Error occured on %s on %s while %s: %s",
|
||||
self, self.__device_path, context, tools.efmt(err))
|
||||
self, self.__device_path, context, tools.efmt(ex))
|
||||
self.__close_device()
|
||||
raise
|
||||
|
||||
|
||||
@@ -111,13 +111,13 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
while True:
|
||||
session = self.__ensure_http_session()
|
||||
try:
|
||||
async with session.get(f"{self.__url}/api/{self.__token}/lights") as response:
|
||||
results = await response.json()
|
||||
async with session.get(f"{self.__url}/api/{self.__token}/lights") as resp:
|
||||
results = await resp.json()
|
||||
for pin in self.__state:
|
||||
if pin in results:
|
||||
self.__state[pin] = bool(results[pin]["state"]["on"])
|
||||
except Exception as err:
|
||||
get_logger().error("Failed Hue bulk GET request: %s", tools.efmt(err))
|
||||
except Exception as ex:
|
||||
get_logger().error("Failed Hue bulk GET request: %s", tools.efmt(ex))
|
||||
self.__state = dict.fromkeys(self.__state, None)
|
||||
if self.__state != prev_state:
|
||||
self._notifier.notify()
|
||||
@@ -140,10 +140,10 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
async with session.put(
|
||||
url=f"{self.__url}/api/{self.__token}/lights/{pin}/state",
|
||||
json={"on": state},
|
||||
) as response:
|
||||
htclient.raise_not_200(response)
|
||||
except Exception as err:
|
||||
get_logger().error("Failed Hue PUT request to pin %s: %s", pin, tools.efmt(err))
|
||||
) as resp:
|
||||
htclient.raise_not_200(resp)
|
||||
except Exception as ex:
|
||||
get_logger().error("Failed Hue PUT request to pin %s: %s", pin, tools.efmt(ex))
|
||||
raise GpioDriverOfflineError(self)
|
||||
self.__update_notifier.notify()
|
||||
|
||||
|
||||
@@ -153,9 +153,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
proc = await aioproc.log_process(**self.__make_ipmitool_kwargs(action), logger=get_logger(0), prefix=str(self))
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"Ipmitool error: retcode={proc.returncode}")
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Can't send IPMI power-%s request to %s:%d: %s",
|
||||
action, self.__host, self.__port, tools.efmt(err))
|
||||
action, self.__host, self.__port, tools.efmt(ex))
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
# =====
|
||||
@@ -171,9 +171,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
self.__online = True
|
||||
return
|
||||
raise RuntimeError(f"Invalid ipmitool response: {text}")
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Can't fetch IPMI power status from %s:%d: %s",
|
||||
self.__host, self.__port, tools.efmt(err))
|
||||
self.__host, self.__port, tools.efmt(ex))
|
||||
self.__power = False
|
||||
self.__online = False
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class Plugin(BaseUserGpioDriver):
|
||||
self.__device_path = device_path
|
||||
|
||||
self.__tasks: dict[int, (asyncio.Task | None)] = {}
|
||||
self.__line_request: (gpiod.LineRequest | None) = None
|
||||
self.__line_req: (gpiod.LineRequest | None) = None
|
||||
|
||||
@classmethod
|
||||
def get_plugin_options(cls) -> dict:
|
||||
@@ -74,7 +74,7 @@ class Plugin(BaseUserGpioDriver):
|
||||
self.__tasks[int(pin)] = None
|
||||
|
||||
def prepare(self) -> None:
|
||||
self.__line_request = gpiod.request_lines(
|
||||
self.__line_req = gpiod.request_lines(
|
||||
self.__device_path,
|
||||
consumer="kvmd::locator",
|
||||
config={
|
||||
@@ -94,9 +94,9 @@ class Plugin(BaseUserGpioDriver):
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
if self.__line_request:
|
||||
if self.__line_req:
|
||||
try:
|
||||
self.__line_request.release()
|
||||
self.__line_req.release()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -115,17 +115,17 @@ class Plugin(BaseUserGpioDriver):
|
||||
|
||||
async def __blink(self, pin: int) -> None:
|
||||
assert pin in self.__tasks
|
||||
assert self.__line_request
|
||||
assert self.__line_req
|
||||
try:
|
||||
state = True
|
||||
while True:
|
||||
self.__line_request.set_value(pin, gpiod.line.Value(state))
|
||||
self.__line_req.set_value(pin, gpiod.line.Value(state))
|
||||
state = (not state)
|
||||
await asyncio.sleep(0.1)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
self.__line_request.set_value(pin, gpiod.line.Value(False))
|
||||
self.__line_req.set_value(pin, gpiod.line.Value(False))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Locator({self._instance_name})"
|
||||
|
||||
@@ -91,9 +91,9 @@ class Plugin(BaseUserGpioDriver):
|
||||
try:
|
||||
with self.__ensure_device("probing"):
|
||||
pass
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
logger.error("Can't probe %s on %s: %s",
|
||||
self, self.__device_path, tools.efmt(err))
|
||||
self, self.__device_path, tools.efmt(ex))
|
||||
self.__reset_pins()
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
@@ -119,9 +119,9 @@ class Plugin(BaseUserGpioDriver):
|
||||
pin, state, self, self.__device_path)
|
||||
try:
|
||||
self.__inner_write(pin, state)
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
logger.error("Can't reset pin=%d of %s on %s: %s",
|
||||
pin, self, self.__device_path, tools.efmt(err))
|
||||
pin, self, self.__device_path, tools.efmt(ex))
|
||||
|
||||
def __inner_write(self, pin: int, state: bool) -> None:
|
||||
assert 0 <= pin <= 7
|
||||
@@ -144,9 +144,9 @@ class Plugin(BaseUserGpioDriver):
|
||||
get_logger(0).info("Opened %s on %s while %s", self, self.__device_path, context)
|
||||
try:
|
||||
yield self.__device
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Error occured on %s on %s while %s: %s",
|
||||
self, self.__device_path, context, tools.efmt(err))
|
||||
self, self.__device_path, context, tools.efmt(ex))
|
||||
self.__close_device()
|
||||
raise
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ from typing import Any
|
||||
|
||||
from ...logging import get_logger
|
||||
|
||||
from ...inotify import InotifyMask
|
||||
from ...inotify import Inotify
|
||||
|
||||
from ... import aiotools
|
||||
@@ -82,15 +81,15 @@ class Plugin(BaseUserGpioDriver):
|
||||
await asyncio.sleep(5)
|
||||
|
||||
with Inotify() as inotify:
|
||||
await inotify.watch(InotifyMask.ALL_MODIFY_EVENTS, os.path.dirname(self.__udc_path))
|
||||
await inotify.watch(InotifyMask.ALL_MODIFY_EVENTS, self.__profile_path)
|
||||
await inotify.watch_all_changes(os.path.dirname(self.__udc_path))
|
||||
await inotify.watch_all_changes(self.__profile_path)
|
||||
self._notifier.notify()
|
||||
while True:
|
||||
need_restart = False
|
||||
need_notify = False
|
||||
for event in (await inotify.get_series(timeout=1)):
|
||||
need_notify = True
|
||||
if event.mask & (InotifyMask.DELETE_SELF | InotifyMask.MOVE_SELF | InotifyMask.UNMOUNT):
|
||||
if event.restart:
|
||||
logger.warning("Got fatal inotify event: %s; reinitializing OTG-bind ...", event)
|
||||
need_restart = True
|
||||
break
|
||||
|
||||
@@ -153,9 +153,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
assert channel is not None
|
||||
self.__send_channel(tty, channel)
|
||||
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
self.__channel_queue.put_nowait(None)
|
||||
if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member
|
||||
if isinstance(ex, serial.SerialException) and ex.errno == errno.ENOENT: # pylint: disable=no-member
|
||||
logger.error("Missing %s serial device: %s", self, self.__device_path)
|
||||
else:
|
||||
logger.exception("Unexpected %s error", self)
|
||||
|
||||
@@ -94,18 +94,18 @@ class Plugin(BaseUserGpioDriver):
|
||||
pwm.period_ns = self.__period
|
||||
pwm.duty_cycle_ns = self.__get_duty_cycle(bool(initial))
|
||||
pwm.enable()
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
logger.error("Can't get PWM chip %d channel %d: %s",
|
||||
self.__chip, pin, tools.efmt(err))
|
||||
self.__chip, pin, tools.efmt(ex))
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
for (pin, pwm) in self.__pwms.items():
|
||||
try:
|
||||
pwm.disable()
|
||||
pwm.close()
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Can't cleanup PWM chip %d channel %d: %s",
|
||||
self.__chip, pin, tools.efmt(err))
|
||||
self.__chip, pin, tools.efmt(ex))
|
||||
|
||||
async def read(self, pin: str) -> bool:
|
||||
try:
|
||||
|
||||
@@ -146,9 +146,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
asyncio.ensure_future(self.__reader.readexactly(6)),
|
||||
timeout=self.__timeout,
|
||||
))[4]
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Can't send command to TESmart KVM [%s]:%d: %s",
|
||||
self.__host, self.__port, tools.efmt(err))
|
||||
self.__host, self.__port, tools.efmt(ex))
|
||||
await self.__close_device()
|
||||
self.__active = -1
|
||||
raise GpioDriverOfflineError(self)
|
||||
@@ -168,9 +168,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
asyncio.ensure_future(asyncio.open_connection(self.__host, self.__port)),
|
||||
timeout=self.__timeout,
|
||||
)
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Can't connect to TESmart KVM [%s]:%d: %s",
|
||||
self.__host, self.__port, tools.efmt(err))
|
||||
self.__host, self.__port, tools.efmt(ex))
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
async def __ensure_device_serial(self) -> None:
|
||||
@@ -179,9 +179,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
serial_asyncio.open_serial_connection(url=self.__device_path, baudrate=self.__speed),
|
||||
timeout=self.__timeout,
|
||||
)
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
get_logger(0).error("Can't connect to TESmart KVM [%s]:%d: %s",
|
||||
self.__device_path, self.__speed, tools.efmt(err))
|
||||
self.__device_path, self.__speed, tools.efmt(ex))
|
||||
raise GpioDriverOfflineError(self)
|
||||
|
||||
async def __close_device(self) -> None:
|
||||
|
||||
@@ -157,9 +157,9 @@ class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attribute
|
||||
if self.__protocol == 2:
|
||||
self.__channel_queue.put_nowait(channel)
|
||||
|
||||
except Exception as err:
|
||||
except Exception as ex:
|
||||
self.__channel_queue.put_nowait(None)
|
||||
if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member
|
||||
if isinstance(ex, serial.SerialException) and ex.errno == errno.ENOENT: # pylint: disable=no-member
|
||||
logger.error("Missing %s serial device: %s", self, self.__device_path)
|
||||
else:
|
||||
logger.exception("Unexpected %s error", self)
|
||||
|
||||
Reference in New Issue
Block a user