Files
controller-wisetopdataserver/python/biopro/server/controller.py
T
2021-12-22 14:28:54 +08:00

1338 lines
42 KiB
Python

import abc
import inspect
import re
import uuid
from datetime import datetime
from functools import wraps
from json import loads as json_parse
from pathlib import Path
from time import time
from typing import Tuple, Dict, TypeVar, Callable, overload, Type
from biopro import *
from biopro.controller import ControlAPI, ComplexResponse
from biopro.device.manager import DeviceManager, DeviceInternalCommandHandler, MasterDeviceError
from biopro.devlib.device import DeviceResponseInfo, DeviceSerialNumber, DeviceInfo
from biopro.devlib.library import CompletedDevice
from biopro.devlib.menu import Menu
from biopro.exp_pro.protocol import ExpProtocolInfo, ExpOption
from biopro.exp_pro.schedule import AbstractExpProtocolState, ExpProtocolState
from biopro.file.export import ExternalExportFunction, ExternalParameter
from biopro.file.manager import FileManager, FileSegment
from biopro.file.segment import FileSegmentContent
from biopro.file.user.alias import UserDeviceAlias
from biopro.file.user.setting import AbstractUserSetting, RestrictUserSetting
from biopro.file.user.setup import UserSetup
from biopro.file.util import RootInfo, DirectoryInfo, FileInfo
from biopro.impl.led import LED
from biopro.message.request import DataMessageHandler
from biopro.recording import RecordingMetaFile
from biopro.text import *
from biopro.util.address import ADDRESS, str_address, address_str, is_address_type
from biopro.util.cli import *
from biopro.util.console import *
from biopro.util.datetime import datetime_str
from biopro.util.json import JsonSerialize, JSON, JSON_OBJECT
from biopro.util.promise import Promise
from biopro.util.stack import print_exception
from biopro.util.text import expr_format, part_suffix, part_prefix
from .cache import FileHandle
from .socket import T_ServerThread
_RUNTIME_COMPILE = False
T = TypeVar('T')
# noinspection PyUnusedLocal
class ControlServerOptions(CliOptions):
DEFAULT_STORAGE_ROOT = '.BPS'
DEFAULT_WEBSOCKET_ADDRESS = '0.0.0.0'
DEFAULT_WEBSOCKET_PORT = 9542
DEFAULT_MQTT_ADDRESS = '52.194.17.114'
DEFAULT_MQTT_PORT = 1883
DEFAULT_CACHE_SIZE = None
DEFAULT_CACHE_TIME = 10
DEFAULT_UPS_WAIT_TIME = 10 * 60 # 10 min
def __init__(self):
# storage root
self.storage_root = str(Path.home() / self.DEFAULT_STORAGE_ROOT)
'''storage root'''
self.export_root = None
'''export root'''
# device
self.shutdown_if_reset_fail = False
'''shutdown server if device reset fail'''
self.expect_available_channel = None
'''given expect device channel, reduce the server setup times'''
# user session
self.user_session_expire_days = 1
'''expire days for user session keeping'''
# file manager
self.flag_disable_routine_trash = False
'''disable trashing routine thread'''
self.trash_routine_time = 24 # 1 day
'''interval of trashing routine'''
self.trash_expire_time = 100 # almost 3 month
'''expire days for trashing the untouched file'''
self.trash_remove_expire_time = 30 # 1 month
'''expire days for removing the trashed file '''
# websocket
self.websocket_address = self.DEFAULT_WEBSOCKET_ADDRESS
'''connection address for websocket service'''
self.websocket_port = self.DEFAULT_WEBSOCKET_PORT
'''connection port for websocket service'''
# MQTT
self.flag_disable_remote_server = False
'''disable remote service'''
self.mqtt_address = self.DEFAULT_MQTT_ADDRESS
'''remote server connection address'''
self.mqtt_port = self.DEFAULT_MQTT_PORT
'''remote server connection port'''
# data server
self.flag_disable_data_server = False
'''disable data server'''
# experimental protocol manager
self.flag_disable_exp_manager = False
'''disable experimental protocol manager'''
# cache server
self.flag_disable_cache_server = False
'''disable cache server'''
self.cache_server_cache_size = self.DEFAULT_CACHE_SIZE
'''data cache buffer size'''
self.cache_server_cache_time = self.DEFAULT_CACHE_TIME
'''data cache time in second'''
# external storage
self.flag_disable_external_storage = False
'''disable external storage'''
self.external_path_map = []
'''external storage mapping'''
# LED control
self.flag_disable_led_control = False
'''disable LED control'''
self.flag_disable_led_refresh = False
'''refresh the LED instruction'''
self.led_count = 16
'''total LED chip number'''
# sha checking
self.flag_disable_sha_check = False
'''disable SHA sum checking'''
# UPS detect
self.flag_disable_ups_detect = False
'''disable UPS detecting'''
self.flag_ups_wait_time = self.DEFAULT_UPS_WAIT_TIME
'''the waiting time for power off after UPS mode'''
@cli_options('-C', '--root', value='PATH')
@cli_help_section('File Manager Options')
def _storage_root(self, opt: str, value: str):
"""change the storage root"""
self.storage_root = value
@cli_options('-E', '--export', value='PATH')
@cli_help_section('File Manager Options')
def _export_root(self, opt: str, value: str):
"""change the export root"""
self.export_root = value
@cli_options('--user-session-expire', value='DAYS')
def _user_session_expire(self, opt: str, value: str):
"""set user session expire days"""
self.user_session_expire_days = int(value)
# remote system
@cli_flags('--disable-remote-server')
@cli_help_section('Remote System')
def _disable_remote_server(self, opt: str):
"""disable remote server connecting"""
self.flag_disable_remote_server = True
if not _RUNTIME_COMPILE:
@cli_options('--remote-server-address', value='IP:PORT')
@cli_help_section('Remote System')
def _remote_server_address(self, opt: str, value: str):
"""change the remote server IP"""
url, port = part_suffix(value, ':', missing=self.DEFAULT_MQTT_PORT)
port = int(port)
self.mqtt_address = url
self.mqtt_port = port
@cli_flags('--disable-data-server')
@cli_help_section('Data Server Options')
def _disable_data_server(self, opt: str):
"""disable data server process"""
self.flag_disable_data_server = True
@cli_flags('--disable-exp-manager')
@cli_help_section('Experimental Protocol Options')
def _disable_exp_pro(self, opt: str):
"""disable experimental protocol"""
self.flag_disable_exp_manager = True
@cli_flags('--disable-cache-server')
@cli_help_section('Cache Manager')
def _disable_cache_server(self, opt: str):
"""disable cache server process"""
self.flag_disable_cache_server = True
@cli_options('--cache-size', value='SIZE')
@cli_help_section('Cache Manager')
def _cache_size(self, opt: str, value: str):
"""cache size limit. default : %DEFAULT_CACHE_SIZE%"""
self.cache_server_cache_size = int(value)
@cli_options('--cache-time', value='SEC')
@cli_help_section('Cache Manager')
def _cache_time(self, opt: str, value: str):
"""cache data time. default : %DEFAULT_CACHE_TIME%"""
self.cache_server_cache_time = float(value)
@cli_flags('--shutdown-if-reset-fail')
@cli_help_section('Device Manager Options')
def _shutdown_if_reset_fail(self, opt: str):
"""shutdown the controller if device reset fail"""
self.shutdown_if_reset_fail = True
@cli_options('--expected-channel', value="CH,...")
@cli_help_section('Device Manager Options')
def _expected_channel(self, opt: str, value: str):
"""expect device channel. could be:
ALL : all channel
NULL : empty
INIT : by check
@FILE : read from file, format: CHANNEL,...
CHANNEL,... : given channel number
"""
from biopro.impl.selector import Selector
if value == 'ALL':
c = list(range(Selector.SIZE))
elif value == 'NULL':
c = []
elif value == 'INIT':
c = None
elif value.startswith('@'):
c = self._expected_channel_file(value[1:])
else:
c = list(map(int, value.split(',')))
self.expect_available_channel = c
def _expected_channel_file(self, filepath: str) -> Optional[List[int]]:
p = Path(filepath)
if not p.exists():
return None
with p.open() as f:
line = f.readline().strip()
return list(map(int, line.split(',')))
@cli_flags('--disable-routine-trash')
@cli_help_section('File Manager Options')
def _disable_routine_trash(self, flag: str):
"""disable routine trashing"""
self.flag_disable_routine_trash = True
@cli_options('--expire-trash', value='DAYS')
@cli_help_section('File Manager Options')
def _expire_trash(self, opt: str, value: str):
"""the expire time for file to be trashed"""
self.trash_expire_time = int(value)
@cli_options('--expire-remove', value='DAYS')
@cli_help_section('File Manager Options')
def _expire_remove(self, opt: str, value: str):
"""the expire time for file in trash to be removed"""
self.trash_remove_expire_time = int(value)
@cli_options('--routine-trash', value='HOURS')
@cli_help_section('File Manager Options')
def _routine_trash(self, opt: str, value: str):
"""routine period for trash files"""
self.trash_routine_time = int(value)
@cli_flags('--disable-external-storage')
@cli_help_section('File Manager Options')
def _disable_external_storage(self, opt: str):
"""disable use external storage device"""
self.flag_disable_external_storage = True
@cli_options('-M', '--external-map', value='NAME:PATH')
@cli_help_section('File Manager Options')
def _external_path_map(self, opt: str, value: str):
"""map external storage path"""
f, t = part_suffix(value, ':')
self.external_path_map.append((f, t))
@cli_options('--websocket-address', value='URL[:PORT]')
@cli_help_section('Websocket Server')
def _server_websocket_address(self, opt: str, value: str):
"""set the websocket address and port"""
addr, port = part_suffix(value, ':', missing=self.DEFAULT_WEBSOCKET_PORT)
self.websocket_address = addr
self.websocket_port = int(port)
@cli_options('--websocket-port', value='PORT')
@cli_help_section('Websocket Server')
def _server_websocket_port(self, opt: str, value: str):
"""set the websocket port"""
self.websocket_port = int(value)
@cli_flags('--disable-led-control')
@cli_help_section('LED control')
def _disable_led_control(self, flag: str):
"""disable LED control"""
self.flag_disable_led_control = True
@cli_flags('--disable-led-refresh')
@cli_help_section('LED control')
def _disable_led_refresh(self, flag: str):
"""disable LED refresh simple color"""
self.flag_disable_led_refresh = True
@cli_options('--led-count', value='COUNT')
@cli_help_section('LED control')
def _led_count(self, opt: str, value: str):
"""set led count"""
self.led_count = int(value)
if not _RUNTIME_COMPILE:
@cli_flags('--disable-sha-check')
def _disable_sha_check(self, flag: str):
"""disable sha checking"""
self.flag_disable_sha_check = True
@cli_flags('--disable-ups-detect')
@cli_help_section('UPS control')
def _disable_ups_detect(self, flag: str):
"""disable UPS power use detect"""
self.flag_disable_ups_detect = True
@cli_options('--ups-wait-time', value='MIN')
@cli_help_section('UPS control')
def _ups_wait_time(self, opt: str, value: str):
"""ups wait time when low power"""
self.flag_ups_wait_time = int(value) * 60
class UserSession:
"""user session. It has unique session ID that the front-end application should keep
this session ID (for example, used as cookie), make server can keep the data during
the request.
To attach a data into session, just assign it as a session attribute.
"""
def __init__(self, user_session: Optional[str]):
if user_session is None:
user_session = str(uuid.uuid1())
else:
user_session = str(uuid.UUID(user_session))
self.__uuid = user_session
self.__time = time()
@property
def user_session(self) -> str:
return self.__uuid
def time(self, update=True) -> float:
ret = self.__time
if update:
self.__time = time()
return ret
@overload
def get_data(self, key: str, type_check: Type[T] = None) -> Optional[T]:
pass
@overload
def get_data(self, key: str, type_check: Type[T], init: Callable[[], T]) -> T:
pass
def get_data(self, key: str, type_check: Type[T] = None, init=None):
try:
ret = getattr(self, key)
if type_check is not None:
if not isinstance(ret, type_check):
raise TypeError
except (AttributeError, TypeError):
if init is None:
return None
ret = init()
setattr(self, key, ret)
return ret
def set_data(self, key: str, value: T) -> T:
setattr(self, key, value)
return value
def del_data(self, key: str):
try:
delattr(self, key)
except AttributeError:
pass
class HardwareInfo(JsonSerialize):
"""hardware information.
::
hardware_info = {
# general information
"name" : str
"version" : str
"build" :str
"vendor" :str
"copyright" :str
"web_site" :str
"device_ID" :str
"device_mac" :str?
"wifi_name" :str
}
"""
def __init__(self,
name: str,
spec = None,
version = None,
device_ID = None,
device_mac = None):
self.name = WIFI_NAME
self.spec = spec
'''server name'''
self.version = version if version else VERSION
'''software API version
**format**
::
major.minor.patch[-mark]
major
update with hardware
minor
update for software
patch
update with bug-fixing
mark
alpha, beta, dev, rc, ...
'''
self.build = REV_VERSION
'''software build version'''
self.vendor = VENDOR
'''software vendor'''
self.copyright = COPYRIGHT
'''software copyright'''
self.web_site = WEB_SITE
'''official web site'''
self.device_ID = device_ID
'''device ID'''
self.device_mac = device_mac
'''device mac address'''
self.wifi_name = WIFI_NAME
def as_json(self) -> JSON_OBJECT:
return {
'name': self.name,
'version': self.version,
'build': self.build,
'vendor': self.vendor,
'copyright': self.copyright,
'web_site': self.web_site,
'device_ID': self.device_ID,
'device_mac': self.device_mac,
'wifi_name': self.wifi_name,
}
def as_json_1(self) -> JSON_OBJECT:
return {
'name': self.name,
'spec': self.spec,
'software_version': self.build,
'mac_address': self.device_mac,
'wifi_name': self.wifi_name,
}
class ControlServerAPI(ControlAPI,
DeviceInternalCommandHandler,
metaclass=abc.ABCMeta):
TEMP_FILENAME_EXPR = '%t-%d'
__slots__ = ()
@property
def is_closed(self) -> bool:
return False
def interrupt(self, exception: BaseException) -> None:
pass
@property
@abc.abstractmethod
def options(self) -> ControlServerOptions:
pass
@property
@abc.abstractmethod
def file_manager(self) -> FileManager:
pass
@property
@abc.abstractmethod
def device_manager(self) -> DeviceManager:
pass
def new_user_session(self) -> str:
"""new user session
:return: user uuid, use for *user_session* in other Relative Methods.
"""
raise NotImplementedError()
def del_user_session(self, user_session: Optional[str] = None):
"""delete user session
:param user_session: user session, None for all
"""
raise NotImplementedError()
def get_user_session(self,
user_session: Optional[str],
new_session=False,
allow_empty=False) -> Optional[UserSession]:
"""get user session
:param user_session: user session ID
:param new_session: new a user session if *user_session* not found.
:param allow_empty: consider empty *user_session* None
:return: user session
"""
raise NotImplementedError()
def expire_user_session(self):
"""expire user session
"""
# TODO who call this methods?
raise NotImplementedError()
def hardware_info(self) -> HardwareInfo:
raise NotImplementedError()
def hardware_datetime(self, content: Optional[str]) -> str:
return datetime_str()
def hardware_scan(self) -> bool:
self.device_manager.scan()
return True
def hardware_menu(self) -> Optional[Menu]:
return self.device_manager.get_menu()
def hardware_device(self) -> List[DeviceResponseInfo]:
return self.device_manager.found()
def hardware_device_found(self, content: Union[str, JSON_OBJECT, ADDRESS, DeviceInfo], *,
scan = False) -> Optional[DeviceResponseInfo]:
if isinstance(content, DeviceInfo):
response = content
else:
response = to_device_info(content)
if isinstance(scan, str):
scan = scan.upper() == 'TRUE'
if scan:
try:
return self.device_manager.scan_device(response)
except MasterDeviceError:
return None
else:
return self.device_manager.found_device(response)
def hardware_device_connect(self, content: Union[str, JSON_OBJECT, DeviceInfo]) -> CompletedDevice:
if isinstance(content, DeviceInfo):
response = content
else:
response = to_device_info(content)
return self.device_manager.connect(response)
def hardware_device_disconnect(self, device: Optional[int], force=False) -> bool:
if device is None:
self.device_manager.disconnect_all(force)
return True
else:
return self.device_manager.disconnect(device, force)
def hardware_send(self, command: str, **options: str) -> JSON:
return None
def get_device_info_all(self) -> List[CompletedDevice]:
return self.device_manager.list_device()
def get_device_info(self, device: int) -> Optional[CompletedDevice]:
return self.device_manager.get_device(device).as_json()
def device_instruction_all(self, device: int) -> List[str]:
return self.device_manager.list_device_instruction(device)
def device_all_instruction(self, instruction: str, content: str) -> bool:
for device in self.device_manager.list_device():
self.device_manager.device_instruction(device.device_id, instruction)
return True
def device_instruction(self, device: int, instruction: str) -> bool:
self.device_manager.device_instruction(device, instruction)
return True
def device_battery(self) -> List[int]:
device_list = self.device_manager.list_device()
if len(device_list) > 0:
battery_list = list(map(lambda x: self.device_manager.get_device(x).battery, device_list))
return battery_list
def device_parent(self, device: int, content: Optional[str] = None) -> List[int]:
if content is not None:
self.device_manager.get_device(device).update_parent_info(JsonSerialize.to_json(content))
return self.device_manager.get_device(device).parent
def device_recording_file_name(self, device: int, content: Optional[str] = None) -> List[int]:
if content is not None:
self.device_manager.get_device(device).update_recording_file_name_info(str(content))
return self.device_manager.get_device(device).recording_file_name
def device_sd_card_status(self, device:int) -> None:
return None
def device_update_calibration(self) -> None:
device_list = self.device_manager.list_device()
for x in device_list:
x.update_calibration_info('TDC4VC')
def device_update_radio_frequency(self, device: int, radio_frequency:int) -> bool:
d = self.device_manager.get_device(device)
res = d.update_radio_frequency(radio_frequency)
return res
def device_update_led_brightness(self, device: int, led_brightness:int) -> bool:
d = self.device_manager.get_device(device)
res = d.update_led_brightness(led_brightness)
return res
def device_update_accelerator_sensitivity(self, device: int, accelerator_sensitivity:int) -> bool:
d = self.device_manager.get_device(device)
res = d.update_accelerator_sensitivity(accelerator_sensitivity)
return res
def device_set_central_rf(self, device: int, content: int) -> bool:
d = self.device_manager.get_device(device)
res = d.central_rf_set(content)
console.log('set_central_ref', res)
def device_get_central_version(self, device: int) -> Optional[list]:
d = self.device_manager.get_device(device)
res = d.central_version_get()
console.log('get_central_ref', res)
def device_parameter_last_update_timestamp(self, device: int) -> int:
d = self.device_manager.get_device(device)
return d.parameter.last_modified_timestamp
def device_parameter(self, device: int, parameter: str, content: Optional[str] = None) -> Any:
return self.device_manager.handle_device_parameter_command(device, parameter, content)
def device_parameter_value(self, device: int, parameter: str) -> Any:
return self.device_manager.handle_device_parameter_value(device, parameter)
def device_save_path(self, device: int, unset: bool) -> Optional[str]:
d = self.device_manager.get_device(device)
if d is None:
raise RuntimeError(DEVICE_NOT_FOUND, str(device))
if unset:
return self.file_manager.unset(device)
else:
info = self.file_manager.use(device)
if info.is_meta_file:
# do not used file_path which contain file extension
return info.parent_path + '/' + info.file_name
directory = info.file_path
content = format_filename(self.TEMP_FILENAME_EXPR, d)
if '/' in content:
return content
else:
return directory + '/' + content
def device_save_path_set(self, device: int, content: str, dry_run: bool) -> str:
d = self.device_manager.get_device(device)
content = re.sub("^ +", "", content)
content = re.sub(" +$", "", content)
content = re.sub(" +", "_", content)
ret = format_filename(content, d)
if dry_run:
return ret
else:
info = self.file_manager.use(device)
if info.is_meta_file:
directory = info.parent_path
else:
directory = info.file_path
if '/' in ret:
return ret
else:
return directory + '/' + ret
def device_history_get(self, user: Optional[str]) -> List[DeviceResponseInfo]:
return []
def device_history_found_all(self, user: str) -> List[DeviceResponseInfo]:
raise []
def device_history_found(self, user: str, device: int) -> Optional[DeviceResponseInfo]:
return None
def device_history_delete(self, user: str, device: Optional[int]) -> bool:
return True
def file_clean_storage(self) -> bool:
return self.file_manager.clean_storage()
def file_index(self) -> RootInfo:
return self.file_manager.index()
def file_list(self, path: str) -> DirectoryInfo:
return self.file_manager.list(path)
def file_list_range(self, path: str, start: int, end: int) -> DirectoryInfo:
return self.file_manager.list_range(path, start, end)
def file_rename(self, path: str, content: str) -> FileInfo:
return self.file_manager.rename(path, content)
def file_mkdir(self, path: str) -> DirectoryInfo:
return self.file_manager.mkdir(path)
def file_delete(self, path: str) -> bool:
return self.file_manager.delete(path)
def file_info(self, path: str) -> FileInfo:
return self.file_manager.info(path)
def file_meta_info(self, path: str) -> RecordingMetaFile:
return self.file_manager.meta(path)
def file_export_info(self, path: str) -> Dict[str, Optional[Path]]:
return JsonSerialize.to_json(self.file_manager.export_info(path))
def file_export_functions(self) -> Tuple[ExternalExportFunction, ...]:
return self.file_manager.get_export_functions()
def file_export_parameters(self, ftype: str) -> Tuple[ExternalParameter, ...]:
return self.file_manager.get_export_parameters(ftype)
def file_export(self, ftype: str, path: str, **options: str) -> str:
return '***********'
if isinstance(options, str):
options = self._file_process_options(options)
elif isinstance(options, dict):
pass
else:
raise RuntimeError('unknown parameter type : ' + options.__class__.__name__)
ret = self.file_manager.export_file(path, None, ftype, async_call=True, **options)
return str(ret)
@staticmethod
def _file_process_options(argv: str) -> Dict[str, Any]:
ret = {}
for arg in re.split('[&;\n]', argv):
k, v = part_suffix(arg, '=', missing=True)
k = k.replace('-', '_')
if isinstance(v, str):
try:
v = float(v)
except ValueError:
try:
v = int(v)
except ValueError:
pass
ret[k] = v
return ret
def file_segment(self, method: str, path: str) -> Union[None, bool, FileSegment]:
# ********
# path = '2020-05-04/2020-05-04-18-35-54-5_2_0.bprf'
if method == 'info':
return self.file_manager.segment_info(path)
elif method == 'new':
# print('*****4', path)
return self.file_manager.segment_info(path, create=True)
elif method == 'reload':
info = self.file_manager.segment_info(path)
if info is not None:
info.reload()
return True
elif method == 'invalid':
info = self.file_manager.segment_info(path)
if info is not None:
info.invalid()
return True
elif method == 'invalid_all':
FileSegment.invalid_all()
return True
else:
raise RuntimeError('unknown method : ' + method)
def file_segment_get(self, segment: int, path: str) -> FileSegmentContent:
info = self.file_manager.segment_info(path)
if info is None:
raise RuntimeError('file segment not create : ' + path)
return info.get_segment(segment)
def file_handle_open(self, content: str) -> Optional[FileHandle]:
return None
def file_handle_info(self, handle: int) -> Optional[FileHandle]:
return None
def file_handle_close(self, handle: int):
pass
def user_all(self) -> List[str]:
return self.file_manager.user_setting.user_list()
def user_info(self, user: str, *, user_session: Optional[str] = None) -> Optional[AbstractUserSetting]:
user_name = self._session_user_name(user_session)
if user_name is not None and len(user_name) == 0:
user_name = None
if len(user) == '' and user_name is None:
# no user login
return None
elif len(user) == '':
# current user
user = user_name
info = self.file_manager.user_setting.user_info(user)
if user == user_name:
return info
else:
# other user, return restrict mode
return RestrictUserSetting(info)
def user_login(self, user: str, content: str, *,
user_session = None) -> ComplexResponse[AbstractUserSetting]:
if not self.file_manager.user_setting.user_validate(user, content):
raise RuntimeError(USER_INCORRECT_PASSWORD, user)
session = self.get_user_session(user_session, new_session=True, allow_empty=True)
session.user_name = user
info = self.file_manager.user_setting.user_info(user)
return response_with_user_session(info, session)
def user_logout(self, user_session: Optional[str] = None) -> None:
session = self.get_user_session(user_session)
if session is not None:
# remove user information from current session
session.del_data('user_name')
def user_add(self, user: str, content: str, *,
user_session = None) -> ComplexResponse[AbstractUserSetting]:
self.file_manager.user_setting.user_add(user, content)
session = self.get_user_session(user_session, new_session=True, allow_empty=True)
session.user_name = user
info = self.file_manager.user_setting.user_info(user)
return response_with_user_session(info, session)
def user_del(self, user: str, content: str, *,
user_session: Optional[str]) -> bool:
session = self.get_user_session(user_session, new_session=False, allow_empty=False)
if session is None:
return False
user_name = session.get_data('user_name')
if user_name is None:
return False
if user_name != user:
# only user can delete himself
# TODO or super-user can delete everyone
return False
if self.file_manager.user_setting.user_del(user, content):
# and also logout user
session.del_data("user_name")
return True
else:
return False
def user_sync(self, user_session: Optional[str]) -> bool:
user_name = self._session_user_name(user_session)
if user_name is None:
# no user login
return False
try:
self.file_manager.user_setting.user_sync(user_name)
return True
except:
return False
def user_configuration_list(self, user_session: Optional[str], **options: str) -> List[str]:
user_name = self._session_user_name(user_session)
if user_name is None:
return []
return self.file_manager.user_setting.configuration_filter(user_name, **options)
def user_configuration_get(self, name: str, user_session: Optional[str] = None) -> Optional[RecordingMetaFile]:
user_name = self._session_user_name(user_session)
if user_name is None:
return None
return self.file_manager.user_setting.configuration_get(user_name, name)
def user_configuration_add(self, name: str, content: str, user_session: Optional[str] = None) -> bool:
user_name = self._session_user_name(user_session)
if user_name is None:
return False
self.file_manager.user_setting.configuration_add(user_name, name, content)
return True
def user_configuration_del(self, name: str, user_session: Optional[str] = None) -> bool:
user_name = self._session_user_name(user_session)
if user_name is None:
return False
return self.file_manager.user_setting.configuration_del(user_name, name)
def user_configuration_clone(self, name: str, content: str, user_session: Optional[str] = None) -> bool:
user_name = self._session_user_name(user_session)
if user_name is None:
return False
from_user, new_name = part_prefix(content, ':')
return self.file_manager.user_setting.configuration_clone(user_name, name, new_name, from_user)
def user_setup_list(self, user_session: Optional[str] = None) -> List[str]:
user_name = self._session_user_name(user_session)
if user_name is None:
return []
return self.file_manager.user_setting.setup_list(user_name)
def user_setup_info(self, name: str, user_session: Optional[str] = None) -> Optional[UserSetup]:
user_name = self._session_user_name(user_session)
if user_name is None:
return None
return self.file_manager.user_setting.setup_info(user_name, name)
def user_setup_new(self, name: str, user_session: Optional[str] = None) -> Optional[UserSetup]:
user_name = self._session_user_name(user_session)
if user_name is None:
return None
return self.file_manager.user_setting.setup_new(user_name, name)
def user_setup_del(self, name: str, user_session: Optional[str] = None) -> bool:
user_name = self._session_user_name(user_session)
if user_name is None:
return False
return self.file_manager.user_setting.setup_del(user_name, name)
def user_alias_list(self, user_session: Optional[str] = None) -> List[UserDeviceAlias]:
user_name = self._session_user_name(user_session)
if user_name is None:
return []
return self.file_manager.user_setting.alias_list(user_name)
def user_alias_info(self, name: str, user_session: Optional[str] = None) -> Optional[UserDeviceAlias]:
user_name = self._session_user_name(user_session)
if user_name is None:
return None
user_setting = self.file_manager.user_setting
try:
address = str_address(name)
except ValueError:
return user_setting.alias_info(user_name, name)
else:
return user_setting.alias_info(user_name, address)
def user_alias_edit(self, name: str, content: str, user_session: Optional[str] = None) -> Optional[UserDeviceAlias]:
user_name = self._session_user_name(user_session)
if user_name is None:
return None
user_setting = self.file_manager.user_setting
address = None
try:
address = str_address(name)
except ValueError:
alias = user_setting.alias_info(user_name, name)
else:
alias = user_setting.alias_info(user_name, address)
if alias is None:
# new alias
if address is None:
return None
alias = user_setting.alias_new(user_name, address)
# try to give device name
device = self.hardware_device_found(address)
if device is not None:
alias.device_name = device.device_name
else:
address = alias.device_address
if len(content) == 0:
# delete alias
user_setting.alias_del(user_name, address)
return None
# update alias name
alias.device_alias = content
return alias
def exp_pro_list(self) -> List[ExpProtocolInfo]:
raise NotImplementedError()
def exp_pro_create(self, content: str, *, user_session: Optional[str] = None) -> int:
raise NotImplementedError()
def exp_pro_options(self, protocol: int) -> List[ExpOption]:
raise NotImplementedError()
def exp_pro_option(self, protocol: int, option: str, content: Optional[str] = None) -> bool:
raise NotImplementedError()
def exp_pro_status_all(self) -> List[AbstractExpProtocolState]:
raise NotImplementedError()
def exp_pro_status(self, protocol: int) -> Optional[ExpProtocolState]:
raise NotImplementedError()
def exp_pro_command(self, protocol: int, content: str) -> bool:
raise NotImplementedError()
def broadcast_command(self, command: str, device: Optional[int] = None):
"""broadcast command to clients
:param command:
:param device:
:return:
"""
pass
def data_request_stream(self, start: bool, handler: DataMessageHandler):
"""
:param start:
:param handler:
:return:
"""
raise NotImplementedError()
def websocket_command(self, command: str) -> Union[None, str, bytes, List[str]]:
"""
:param command:
"""
raise NotImplementedError()
def websocket_data_request(self, handler: DataMessageHandler, command: Optional[str]):
"""
:param handler:
:param command:
"""
raise NotImplementedError()
def get_led_state(self) -> str:
return LED.AVAILABLE
def set_led_state(self, state: str):
pass
# thread
@abc.abstractmethod
def submit_thread(self, f, *args):
pass
@abc.abstractmethod
def run_thread(self, thread: T_ServerThread) -> T_ServerThread:
pass
def submit_promise(self, name: str, f: Callable[..., T], *args) -> Promise[T]:
promise = Promise(name)
def _promise():
try:
ret = f(*args)
except BaseException as e:
print_exception()
promise.reject(e)
else:
promise.complete(ret)
self.submit_thread(_promise)
return promise
def restart_server(self) -> None:
raise NotImplementedError()
# utility function
def _session_user_name(self, user_session: Optional[str]) -> Optional[str]:
"""get the name of the current login-ed user
:param user_session: cookie
:return: the name of the current login-ed user
"""
if user_session is None:
return None
session = self.get_user_session(user_session)
return session.get_data('user_name')
def response_with_user_session(response: T, session: UserSession) -> ComplexResponse[T]:
ret = ComplexResponse(response)
ret.set_cookie(user_session=session.user_session)
return ret
def include_session(f: Callable[..., T]) -> Callable[..., ComplexResponse[T]]:
"""**decorator**"""
signature = inspect.signature(f)
@wraps(f)
def _inner(self, *args, **kwargs):
bind = signature.bind(self, *args, **kwargs)
user_session = bind.arguments['user_session']
session = self.get_user_session(user_session, new_session=True, allow_empty=True)
bind.arguments['user_session'] = session
ret = f(*bind.args, **bind.kwargs)
return response_with_user_session(ret, session)
return _inner
def format_filename(expr: str, device: CompletedDevice) -> str:
t = datetime.now()
def _mapper(flag: str) -> Optional[str]:
if flag == 't':
return '%4d-%02d-%02d-%02d-%02d-%02d' % (t.year, t.month, t.day, t.hour, t.minute, t.second)
elif flag == 'Td':
return '%4d-%02d-%02d' % (t.year, t.month, t.day)
elif flag == 'Tt':
return '%02d:%02d:%02d' % (t.hour, t.minute, t.second)
elif flag == 'TY':
return '%4d' % t.year
elif flag == 'Tn':
return '%d' % t.month
elif flag == 'TN':
return ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][t.month]
elif flag == 'Tw':
return '%d' % t.isoweekday()
elif flag == 'TW':
return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][t.weekday()]
elif flag == 'TD':
return '%d' % t.day
elif flag == 'Th':
return '%d' % (t.hour % 12)
elif flag == 'TH':
return '%d' % t.hour
elif flag == 'Tm':
return '%d' % t.minute
elif flag == 'Ts':
return '%d' % t.second
elif flag == 'd':
return '%d' % device.device_id
elif flag == 'Dn':
return device.device_name
elif flag == 'Dm':
return address_str(device.mac_address)
elif flag == 'Dt':
return device.library_name
else:
return None
return expr_format(expr, _mapper)
def to_device_info(content: Union[str, ADDRESS, JSON_OBJECT]) -> DeviceResponseInfo:
if is_address_type(content):
return DeviceResponseInfo(None,
serial_number=DeviceSerialNumber.UNKNOWN, # match all
mac_address=content)
if isinstance(content, str):
json = json_parse(content)
if isinstance(json, str):
json = {'device_address': json}
else:
json = content
try:
device_address = json['device_address']
except KeyError as e:
raise RuntimeError(DEVICE_NOT_FOUND) from e
address = str_address(device_address)
serial = json.get('serial_number', None)
if not isinstance(serial, list):
# illegal format
raise RuntimeError(DEVICE_NOT_FOUND)
if serial is not None:
serial = DeviceSerialNumber(*serial)
return DeviceResponseInfo(None,
serial_number=serial,
mac_address=address)