mirror of
https://github.com/CopterExpress/clever-show.git
synced 2026-05-26 07:07:58 +00:00
Move libs to lib and tests to test, add assets folder to test
This commit is contained in:
357
lib/config.py
Normal file
357
lib/config.py
Normal file
@@ -0,0 +1,357 @@
|
||||
import os
|
||||
import copy
|
||||
import collections
|
||||
|
||||
from configobj import ConfigObj, Section, flatten_errors
|
||||
from validate import Validator, is_tuple, is_boolean, is_integer
|
||||
|
||||
|
||||
def modify_filename(path, pattern): # TODO move to core
|
||||
old_path, filename = os.path.split(path)
|
||||
filename, ext = os.path.splitext(filename)
|
||||
newfilename = pattern.format(filename) + ext
|
||||
return os.path.join(old_path, newfilename)
|
||||
|
||||
|
||||
def parent_path(path, levels=1):
|
||||
for i in range(levels):
|
||||
path = os.path.abspath(os.path.join(path, os.pardir))
|
||||
return path
|
||||
|
||||
|
||||
def parent_dir(path):
|
||||
return os.path.basename(os.path.normpath(path))
|
||||
|
||||
|
||||
def is_preset_param(value):
|
||||
parsed = is_tuple(value, min=2, max=2)
|
||||
return is_boolean(parsed[0]), is_integer(parsed[1], min=0)
|
||||
|
||||
|
||||
class ValidationError(ValueError):
|
||||
def __init__(self, message, config, errors):
|
||||
super(ValidationError, self).__init__(message)
|
||||
self.config = config
|
||||
self.errors = errors
|
||||
|
||||
def __str__(self):
|
||||
return "{} - {}".format(self.args[0], " ".join(self.flatten_errors()))
|
||||
|
||||
def flatten_errors(self):
|
||||
for entry in flatten_errors(self.config, self.errors):
|
||||
section_list, key, error = entry
|
||||
if key is not None:
|
||||
section_list.append(key)
|
||||
else:
|
||||
section_list.append('[missing section]')
|
||||
section_string = ', '.join(section_list)
|
||||
if error == False: # Important syntax
|
||||
error = 'Missing value or section.'
|
||||
yield "[{}]: {}".format(section_string, error)
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
def __init__(self, config=None):
|
||||
self.config = ConfigObj() if config is None else config
|
||||
|
||||
self._name_dict = {}
|
||||
|
||||
def get(self, section, option):
|
||||
return self.config[section][option]
|
||||
|
||||
def set(self, section, option, value, write=False):
|
||||
self.config[section][option] = value
|
||||
if write:
|
||||
self.write()
|
||||
|
||||
def get_chain(self, *keys):
|
||||
current = self.config
|
||||
for key in keys:
|
||||
current = current[key]
|
||||
return current
|
||||
|
||||
def set_chain(self, value, *keys): # will create new sections!
|
||||
current = self.config
|
||||
for key in keys[:-1]:
|
||||
current = current.setdefault(key, {})
|
||||
current[keys[-1]] = value
|
||||
|
||||
def write(self):
|
||||
self.config.write()
|
||||
|
||||
@property
|
||||
def validated(self):
|
||||
return self.config.configspec is not None
|
||||
|
||||
def set_config(self, config):
|
||||
self.config = config
|
||||
self._name_dict = self.flatten_keys(config)
|
||||
|
||||
def validate_config(self, config=None, copy_defaults=False):
|
||||
config = self.config if config is None else config
|
||||
vdt = Validator({"preset_param": is_preset_param})
|
||||
|
||||
test = config.validate(vdt, copy=copy_defaults, preserve_errors=True)
|
||||
if test != True: # Important syntax, do no change
|
||||
raise ValidationError('Some config values are wrong', config, test)
|
||||
|
||||
self.set_config(config)
|
||||
|
||||
@classmethod
|
||||
def _full_dict(cls, item, include_defaults=False):
|
||||
if not isinstance(item, Section):
|
||||
return item
|
||||
|
||||
data = collections.OrderedDict()
|
||||
default_values = item.default_values
|
||||
defaults = item.defaults
|
||||
comments = item.comments
|
||||
inline_comments = item.inline_comments
|
||||
|
||||
for key, value in item.items():
|
||||
result = cls._full_dict(value, include_defaults)
|
||||
if not isinstance(result, dict):
|
||||
item_d = {'__value__': value}
|
||||
|
||||
comment = comments.get(key, [])
|
||||
if comment and comment != ['']:
|
||||
item_d.update({'comments': comment})
|
||||
|
||||
inline_comment = inline_comments.get(key, None)
|
||||
if inline_comment:
|
||||
item_d.update({'inline_comment': inline_comments})
|
||||
|
||||
if include_defaults:
|
||||
item_d.update({'default': default_values.get(key, None),
|
||||
'unchanged': key in defaults,
|
||||
})
|
||||
data[key] = item_d
|
||||
else:
|
||||
data[key] = result
|
||||
|
||||
return data
|
||||
|
||||
def full_dict(self, include_defaults=False):
|
||||
d = self._full_dict(self.config, include_defaults=include_defaults)
|
||||
d['initial_comment'] = self.config.initial_comment
|
||||
d['final_comment'] = self.config.final_comment
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def flatten_keys(cls, d, parent_keys=(), sep='_'):
|
||||
items = {}
|
||||
for key, value in d.items():
|
||||
keys = parent_keys + (key,)
|
||||
if isinstance(value, dict):
|
||||
items.update(cls.flatten_keys(value, keys, sep=sep))
|
||||
formatted_keys = [key.lower().strip().replace(' ', sep) for key in keys]
|
||||
formatted_key = sep.join(formatted_keys)
|
||||
items.update({formatted_key: keys})
|
||||
return dict(items)
|
||||
|
||||
def __getattr__(self, item):
|
||||
try:
|
||||
keys = self.__dict__['_name_dict'][item]
|
||||
return self.get_chain(*keys)
|
||||
except (ValueError, KeyError):
|
||||
return self.__dict__[item]
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
try:
|
||||
keys = self.__dict__['_name_dict'][key]
|
||||
self.set_chain(value, *keys)
|
||||
except (ValueError, KeyError):
|
||||
self.__dict__[key] = value
|
||||
|
||||
@staticmethod
|
||||
def config_exists(path):
|
||||
return os.path.isfile(path) and os.path.splitext(path)[1] == '.ini'
|
||||
|
||||
@staticmethod
|
||||
def _get_spec_path(path):
|
||||
return modify_filename(path, 'spec/configspec_{}')
|
||||
|
||||
@staticmethod
|
||||
def _get_config_path(path):
|
||||
filename = os.path.split(path)[1]
|
||||
return os.path.join(parent_path(path, levels=2),
|
||||
filename.replace('configspec_', ''))
|
||||
|
||||
def load_from_file(self, path):
|
||||
if not self.config_exists(path):
|
||||
raise ValueError('Config file do not exist!')
|
||||
|
||||
f_path, filename = os.path.split(path)
|
||||
if filename.startswith('configspec_'):
|
||||
config_path = self._get_config_path(path)
|
||||
|
||||
if self.config_exists(config_path):
|
||||
return self.load_config_and_spec(config_path)
|
||||
|
||||
generate_file = parent_dir(f_path) == 'spec'
|
||||
if generate_file:
|
||||
self.generate_default_config(config_path)
|
||||
|
||||
return self.load_only_spec(path, generate_file)
|
||||
|
||||
else:
|
||||
spec_path = self._get_spec_path(path)
|
||||
if self.config_exists(spec_path):
|
||||
return self.load_config_and_spec(path)
|
||||
|
||||
return self.load_only_config(path)
|
||||
|
||||
def load_config_and_spec(self, path):
|
||||
self.generate_default_config(path)
|
||||
config = ConfigObj(infile=path,
|
||||
configspec=self._get_spec_path(path))
|
||||
|
||||
self.validate_config(config)
|
||||
|
||||
def load_only_config(self, path):
|
||||
config = ConfigObj(infile=path)
|
||||
self.set_config(config)
|
||||
|
||||
def load_only_spec(self, path, generate_filename=True):
|
||||
config = ConfigObj(configspec=path)
|
||||
if generate_filename:
|
||||
config.filename = self._get_config_path(path)
|
||||
|
||||
self.validate_config(config, copy_defaults=True)
|
||||
|
||||
@classmethod
|
||||
def generate_default_config(cls, cfg_path):
|
||||
if cls.config_exists(cfg_path):
|
||||
return False
|
||||
|
||||
vdt = Validator()
|
||||
config = ConfigObj(configspec=cls._get_spec_path(cfg_path))
|
||||
config.filename = cfg_path
|
||||
config.validate(vdt, copy=True)
|
||||
config.indent_type = ''
|
||||
config.initial_comment = ('This is generated config with default values',
|
||||
'Modify to configure')
|
||||
config.write()
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def _extract_values(cls, d):
|
||||
result = collections.OrderedDict()
|
||||
for key, val in d.items():
|
||||
if not isinstance(val, dict): # Pure dict option
|
||||
result[key] = val
|
||||
elif '__value__' in val: # Full-dict option with params
|
||||
if not val.get('unchanged', False):
|
||||
result[key] = val.get('__value__')
|
||||
else: # Section
|
||||
result[key] = cls._extract_values(val)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _load_comments(cls, d, section):
|
||||
comments = section.comments
|
||||
inline_comments = section.inline_comments
|
||||
|
||||
for key, val in d.items():
|
||||
if not isinstance(val, dict): # Pure dict option
|
||||
comments[key] = []
|
||||
inline_comments[key] = None
|
||||
elif '__value__' in val: # Full-dict option with params
|
||||
comment = val.get('comments', [])
|
||||
comments[key] = [] if comment == [''] else comment
|
||||
inline_comments[key] = val.get('inline_comment', None)
|
||||
else: # Section
|
||||
cls._load_comments(val, section[key])
|
||||
comments[key] = ['']
|
||||
inline_comments[key] = None
|
||||
|
||||
section.comments = comments
|
||||
section.inline_comments = inline_comments
|
||||
|
||||
def load_from_dict(self, d, configspec=None):
|
||||
initial_comment = d.pop('initial_comment', [''])
|
||||
final_comment = d.pop('final_comment', [''])
|
||||
|
||||
kwargs = {'infile': self._extract_values(d), 'indent_type': ''}
|
||||
filename = None
|
||||
if isinstance(configspec, dict):
|
||||
kwargs.update({'configspec': configspec})
|
||||
elif isinstance(configspec, str):
|
||||
spec_path = self._get_spec_path(configspec)
|
||||
if self.config_exists(spec_path): # when 'configspec' points to configuration file and configspec exists
|
||||
kwargs.update({'configspec': spec_path})
|
||||
filename = configspec
|
||||
elif self.config_exists(configspec): # when 'configspec' points to configspec file
|
||||
kwargs.update({'configspec': configspec})
|
||||
if parent_dir(configspec) == 'spec':
|
||||
filename = self._get_config_path(configspec)
|
||||
else:
|
||||
raise ValueError("Configspec does not exist")
|
||||
|
||||
config = ConfigObj(**kwargs)
|
||||
config.filename = filename
|
||||
config.initial_comment = initial_comment
|
||||
config.final_comment = final_comment
|
||||
|
||||
if config.configspec is not None:
|
||||
self.validate_config(config)
|
||||
else:
|
||||
self.set_config(config)
|
||||
|
||||
self._load_comments(d, self.config)
|
||||
|
||||
def merge(self, config, validate=True):
|
||||
current = copy.deepcopy(self.config)
|
||||
current.merge(config.config)
|
||||
if validate:
|
||||
self.validate_config(current)
|
||||
else:
|
||||
self.set_config(current)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cfg = ConfigManager()
|
||||
cfg.load_from_file('drone/config/client.ini')
|
||||
# cfg.load_from_file('server/config/server.ini')
|
||||
#cfg.load_from_file('drone/config/spec/configspec_client.ini')
|
||||
print(dict(cfg.full_dict(include_defaults=True)))
|
||||
cfg.config.pop("PRIVATE", None)
|
||||
print(cfg.config)
|
||||
|
||||
|
||||
# cfg.load_config_and_spec('drone/config/client.ini')
|
||||
# #print(cfg.config.comments)
|
||||
# #print(cfg.server_host)
|
||||
# cfg.server_host = '192.168.1.103'
|
||||
#
|
||||
# print(cfg.get('SERVER', 'host'))
|
||||
# cfg.set('SERVER', 'host', '192.168.1.103')
|
||||
# print(cfg.get('SERVER', 'host'))
|
||||
#
|
||||
# print(cfg.config.initial_comment, cfg.config.final_comment)
|
||||
#
|
||||
# # print(cfg.config)
|
||||
# # print(cfg.default_values)
|
||||
# # print(cfg.unchanged_defaults)
|
||||
#
|
||||
# # print(11111)
|
||||
import pprint
|
||||
#pprint.pprint(cfg.full_dict)
|
||||
# cfg2 = ConfigManager()
|
||||
# #cfg2.load_from_dict({"PRIVATE": {"offset": [1, 2, 3]}}, configspec='drone/config/spec/configspec_client.ini')
|
||||
# cfg2.load_from_dict({"PRIVATE": {"id": "heh"}})
|
||||
# #pprint.pprint(cfg2.full_dict)
|
||||
# #cfg.merge(cfg2)
|
||||
# #pprint.pprint(cfg.full_dict)
|
||||
# print(cfg2.full_dict(include_defaults=True))
|
||||
#print(dict(cfg2.config.configspec))
|
||||
#print(cfg2.config.PRIVATE)
|
||||
#print(dict(ConfigManager(cfg.config.configspec).config))
|
||||
|
||||
# #print(cfg.full_dict)
|
||||
#
|
||||
# #cfg.load_from_dict(cfg.full_dict, 'drone/config/client.ini')
|
||||
# #print(cfg.config.initial_comment, cfg.config.final_comment)
|
||||
# #cfg.write()
|
||||
#
|
||||
|
||||
3
lib/lib.py
Normal file
3
lib/lib.py
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
def b_partial(func, *args, **kwargs): # call argument blocker partial
|
||||
return lambda *a: func(*args, **kwargs)
|
||||
625
lib/messaging_lib.py
Normal file
625
lib/messaging_lib.py
Normal file
@@ -0,0 +1,625 @@
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import socket
|
||||
import struct
|
||||
import random
|
||||
import logging
|
||||
import threading
|
||||
import collections
|
||||
import platform
|
||||
import traceback
|
||||
|
||||
from contextlib import closing
|
||||
|
||||
try:
|
||||
import selectors
|
||||
except ImportError:
|
||||
import selectors2 as selectors
|
||||
|
||||
|
||||
class Namespace:
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__dict__[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.__dict__[key] = value
|
||||
|
||||
|
||||
class PendingRequest(Namespace): pass
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_ip_address():
|
||||
try:
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as ip_socket:
|
||||
ip_socket.connect(("8.8.8.8", 80))
|
||||
return ip_socket.getsockname()[0]
|
||||
except OSError:
|
||||
logger.warning("No network connection detected, using localhost")
|
||||
return "localhost"
|
||||
|
||||
|
||||
def set_keepalive(sock, after_idle_sec=1, interval_sec=3, max_fails=5):
|
||||
current_platform = platform.system() # could be empty
|
||||
if current_platform == "Linux":
|
||||
return _set_keepalive_linux(sock, after_idle_sec, interval_sec, max_fails)
|
||||
if current_platform == "Windows":
|
||||
return _set_keepalive_windows(sock, after_idle_sec, interval_sec)
|
||||
if current_platform == "Darwin":
|
||||
return _set_keepalive_osx(sock, interval_sec)
|
||||
|
||||
def _set_keepalive_linux(sock, after_idle_sec, interval_sec, max_fails):
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, after_idle_sec)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, interval_sec)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, max_fails)
|
||||
|
||||
def _set_keepalive_windows(sock, after_idle_sec, interval_sec):
|
||||
sock.ioctl(socket.SIO_KEEPALIVE_VALS, (1, after_idle_sec*1000, interval_sec*1000))
|
||||
|
||||
def _set_keepalive_osx(sock, interval_sec):
|
||||
TCP_KEEPALIVE = 0x10
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, TCP_KEEPALIVE, interval_sec)
|
||||
|
||||
|
||||
class _Singleton(type):
|
||||
""" A metaclass that creates a Singleton base class when called. """
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class Singleton(_Singleton('SingletonMeta', (object,), {})): pass
|
||||
|
||||
|
||||
class MessageManager:
|
||||
def __init__(self):
|
||||
self.income_raw = b""
|
||||
self._jsonheader_len = None
|
||||
self.jsonheader = None
|
||||
self.content = None
|
||||
|
||||
@staticmethod
|
||||
def _json_encode(obj, encoding="utf-8"):
|
||||
return json.dumps(obj, ensure_ascii=False).encode(encoding)
|
||||
|
||||
@staticmethod
|
||||
def _json_decode(json_bytes, encoding="utf-8"):
|
||||
with io.TextIOWrapper(io.BytesIO(json_bytes), encoding=encoding, newline="") as tiow:
|
||||
obj = json.load(tiow, object_pairs_hook=collections.OrderedDict)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def create_message(cls, content_bytes, content_type, message_type, content_encoding="utf-8",
|
||||
additional_headers=None):
|
||||
jsonheader = {
|
||||
"byteorder": sys.byteorder,
|
||||
"content-type": content_type,
|
||||
"content-encoding": content_encoding,
|
||||
"content-length": len(content_bytes),
|
||||
"message-type": message_type,
|
||||
}
|
||||
if additional_headers:
|
||||
jsonheader.update(additional_headers)
|
||||
|
||||
jsonheader_bytes = cls._json_encode(jsonheader, "utf-8")
|
||||
message_hdr = struct.pack(">H", len(jsonheader_bytes))
|
||||
message = message_hdr + jsonheader_bytes + content_bytes
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
def create_json_message(cls, contents, additional_headers=None):
|
||||
message = cls.create_message(cls._json_encode(contents), "json", "message",
|
||||
additional_headers=additional_headers)
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
def create_action_message(cls, action, args=(), kwargs=None):
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
message = cls.create_json_message({"args": args, "kwargs": kwargs}, {"action": action, })
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
def create_request(cls, requested_value, request_id, args=(), kwargs=None):
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
contents = {"requested_value": requested_value,
|
||||
"request_id": request_id,
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
message = cls.create_message(cls._json_encode(contents), "json", "request")
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
def create_response(cls, requested_value, request_id, value, filetransfer=False):
|
||||
headers = {"requested_value": requested_value,
|
||||
"request_id": request_id, # TODO status
|
||||
}
|
||||
if filetransfer:
|
||||
contents = value
|
||||
else:
|
||||
contents = cls._json_encode({"value": value, })
|
||||
message = cls.create_message(contents, "binary" if filetransfer else "json",
|
||||
"response", additional_headers=headers)
|
||||
return message
|
||||
|
||||
def _process_protoheader(self):
|
||||
header_len = 2
|
||||
if len(self.income_raw) >= header_len:
|
||||
self._jsonheader_len = struct.unpack(">H", self.income_raw[:header_len])[0]
|
||||
self.income_raw = self.income_raw[header_len:]
|
||||
|
||||
def _process_jsonheader(self):
|
||||
header_len = self._jsonheader_len
|
||||
if len(self.income_raw) >= header_len:
|
||||
self.jsonheader = self._json_decode(self.income_raw[:header_len], "utf-8")
|
||||
self.income_raw = self.income_raw[header_len:]
|
||||
for reqhdr in (
|
||||
"byteorder",
|
||||
"content-length",
|
||||
"content-type",
|
||||
"content-encoding",
|
||||
"message-type",
|
||||
):
|
||||
if reqhdr not in self.jsonheader:
|
||||
raise ValueError('Missing required header {}'.format(reqhdr))
|
||||
|
||||
def _process_content(self):
|
||||
content_len = self.jsonheader["content-length"]
|
||||
if not len(self.income_raw) >= content_len:
|
||||
return
|
||||
data = self.income_raw[:content_len]
|
||||
self.income_raw = self.income_raw[content_len:]
|
||||
if self.jsonheader["content-type"] == "json":
|
||||
encoding = self.jsonheader["content-encoding"]
|
||||
self.content = self._json_decode(data, encoding)
|
||||
else:
|
||||
self.content = data
|
||||
|
||||
def process_message(self):
|
||||
if self._jsonheader_len is None:
|
||||
self._process_protoheader()
|
||||
|
||||
if self._jsonheader_len is not None:
|
||||
if self.jsonheader is None:
|
||||
self._process_jsonheader()
|
||||
|
||||
if self.jsonheader:
|
||||
if self.content is None:
|
||||
self._process_content()
|
||||
|
||||
|
||||
def message_callback(action_string):
|
||||
def inner(f):
|
||||
ConnectionManager.messages_callbacks[action_string] = f
|
||||
logger.debug("Registered message function {} for {}".format(f, action_string))
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def request_callback(string_command):
|
||||
def inner(f):
|
||||
ConnectionManager.requests_callbacks[string_command] = f
|
||||
logger.debug("Registered callback function {} for {}".format(f, string_command))
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
class ConnectionManager(object):
|
||||
messages_callbacks = {}
|
||||
requests_callbacks = {}
|
||||
|
||||
def __init__(self, whoami="computer"):
|
||||
self.selector = None
|
||||
self.socket = None
|
||||
self.addr = None
|
||||
|
||||
self._should_close = False
|
||||
|
||||
self._recv_buffer = b""
|
||||
self._send_buffer = b""
|
||||
|
||||
self.whoami = whoami
|
||||
|
||||
self._send_queue = collections.deque()
|
||||
self._received_queue = collections.deque()
|
||||
self._request_queue = collections.OrderedDict()
|
||||
|
||||
self._send_lock = threading.Lock()
|
||||
self._request_lock = threading.Lock()
|
||||
self._close_lock = threading.Lock()
|
||||
|
||||
self.buffer_size = 1024
|
||||
self.resume_queue = False
|
||||
self.resend_requests = True
|
||||
|
||||
def _set_selector_events_mask(self, mode):
|
||||
"""Set selector to listen for events: mode is 'r', 'w', 'rw'."""
|
||||
if mode == "r":
|
||||
events = selectors.EVENT_READ
|
||||
elif mode == "w":
|
||||
events = selectors.EVENT_WRITE
|
||||
elif mode == "rw":
|
||||
events = selectors.EVENT_READ | selectors.EVENT_WRITE
|
||||
else:
|
||||
raise ValueError("Invalid events mask mode {}.".format(mode))
|
||||
|
||||
key = self.selector.modify(self.socket, events, data=self)
|
||||
logger.debug("Switched selector of {} to mode {}".format(self.addr, key.events))
|
||||
return key
|
||||
|
||||
def connect(self, client_selector, client_socket, client_addr):
|
||||
self.selector = client_selector
|
||||
self.socket = client_socket
|
||||
self.addr = client_addr
|
||||
|
||||
self._clear()
|
||||
|
||||
self._set_selector_events_mask('r')
|
||||
if self.resend_requests:
|
||||
self._resend_requests()
|
||||
|
||||
def _clear(self):
|
||||
if not self.resume_queue: # maybe needs locks
|
||||
self._recv_buffer = b''
|
||||
self._send_buffer = b''
|
||||
self._received_queue.clear()
|
||||
self._send_queue.clear()
|
||||
|
||||
def close(self):
|
||||
with self._close_lock:
|
||||
self._should_close = True
|
||||
|
||||
self._set_selector_events_mask('w')
|
||||
NotifierSock().notify()
|
||||
|
||||
def _close(self):
|
||||
logger.info("Closing connection to {}".format(self.addr))
|
||||
|
||||
try:
|
||||
logger.info("Unregistering selector of {}".format(self.addr))
|
||||
self.selector.unregister(self.socket)
|
||||
except AttributeError:
|
||||
pass
|
||||
except Exception as error:
|
||||
logger.error("{}: Error during selector unregistering: {}".format(self.addr, error))
|
||||
finally:
|
||||
self.selector = None
|
||||
|
||||
try:
|
||||
logger.info("Closing socket of of {}".format(self.addr))
|
||||
self.socket.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
except OSError as error:
|
||||
logger.error("{}: Error during socket closing: {}".format(self.addr, error))
|
||||
finally:
|
||||
self.socket = None
|
||||
|
||||
with self._close_lock:
|
||||
self._should_close = False
|
||||
|
||||
self._clear()
|
||||
logger.info("CLOSED connection to {}".format(self.addr))
|
||||
|
||||
def process_events(self, mask):
|
||||
with self._close_lock:
|
||||
close = self._should_close
|
||||
if close:
|
||||
self._close()
|
||||
return
|
||||
|
||||
if mask & selectors.EVENT_READ:
|
||||
self.read()
|
||||
if mask & selectors.EVENT_WRITE:
|
||||
self.write()
|
||||
|
||||
def read(self):
|
||||
self._read()
|
||||
while self._recv_buffer:
|
||||
# add new message object if queue is empty or last message already processed
|
||||
if not self._received_queue or (self._received_queue[0].content is not None):
|
||||
self._received_queue.appendleft(MessageManager())
|
||||
|
||||
last_message = self._received_queue[0]
|
||||
|
||||
last_message.income_raw += self._recv_buffer
|
||||
self._recv_buffer = b''
|
||||
last_message.process_message()
|
||||
|
||||
# if something left after processing message - put it back
|
||||
if last_message.content is not None and last_message.income_raw:
|
||||
self._recv_buffer = last_message.income_raw + self._recv_buffer
|
||||
last_message.income_raw = b''
|
||||
|
||||
if self._received_queue and last_message.content is not None:
|
||||
self.process_received(self._received_queue.popleft())
|
||||
|
||||
def _read(self):
|
||||
try:
|
||||
data = self.socket.recv(self.buffer_size)
|
||||
except io.BlockingIOError:
|
||||
# Resource temporarily unavailable (errno EWOULDBLOCK)
|
||||
pass
|
||||
else:
|
||||
if data:
|
||||
self._recv_buffer += data
|
||||
logger.debug("Received {} bytes from {}".format(len(data), self.addr))
|
||||
else:
|
||||
logger.warning("Connection to {} lost!".format(self.addr))
|
||||
|
||||
raise RuntimeError("Peer closed.")
|
||||
|
||||
def process_received(self, message):
|
||||
message_type = message.jsonheader["message-type"]
|
||||
content = message.content if message.jsonheader["content-type"] != "binary"\
|
||||
else message.content[:256]
|
||||
logger.debug(
|
||||
"Received message! Header: {}, content: {}".format(message.jsonheader, content))
|
||||
|
||||
if message_type == "message":
|
||||
self._process_message(message)
|
||||
elif message_type == "response":
|
||||
self._process_response(message)
|
||||
elif message_type == "request":
|
||||
self._process_request(message)
|
||||
|
||||
def _process_message(self, message):
|
||||
if message.jsonheader["action"] == "filetransfer":
|
||||
self._process_filetransfer(message.content, message.jsonheader["filepath"])
|
||||
else:
|
||||
self._process_action(message)
|
||||
|
||||
def _process_action(self, message):
|
||||
action = message.jsonheader["action"]
|
||||
args = message.content["args"]
|
||||
kwargs = message.content["kwargs"]
|
||||
callback = self.messages_callbacks.get(action, None)
|
||||
if callback is None:
|
||||
logger.warning("Action {} does not exist!".format(action))
|
||||
return
|
||||
try:
|
||||
callback(self, *args, **kwargs)
|
||||
except Exception as error:
|
||||
logger.error("Error during action {} execution: {}".format(action, error))
|
||||
traceback.print_exc()
|
||||
|
||||
def _process_request(self, message):
|
||||
requested_value = message.content["requested_value"]
|
||||
request_id = message.content["request_id"]
|
||||
args = message.content["args"]
|
||||
kwargs = message.content["kwargs"]
|
||||
|
||||
filetransfer = requested_value == "filetransfer"
|
||||
try:
|
||||
if filetransfer:
|
||||
value = self._read_file(kwargs["filepath"])
|
||||
else:
|
||||
callback = self.requests_callbacks.get(requested_value, None)
|
||||
if callback is None:
|
||||
logger.warning("Request {} does not exist!".format(requested_value))
|
||||
return
|
||||
|
||||
value = callback(self, *args, **kwargs)
|
||||
except Exception as error: # TODO send response error\cancel
|
||||
logger.error("Error during request {} processing: {}".format(requested_value, error))
|
||||
else:
|
||||
self._send_response(requested_value, request_id, value, filetransfer)
|
||||
|
||||
def _process_response(self, message):
|
||||
request_id, requested_value = message.jsonheader["request_id"], message.jsonheader["requested_value"]
|
||||
|
||||
with self._request_lock:
|
||||
request = self._request_queue.pop(request_id, None)
|
||||
if (request is None) or (request.requested_value != requested_value):
|
||||
logger.warning("Unexpected response!")
|
||||
return
|
||||
|
||||
if requested_value == "filetransfer":
|
||||
value = True
|
||||
self._process_filetransfer(message.content, request.callback_kwargs["filepath"])
|
||||
logger.debug(
|
||||
"Request {} successfully closed with file bytes {}...".format(request, message.content[:256])
|
||||
)
|
||||
else:
|
||||
value = message.content["value"]
|
||||
logger.debug(
|
||||
"Request {} successfully closed with value {}".format(request, message.content["value"])
|
||||
)
|
||||
if request.callback is not None:
|
||||
try:
|
||||
request.callback(self, value, *request.callback_args, **request.callback_kwargs)
|
||||
except Exception as error:
|
||||
logger.error("Error during response {} processing: {}".format(request, error))
|
||||
else:
|
||||
logger.info("No callback were registered for response: {}".format(request))
|
||||
|
||||
@staticmethod
|
||||
def _read_file(filepath):
|
||||
with open(filepath, mode='rb') as f:
|
||||
return f.read()
|
||||
|
||||
def _process_filetransfer(self, content, filepath):
|
||||
try:
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(content)
|
||||
except OSError as error:
|
||||
logger.error("File {} can not be written due error: {}".format(filepath, error))
|
||||
else:
|
||||
logger.info("File {} successfully received ".format(filepath))
|
||||
if self.whoami == "pi":
|
||||
logger.info("Return rights to pi:pi after file transfer")
|
||||
os.system("chown pi:pi {}".format(filepath))
|
||||
|
||||
def write(self):
|
||||
with self._send_lock:
|
||||
if (not self._send_buffer) and self._send_queue:
|
||||
message = self._send_queue.popleft()
|
||||
self._send_buffer += message
|
||||
if self._send_buffer:
|
||||
self._write()
|
||||
else:
|
||||
self._set_selector_events_mask('r') # we're done writing
|
||||
|
||||
def _write(self):
|
||||
try:
|
||||
sent = self.socket.send(self._send_buffer[:self.buffer_size])
|
||||
except io.BlockingIOError:
|
||||
# Resource temporarily unavailable (errno EWOULDBLOCK)
|
||||
pass
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
"Attempt to send message {} to {} failed due error: {}".format(self._send_buffer, self.addr, error))
|
||||
|
||||
raise error
|
||||
else:
|
||||
self._send_buffer = self._send_buffer[sent:]
|
||||
left = len(self._send_buffer)
|
||||
logger.debug("Sent message to {}: sent {} bytes, {} bytes left.".format(self.addr, sent, left))
|
||||
|
||||
def _send(self, data):
|
||||
with self._send_lock:
|
||||
self._send_queue.append(data)
|
||||
|
||||
if self.selector.get_key(self.socket).events != selectors.EVENT_WRITE:
|
||||
self._set_selector_events_mask('rw')
|
||||
NotifierSock().notify()
|
||||
|
||||
def get_response(self, requested_value, callback, # timeout=30,
|
||||
request_args=(), request_kwargs=None,
|
||||
callback_args=(), callback_kwargs=None, ):
|
||||
if request_kwargs is None:
|
||||
request_kwargs = {}
|
||||
if callback_kwargs is None:
|
||||
callback_kwargs = {}
|
||||
|
||||
request_id = str(random.randint(0, 9999)).zfill(4) # maybe hash
|
||||
with self._request_lock:
|
||||
self._request_queue[request_id] = PendingRequest(
|
||||
requested_value=requested_value,
|
||||
value=None,
|
||||
# expires_on=Server.time_now()+timeout, #TODO
|
||||
callback=callback,
|
||||
callback_args=callback_args,
|
||||
callback_kwargs=callback_kwargs,
|
||||
request_args=request_args,
|
||||
request_kwargs=request_kwargs,
|
||||
resend=True,
|
||||
)
|
||||
self._send(MessageManager.create_request(requested_value, request_id, request_args, request_kwargs))
|
||||
|
||||
def get_file(self, client_filepath, filepath=None, callback=None,
|
||||
callback_args=(), callback_kwargs=None, ):
|
||||
if callback_kwargs is None:
|
||||
callback_kwargs = {}
|
||||
|
||||
if filepath is None:
|
||||
filepath = os.path.split(client_filepath)[1]
|
||||
|
||||
request_kwargs = {"filepath": client_filepath}
|
||||
callback_kwargs.update({"filepath": filepath})
|
||||
|
||||
self.get_response("filetransfer", callback, request_kwargs=request_kwargs,
|
||||
callback_args=callback_args, callback_kwargs=callback_kwargs)
|
||||
|
||||
def _resend_requests(self):
|
||||
with self._request_lock:
|
||||
for request_id, request in self._request_queue.items(): # TODO filter
|
||||
if request.resend:
|
||||
self._send(MessageManager.create_request(
|
||||
request.requested_value, request_id, request.request_kwargs.update(resend=request.resend))
|
||||
)
|
||||
request.resend = False
|
||||
|
||||
def send_message(self, action, args=(), kwargs=None):
|
||||
self._send(MessageManager.create_action_message(action, args, kwargs))
|
||||
|
||||
def _send_response(self, requested_value, request_id, value, filetransfer=False):
|
||||
self._send(MessageManager.create_response(requested_value, request_id, value, filetransfer))
|
||||
|
||||
def send_file(self, filepath, dest_filepath): # clever_restart=False
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
data = f.read()
|
||||
except OSError as error:
|
||||
logger.warning("File can not be opened due error: ".format(error))
|
||||
else:
|
||||
logger.info("Sending file {} to {} (as: {})".format(filepath, self.addr, dest_filepath))
|
||||
self._send(MessageManager.create_message(data, "binary", "message",
|
||||
additional_headers={"action": "filetransfer", "filepath": dest_filepath}))
|
||||
|
||||
|
||||
class NotifierSock(Singleton):
|
||||
def __init__(self):
|
||||
self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self._server_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
self._sending_sock = socket.socket()
|
||||
self._send_lock = threading.Lock()
|
||||
|
||||
self._receiving_sock = None
|
||||
|
||||
def init(self, selector, port=26000):
|
||||
port += random.randint(0, 100) # local testing fix
|
||||
|
||||
self._server_socket.bind(('', port))
|
||||
self._server_socket.listen(1)
|
||||
self._sending_sock.connect(('127.0.0.1', port))
|
||||
self._receiving_sock, _ = self._server_socket.accept()
|
||||
logger.info("Notify socket: connected")
|
||||
|
||||
selector.register(self._receiving_sock, selectors.EVENT_READ, data=self)
|
||||
logger.info("Notify socket: selector registered")
|
||||
|
||||
def get_sock(self):
|
||||
return self._receiving_sock
|
||||
|
||||
def notify(self):
|
||||
with self._send_lock:
|
||||
if self._receiving_sock is None:
|
||||
return
|
||||
self._sending_sock.sendall(bytes(1))
|
||||
logger.debug("Notify socket: notified")
|
||||
|
||||
def process_events(self, mask):
|
||||
if mask & selectors.EVENT_READ and self._receiving_sock is not None:
|
||||
try:
|
||||
self._receiving_sock.recv(1024)
|
||||
logger.debug("Notify socket: received")
|
||||
except io.BlockingIOError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self._server_socket.close()
|
||||
self._sending_sock.close()
|
||||
self._receiving_sock.close()
|
||||
except (OSError, KeyError) as error:
|
||||
logger.error("Error during unregistring notifier socket: {}".format(error))
|
||||
Reference in New Issue
Block a user