mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-12 09:10:30 +08:00
otg msd and big refactoring
This commit is contained in:
parent
f6214191af
commit
10f8c2b335
2
Makefile
2
Makefile
@ -57,7 +57,7 @@ tox: testenv
|
|||||||
&& cp /usr/share/kvmd/configs.default/kvmd/main/v1-vga.yaml /etc/kvmd/main.yaml \
|
&& cp /usr/share/kvmd/configs.default/kvmd/main/v1-vga.yaml /etc/kvmd/main.yaml \
|
||||||
&& cp /src/testenv/v1-vga.override.yaml /etc/kvmd/override.yaml \
|
&& cp /src/testenv/v1-vga.override.yaml /etc/kvmd/override.yaml \
|
||||||
&& cd /src \
|
&& cd /src \
|
||||||
&& tox -c testenv/tox.ini $(if $(E),-e $(E),-p auto) \
|
&& tox -q -c testenv/tox.ini $(if $(E),-e $(E),-p auto) \
|
||||||
"
|
"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
# ========================================================================== #
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import contextlib
|
import contextlib
|
||||||
@ -34,6 +35,9 @@ from typing import AsyncGenerator
|
|||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
import aiofiles.base
|
||||||
|
|
||||||
from . import aioregion
|
from . import aioregion
|
||||||
|
|
||||||
from .logging import get_logger
|
from .logging import get_logger
|
||||||
@ -118,3 +122,10 @@ async def unlock_only_on_exception(lock: asyncio.Lock) -> AsyncGenerator[None, N
|
|||||||
except: # noqa: E722
|
except: # noqa: E722
|
||||||
lock.release()
|
lock.release()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
async def afile_write_now(afile: aiofiles.base.AiofilesContextManager, data: bytes) -> None:
|
||||||
|
await afile.write(data)
|
||||||
|
await afile.flush()
|
||||||
|
await run_async(os.fsync, afile.fileno())
|
||||||
|
|||||||
@ -368,7 +368,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
self.__broadcast_event(_Events.INFO_STATE, (await self.__make_info())),
|
self.__broadcast_event(_Events.INFO_STATE, (await self.__make_info())),
|
||||||
self.__broadcast_event(_Events.HID_STATE, self.__hid.get_state()),
|
self.__broadcast_event(_Events.HID_STATE, self.__hid.get_state()),
|
||||||
self.__broadcast_event(_Events.ATX_STATE, self.__atx.get_state()),
|
self.__broadcast_event(_Events.ATX_STATE, self.__atx.get_state()),
|
||||||
self.__broadcast_event(_Events.MSD_STATE, self.__msd.get_state()),
|
self.__broadcast_event(_Events.MSD_STATE, (await self.__msd.get_state())),
|
||||||
self.__broadcast_event(_Events.STREAMER_STATE, (await self.__streamer.get_state())),
|
self.__broadcast_event(_Events.STREAMER_STATE, (await self.__streamer.get_state())),
|
||||||
])
|
])
|
||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
@ -469,52 +469,60 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
@_exposed("GET", "/msd")
|
@_exposed("GET", "/msd")
|
||||||
async def __msd_state_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
|
async def __msd_state_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
return _json(self.__msd.get_state())
|
return _json(await self.__msd.get_state())
|
||||||
|
|
||||||
|
@_exposed("POST", "/msd/set_params")
|
||||||
|
async def __msd_set_params_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
|
params = {
|
||||||
|
key: validator(request.query.get(param))
|
||||||
|
for (param, key, validator) in [
|
||||||
|
("image", "name", (lambda arg: str(arg).strip() and valid_msd_image_name(arg))),
|
||||||
|
("cdrom", "cdrom", valid_bool),
|
||||||
|
]
|
||||||
|
if request.query.get(param) is not None
|
||||||
|
}
|
||||||
|
await self.__msd.set_params(**params) # type: ignore
|
||||||
|
return _json()
|
||||||
|
|
||||||
@_exposed("POST", "/msd/connect")
|
@_exposed("POST", "/msd/connect")
|
||||||
async def __msd_connect_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
|
async def __msd_connect_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
return _json(await self.__msd.connect())
|
await self.__msd.connect()
|
||||||
|
return _json()
|
||||||
|
|
||||||
@_exposed("POST", "/msd/disconnect")
|
@_exposed("POST", "/msd/disconnect")
|
||||||
async def __msd_disconnect_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
|
async def __msd_disconnect_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
return _json(await self.__msd.disconnect())
|
await self.__msd.disconnect()
|
||||||
|
return _json()
|
||||||
@_exposed("POST", "/msd/select")
|
|
||||||
async def __msd_select_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
|
|
||||||
image_name = valid_msd_image_name(request.query.get("image_name"))
|
|
||||||
cdrom = valid_bool(request.query.get("cdrom", "true"))
|
|
||||||
return _json(await self.__msd.select(image_name, cdrom))
|
|
||||||
|
|
||||||
@_exposed("POST", "/msd/remove")
|
|
||||||
async def __msd_remove_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
|
|
||||||
return _json(await self.__msd.remove(valid_msd_image_name(request.query.get("image_name"))))
|
|
||||||
|
|
||||||
@_exposed("POST", "/msd/write")
|
@_exposed("POST", "/msd/write")
|
||||||
async def __msd_write_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
|
async def __msd_write_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
assert self.__sync_chunk_size is not None
|
assert self.__sync_chunk_size is not None
|
||||||
logger = get_logger(0)
|
logger = get_logger(0)
|
||||||
reader = await request.multipart()
|
reader = await request.multipart()
|
||||||
image_name = ""
|
name = ""
|
||||||
written = 0
|
written = 0
|
||||||
try:
|
try:
|
||||||
async with self.__msd:
|
name_field = await _get_multipart_field(reader, "image")
|
||||||
name_field = await _get_multipart_field(reader, "image_name")
|
name = valid_msd_image_name((await name_field.read()).decode("utf-8"))
|
||||||
image_name = valid_msd_image_name((await name_field.read()).decode("utf-8"))
|
|
||||||
|
|
||||||
data_field = await _get_multipart_field(reader, "image_data")
|
data_field = await _get_multipart_field(reader, "data")
|
||||||
|
|
||||||
logger.info("Writing image %r to MSD ...", image_name)
|
async with self.__msd.write_image(name):
|
||||||
await self.__msd.write_image_info(image_name, False)
|
logger.info("Writing image %r to MSD ...", name)
|
||||||
while True:
|
while True:
|
||||||
chunk = await data_field.read_chunk(self.__sync_chunk_size)
|
chunk = await data_field.read_chunk(self.__sync_chunk_size)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
written = await self.__msd.write_image_chunk(chunk)
|
written = await self.__msd.write_image_chunk(chunk)
|
||||||
await self.__msd.write_image_info(image_name, True)
|
|
||||||
finally:
|
finally:
|
||||||
if written != 0:
|
if written != 0:
|
||||||
logger.info("Written image %r with size=%d bytes to MSD", image_name, written)
|
logger.info("Written image %r with size=%d bytes to MSD", name, written)
|
||||||
return _json({"image": {"name": image_name, "size": written}})
|
return _json({"image": {"name": name, "size": written}})
|
||||||
|
|
||||||
|
@_exposed("POST", "/msd/remove")
|
||||||
|
async def __msd_remove_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
|
await self.__msd.remove(valid_msd_image_name(request.query.get("image")))
|
||||||
|
return _json()
|
||||||
|
|
||||||
@_exposed("POST", "/msd/reset")
|
@_exposed("POST", "/msd/reset")
|
||||||
async def __msd_reset_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
|
async def __msd_reset_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
|
|||||||
335
kvmd/inotify.py
Normal file
335
kvmd/inotify.py
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
# ========================================================================== #
|
||||||
|
# #
|
||||||
|
# KVMD - The main Pi-KVM daemon. #
|
||||||
|
# #
|
||||||
|
# Copyright (C) 2018 Maxim Devaev <mdevaev@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This source file is partially based on python-watchdog module. #
|
||||||
|
# #
|
||||||
|
# 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 asyncio
|
||||||
|
import asyncio.queues
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
import struct
|
||||||
|
import dataclasses
|
||||||
|
import types
|
||||||
|
import errno
|
||||||
|
|
||||||
|
from ctypes import c_int
|
||||||
|
from ctypes import c_char_p
|
||||||
|
from ctypes import c_uint32
|
||||||
|
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import List
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Type
|
||||||
|
from typing import Generator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .logging import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
def _load_libc() -> ctypes.CDLL:
|
||||||
|
try:
|
||||||
|
path = ctypes.util.find_library("c")
|
||||||
|
except (OSError, IOError, RuntimeError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if path:
|
||||||
|
return ctypes.CDLL(path)
|
||||||
|
|
||||||
|
names = ["libc.so", "libc.so.6", "libc.so.0"]
|
||||||
|
for (index, name) in enumerate(names):
|
||||||
|
try:
|
||||||
|
return ctypes.CDLL(name)
|
||||||
|
except (OSError, IOError):
|
||||||
|
if index == len(names) - 1:
|
||||||
|
raise
|
||||||
|
|
||||||
|
raise RuntimeError("Where is libc?")
|
||||||
|
|
||||||
|
|
||||||
|
_libc = _load_libc()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_libc_func(name: str, restype, argtypes=None): # type: ignore
|
||||||
|
return ctypes.CFUNCTYPE(restype, *(argtypes or []), use_errno=True)((name, _libc))
|
||||||
|
|
||||||
|
|
||||||
|
_inotify_init = _get_libc_func("inotify_init", c_int)
|
||||||
|
_inotify_add_watch = _get_libc_func("inotify_add_watch", c_int, [c_int, c_char_p, c_uint32])
|
||||||
|
_inotify_rm_watch = _get_libc_func("inotify_rm_watch", c_int, [c_int, c_uint32])
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
_EVENT_HEAD_FMT = "iIII"
|
||||||
|
_EVENT_HEAD_SIZE = struct.calcsize(_EVENT_HEAD_FMT)
|
||||||
|
_EVENTS_BUFFER_LENGTH = 4096 * (_EVENT_HEAD_SIZE + 256) # count * (head + max_file_name_size + null_character)
|
||||||
|
|
||||||
|
_FS_FALLBACK_ENCODING = "utf-8"
|
||||||
|
_FS_ENCODING = (sys.getfilesystemencoding() or _FS_FALLBACK_ENCODING)
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
def _inotify_parsed_buffer(data: bytes) -> Generator[Tuple[int, int, int, bytes], None, None]:
|
||||||
|
offset = 0
|
||||||
|
while offset + _EVENT_HEAD_SIZE <= len(data):
|
||||||
|
(wd, mask, cookie, length) = struct.unpack_from("iIII", data, offset)
|
||||||
|
name = data[
|
||||||
|
offset + _EVENT_HEAD_SIZE # noqa: E203
|
||||||
|
:
|
||||||
|
offset + _EVENT_HEAD_SIZE + length
|
||||||
|
].rstrip(b"\0")
|
||||||
|
offset += _EVENT_HEAD_SIZE + length
|
||||||
|
if wd >= 0:
|
||||||
|
yield (wd, mask, cookie, name)
|
||||||
|
|
||||||
|
|
||||||
|
def _inotify_check(retval: int) -> int:
|
||||||
|
if retval < 0:
|
||||||
|
c_errno = ctypes.get_errno()
|
||||||
|
if c_errno == errno.ENOSPC: # pylint: disable=no-else-raise
|
||||||
|
raise OSError(c_errno, "Inotify watch limit reached")
|
||||||
|
elif c_errno == errno.EMFILE:
|
||||||
|
raise OSError(c_errno, "Inotify instance limit reached")
|
||||||
|
else:
|
||||||
|
raise OSError(c_errno, os.strerror(c_errno))
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
def _fs_encode(path: str) -> bytes:
|
||||||
|
try:
|
||||||
|
return path.encode(_FS_ENCODING, "strict")
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
return path.encode(_FS_FALLBACK_ENCODING, "strict")
|
||||||
|
|
||||||
|
|
||||||
|
def _fs_decode(path: bytes) -> str:
|
||||||
|
try:
|
||||||
|
return path.decode(_FS_ENCODING, "strict")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return path.decode(_FS_FALLBACK_ENCODING, "strict")
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
class InotifyMask:
|
||||||
|
# Userspace events
|
||||||
|
ACCESS = 0x00000001 # File was accessed
|
||||||
|
ATTRIB = 0x00000004 # Meta-data changed
|
||||||
|
CLOSE_WRITE = 0x00000008 # Writable file was closed
|
||||||
|
CLOSE_NOWRITE = 0x00000010 # Unwritable file closed
|
||||||
|
CREATE = 0x00000100 # Subfile was created
|
||||||
|
DELETE = 0x00000200 # Subfile was deleted
|
||||||
|
DELETE_SELF = 0x00000400 # Self was deleted
|
||||||
|
MODIFY = 0x00000002 # File was modified
|
||||||
|
MOVE_SELF = 0x00000800 # Self was moved
|
||||||
|
MOVED_FROM = 0x00000040 # File was moved from X
|
||||||
|
MOVED_TO = 0x00000080 # File was moved to Y
|
||||||
|
OPEN = 0x00000020 # File was opened
|
||||||
|
|
||||||
|
# Events sent by the kernel to a watch
|
||||||
|
IGNORED = 0x00008000 # File was ignored
|
||||||
|
ISDIR = 0x40000000 # Event occurred against directory
|
||||||
|
Q_OVERFLOW = 0x00004000 # Event queued overflowed
|
||||||
|
UNMOUNT = 0x00002000 # Backing file system was unmounted
|
||||||
|
|
||||||
|
# Helper userspace events
|
||||||
|
# CLOSE = CLOSE_WRITE | CLOSE_NOWRITE # Close
|
||||||
|
# MOVE = MOVED_FROM | MOVED_TO # Moves
|
||||||
|
|
||||||
|
# Helper for userspace events
|
||||||
|
# ALL_EVENTS = (
|
||||||
|
# ACCESS
|
||||||
|
# | ATTRIB
|
||||||
|
# | CLOSE_WRITE
|
||||||
|
# | CLOSE_NOWRITE
|
||||||
|
# | CREATE
|
||||||
|
# | DELETE
|
||||||
|
# | DELETE_SELF
|
||||||
|
# | MODIFY
|
||||||
|
# | MOVE_SELF
|
||||||
|
# | MOVED_FROM
|
||||||
|
# | MOVED_TO
|
||||||
|
# | OPEN
|
||||||
|
# )
|
||||||
|
|
||||||
|
# Helper for all modify events
|
||||||
|
ALL_MODIFY_EVENTS = (
|
||||||
|
CLOSE_WRITE
|
||||||
|
| CREATE
|
||||||
|
| DELETE
|
||||||
|
| DELETE_SELF
|
||||||
|
| MODIFY
|
||||||
|
| MOVE_SELF
|
||||||
|
| MOVED_FROM
|
||||||
|
| MOVED_TO
|
||||||
|
)
|
||||||
|
|
||||||
|
# Special flags for watch()
|
||||||
|
# DONT_FOLLOW = 0x02000000 # Don't follow a symbolic link
|
||||||
|
# EXCL_UNLINK = 0x04000000 # Exclude events on unlinked objects
|
||||||
|
# MASK_CREATE = 0x10000000 # Don't overwrite existent watchers (since 4.18)
|
||||||
|
# MASK_ADD = 0x20000000 # Add to the mask of an existing watch
|
||||||
|
# ONESHOT = 0x80000000 # Only send event once
|
||||||
|
# ONLYDIR = 0x01000000 # Only watch the path if it's a directory
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_string(cls, mask: int) -> str:
|
||||||
|
flags: List[str] = []
|
||||||
|
for name in dir(cls):
|
||||||
|
if (
|
||||||
|
name[0].isupper()
|
||||||
|
and not name.startswith("ALL_")
|
||||||
|
and name not in ["CLOSE", "MOVE"]
|
||||||
|
and mask & getattr(cls, name)
|
||||||
|
):
|
||||||
|
flags.append(name)
|
||||||
|
return "|".join(flags)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True, repr=False)
|
||||||
|
class InotifyEvent:
|
||||||
|
wd: int
|
||||||
|
mask: int
|
||||||
|
cookie: int
|
||||||
|
name: str
|
||||||
|
path: str
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<InotifyEvent: wd={self.wd}, mask={InotifyMask.to_string(self.mask)},"
|
||||||
|
f" cookie={self.cookie}, name={self.name}, path={self.path}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Inotify:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.__fd = -1
|
||||||
|
|
||||||
|
self.__wd_by_path: Dict[str, int] = {}
|
||||||
|
self.__path_by_wd: Dict[int, str] = {}
|
||||||
|
|
||||||
|
self.__moved: Dict[int, str] = {}
|
||||||
|
|
||||||
|
self.__events_queue: asyncio.queues.Queue = asyncio.Queue()
|
||||||
|
|
||||||
|
def watch(self, path: str, mask: int) -> None:
|
||||||
|
path = os.path.normpath(path)
|
||||||
|
assert path not in self.__wd_by_path, path
|
||||||
|
get_logger().info("Watching for %s: %s", path, InotifyMask.to_string(mask))
|
||||||
|
wd = _inotify_check(_inotify_add_watch(self.__fd, _fs_encode(path), mask))
|
||||||
|
self.__wd_by_path[path] = wd
|
||||||
|
self.__path_by_wd[wd] = path
|
||||||
|
|
||||||
|
# def unwatch(self, path: str) -> None:
|
||||||
|
# path = os.path.normpath(path)
|
||||||
|
# assert path in self.__wd_by_path, path
|
||||||
|
# get_logger().info("Unwatching %s", path)
|
||||||
|
# wd = self.__wd_by_path[path]
|
||||||
|
# _inotify_check(_inotify_rm_watch(self.__fd, wd))
|
||||||
|
# del self.__wd_by_path[path]
|
||||||
|
# del self.__path_by_wd[wd]
|
||||||
|
|
||||||
|
# def has_events(self) -> bool:
|
||||||
|
# return (not self.__events_queue.empty())
|
||||||
|
|
||||||
|
async def get_event(self, timeout: float) -> Optional[InotifyEvent]:
|
||||||
|
assert timeout > 0
|
||||||
|
try:
|
||||||
|
return (await asyncio.wait_for(self.__events_queue.get(), timeout=timeout))
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_series(self, timeout: float) -> List[InotifyEvent]:
|
||||||
|
series: List[InotifyEvent] = []
|
||||||
|
event = await self.get_event(timeout)
|
||||||
|
if event:
|
||||||
|
series.append(event)
|
||||||
|
while event:
|
||||||
|
event = await self.get_event(timeout)
|
||||||
|
if event:
|
||||||
|
series.append(event)
|
||||||
|
return series
|
||||||
|
|
||||||
|
def __read_and_queue_events(self) -> None:
|
||||||
|
logger = get_logger()
|
||||||
|
for event in self.__read_parsed_events():
|
||||||
|
# XXX: Ни в коем случае не приводить self.__read_parsed_events() к списку.
|
||||||
|
# Он использует self.__wd_by_path и self.__path_by_wd, содержимое которых
|
||||||
|
# корректируется кодом ниже. В противном случае все сломается.
|
||||||
|
|
||||||
|
if event.mask & InotifyMask.MOVED_FROM:
|
||||||
|
self.__moved[event.cookie] = event.path # Save moved_from_path
|
||||||
|
elif event.mask & InotifyMask.MOVED_TO:
|
||||||
|
moved_from_path = self.__moved.pop(event.cookie, None)
|
||||||
|
if moved_from_path is not None:
|
||||||
|
wd = self.__wd_by_path.pop(moved_from_path, None)
|
||||||
|
if wd is not None:
|
||||||
|
self.__wd_by_path[event.path] = wd
|
||||||
|
self.__path_by_wd[wd] = event.path
|
||||||
|
|
||||||
|
if event.mask & InotifyMask.IGNORED:
|
||||||
|
ignored_path = self.__path_by_wd[event.wd]
|
||||||
|
if self.__wd_by_path[ignored_path] == event.wd:
|
||||||
|
logger.info("Unwatching %s because IGNORED was received", ignored_path)
|
||||||
|
del self.__wd_by_path[ignored_path]
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.__events_queue.put_nowait(event)
|
||||||
|
|
||||||
|
def __read_parsed_events(self) -> Generator[InotifyEvent, None, None]:
|
||||||
|
for (wd, mask, cookie, name_bytes) in _inotify_parsed_buffer(self.__read_buffer()):
|
||||||
|
wd_path = self.__path_by_wd.get(wd, None)
|
||||||
|
if wd_path is not None:
|
||||||
|
name = _fs_decode(name_bytes)
|
||||||
|
path = (os.path.join(wd_path, name) if name else wd_path) # Avoid trailing slash
|
||||||
|
yield InotifyEvent(wd, mask, cookie, name, path)
|
||||||
|
|
||||||
|
def __read_buffer(self) -> bytes:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return os.read(self.__fd, _EVENTS_BUFFER_LENGTH)
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno == errno.EINTR:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __enter__(self) -> "Inotify":
|
||||||
|
assert self.__fd < 0
|
||||||
|
self.__fd = _inotify_check(_inotify_init())
|
||||||
|
asyncio.get_event_loop().add_reader(self.__fd, self.__read_and_queue_events)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
_exc_type: Type[BaseException],
|
||||||
|
_exc: BaseException,
|
||||||
|
_tb: types.TracebackType,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
if self.__fd >= 0:
|
||||||
|
asyncio.get_event_loop().remove_reader(self.__fd)
|
||||||
|
for wd in list(self.__wd_by_path.values()):
|
||||||
|
_inotify_rm_watch(self.__fd, wd)
|
||||||
|
try:
|
||||||
|
os.close(self.__fd)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
@ -20,11 +20,12 @@
|
|||||||
# ========================================================================== #
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
import types
|
import contextlib
|
||||||
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from .. import BasePlugin
|
from .. import BasePlugin
|
||||||
from .. import get_plugin_class
|
from .. import get_plugin_class
|
||||||
@ -44,19 +45,29 @@ class MsdOfflineError(MsdOperationError):
|
|||||||
super().__init__("MSD is not found")
|
super().__init__("MSD is not found")
|
||||||
|
|
||||||
|
|
||||||
class MsdAlreadyConnectedError(MsdOperationError):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__("MSD is already connected to Server")
|
|
||||||
|
|
||||||
|
|
||||||
class MsdAlreadyDisconnectedError(MsdOperationError):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__("MSD is already disconnected from Server")
|
|
||||||
|
|
||||||
|
|
||||||
class MsdConnectedError(MsdOperationError):
|
class MsdConnectedError(MsdOperationError):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__("MSD connected to Server, but should not")
|
super().__init__("MSD is connected to Server, but shouldn't for this operation")
|
||||||
|
|
||||||
|
|
||||||
|
class MsdDisconnectedError(MsdOperationError):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__("MSD is disconnected from Server, but should be for this operation")
|
||||||
|
|
||||||
|
|
||||||
|
class MsdImageNotSelected(MsdOperationError):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__("The image is not selected")
|
||||||
|
|
||||||
|
|
||||||
|
class MsdUnknownImageError(MsdOperationError):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__("The image is not found in the storage")
|
||||||
|
|
||||||
|
|
||||||
|
class MsdImageExistsError(MsdOperationError):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__("This image is already exists")
|
||||||
|
|
||||||
|
|
||||||
class MsdIsBusyError(MsdOperationError):
|
class MsdIsBusyError(MsdOperationError):
|
||||||
@ -69,52 +80,51 @@ class MsdMultiNotSupported(MsdOperationError):
|
|||||||
super().__init__("This MSD does not support storing multiple images")
|
super().__init__("This MSD does not support storing multiple images")
|
||||||
|
|
||||||
|
|
||||||
|
class MsdCdromNotSupported(MsdOperationError):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__("This MSD does not support CD-ROM emulation")
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
class BaseMsd(BasePlugin):
|
class BaseMsd(BasePlugin):
|
||||||
def get_state(self) -> Dict:
|
async def get_state(self) -> Dict:
|
||||||
raise NotImplementedError
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def poll_state(self) -> AsyncGenerator[Dict, None]:
|
async def poll_state(self) -> AsyncGenerator[Dict, None]:
|
||||||
yield {}
|
if True: # pylint: disable=using-constant-test
|
||||||
raise NotImplementedError
|
# XXX: Vulture hack
|
||||||
|
raise NotImplementedError()
|
||||||
|
yield
|
||||||
|
|
||||||
async def reset(self) -> None:
|
async def reset(self) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def cleanup(self) -> None:
|
async def cleanup(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|
||||||
async def connect(self) -> Dict:
|
async def set_params(self, name: Optional[str]=None, cdrom: Optional[bool]=None) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def disconnect(self) -> Dict:
|
async def connect(self) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def select(self, name: str, cdrom: bool) -> Dict:
|
async def disconnect(self) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def remove(self, name: str) -> Dict:
|
@contextlib.asynccontextmanager
|
||||||
raise NotImplementedError
|
async def write_image(self, name: str) -> AsyncGenerator[None, None]:
|
||||||
|
if True: # pylint: disable=using-constant-test
|
||||||
async def __aenter__(self) -> "BaseMsd":
|
# XXX: Vulture hack
|
||||||
raise NotImplementedError
|
raise NotImplementedError()
|
||||||
|
yield
|
||||||
async def write_image_info(self, name: str, complete: bool) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def write_image_chunk(self, chunk: bytes) -> int:
|
async def write_image_chunk(self, chunk: bytes) -> int:
|
||||||
raise NotImplementedError
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def __aexit__(
|
async def remove(self, name: str) -> None:
|
||||||
self,
|
raise NotImplementedError()
|
||||||
_exc_type: Type[BaseException],
|
|
||||||
_exc: BaseException,
|
|
||||||
_tb: types.TracebackType,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|||||||
@ -21,11 +21,11 @@
|
|||||||
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import types
|
import contextlib
|
||||||
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Type
|
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from . import MsdOperationError
|
from . import MsdOperationError
|
||||||
from . import BaseMsd
|
from . import BaseMsd
|
||||||
@ -39,23 +39,22 @@ class MsdDisabledError(MsdOperationError):
|
|||||||
|
|
||||||
# =====
|
# =====
|
||||||
class Plugin(BaseMsd):
|
class Plugin(BaseMsd):
|
||||||
def get_state(self) -> Dict:
|
async def get_state(self) -> Dict:
|
||||||
return {
|
return {
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
"multi": False,
|
|
||||||
"online": False,
|
"online": False,
|
||||||
"busy": False,
|
"busy": False,
|
||||||
"uploading": False,
|
|
||||||
"written": 0,
|
|
||||||
"current": None,
|
|
||||||
"storage": None,
|
"storage": None,
|
||||||
"cdrom": None,
|
"drive": None,
|
||||||
"connected": False,
|
"features": {
|
||||||
|
"multi": False,
|
||||||
|
"cdrom": False,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def poll_state(self) -> AsyncGenerator[Dict, None]:
|
async def poll_state(self) -> AsyncGenerator[Dict, None]:
|
||||||
while True:
|
while True:
|
||||||
yield self.get_state()
|
yield (await self.get_state())
|
||||||
await asyncio.sleep(60)
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
async def reset(self) -> None:
|
async def reset(self) -> None:
|
||||||
@ -63,32 +62,24 @@ class Plugin(BaseMsd):
|
|||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|
||||||
async def connect(self) -> Dict:
|
async def set_params(self, name: Optional[str]=None, cdrom: Optional[bool]=None) -> None:
|
||||||
raise MsdDisabledError()
|
raise MsdDisabledError()
|
||||||
|
|
||||||
async def disconnect(self) -> Dict:
|
async def connect(self) -> None:
|
||||||
raise MsdDisabledError()
|
raise MsdDisabledError()
|
||||||
|
|
||||||
async def select(self, name: str, cdrom: bool) -> Dict:
|
async def disconnect(self) -> None:
|
||||||
raise MsdDisabledError()
|
raise MsdDisabledError()
|
||||||
|
|
||||||
async def remove(self, name: str) -> Dict:
|
@contextlib.asynccontextmanager
|
||||||
raise MsdDisabledError()
|
async def write_image(self, name: str) -> AsyncGenerator[None, None]:
|
||||||
|
if True: # pylint: disable=using-constant-test
|
||||||
async def __aenter__(self) -> BaseMsd:
|
# XXX: Vulture hack
|
||||||
raise MsdDisabledError()
|
|
||||||
|
|
||||||
async def write_image_info(self, name: str, complete: bool) -> None:
|
|
||||||
raise MsdDisabledError()
|
raise MsdDisabledError()
|
||||||
|
yield
|
||||||
|
|
||||||
async def write_image_chunk(self, chunk: bytes) -> int:
|
async def write_image_chunk(self, chunk: bytes) -> int:
|
||||||
raise MsdDisabledError()
|
raise MsdDisabledError()
|
||||||
|
|
||||||
async def __aexit__(
|
async def remove(self, name: str) -> None:
|
||||||
self,
|
|
||||||
_exc_type: Type[BaseException],
|
|
||||||
_exc: BaseException,
|
|
||||||
_tb: types.TracebackType,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
raise MsdDisabledError()
|
raise MsdDisabledError()
|
||||||
|
|||||||
@ -1,108 +0,0 @@
|
|||||||
# ========================================================================== #
|
|
||||||
# #
|
|
||||||
# KVMD - The main Pi-KVM daemon. #
|
|
||||||
# #
|
|
||||||
# Copyright (C) 2018 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 asyncio
|
|
||||||
import types
|
|
||||||
|
|
||||||
from typing import Dict
|
|
||||||
from typing import Type
|
|
||||||
from typing import AsyncGenerator
|
|
||||||
|
|
||||||
from ...yamlconf import Option
|
|
||||||
|
|
||||||
from ...validators.os import valid_abs_dir
|
|
||||||
from ...validators.os import valid_command
|
|
||||||
|
|
||||||
from . import MsdOperationError
|
|
||||||
from . import BaseMsd
|
|
||||||
|
|
||||||
|
|
||||||
# =====
|
|
||||||
class MsdCliOnlyError(MsdOperationError):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__("Only CLI")
|
|
||||||
|
|
||||||
|
|
||||||
# =====
|
|
||||||
class Plugin(BaseMsd):
|
|
||||||
@classmethod
|
|
||||||
def get_plugin_options(cls) -> Dict:
|
|
||||||
sudo = ["/usr/bin/sudo", "--non-interactive"]
|
|
||||||
return {
|
|
||||||
"storage": Option("/var/lib/kvmd/msd", type=valid_abs_dir, unpack_as="storage_path"),
|
|
||||||
"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),
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_state(self) -> Dict:
|
|
||||||
return {
|
|
||||||
"enabled": False,
|
|
||||||
"multi": False,
|
|
||||||
"online": False,
|
|
||||||
"busy": False,
|
|
||||||
"uploading": False,
|
|
||||||
"written": 0,
|
|
||||||
"current": None,
|
|
||||||
"storage": None,
|
|
||||||
"cdrom": None,
|
|
||||||
"connected": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def poll_state(self) -> AsyncGenerator[Dict, None]:
|
|
||||||
while True:
|
|
||||||
yield self.get_state()
|
|
||||||
await asyncio.sleep(60)
|
|
||||||
|
|
||||||
async def reset(self) -> None:
|
|
||||||
raise MsdCliOnlyError()
|
|
||||||
|
|
||||||
# =====
|
|
||||||
|
|
||||||
async def connect(self) -> Dict:
|
|
||||||
raise MsdCliOnlyError()
|
|
||||||
|
|
||||||
async def disconnect(self) -> Dict:
|
|
||||||
raise MsdCliOnlyError()
|
|
||||||
|
|
||||||
async def select(self, name: str, cdrom: bool) -> Dict:
|
|
||||||
raise MsdCliOnlyError()
|
|
||||||
|
|
||||||
async def remove(self, name: str) -> Dict:
|
|
||||||
raise MsdCliOnlyError()
|
|
||||||
|
|
||||||
async def __aenter__(self) -> BaseMsd:
|
|
||||||
raise MsdCliOnlyError()
|
|
||||||
|
|
||||||
async def write_image_info(self, name: str, complete: bool) -> None:
|
|
||||||
raise MsdCliOnlyError()
|
|
||||||
|
|
||||||
async def write_image_chunk(self, chunk: bytes) -> int:
|
|
||||||
raise MsdCliOnlyError()
|
|
||||||
|
|
||||||
async def __aexit__(
|
|
||||||
self,
|
|
||||||
_exc_type: Type[BaseException],
|
|
||||||
_exc: BaseException,
|
|
||||||
_tb: types.TracebackType,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
raise MsdCliOnlyError()
|
|
||||||
531
kvmd/plugins/msd/otg/__init__.py
Normal file
531
kvmd/plugins/msd/otg/__init__.py
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
# ========================================================================== #
|
||||||
|
# #
|
||||||
|
# KVMD - The main Pi-KVM daemon. #
|
||||||
|
# #
|
||||||
|
# Copyright (C) 2018 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
|
||||||
|
import contextlib
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
from typing import Dict
|
||||||
|
from typing import AsyncGenerator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
import aiofiles.base
|
||||||
|
|
||||||
|
from ....logging import get_logger
|
||||||
|
|
||||||
|
from ....inotify import InotifyMask
|
||||||
|
from ....inotify import Inotify
|
||||||
|
|
||||||
|
from ....yamlconf import Option
|
||||||
|
|
||||||
|
from ....validators.os import valid_abs_dir
|
||||||
|
from ....validators.os import valid_command
|
||||||
|
|
||||||
|
from .... import aiotools
|
||||||
|
from .... import aioregion
|
||||||
|
|
||||||
|
from .. import MsdError
|
||||||
|
from .. import MsdOfflineError
|
||||||
|
from .. import MsdConnectedError
|
||||||
|
from .. import MsdDisconnectedError
|
||||||
|
from .. import MsdImageNotSelected
|
||||||
|
from .. import MsdUnknownImageError
|
||||||
|
from .. import MsdImageExistsError
|
||||||
|
from .. import MsdIsBusyError
|
||||||
|
from .. import BaseMsd
|
||||||
|
|
||||||
|
from .drive import Drive
|
||||||
|
|
||||||
|
from .helpers import remount_storage
|
||||||
|
from .helpers import unlock_drive
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class _DriveImage:
|
||||||
|
name: str
|
||||||
|
path: str
|
||||||
|
size: int
|
||||||
|
complete: bool
|
||||||
|
in_storage: bool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class _DriveState:
|
||||||
|
image: Optional[_DriveImage]
|
||||||
|
cdrom: bool
|
||||||
|
rw: bool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class _StorageState:
|
||||||
|
size: int
|
||||||
|
free: int
|
||||||
|
images: Dict[str, _DriveImage]
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class _VirtualDriveState:
|
||||||
|
image: Optional[_DriveImage]
|
||||||
|
connected: bool
|
||||||
|
cdrom: bool
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_drive_state(cls, state: _DriveState) -> "_VirtualDriveState":
|
||||||
|
return _VirtualDriveState(
|
||||||
|
image=state.image,
|
||||||
|
connected=bool(state.image),
|
||||||
|
cdrom=state.cdrom,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _State:
|
||||||
|
def __init__(self, changes_queue: asyncio.queues.Queue) -> None:
|
||||||
|
self.__changes_queue = changes_queue
|
||||||
|
|
||||||
|
self.storage: Optional[_StorageState] = None
|
||||||
|
self.vd: Optional[_VirtualDriveState] = None
|
||||||
|
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
self._region = aioregion.AioExclusiveRegion(MsdIsBusyError)
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def busy(self, check_online: bool=True) -> AsyncGenerator[None, None]:
|
||||||
|
with self._region:
|
||||||
|
async with self._lock:
|
||||||
|
await self.__changes_queue.put(None)
|
||||||
|
if check_online:
|
||||||
|
if self.vd is None:
|
||||||
|
raise MsdOfflineError()
|
||||||
|
assert self.storage
|
||||||
|
yield
|
||||||
|
await self.__changes_queue.put(None)
|
||||||
|
|
||||||
|
def is_busy(self) -> bool:
|
||||||
|
return self._region.is_busy()
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||||
|
def __init__( # pylint: disable=super-init-not-called
|
||||||
|
self,
|
||||||
|
storage_path: str,
|
||||||
|
|
||||||
|
remount_cmd: List[str],
|
||||||
|
unlock_cmd: List[str],
|
||||||
|
|
||||||
|
gadget: str, # XXX: Not from options, see /kvmd/apps/kvmd/__init__.py for details
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
self.__storage_path = os.path.normpath(storage_path)
|
||||||
|
self.__images_path = os.path.join(self.__storage_path, "images")
|
||||||
|
self.__meta_path = os.path.join(self.__storage_path, "meta")
|
||||||
|
|
||||||
|
self.__remount_cmd = remount_cmd
|
||||||
|
self.__unlock_cmd = unlock_cmd
|
||||||
|
|
||||||
|
self.__drive = Drive(gadget, instance=0, lun=0)
|
||||||
|
|
||||||
|
self.__new_file: Optional[aiofiles.base.AiofilesContextManager] = None
|
||||||
|
self.__new_file_written = 0
|
||||||
|
|
||||||
|
self.__changes_queue: asyncio.queues.Queue = asyncio.Queue()
|
||||||
|
|
||||||
|
self.__state = _State(self.__changes_queue)
|
||||||
|
|
||||||
|
logger = get_logger(0)
|
||||||
|
logger.info("Using OTG gadget %r as MSD", gadget)
|
||||||
|
aiotools.run_sync(self.__reload_state())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_plugin_options(cls) -> Dict:
|
||||||
|
sudo = ["/usr/bin/sudo", "--non-interactive"]
|
||||||
|
return {
|
||||||
|
"storage": Option("/var/lib/kvmd/msd", type=valid_abs_dir, unpack_as="storage_path"),
|
||||||
|
"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),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_state(self) -> Dict:
|
||||||
|
async with self.__state._lock: # pylint: disable=protected-access
|
||||||
|
storage: Optional[Dict] = None
|
||||||
|
if self.__state.storage:
|
||||||
|
storage = dataclasses.asdict(self.__state.storage)
|
||||||
|
for name in list(storage["images"]):
|
||||||
|
del storage["images"][name]["path"]
|
||||||
|
del storage["images"][name]["in_storage"]
|
||||||
|
storage["uploading"] = bool(self.__new_file)
|
||||||
|
|
||||||
|
vd: Optional[Dict] = None
|
||||||
|
if self.__state.vd:
|
||||||
|
vd = dataclasses.asdict(self.__state.vd)
|
||||||
|
if vd["image"]:
|
||||||
|
del vd["image"]["path"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"enabled": False, # FIXME
|
||||||
|
"online": bool(self.__state.vd),
|
||||||
|
"busy": self.__state.is_busy(),
|
||||||
|
"storage": storage,
|
||||||
|
"drive": vd,
|
||||||
|
"features": {
|
||||||
|
"multi": True,
|
||||||
|
"cdrom": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async def poll_state(self) -> AsyncGenerator[Dict, None]:
|
||||||
|
inotify_task = asyncio.create_task(self.__watch_inotify())
|
||||||
|
prev_state: Dict = {}
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if inotify_task.cancelled():
|
||||||
|
break
|
||||||
|
if inotify_task.done():
|
||||||
|
RuntimeError("Inotify task is dead")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self.__changes_queue.get(), timeout=0.1)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
state = await self.get_state()
|
||||||
|
if state != prev_state:
|
||||||
|
yield state
|
||||||
|
prev_state = state
|
||||||
|
finally:
|
||||||
|
if not inotify_task.done():
|
||||||
|
inotify_task.cancel()
|
||||||
|
await inotify_task
|
||||||
|
|
||||||
|
@aiotools.atomic
|
||||||
|
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)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
get_logger(0).exception("Can't reset MSD")
|
||||||
|
|
||||||
|
@aiotools.atomic
|
||||||
|
async def cleanup(self) -> None:
|
||||||
|
await self.__close_new_file()
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
@aiotools.atomic
|
||||||
|
async def set_params(self, name: Optional[str]=None, cdrom: Optional[bool]=None) -> None:
|
||||||
|
async with self.__state.busy():
|
||||||
|
assert self.__state.storage
|
||||||
|
assert self.__state.vd
|
||||||
|
|
||||||
|
if self.__state.vd.connected or self.__drive.get_image_path():
|
||||||
|
raise MsdConnectedError()
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
if name:
|
||||||
|
image = self.__state.storage.images.get(name)
|
||||||
|
if image is None or not os.path.exists(image.path):
|
||||||
|
raise MsdUnknownImageError()
|
||||||
|
assert image.in_storage
|
||||||
|
self.__state.vd.image = image
|
||||||
|
else:
|
||||||
|
self.__state.vd.image = None
|
||||||
|
|
||||||
|
if cdrom is not None:
|
||||||
|
self.__state.vd.cdrom = cdrom
|
||||||
|
|
||||||
|
@aiotools.atomic
|
||||||
|
async def connect(self) -> None:
|
||||||
|
async with self.__state.busy():
|
||||||
|
assert self.__state.vd
|
||||||
|
|
||||||
|
if self.__state.vd.connected or self.__drive.get_image_path():
|
||||||
|
raise MsdConnectedError()
|
||||||
|
if self.__state.vd.image is None:
|
||||||
|
raise MsdImageNotSelected()
|
||||||
|
|
||||||
|
assert self.__state.vd.image.in_storage
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.__state.vd.connected = True
|
||||||
|
|
||||||
|
@aiotools.atomic
|
||||||
|
async def disconnect(self) -> None:
|
||||||
|
async with self.__state.busy():
|
||||||
|
assert self.__state.vd
|
||||||
|
|
||||||
|
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 = False
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def write_image(self, name: str) -> AsyncGenerator[None, None]:
|
||||||
|
try:
|
||||||
|
with self.__state._region: # pylint: disable=protected-access
|
||||||
|
try:
|
||||||
|
async with self.__state._lock: # pylint: disable=protected-access
|
||||||
|
await self.__changes_queue.put(None)
|
||||||
|
assert self.__state.storage
|
||||||
|
assert self.__state.vd
|
||||||
|
|
||||||
|
if self.__state.vd.connected or self.__drive.get_image_path():
|
||||||
|
raise MsdConnectedError()
|
||||||
|
|
||||||
|
path = os.path.join(self.__images_path, name)
|
||||||
|
if name in self.__state.storage.images or os.path.exists(path):
|
||||||
|
raise MsdImageExistsError()
|
||||||
|
|
||||||
|
await self.__remount_storage(rw=True)
|
||||||
|
self.__set_image_complete(name, False)
|
||||||
|
self.__new_file_written = 0
|
||||||
|
self.__new_file = await aiofiles.open(path, mode="w+b", buffering=0)
|
||||||
|
|
||||||
|
await self.__changes_queue.put(None)
|
||||||
|
yield
|
||||||
|
self.__set_image_complete(name, True)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await self.__close_new_file()
|
||||||
|
try:
|
||||||
|
await self.__remount_storage(rw=False)
|
||||||
|
except asyncio.CancelledError: # pylint: disable=try-except-raise
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
await self.__changes_queue.put(None)
|
||||||
|
|
||||||
|
@aiotools.atomic
|
||||||
|
async def write_image_chunk(self, chunk: bytes) -> int:
|
||||||
|
assert self.__new_file
|
||||||
|
await aiotools.afile_write_now(self.__new_file, chunk)
|
||||||
|
self.__new_file_written += len(chunk)
|
||||||
|
return self.__new_file_written
|
||||||
|
|
||||||
|
async def remove(self, name: str) -> None:
|
||||||
|
async with self.__state.busy():
|
||||||
|
assert self.__state.storage
|
||||||
|
assert self.__state.vd
|
||||||
|
|
||||||
|
if self.__state.vd.connected or self.__drive.get_image_path():
|
||||||
|
raise MsdConnectedError()
|
||||||
|
|
||||||
|
image = self.__state.storage.images.get(name)
|
||||||
|
if image is None or not os.path.exists(image.path):
|
||||||
|
raise MsdUnknownImageError()
|
||||||
|
assert image.in_storage
|
||||||
|
|
||||||
|
if self.__state.vd.image == image:
|
||||||
|
self.__state.vd.image = None
|
||||||
|
del self.__state.storage.images[name]
|
||||||
|
|
||||||
|
await self.__remount_storage(rw=True)
|
||||||
|
os.remove(image.path)
|
||||||
|
self.__set_image_complete(name, False)
|
||||||
|
await self.__remount_storage(rw=False)
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
async def __close_new_file(self) -> None:
|
||||||
|
try:
|
||||||
|
if self.__new_file:
|
||||||
|
get_logger().info("Closing new image file ...")
|
||||||
|
await self.__new_file.close()
|
||||||
|
except asyncio.CancelledError: # pylint: disable=try-except-raise
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
get_logger().exception("Can't close device file")
|
||||||
|
finally:
|
||||||
|
self.__new_file = None
|
||||||
|
self.__new_file_written = 0
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
async def __watch_inotify(self) -> None:
|
||||||
|
logger = get_logger(0)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
# Активно ждем, пока не будут на месте все каталоги.
|
||||||
|
await self.__reload_state()
|
||||||
|
await self.__changes_queue.put(None)
|
||||||
|
if self.__state.vd:
|
||||||
|
break
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
with Inotify() as inotify:
|
||||||
|
inotify.watch(self.__images_path, InotifyMask.ALL_MODIFY_EVENTS)
|
||||||
|
inotify.watch(self.__meta_path, InotifyMask.ALL_MODIFY_EVENTS)
|
||||||
|
inotify.watch(self.__drive.get_sysfs_path(), InotifyMask.ALL_MODIFY_EVENTS)
|
||||||
|
|
||||||
|
# После установки вотчеров еще раз проверяем стейт, чтобы ничего не потерять
|
||||||
|
await self.__reload_state()
|
||||||
|
await self.__changes_queue.put(None)
|
||||||
|
|
||||||
|
while self.__state.vd: # Если живы после предыдущей проверки
|
||||||
|
need_restart = 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):
|
||||||
|
# Если выгрузили OTG, что-то отмонтировали или делают еще какую-то странную фигню
|
||||||
|
logger.warning("Got fatal inotify event: %s; reinitializing MSD ...", event)
|
||||||
|
need_restart = True
|
||||||
|
break
|
||||||
|
if need_restart:
|
||||||
|
break
|
||||||
|
if need_reload_state:
|
||||||
|
await self.__reload_state()
|
||||||
|
await self.__changes_queue.put(None)
|
||||||
|
except asyncio.CancelledError: # pylint: disable=try-except-raise
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Unexpected MSD watcher error")
|
||||||
|
|
||||||
|
async def __reload_state(self) -> None:
|
||||||
|
logger = get_logger(0)
|
||||||
|
async with self.__state._lock: # pylint: disable=protected-access
|
||||||
|
try:
|
||||||
|
drive_state = self.__get_drive_state()
|
||||||
|
if drive_state.rw:
|
||||||
|
# Внештатное использование MSD, ломаемся
|
||||||
|
raise MsdError("MSD has been switched to RW-mode manually")
|
||||||
|
|
||||||
|
if self.__state.vd is None and drive_state.image is None:
|
||||||
|
# Если только что включились и образ не подключен - попробовать
|
||||||
|
# перемонтировать хранилище (и создать images и meta).
|
||||||
|
logger.info("Probing to remount storage ...")
|
||||||
|
await self.__remount_storage(rw=True)
|
||||||
|
await self.__remount_storage(rw=False)
|
||||||
|
|
||||||
|
storage_state = self.__get_storage_state()
|
||||||
|
except asyncio.CancelledError: # pylint: disable=try-except-raise
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error while reloading MSD state; switching to offline")
|
||||||
|
self.__state.storage = None
|
||||||
|
self.__state.vd = None
|
||||||
|
else:
|
||||||
|
self.__state.storage = storage_state
|
||||||
|
if drive_state.image:
|
||||||
|
# При подключенном образе виртуальный стейт заменяется реальным
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.__state.vd.image
|
||||||
|
and (not self.__state.vd.image.in_storage or not os.path.exists(self.__state.vd.image.path))
|
||||||
|
):
|
||||||
|
# Если только что отключили ручной образ вне хранилища или ранее выбранный образ был удален
|
||||||
|
self.__state.vd.image = None
|
||||||
|
|
||||||
|
self.__state.vd.connected = False
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
def __get_storage_state(self) -> _StorageState:
|
||||||
|
images: Dict[str, _DriveImage] = {}
|
||||||
|
for name in os.listdir(self.__images_path):
|
||||||
|
path = os.path.join(self.__images_path, name)
|
||||||
|
if os.path.exists(path):
|
||||||
|
size = self.__get_file_size(path)
|
||||||
|
if size >= 0:
|
||||||
|
images[name] = _DriveImage(
|
||||||
|
name=name,
|
||||||
|
path=path,
|
||||||
|
size=size,
|
||||||
|
complete=self.__is_image_complete(name),
|
||||||
|
in_storage=True,
|
||||||
|
)
|
||||||
|
st = os.statvfs(self.__storage_path)
|
||||||
|
return _StorageState(
|
||||||
|
size=(st.f_blocks * st.f_frsize),
|
||||||
|
free=(st.f_bavail * st.f_frsize),
|
||||||
|
images=images,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __get_drive_state(self) -> _DriveState:
|
||||||
|
image: Optional[_DriveImage] = None
|
||||||
|
path = self.__drive.get_image_path()
|
||||||
|
if path:
|
||||||
|
name = os.path.basename(path)
|
||||||
|
in_storage = (os.path.dirname(path) == self.__images_path)
|
||||||
|
image = _DriveImage(
|
||||||
|
name=name,
|
||||||
|
path=path,
|
||||||
|
size=max(self.__get_file_size(path), 0),
|
||||||
|
complete=(self.__is_image_complete(name) if in_storage else True),
|
||||||
|
in_storage=in_storage,
|
||||||
|
)
|
||||||
|
return _DriveState(
|
||||||
|
image=image,
|
||||||
|
cdrom=self.__drive.get_cdrom_flag(),
|
||||||
|
rw=self.__drive.get_rw_flag(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
def __get_file_size(self, path: str) -> int:
|
||||||
|
try:
|
||||||
|
return os.path.getsize(path)
|
||||||
|
except Exception as err:
|
||||||
|
get_logger().warning("Can't get size of file %s: %s", path, err)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def __is_image_complete(self, name: str) -> bool:
|
||||||
|
return os.path.exists(os.path.join(self.__meta_path, name + ".complete"))
|
||||||
|
|
||||||
|
def __set_image_complete(self, name: str, flag: bool) -> None:
|
||||||
|
path = os.path.join(self.__meta_path, name + ".complete")
|
||||||
|
if flag:
|
||||||
|
open(path, "w").close()
|
||||||
|
else:
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.remove(path)
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
async def __remount_storage(self, rw: bool) -> None:
|
||||||
|
await remount_storage(self.__remount_cmd, rw)
|
||||||
|
|
||||||
|
async def __unlock_drive(self) -> None:
|
||||||
|
await unlock_drive(self.__unlock_cmd)
|
||||||
80
kvmd/plugins/msd/otg/drive.py
Normal file
80
kvmd/plugins/msd/otg/drive.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# ========================================================================== #
|
||||||
|
# #
|
||||||
|
# KVMD - The main Pi-KVM daemon. #
|
||||||
|
# #
|
||||||
|
# Copyright (C) 2018 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 errno
|
||||||
|
|
||||||
|
from .. import MsdOperationError
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
class MsdDriveLockedError(MsdOperationError):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__("MSD drive is locked on IO operation")
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
class Drive:
|
||||||
|
def __init__(self, gadget: str, instance: int, lun: int) -> None:
|
||||||
|
self.__path = os.path.join(
|
||||||
|
"/sys/kernel/config/usb_gadget",
|
||||||
|
gadget,
|
||||||
|
f"functions/mass_storage.usb{instance}/lun.{lun}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_sysfs_path(self) -> str:
|
||||||
|
return self.__path
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
def set_image_path(self, path: str) -> None:
|
||||||
|
self.__set_param("file", path)
|
||||||
|
|
||||||
|
def get_image_path(self) -> str:
|
||||||
|
return self.__get_param("file")
|
||||||
|
|
||||||
|
def set_cdrom_flag(self, flag: bool) -> None:
|
||||||
|
self.__set_param("cdrom", str(int(flag)))
|
||||||
|
|
||||||
|
def get_cdrom_flag(self) -> bool:
|
||||||
|
return bool(int(self.__get_param("cdrom")))
|
||||||
|
|
||||||
|
def set_rw_flag(self, flag: bool) -> None:
|
||||||
|
self.__set_param("ro", str(int(not flag)))
|
||||||
|
|
||||||
|
def get_rw_flag(self) -> bool:
|
||||||
|
return (not int(self.__get_param("ro")))
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
def __get_param(self, param: str) -> str:
|
||||||
|
with open(os.path.join(self.__path, param)) as param_file:
|
||||||
|
return param_file.read().strip()
|
||||||
|
|
||||||
|
def __set_param(self, param: str, value: str) -> None:
|
||||||
|
try:
|
||||||
|
with open(os.path.join(self.__path, param), "w") as param_file:
|
||||||
|
param_file.write(value + "\n")
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno == errno.EBUSY:
|
||||||
|
raise MsdDriveLockedError()
|
||||||
|
raise
|
||||||
79
kvmd/plugins/msd/otg/helpers.py
Normal file
79
kvmd/plugins/msd/otg/helpers.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# ========================================================================== #
|
||||||
|
# #
|
||||||
|
# KVMD - The main Pi-KVM daemon. #
|
||||||
|
# #
|
||||||
|
# Copyright (C) 2018 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 signal
|
||||||
|
import asyncio
|
||||||
|
import asyncio.subprocess
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from ....logging import get_logger
|
||||||
|
|
||||||
|
from .. import MsdError
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
async def remount_storage(base_cmd: List[str], rw: bool) -> None:
|
||||||
|
logger = get_logger(0)
|
||||||
|
mode = ("rw" if rw else "ro")
|
||||||
|
cmd = [
|
||||||
|
part.format(mode=mode)
|
||||||
|
for part in base_cmd
|
||||||
|
]
|
||||||
|
logger.info("Remounting internal storage to %s ...", mode.upper())
|
||||||
|
try:
|
||||||
|
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 asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.STDOUT,
|
||||||
|
preexec_fn=(lambda: signal.signal(signal.SIGINT, signal.SIG_IGN)),
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout = (await proc.communicate())[0].decode(errors="ignore").strip()
|
||||||
|
if stdout:
|
||||||
|
log = (logger.info if proc.returncode == 0 else logger.error)
|
||||||
|
for line in stdout.split("\n"):
|
||||||
|
log("Console: %s", line)
|
||||||
|
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise MsdError(f"Error while helper execution: pid={proc.pid}; retcode={proc.returncode}")
|
||||||
@ -26,16 +26,13 @@ import fcntl
|
|||||||
import struct
|
import struct
|
||||||
import asyncio
|
import asyncio
|
||||||
import asyncio.queues
|
import asyncio.queues
|
||||||
|
import contextlib
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import types
|
|
||||||
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import IO
|
from typing import IO
|
||||||
from typing import Callable
|
|
||||||
from typing import Type
|
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import aiofiles.base
|
import aiofiles.base
|
||||||
@ -57,11 +54,11 @@ from ...validators.hw import valid_gpio_pin
|
|||||||
|
|
||||||
from . import MsdError
|
from . import MsdError
|
||||||
from . import MsdOfflineError
|
from . import MsdOfflineError
|
||||||
from . import MsdAlreadyConnectedError
|
|
||||||
from . import MsdAlreadyDisconnectedError
|
|
||||||
from . import MsdConnectedError
|
from . import MsdConnectedError
|
||||||
|
from . import MsdDisconnectedError
|
||||||
from . import MsdIsBusyError
|
from . import MsdIsBusyError
|
||||||
from . import MsdMultiNotSupported
|
from . import MsdMultiNotSupported
|
||||||
|
from . import MsdCdromNotSupported
|
||||||
from . import BaseMsd
|
from . import BaseMsd
|
||||||
|
|
||||||
|
|
||||||
@ -83,11 +80,11 @@ class _DeviceInfo:
|
|||||||
|
|
||||||
_IMAGE_INFO_SIZE = 4096
|
_IMAGE_INFO_SIZE = 4096
|
||||||
_IMAGE_INFO_MAGIC_SIZE = 16
|
_IMAGE_INFO_MAGIC_SIZE = 16
|
||||||
_IMAGE_INFO_IMAGE_NAME_SIZE = 256
|
_IMAGE_INFO_NAME_SIZE = 256
|
||||||
_IMAGE_INFO_PADS_SIZE = _IMAGE_INFO_SIZE - _IMAGE_INFO_IMAGE_NAME_SIZE - 1 - 8 - _IMAGE_INFO_MAGIC_SIZE * 8
|
_IMAGE_INFO_PADS_SIZE = _IMAGE_INFO_SIZE - _IMAGE_INFO_NAME_SIZE - 1 - 8 - _IMAGE_INFO_MAGIC_SIZE * 8
|
||||||
_IMAGE_INFO_FORMAT = ">%dL%dc?Q%dx%dL" % (
|
_IMAGE_INFO_FORMAT = ">%dL%dc?Q%dx%dL" % (
|
||||||
_IMAGE_INFO_MAGIC_SIZE,
|
_IMAGE_INFO_MAGIC_SIZE,
|
||||||
_IMAGE_INFO_IMAGE_NAME_SIZE,
|
_IMAGE_INFO_NAME_SIZE,
|
||||||
_IMAGE_INFO_PADS_SIZE,
|
_IMAGE_INFO_PADS_SIZE,
|
||||||
_IMAGE_INFO_MAGIC_SIZE,
|
_IMAGE_INFO_MAGIC_SIZE,
|
||||||
)
|
)
|
||||||
@ -100,8 +97,8 @@ def _make_image_info_bytes(name: str, size: int, complete: bool) -> bytes:
|
|||||||
*_IMAGE_INFO_MAGIC,
|
*_IMAGE_INFO_MAGIC,
|
||||||
*memoryview(( # type: ignore
|
*memoryview(( # type: ignore
|
||||||
name.encode("utf-8")
|
name.encode("utf-8")
|
||||||
+ b"\x00" * _IMAGE_INFO_IMAGE_NAME_SIZE
|
+ b"\x00" * _IMAGE_INFO_NAME_SIZE
|
||||||
)[:_IMAGE_INFO_IMAGE_NAME_SIZE]).cast("c"),
|
)[:_IMAGE_INFO_NAME_SIZE]).cast("c"),
|
||||||
complete,
|
complete,
|
||||||
size,
|
size,
|
||||||
*_IMAGE_INFO_MAGIC,
|
*_IMAGE_INFO_MAGIC,
|
||||||
@ -117,11 +114,15 @@ def _parse_image_info_bytes(data: bytes) -> Optional[_ImageInfo]:
|
|||||||
magic_begin = parsed[:_IMAGE_INFO_MAGIC_SIZE]
|
magic_begin = parsed[:_IMAGE_INFO_MAGIC_SIZE]
|
||||||
magic_end = parsed[-_IMAGE_INFO_MAGIC_SIZE:]
|
magic_end = parsed[-_IMAGE_INFO_MAGIC_SIZE:]
|
||||||
if magic_begin == magic_end == _IMAGE_INFO_MAGIC:
|
if magic_begin == magic_end == _IMAGE_INFO_MAGIC:
|
||||||
image_name_bytes = b"".join(parsed[_IMAGE_INFO_MAGIC_SIZE:_IMAGE_INFO_MAGIC_SIZE + _IMAGE_INFO_IMAGE_NAME_SIZE])
|
image_name_bytes = b"".join(parsed[
|
||||||
|
_IMAGE_INFO_MAGIC_SIZE # noqa: E203
|
||||||
|
:
|
||||||
|
_IMAGE_INFO_MAGIC_SIZE + _IMAGE_INFO_NAME_SIZE
|
||||||
|
])
|
||||||
return _ImageInfo(
|
return _ImageInfo(
|
||||||
name=image_name_bytes.decode("utf-8", errors="ignore").strip("\x00").strip(),
|
name=image_name_bytes.decode("utf-8", errors="ignore").strip("\x00").strip(),
|
||||||
size=parsed[_IMAGE_INFO_MAGIC_SIZE + _IMAGE_INFO_IMAGE_NAME_SIZE + 1],
|
size=parsed[_IMAGE_INFO_MAGIC_SIZE + _IMAGE_INFO_NAME_SIZE + 1],
|
||||||
complete=parsed[_IMAGE_INFO_MAGIC_SIZE + _IMAGE_INFO_IMAGE_NAME_SIZE],
|
complete=parsed[_IMAGE_INFO_MAGIC_SIZE + _IMAGE_INFO_NAME_SIZE],
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -152,14 +153,7 @@ def _explore_device(device_path: str) -> _DeviceInfo:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _msd_working(method: Callable) -> Callable:
|
# =====
|
||||||
async def wrapper(self: "Plugin", *args: Any, **kwargs: Any) -> Any:
|
|
||||||
if not self._device_info: # pylint: disable=protected-access
|
|
||||||
raise MsdOfflineError()
|
|
||||||
return (await method(self, *args, **kwargs))
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||||
def __init__( # pylint: disable=super-init-not-called
|
def __init__( # pylint: disable=super-init-not-called
|
||||||
self,
|
self,
|
||||||
@ -182,10 +176,11 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
self.__region = aioregion.AioExclusiveRegion(MsdIsBusyError)
|
self.__region = aioregion.AioExclusiveRegion(MsdIsBusyError)
|
||||||
|
|
||||||
self._device_info: Optional[_DeviceInfo] = None
|
self.__device_info: Optional[_DeviceInfo] = None
|
||||||
|
self.__connected = False
|
||||||
|
|
||||||
self.__device_file: Optional[aiofiles.base.AiofilesContextManager] = None
|
self.__device_file: Optional[aiofiles.base.AiofilesContextManager] = None
|
||||||
self.__written = 0
|
self.__written = 0
|
||||||
self.__on_kvm = True
|
|
||||||
|
|
||||||
self.__state_queue: asyncio.queues.Queue = asyncio.Queue()
|
self.__state_queue: asyncio.queues.Queue = asyncio.Queue()
|
||||||
|
|
||||||
@ -209,27 +204,29 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
"reset_delay": Option(1.0, type=valid_float_f01),
|
"reset_delay": Option(1.0, type=valid_float_f01),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_state(self) -> Dict:
|
async def get_state(self) -> Dict:
|
||||||
current: Optional[Dict] = None
|
|
||||||
storage: Optional[Dict] = None
|
storage: Optional[Dict] = None
|
||||||
if self._device_info:
|
drive: Optional[Dict] = None
|
||||||
|
if self.__device_info:
|
||||||
storage = {
|
storage = {
|
||||||
"size": self._device_info.size,
|
"size": self.__device_info.size,
|
||||||
"free": self._device_info.free,
|
"free": self.__device_info.free,
|
||||||
|
"uploading": bool(self.__device_file)
|
||||||
|
}
|
||||||
|
drive = {
|
||||||
|
"image": (self.__device_info.image and dataclasses.asdict(self.__device_info.image)),
|
||||||
|
"connected": self.__connected,
|
||||||
}
|
}
|
||||||
if self._device_info.image:
|
|
||||||
current = dataclasses.asdict(self._device_info.image)
|
|
||||||
return {
|
return {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"multi": False,
|
"online": bool(self.__device_info),
|
||||||
"online": bool(self._device_info),
|
|
||||||
"busy": self.__region.is_busy(),
|
"busy": self.__region.is_busy(),
|
||||||
"uploading": bool(self.__device_file),
|
|
||||||
"written": self.__written,
|
|
||||||
"current": current,
|
|
||||||
"storage": storage,
|
"storage": storage,
|
||||||
"cdrom": None,
|
"drive": drive,
|
||||||
"connected": (not self.__on_kvm),
|
"features": {
|
||||||
|
"multi": False,
|
||||||
|
"cdrom": False,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def poll_state(self) -> AsyncGenerator[Dict, None]:
|
async def poll_state(self) -> AsyncGenerator[Dict, None]:
|
||||||
@ -250,7 +247,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
gpio.write(self.__reset_pin, False)
|
gpio.write(self.__reset_pin, False)
|
||||||
|
|
||||||
gpio.write(self.__target_pin, False)
|
gpio.write(self.__target_pin, False)
|
||||||
self.__on_kvm = True
|
self.__connected = False
|
||||||
|
|
||||||
await self.__load_device_info()
|
await self.__load_device_info()
|
||||||
get_logger(0).info("MSD reset has been successful")
|
get_logger(0).info("MSD reset has been successful")
|
||||||
@ -259,7 +256,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
gpio.write(self.__reset_pin, False)
|
gpio.write(self.__reset_pin, False)
|
||||||
finally:
|
finally:
|
||||||
self.__region.exit()
|
self.__region.exit()
|
||||||
await self.__state_queue.put(self.get_state())
|
await self.__state_queue.put(await self.get_state())
|
||||||
|
|
||||||
@aiotools.atomic
|
@aiotools.atomic
|
||||||
async def cleanup(self) -> None:
|
async def cleanup(self) -> None:
|
||||||
@ -269,116 +266,107 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|
||||||
@_msd_working
|
async def set_params(self, name: Optional[str]=None, cdrom: Optional[bool]=None) -> None:
|
||||||
|
async with self.__working():
|
||||||
|
if name is not None:
|
||||||
|
raise MsdMultiNotSupported()
|
||||||
|
if cdrom is not None:
|
||||||
|
raise MsdCdromNotSupported()
|
||||||
|
|
||||||
@aiotools.atomic
|
@aiotools.atomic
|
||||||
async def connect(self) -> Dict:
|
async def connect(self) -> None:
|
||||||
|
async with self.__working():
|
||||||
notify = False
|
notify = False
|
||||||
state: Dict = {}
|
|
||||||
try:
|
try:
|
||||||
with self.__region:
|
with self.__region:
|
||||||
if not self.__on_kvm:
|
if self.__connected:
|
||||||
raise MsdAlreadyConnectedError()
|
raise MsdConnectedError()
|
||||||
notify = True
|
notify = True
|
||||||
|
|
||||||
gpio.write(self.__target_pin, True)
|
gpio.write(self.__target_pin, True)
|
||||||
self.__on_kvm = False
|
self.__connected = True
|
||||||
get_logger(0).info("MSD switched to Server")
|
get_logger(0).info("MSD switched to Server")
|
||||||
|
|
||||||
state = self.get_state()
|
|
||||||
return state
|
|
||||||
finally:
|
finally:
|
||||||
if notify:
|
if notify:
|
||||||
await self.__state_queue.put(state or self.get_state())
|
await self.__state_queue.put(await self.get_state())
|
||||||
|
|
||||||
@_msd_working
|
|
||||||
@aiotools.atomic
|
@aiotools.atomic
|
||||||
async def disconnect(self) -> Dict:
|
async def disconnect(self) -> None:
|
||||||
|
async with self.__working():
|
||||||
notify = False
|
notify = False
|
||||||
state: Dict = {}
|
|
||||||
try:
|
try:
|
||||||
with self.__region:
|
with self.__region:
|
||||||
if self.__on_kvm:
|
if not self.__connected:
|
||||||
raise MsdAlreadyDisconnectedError()
|
raise MsdDisconnectedError()
|
||||||
notify = True
|
notify = True
|
||||||
|
|
||||||
gpio.write(self.__target_pin, False)
|
gpio.write(self.__target_pin, False)
|
||||||
try:
|
try:
|
||||||
await self.__load_device_info()
|
await self.__load_device_info()
|
||||||
except Exception:
|
except Exception:
|
||||||
if not self.__on_kvm:
|
if self.__connected:
|
||||||
gpio.write(self.__target_pin, True)
|
gpio.write(self.__target_pin, True)
|
||||||
raise
|
raise
|
||||||
self.__on_kvm = True
|
self.__connected = False
|
||||||
get_logger(0).info("MSD switched to KVM: %s", self._device_info)
|
get_logger(0).info("MSD switched to KVM: %s", self.__device_info)
|
||||||
|
|
||||||
state = self.get_state()
|
|
||||||
return state
|
|
||||||
finally:
|
finally:
|
||||||
if notify:
|
if notify:
|
||||||
await self.__state_queue.put(state or self.get_state())
|
await self.__state_queue.put(await self.get_state())
|
||||||
|
|
||||||
@_msd_working
|
@contextlib.asynccontextmanager
|
||||||
async def select(self, name: str, cdrom: bool) -> Dict:
|
async def write_image(self, name: str) -> AsyncGenerator[None, None]:
|
||||||
raise MsdMultiNotSupported()
|
async with self.__working():
|
||||||
|
|
||||||
@_msd_working
|
|
||||||
async def remove(self, name: str) -> Dict:
|
|
||||||
raise MsdMultiNotSupported()
|
|
||||||
|
|
||||||
@_msd_working
|
|
||||||
@aiotools.atomic
|
|
||||||
async def __aenter__(self) -> "Plugin":
|
|
||||||
assert self._device_info
|
|
||||||
self.__region.enter()
|
self.__region.enter()
|
||||||
try:
|
try:
|
||||||
if not self.__on_kvm:
|
assert self.__device_info
|
||||||
|
if self.__connected:
|
||||||
raise MsdConnectedError()
|
raise MsdConnectedError()
|
||||||
self.__device_file = await aiofiles.open(self._device_info.path, mode="w+b", buffering=0)
|
|
||||||
|
self.__device_file = await aiofiles.open(self.__device_info.path, mode="w+b", buffering=0)
|
||||||
self.__written = 0
|
self.__written = 0
|
||||||
return self
|
|
||||||
except Exception:
|
await self.__write_image_info(name, complete=False)
|
||||||
self.__region.exit()
|
await self.__state_queue.put(await self.get_state())
|
||||||
raise
|
yield
|
||||||
|
await self.__write_image_info(name, complete=True)
|
||||||
finally:
|
finally:
|
||||||
await self.__state_queue.put(self.get_state())
|
|
||||||
|
|
||||||
@aiotools.atomic
|
|
||||||
async def write_image_info(self, name: str, complete: bool) -> None:
|
|
||||||
assert self.__device_file
|
|
||||||
assert self._device_info
|
|
||||||
if self._device_info.size - self.__written > _IMAGE_INFO_SIZE:
|
|
||||||
await self.__device_file.seek(self._device_info.size - _IMAGE_INFO_SIZE)
|
|
||||||
await self.__write_to_device_file(_make_image_info_bytes(name, self.__written, complete))
|
|
||||||
await self.__device_file.seek(0)
|
|
||||||
else:
|
|
||||||
get_logger().error("Can't write image info because device is full")
|
|
||||||
|
|
||||||
@aiotools.atomic
|
|
||||||
async def write_image_chunk(self, chunk: bytes) -> int:
|
|
||||||
await self.__write_to_device_file(chunk)
|
|
||||||
self.__written += len(chunk)
|
|
||||||
return self.__written
|
|
||||||
|
|
||||||
@aiotools.atomic
|
|
||||||
async def __aexit__(
|
|
||||||
self,
|
|
||||||
_exc_type: Type[BaseException],
|
|
||||||
_exc: BaseException,
|
|
||||||
_tb: types.TracebackType,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.__close_device_file()
|
await self.__close_device_file()
|
||||||
await self.__load_device_info()
|
await self.__load_device_info()
|
||||||
finally:
|
finally:
|
||||||
self.__region.exit()
|
self.__region.exit()
|
||||||
await self.__state_queue.put(self.get_state())
|
await self.__state_queue.put(await self.get_state())
|
||||||
|
|
||||||
async def __write_to_device_file(self, data: bytes) -> None:
|
@aiotools.atomic
|
||||||
|
async def write_image_chunk(self, chunk: bytes) -> int:
|
||||||
assert self.__device_file
|
assert self.__device_file
|
||||||
await self.__device_file.write(data)
|
await aiotools.afile_write_now(self.__device_file, chunk)
|
||||||
await self.__device_file.flush()
|
self.__written += len(chunk)
|
||||||
await aiotools.run_async(os.fsync, self.__device_file.fileno())
|
return self.__written
|
||||||
|
|
||||||
|
async def remove(self, name: str) -> None:
|
||||||
|
async with self.__working():
|
||||||
|
raise MsdMultiNotSupported()
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def __working(self) -> AsyncGenerator[None, None]:
|
||||||
|
if not self.__device_info:
|
||||||
|
raise MsdOfflineError()
|
||||||
|
yield
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
async def __write_image_info(self, name: str, complete: bool) -> None:
|
||||||
|
assert self.__device_file
|
||||||
|
assert self.__device_info
|
||||||
|
if self.__device_info.size - self.__written > _IMAGE_INFO_SIZE:
|
||||||
|
await self.__device_file.seek(self.__device_info.size - _IMAGE_INFO_SIZE)
|
||||||
|
await aiotools.afile_write_now(self.__device_file, _make_image_info_bytes(name, self.__written, complete))
|
||||||
|
await self.__device_file.seek(0)
|
||||||
|
else:
|
||||||
|
get_logger().error("Can't write image info because device is full")
|
||||||
|
|
||||||
async def __close_device_file(self) -> None:
|
async def __close_device_file(self) -> None:
|
||||||
try:
|
try:
|
||||||
@ -398,13 +386,13 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(self.__init_delay)
|
await asyncio.sleep(self.__init_delay)
|
||||||
try:
|
try:
|
||||||
self._device_info = await aiotools.run_async(_explore_device, self.__device_path)
|
self.__device_info = await aiotools.run_async(_explore_device, self.__device_path)
|
||||||
break
|
break
|
||||||
except asyncio.CancelledError: # pylint: disable=try-except-raise
|
except asyncio.CancelledError: # pylint: disable=try-except-raise
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
if retries == 0:
|
if retries == 0:
|
||||||
self._device_info = None
|
self.__device_info = None
|
||||||
raise MsdError("Can't load device info")
|
raise MsdError("Can't load device info")
|
||||||
get_logger().exception("Can't load device info; retries=%d", retries)
|
get_logger().exception("Can't load device info; retries=%d", retries)
|
||||||
retries -= 1
|
retries -= 1
|
||||||
|
|||||||
1
setup.py
1
setup.py
@ -85,6 +85,7 @@ def main() -> None:
|
|||||||
"kvmd.plugins.hid.otg",
|
"kvmd.plugins.hid.otg",
|
||||||
"kvmd.plugins.atx",
|
"kvmd.plugins.atx",
|
||||||
"kvmd.plugins.msd",
|
"kvmd.plugins.msd",
|
||||||
|
"kvmd.plugins.msd.otg",
|
||||||
"kvmd.apps",
|
"kvmd.apps",
|
||||||
"kvmd.apps.kvmd",
|
"kvmd.apps.kvmd",
|
||||||
"kvmd.apps.otg",
|
"kvmd.apps.otg",
|
||||||
|
|||||||
@ -1,3 +1,21 @@
|
|||||||
|
InotifyMask.ACCESS
|
||||||
|
InotifyMask.ATTRIB
|
||||||
|
InotifyMask.CLOSE_WRITE
|
||||||
|
InotifyMask.CLOSE_NOWRITE
|
||||||
|
InotifyMask.CREATE
|
||||||
|
InotifyMask.DELETE
|
||||||
|
InotifyMask.DELETE_SELF
|
||||||
|
InotifyMask.MODIFY
|
||||||
|
InotifyMask.MOVE_SELF
|
||||||
|
InotifyMask.MOVED_FROM
|
||||||
|
InotifyMask.MOVED_TO
|
||||||
|
InotifyMask.OPEN
|
||||||
|
|
||||||
|
InotifyMask.IGNORED
|
||||||
|
InotifyMask.ISDIR
|
||||||
|
InotifyMask.Q_OVERFLOW
|
||||||
|
InotifyMask.UNMOUNT
|
||||||
|
|
||||||
IpmiServer.handle_raw_request
|
IpmiServer.handle_raw_request
|
||||||
|
|
||||||
fake_rpi.RPi.GPIO
|
fake_rpi.RPi.GPIO
|
||||||
|
|||||||
@ -162,7 +162,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="msd-current-image-broken" class="msd-message">
|
<div id="msd-drive-image-broken" class="msd-message">
|
||||||
<div class="menu-item-content-text">
|
<div class="menu-item-content-text">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
@ -197,11 +197,11 @@
|
|||||||
<table class="msd-info">
|
<table class="msd-info">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Current image:</td>
|
<td>Current image:</td>
|
||||||
<td id="msd-current-image-name" class="msd-info-value"></td>
|
<td id="msd-drive-image-name" class="msd-info-value"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Image size:</td>
|
<td>Image size:</td>
|
||||||
<td id="msd-current-image-size" class="msd-info-value"></td>
|
<td id="msd-drive-image-size" class="msd-info-value"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Storage size:</td>
|
<td>Storage size:</td>
|
||||||
|
|||||||
@ -60,8 +60,8 @@ export function Msd() {
|
|||||||
|
|
||||||
var __clickUploadNewImageButton = function() {
|
var __clickUploadNewImageButton = function() {
|
||||||
let form_data = new FormData();
|
let form_data = new FormData();
|
||||||
form_data.append("image_name", __image_file.name);
|
form_data.append("image", __image_file.name);
|
||||||
form_data.append("image_data", __image_file);
|
form_data.append("data", __image_file);
|
||||||
|
|
||||||
__upload_http = new XMLHttpRequest();
|
__upload_http = new XMLHttpRequest();
|
||||||
__upload_http.open("POST", "/api/msd/write", true);
|
__upload_http.open("POST", "/api/msd/write", true);
|
||||||
@ -132,11 +132,11 @@ export function Msd() {
|
|||||||
$("msd-reset-button").classList.add("feature-disabled");
|
$("msd-reset-button").classList.add("feature-disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (__state.connected) {
|
if (__state.online && __state.drive.connected) {
|
||||||
$("msd-another-another-user-uploads").style.display = "none";
|
$("msd-another-another-user-uploads").style.display = "none";
|
||||||
$("msd-led").className = "led-green";
|
$("msd-led").className = "led-green";
|
||||||
$("msd-status").innerHTML = $("msd-led").title = "Connected to Server";
|
$("msd-status").innerHTML = $("msd-led").title = "Connected to Server";
|
||||||
} else if (__state.uploading) {
|
} else if (__state.online && __state.storage.uploading) {
|
||||||
if (!__upload_http) {
|
if (!__upload_http) {
|
||||||
$("msd-another-another-user-uploads").style.display = "block";
|
$("msd-another-another-user-uploads").style.display = "block";
|
||||||
}
|
}
|
||||||
@ -153,19 +153,19 @@ export function Msd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$("msd-offline").style.display = (__state.online ? "none" : "block");
|
$("msd-offline").style.display = (__state.online ? "none" : "block");
|
||||||
$("msd-current-image-broken").style.display = (
|
$("msd-drive-image-broken").style.display = (
|
||||||
__state.online && __state.current &&
|
__state.online && __state.drive.image &&
|
||||||
!__state.current.complete && !__state.uploading ? "block" : "none"
|
!__state.drive.image.complete && !__state.drive.uploading ? "block" : "none"
|
||||||
);
|
);
|
||||||
|
|
||||||
$("msd-current-image-name").innerHTML = (__state.online && __state.current ? __state.current.name : "None");
|
$("msd-drive-image-name").innerHTML = (__state.online && __state.drive.image ? __state.drive.image.name : "None");
|
||||||
$("msd-current-image-size").innerHTML = (__state.online && __state.current ? __formatSize(__state.current.size) : "None");
|
$("msd-drive-image-size").innerHTML = (__state.online && __state.drive.image ? __formatSize(__state.drive.image.size) : "None");
|
||||||
$("msd-storage-size").innerHTML = (__state.online ? __formatSize(__state.storage.size) : "Unavailable");
|
$("msd-storage-size").innerHTML = (__state.online ? __formatSize(__state.storage.size) : "Unavailable");
|
||||||
|
|
||||||
wm.switchDisabled($("msd-connect-button"), (!__state.online || __state.connected || __state.busy));
|
wm.switchDisabled($("msd-connect-button"), (!__state.online || __state.drive.connected || __state.busy));
|
||||||
wm.switchDisabled($("msd-disconnect-button"), (!__state.online || !__state.connected || __state.busy));
|
wm.switchDisabled($("msd-disconnect-button"), (!__state.online || !__state.drive.connected || __state.busy));
|
||||||
wm.switchDisabled($("msd-select-new-image-button"), (!__state.online || __state.connected || __state.busy || __upload_http));
|
wm.switchDisabled($("msd-select-new-image-button"), (!__state.online || __state.drive.connected || __state.busy || __upload_http));
|
||||||
wm.switchDisabled($("msd-upload-new-image-button"), (!__state.online || __state.connected || __state.busy || !__image_file));
|
wm.switchDisabled($("msd-upload-new-image-button"), (!__state.online || __state.drive.connected || __state.busy || !__image_file));
|
||||||
wm.switchDisabled($("msd-abort-uploading-button"), (!__state.online || !__upload_http));
|
wm.switchDisabled($("msd-abort-uploading-button"), (!__state.online || !__upload_http));
|
||||||
wm.switchDisabled($("msd-reset-button"), (!__state.enabled || __state.busy));
|
wm.switchDisabled($("msd-reset-button"), (!__state.enabled || __state.busy));
|
||||||
|
|
||||||
@ -181,9 +181,9 @@ export function Msd() {
|
|||||||
$("msd-status").innerHTML = "";
|
$("msd-status").innerHTML = "";
|
||||||
$("msd-led").title = "";
|
$("msd-led").title = "";
|
||||||
$("msd-offline").style.display = "none";
|
$("msd-offline").style.display = "none";
|
||||||
$("msd-current-image-broken").style.display = "none";
|
$("msd-drive-image-broken").style.display = "none";
|
||||||
$("msd-current-image-name").innerHTML = "";
|
$("msd-drive-image-name").innerHTML = "";
|
||||||
$("msd-current-image-size").innerHTML = "";
|
$("msd-drive-image-size").innerHTML = "";
|
||||||
$("msd-storage-size").innerHTML = "";
|
$("msd-storage-size").innerHTML = "";
|
||||||
|
|
||||||
wm.switchDisabled($("msd-connect-button"), true);
|
wm.switchDisabled($("msd-connect-button"), true);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user