improve Py2/3 compat

Change-Id: I1dfdcb88a492401851d5157c734cd708496bf004
Reviewed-on: https://forge.frm2.tum.de/review/17734
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
This commit is contained in:
Enrico Faulhaber
2018-04-16 14:08:12 +02:00
parent 0d25dc35e0
commit 3b802e67c8
39 changed files with 917 additions and 1618 deletions

View File

@ -36,24 +36,28 @@ Interface to the modules:
- remove_module(modulename_or_obj): removes the module (during shutdown)
"""
from __future__ import print_function
import time
from time import time as currenttime
import threading
from secop.protocol.messages import Value, CommandReply, HelpMessage, \
DeactivateReply, IdentifyReply, DescribeReply, HeartbeatReply, \
ActivateReply, WriteReply
from secop.protocol.messages import Message, EVENTREPLY, IDENTREQUEST
from secop.protocol.errors import SECOPError, NoSuchModuleError, \
NoSuchCommandError, NoSuchParamError, BadValueError, ReadonlyError
from secop.lib.parsing import format_time
from secop.lib import formatExtendedStack, formatException
try:
unicode('a')
except NameError:
# no unicode on py3
unicode = str # pylint: disable=redefined-builtin
class Dispatcher(object):
def __init__(self, logger, options):
# to avoid errors, we want to eat all options here
self.equipment_id = options['equipment_id']
self.equipment_id = options[u'equipment_id']
self.nodeopts = {}
for k in list(options):
self.nodeopts[k] = options.pop(k)
@ -71,80 +75,43 @@ class Dispatcher(object):
self._subscriptions = {}
self._lock = threading.RLock()
def handle_request(self, conn, msg):
"""handles incoming request
will call 'queue.request(data)' on conn to send reply before returning
"""
self.log.debug('Dispatcher: handling msg: %r' % msg)
# play thread safe !
with self._lock:
reply = None
# generate reply (coded and framed)
msgname = msg.__class__.__name__
if msgname.endswith('Request'):
msgname = msgname[:-len('Request')]
if msgname.endswith('Message'):
msgname = msgname[:-len('Message')]
self.log.debug('Looking for handle_%s' % msgname)
handler = getattr(self, 'handle_%s' % msgname, None)
if handler:
try:
reply = handler(conn, msg)
except SECOPError as err:
self.log.exception(err)
reply = msg.get_error(
errorclass=err.__class__.__name__,
errorinfo=[repr(err), str(msg)])
except (ValueError, TypeError) as err:
self.log.exception(err)
reply = msg.get_error(
errorclass='BadValue',
errorinfo=[repr(err), str(msg)])
except Exception as err:
self.log.exception(err)
reply = msg.get_error(
errorclass='InternalError', errorinfo=[
formatException(), str(msg), formatExtendedStack()])
else:
self.log.debug('Can not handle msg %r' % msg)
reply = self.unhandled(conn, msg)
if reply:
conn.queue_reply(reply)
def broadcast_event(self, msg, reallyall=False):
"""broadcasts a msg to all active connections"""
"""broadcasts a msg to all active connections
used from the dispatcher"""
if reallyall:
listeners = self._connections
else:
if getattr(msg, 'command', None) is None:
eventname = '%s:%s' % (msg.module, msg.parameter
if msg.parameter else 'value')
if getattr(msg, u'command', None) is None:
eventname = u'%s:%s' % (msg.module, msg.parameter
if msg.parameter else u'value')
else:
eventname = '%s:%s()' % (msg.module, msg.command)
listeners = self._subscriptions.get(eventname, [])
listeners += list(self._active_connections)
eventname = u'%s:%s()' % (msg.module, msg.command)
listeners = self._subscriptions.get(eventname, set()).copy()
listeners.update(self._subscriptions.get(msg.module, set()))
listeners.update(self._active_connections)
for conn in listeners:
conn.queue_async_reply(msg)
def announce_update(self, moduleobj, pname, pobj):
"""called by modules param setters to notify subscribers of new values
"""
msg = Value(
moduleobj.name,
parameter=pname,
value=pobj.export_value(),
t=pobj.timestamp)
msg = Message(EVENTREPLY, module=moduleobj.name, parameter=pname)
msg.set_result(pobj.export_value(), dict(t=pobj.timestamp))
self.broadcast_event(msg)
def subscribe(self, conn, modulename, pname='value'):
eventname = '%s:%s' % (modulename, pname)
def subscribe(self, conn, modulename, pname=u'value'):
eventname = modulename
if pname:
eventname = u'%s:%s' % (modulename, pname)
self._subscriptions.setdefault(eventname, set()).add(conn)
def unsubscribe(self, conn, modulename, pname='value'):
eventname = '%s:%s' % (modulename, pname)
def unsubscribe(self, conn, modulename, pname=u'value'):
eventname = modulename
if pname:
eventname = u'%s:%s' % (modulename, pname)
if eventname in self._subscriptions:
self._subscriptions.remove(conn)
self._subscriptions.setdefault(eventname, set()).discard(conn)
def add_connection(self, conn):
"""registers new connection"""
@ -154,17 +121,11 @@ class Dispatcher(object):
"""removes now longer functional connection"""
if conn in self._connections:
self._connections.remove(conn)
for _evt, conns in self._subscriptions.items():
for _evt, conns in list(self._subscriptions.items()):
conns.discard(conn)
def activate_connection(self, conn):
self._active_connections.add(conn)
def deactivate_connection(self, conn):
self._active_connections.discard(conn)
def register_module(self, moduleobj, modulename, export=True):
self.log.debug('registering module %r as %s (export=%r)' %
self.log.debug(u'registering module %r as %s (export=%r)' %
(moduleobj, modulename, export))
self._modules[modulename] = moduleobj
if export:
@ -173,9 +134,9 @@ class Dispatcher(object):
def get_module(self, modulename):
if modulename in self._modules:
return self._modules[modulename]
elif modulename in self._modules.values():
elif modulename in list(self._modules.values()):
return modulename
raise NoSuchModuleError(module=str(modulename))
raise NoSuchModuleError(module=unicode(modulename))
def remove_module(self, modulename_or_obj):
moduleobj = self.get_module(modulename_or_obj) or modulename_or_obj
@ -190,75 +151,55 @@ class Dispatcher(object):
return self._export[:]
def list_module_params(self, modulename, only_static=False):
self.log.debug('list_module_params(%r)' % modulename)
self.log.debug(u'list_module_params(%r)' % modulename)
if modulename in self._export:
# omit export=False params!
res = {}
for paramname, param in self.get_module(modulename).parameters.items():
for paramname, param in list(self.get_module(modulename).parameters.items()):
if param.export:
res[paramname] = param.as_dict(only_static)
self.log.debug('list params for module %s -> %r' %
self.log.debug(u'list params for module %s -> %r' %
(modulename, res))
return res
self.log.debug('-> module is not to be exported!')
self.log.debug(u'-> module is not to be exported!')
return {}
def list_module_cmds(self, modulename):
self.log.debug('list_module_cmds(%r)' % modulename)
self.log.debug(u'list_module_cmds(%r)' % modulename)
if modulename in self._export:
# omit export=False params!
res = {}
for cmdname, cmdobj in self.get_module(modulename).commands.items():
for cmdname, cmdobj in list(self.get_module(modulename).commands.items()):
res[cmdname] = cmdobj.as_dict()
self.log.debug('list cmds for module %s -> %r' % (modulename, res))
self.log.debug(u'list cmds for module %s -> %r' % (modulename, res))
return res
self.log.debug('-> module is not to be exported!')
self.log.debug(u'-> module is not to be exported!')
return {}
def get_descriptive_data(self):
"""returns a python object which upon serialisation results in the descriptive data"""
# XXX: be lazy and cache this?
# format: {[{[{[, specific entries first
result = {'modules': []}
result = {u'modules': []}
for modulename in self._export:
module = self.get_module(modulename)
# some of these need rework !
mod_desc = {'parameters': [], 'commands': []}
for pname, param in self.list_module_params(
modulename, only_static=True).items():
mod_desc['parameters'].extend([pname, param])
for cname, cmd in self.list_module_cmds(modulename).items():
mod_desc['commands'].extend([cname, cmd])
for propname, prop in module.properties.items():
mod_desc = {u'parameters': [], u'commands': []}
for pname, param in list(self.list_module_params(
modulename, only_static=True).items()):
mod_desc[u'parameters'].extend([pname, param])
for cname, cmd in list(self.list_module_cmds(modulename).items()):
mod_desc[u'commands'].extend([cname, cmd])
for propname, prop in list(module.properties.items()):
mod_desc[propname] = prop
result['modules'].extend([modulename, mod_desc])
result['equipment_id'] = self.equipment_id
result['firmware'] = 'The SECoP playground'
result['version'] = "2017.07"
result[u'modules'].extend([modulename, mod_desc])
result[u'equipment_id'] = self.equipment_id
result[u'firmware'] = u'The SECoP playground'
result[u'version'] = u'2017.07'
result.update(self.nodeopts)
# XXX: what else?
return result
def get_descriptive_data_old(self):
# XXX: be lazy and cache this?
result = {'modules': {}}
for modulename in self._export:
module = self.get_module(modulename)
# some of these need rework !
dd = {
'parameters': self.list_module_params(
modulename,
only_static=True),
'commands': self.list_module_cmds(modulename),
'properties': module.properties,
}
result['modules'][modulename] = dd
result['equipment_id'] = self.equipment_id
result['firmware'] = 'The SECoP playground'
result['version'] = "2017.01"
# XXX: what else?
return result
def _execute_command(self, modulename, command, arguments=None):
if arguments is None:
arguments = []
@ -274,19 +215,13 @@ class Dispatcher(object):
raise BadValueError(
module=modulename,
command=command,
reason='Wrong number of arguments!')
reason=u'Wrong number of arguments!')
# now call func and wrap result as value
# note: exceptions are handled in handle_request, not here!
func = getattr(moduleobj, 'do_' + command)
func = getattr(moduleobj, u'do_' + command)
res = func(*arguments)
res = CommandReply(
module=modulename,
command=command,
result=res,
qualifiers=dict(t=time.time()))
# res = Value(modulename, command=command, value=func(*arguments), t=time.time())
return res
return res, dict(t=currenttime())
def _setParamValue(self, modulename, pname, value):
moduleobj = self.get_module(modulename)
@ -299,19 +234,15 @@ class Dispatcher(object):
if pobj.readonly:
raise ReadonlyError(module=modulename, parameter=pname)
writefunc = getattr(moduleobj, 'write_%s' % pname, None)
writefunc = getattr(moduleobj, u'write_%s' % pname, None)
# note: exceptions are handled in handle_request, not here!
if writefunc:
value = writefunc(value)
else:
setattr(moduleobj, pname, value)
if pobj.timestamp:
return WriteReply(
module=modulename,
parameter=pname,
value=[pobj.value, dict(t=format_time(pobj.timestamp))])
return WriteReply(
module=modulename, parameter=pname, value=[pobj.value, {}])
return pobj.export_value(), dict(t=pobj.timestamp)
return pobj.export_value(), {}
def _getParamValue(self, modulename, pname):
moduleobj = self.get_module(modulename)
@ -322,121 +253,181 @@ class Dispatcher(object):
if pobj is None:
raise NoSuchParamError(module=modulename, parameter=pname)
readfunc = getattr(moduleobj, 'read_%s' % pname, None)
readfunc = getattr(moduleobj, u'read_%s' % pname, None)
if readfunc:
# should also update the pobj (via the setter from the metaclass)
# note: exceptions are handled in handle_request, not here!
readfunc()
if pobj.timestamp:
res = Value(
modulename,
parameter=pname,
value=pobj.export_value(),
t=pobj.timestamp)
else:
res = Value(modulename, parameter=pname, value=pobj.export_value())
return res
return pobj.export_value(), dict(t=pobj.timestamp)
return pobj.export_value(), {}
#
# api to be called from the 'interface'
# any method above has no idea about 'messages', this is handled here
#
def handle_request(self, conn, msg):
"""handles incoming request
will call 'queue.request(data)' on conn to send reply before returning
"""
self.log.debug(u'Dispatcher: handling msg: %r' % msg)
# if there was an error in the frontend, bounce the resulting
# error msgObj directly back to the client
if msg.errorclass:
return msg
# play thread safe !
with self._lock:
if msg.action == IDENTREQUEST:
self.log.debug(u'Looking for handle_ident')
handler = self.handle_ident
else:
self.log.debug(u'Looking for handle_%s' % msg.action)
handler = getattr(self, u'handle_%s' % msg.action, None)
if handler:
try:
reply = handler(conn, msg)
if reply:
conn.queue_reply(reply)
return None
except SECOPError as err:
self.log.exception(err)
msg.set_error(err.name, unicode(err), {})#u'traceback': formatException(),
#u'extended_stack':formatExtendedStack()})
return msg
except (ValueError, TypeError) as err:
self.log.exception(err)
msg.set_error(u'BadValue', unicode(err), {u'traceback': formatException()})
print(u'--------------------')
print(formatExtendedStack())
print(u'====================')
return msg
except Exception as err:
self.log.exception(err)
msg.set_error(u'InternalError', unicode(err), {u'traceback': formatException()})
print(u'--------------------')
print(formatExtendedStack())
print(u'====================')
return msg
else:
self.log.error(u'Can not handle msg %r' % msg)
msg.set_error(u'Protocol', u'unhandled msg', {})
return msg
# now the (defined) handlers for the different requests
def handle_Help(self, conn, msg):
return HelpMessage()
def handle_help(self, conn, msg):
msg.mkreply()
return msg
def handle_Identify(self, conn, msg):
return IdentifyReply(version_string='currently,is,ignored,here')
def handle_ident(self, conn, msg):
msg.mkreply()
return msg
def handle_Describe(self, conn, msg):
def handle_describe(self, conn, msg):
# XXX:collect descriptive data
return DescribeReply(
equipment_id=self.equipment_id,
description=self.get_descriptive_data())
msg.setvalue(u'specifier', u'.')
msg.setvalue(u'data', self.get_descriptive_data())
msg.mkreply()
return msg
def handle_Poll(self, conn, msg):
def handle_read(self, conn, msg):
# XXX: trigger polling and force sending event
res = self._getParamValue(msg.module, msg.parameter or 'value')
# self.broadcast_event(res)
if conn in self._active_connections:
return None # already send to myself
return res # send reply to inactive conns
if not msg.parameter:
msg.parameter = u'value'
msg.set_result(*self._getParamValue(msg.module, msg.parameter))
def handle_Write(self, conn, msg):
# notify all by sending WriteReply
#msg1 = WriteReply(**msg.as_dict())
# self.broadcast_event(msg1)
#if conn in self._active_connections:
# return None # already send to myself
#if conn in self._subscriptions.get(msg.module, set()):
# return None # already send to myself
msg.mkreply()
return msg # send reply to inactive conns
def handle_change(self, conn, msg):
# try to actually write XXX: should this be done asyncron? we could
# just return the reply in that case
if msg.parameter:
res = self._setParamValue(msg.module, msg.parameter, msg.value)
else:
# first check if module has a target
if 'target' not in self.get_module(msg.module).parameters:
raise ReadonlyError(module=msg.module, parameter=None)
res = self._setParamValue(msg.module, 'target', msg.value)
res.parameter = 'target'
# self.broadcast_event(res)
# if conn in self._active_connections:
# return None # already send to myself
return res
if not msg.parameter:
msg.parameter = u'target'
msg.set_result(*self._setParamValue(msg.module, msg.parameter, msg.data))
def handle_Command(self, conn, msg):
# notify all by sending CommandReply
#msg1 = CommandReply(**msg.as_dict())
# self.broadcast_event(msg1)
#if conn in self._active_connections:
# return None # already send to myself
#if conn in self._subscriptions.get(msg.module, set()):
# return None # already send to myself
msg.mkreply()
return msg # send reply to inactive conns
def handle_do(self, conn, msg):
# XXX: should this be done asyncron? we could just return the reply in
# that case
if not msg.args:
msg.args = []
# try to actually execute command
res = self._execute_command(msg.module, msg.command, msg.arguments)
# self.broadcast_event(res)
# if conn in self._active_connections:
msg.set_result(*self._execute_command(msg.module, msg.command, msg.args))
#if conn in self._active_connections:
# return None # already send to myself
return res # send reply to inactive conns
#if conn in self._subscriptions.get(msg.module, set()):
# return None # already send to myself
msg.mkreply()
return msg # send reply to inactive conns
def handle_Heartbeat(self, conn, msg):
return HeartbeatReply(**msg.as_dict())
def handle_ping(self, conn, msg):
msg.setvalue(u'data', {u't':currenttime()})
msg.mkreply()
return msg
def handle_Activate(self, conn, msg):
self.activate_connection(conn)
# easy approach: poll all values...
for modulename, moduleobj in self._modules.items():
def handle_activate(self, conn, msg):
if msg.module:
if msg.module not in self._modules:
raise NoSuchModuleError()
# activate only ONE module
self.subscribe(conn, msg.specifier, u'')
modules = [msg.specifier]
else:
# activate all modules
self._active_connections.add(conn)
modules = self._modules
# for initial update poll all values...
for modulename in modules:
moduleobj = self._modules.get(modulename, None)
if moduleobj is None:
self.log.error(u'activate: can not lookup module %r, skipping it' % modulename)
continue
for pname, pobj in moduleobj.parameters.items():
if not pobj.export:
if not pobj.export: # XXX: handle export_as cases!
continue
# WARNING: THIS READS ALL parameters FROM HW!
# XXX: should we send the cached values instead? (pbj.value)
# also: ignore errors here.
try:
res = self._getParamValue(modulename, pname)
if res[0] == Ellipsis: # means we do not have a value at all so skip this
self.log.error(
u'activate: got no value for %s:%s!' %
(modulename, pname))
#else:
#rm = Message(EVENTREPLY, u'%s:%s' % (modulename, pname))
#rm.set_result(*res)
#self.broadcast_event(rm)
except SECOPError as e:
self.log.error('decide what to do here!')
self.log.error(u'decide what to do here! (ignore error and skip update)')
self.log.exception(e)
res = Value(
module=modulename,
parameter=pname,
value=pobj.export_value(),
t=pobj.timestamp,
unit=pobj.unit)
if res.value != Ellipsis: # means we do not have a value at all so skip this
self.broadcast_event(res)
else:
self.log.error(
'activate: got no value for %s:%s!' %
modulename, pname)
conn.queue_async_reply(ActivateReply(**msg.as_dict()))
msg.mkreply()
conn.queue_async_reply(msg) # should be sent AFTER all the ^^initial updates
return None
def handle_Deactivate(self, conn, msg):
self.deactivate_connection(conn)
conn.queue_async_reply(DeactivateReply(**msg.as_dict()))
return None
def handle_Error(self, conn, msg):
def handle_deactivate(self, conn, msg):
if msg.specifier:
self.unsubscribe(conn, msg.specifier, u'')
else:
self._active_connections.discard(conn)
# XXX: also check all entries in self._subscriptions?
msg.mkreply()
return msg
def unhandled(self, conn, msg):
"""handler for unhandled Messages
(no handle_<messagename> method was defined)
"""
self.log.error('IGN: got unhandled request %s' % msg)
return msg.get_error(
errorclass="InternalError", errorinfo="Unhandled Request")
def handle_error(self, conn, msg):
# is already an error-reply (came from interface frontend) -> just send it back
return msg

View File

@ -1,332 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# 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 2 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, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Encoding/decoding Messages"""
# implement as class as they may need some internal 'state' later on
# (think compressors)
from __future__ import print_function
# Base class
class MessageEncoder(object):
"""en/decode a single Messageobject"""
def encode(self, msg):
"""encodes the given message object into a frame"""
raise NotImplementedError
def decode(self, encoded):
"""decodes the given frame to a message object"""
raise NotImplementedError
import re
import json
#from secop.lib.parsing import format_time
from secop.protocol.messages import Value, IdentifyRequest, IdentifyReply, \
DescribeRequest, DescribeReply, ActivateRequest, ActivateReply, \
DeactivateRequest, DeactivateReply, CommandRequest, CommandReply, \
WriteRequest, WriteReply, PollRequest, HeartbeatRequest, HeartbeatReply, \
ErrorMessage, HelpMessage
#from secop.protocol.errors import ProtocolError
# each message is like <messagetype> [ \space <messageargs> [ \space
# <json> ]] \lf
# note: the regex allow <> for spec for testing only!
SECOP_RE = re.compile(
r"""^(?P<msgtype>[\*\?\w]+)(?:\s(?P<spec>[\w:<>]+)(?:\s(?P<json>.*))?)?$""",
re.X)
#"""
# messagetypes:
IDENTREQUEST = '*IDN?' # literal
# literal! first part is fixed!
#IDENTREPLY = 'SECoP, SECoPTCP, V2016-11-30, rc1'
#IDENTREPLY = 'SINE2020&ISSE,SECoP,V2016-11-30,rc1'
IDENTREPLY = 'SINE2020&ISSE,SECoP,V2017-01-25,rc1'
DESCRIPTIONSREQUEST = 'describe' # literal
DESCRIPTIONREPLY = 'describing' # +<id> +json
ENABLEEVENTSREQUEST = 'activate' # literal
ENABLEEVENTSREPLY = 'active' # literal, is end-of-initial-data-transfer
DISABLEEVENTSREQUEST = 'deactivate' # literal
DISABLEEVENTSREPLY = 'inactive' # literal
COMMANDREQUEST = 'do' # +module:command +json args (if needed)
# +module:command +json args (if needed) # send after the command finished !
COMMANDREPLY = 'done'
# +module[:parameter] +json_value -> NO direct reply, calls TRIGGER internally!
WRITEREQUEST = 'change'
# +module[:parameter] +json_value # send with the read back value
WRITEREPLY = 'changed'
# +module[:parameter] -> NO direct reply, calls TRIGGER internally!
TRIGGERREQUEST = 'read'
EVENT = 'update' # +module[:parameter] +json_value (value, qualifiers_as_dict)
HEARTBEATREQUEST = 'ping' # +nonce_without_space
HEARTBEATREPLY = 'pong' # +nonce_without_space
ERRORREPLY = 'error' # +errorclass +json_extended_info
HELPREQUEST = 'help' # literal
HELPREPLY = 'helping' # +line number +json_text
ERRORCLASSES = [
'NoSuchModule',
'NoSuchParameter',
'NoSuchCommand',
'CommandFailed',
'ReadOnly',
'BadValue',
'CommunicationFailed',
'IsBusy',
'IsError',
'ProtocolError',
'InternalError',
'CommandRunning',
'Disabled',
]
# note: above strings need to be unique in the sense, that none is/or
# starts with another
def encode_cmd_result(msgobj):
q = msgobj.qualifiers.copy()
if 't' in q:
q['t'] = str(q['t'])
return msgobj.result, q
def encode_value_data(vobj):
q = vobj.qualifiers.copy()
if 't' in q:
q['t'] = str(q['t'])
return vobj.value, q
def encode_error_msg(emsg):
# note: result is JSON-ified....
return [
emsg.origin, dict((k, getattr(emsg, k)) for k in emsg.ARGS
if k != 'origin')
]
class SECoPEncoder(MessageEncoder):
# map of msg to msgtype string as defined above.
ENCODEMAP = {
IdentifyRequest: (IDENTREQUEST, ),
IdentifyReply: (IDENTREPLY, ),
DescribeRequest: (DESCRIPTIONSREQUEST, ),
DescribeReply: (
DESCRIPTIONREPLY,
'equipment_id',
'description', ),
ActivateRequest: (ENABLEEVENTSREQUEST, ),
ActivateReply: (ENABLEEVENTSREPLY, ),
DeactivateRequest: (DISABLEEVENTSREQUEST, ),
DeactivateReply: (DISABLEEVENTSREPLY, ),
CommandRequest: (
COMMANDREQUEST,
lambda msg: "%s:%s" % (msg.module, msg.command),
'arguments', ),
CommandReply: (
COMMANDREPLY,
lambda msg: "%s:%s" % (msg.module, msg.command),
encode_cmd_result, ),
WriteRequest: (
WRITEREQUEST,
lambda msg: "%s:%s" % (
msg.module, msg.parameter) if msg.parameter else msg.module,
'value', ),
WriteReply: (
WRITEREPLY,
lambda msg: "%s:%s" % (
msg.module, msg.parameter) if msg.parameter else msg.module,
'value', ),
PollRequest: (
TRIGGERREQUEST,
lambda msg: "%s:%s" % (
msg.module, msg.parameter) if msg.parameter else msg.module,
),
HeartbeatRequest: (
HEARTBEATREQUEST,
'nonce', ),
HeartbeatReply: (
HEARTBEATREPLY,
'nonce', ),
HelpMessage: (HELPREQUEST, ),
ErrorMessage: (
ERRORREPLY,
"errorclass",
encode_error_msg, ),
Value: (
EVENT,
lambda msg: "%s:%s" % (msg.module, msg.parameter or (
msg.command + '()')) if msg.parameter or msg.command else msg.module,
encode_value_data, ),
}
DECODEMAP = {
IDENTREQUEST: lambda spec, data: IdentifyRequest(),
# handled specially, listed here for completeness
# IDENTREPLY: lambda spec, data: IdentifyReply(encoded),
DESCRIPTIONSREQUEST: lambda spec, data: DescribeRequest(),
DESCRIPTIONREPLY: lambda spec, data: DescribeReply(equipment_id=spec[0], description=data),
ENABLEEVENTSREQUEST: lambda spec, data: ActivateRequest(),
ENABLEEVENTSREPLY: lambda spec, data: ActivateReply(),
DISABLEEVENTSREQUEST: lambda spec, data: DeactivateRequest(),
DISABLEEVENTSREPLY: lambda spec, data: DeactivateReply(),
COMMANDREQUEST: lambda spec, data: CommandRequest(module=spec[0], command=spec[1], arguments=data),
COMMANDREPLY: lambda spec, data: CommandReply(module=spec[0], command=spec[1], result=data),
WRITEREQUEST: lambda spec, data: WriteRequest(module=spec[0], parameter=spec[1], value=data),
WRITEREPLY: lambda spec, data: WriteReply(module=spec[0], parameter=spec[1], value=data),
TRIGGERREQUEST: lambda spec, data: PollRequest(module=spec[0], parameter=spec[1]),
HEARTBEATREQUEST: lambda spec, data: HeartbeatRequest(nonce=spec[0]),
HEARTBEATREPLY: lambda spec, data: HeartbeatReply(nonce=spec[0]),
HELPREQUEST: lambda spec, data: HelpMessage(),
# HELPREPLY: lambda spec, data:None, # ignore this
ERRORREPLY: lambda spec, data: ErrorMessage(errorclass=spec[0], errorinfo=data),
EVENT: lambda spec, data: Value(module=spec[0], parameter=spec[1], value=data[0],
qualifiers=data[1] if len(data) > 1 else {}),
}
def __init__(self, *args, **kwds):
MessageEncoder.__init__(self, *args, **kwds)
# self.tests()
def encode(self, msg):
"""msg object -> transport layer message"""
# fun for Humans
if isinstance(msg, HelpMessage):
text = """Try one of the following:
'%s' to query protocol version
'%s' to read the description
'%s <module>[:<parameter>]' to request reading a value
'%s <module>[:<parameter>] value' to request changing a value
'%s <module>[:<command>()]' to execute a command
'%s <nonce>' to request a heartbeat response
'%s' to activate async updates
'%s' to deactivate updates
""" % (IDENTREQUEST, DESCRIPTIONSREQUEST, TRIGGERREQUEST,
WRITEREQUEST, COMMANDREQUEST, HEARTBEATREQUEST,
ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST)
return '\n'.join('%s %d %s' % (HELPREPLY, i + 1, l.strip())
for i, l in enumerate(text.split('\n')[:-1]))
if isinstance(msg, HeartbeatRequest):
if msg.nonce:
return 'ping %s' % msg.nonce
return 'ping'
if isinstance(msg, HeartbeatReply):
if msg.nonce:
return 'pong %s' % msg.nonce
return 'pong'
for msgcls, parts in self.ENCODEMAP.items():
if isinstance(msg, msgcls):
# resolve lambdas
parts = [parts[0]] + [
p(msg) if callable(p) else getattr(msg, p)
for p in parts[1:]
]
if len(parts) > 1:
parts[1] = str(parts[1])
if len(parts) == 3:
parts[2] = json.dumps(parts[2])
return ' '.join(parts)
def decode(self, encoded):
# first check beginning
match = SECOP_RE.match(encoded)
if not match:
print(repr(encoded), repr(IDENTREPLY))
if encoded == IDENTREPLY: # XXX:better just check the first 2 parts...
return IdentifyReply(version_string=encoded)
return HelpMessage()
# return ErrorMessage(errorclass='Protocol',
# errorinfo='Regex did not match!',
# is_request=True)
msgtype, msgspec, data = match.groups()
if msgspec is None and data:
return ErrorMessage(
errorclass='Internal',
errorinfo='Regex matched json, but not spec!',
is_request=True,
origin=encoded)
if msgtype in self.DECODEMAP:
if msgspec and ':' in msgspec:
msgspec = msgspec.split(':', 1)
else:
msgspec = (msgspec, None)
if data:
try:
data = json.loads(data)
except ValueError as err:
return ErrorMessage(
errorclass='BadValue',
errorinfo=[repr(err), str(encoded)],
origin=encoded)
msg = self.DECODEMAP[msgtype](msgspec, data)
msg.setvalue("origin", encoded)
return msg
return ErrorMessage(
errorclass='Protocol',
errorinfo='%r: No Such Messagetype defined!' % encoded,
is_request=True,
origin=encoded)
def tests(self):
print("---- Testing encoding -----")
for msgclass in sorted(self.ENCODEMAP):
print(msgclass)
e = self.encode(
msgclass(
module='<module>',
parameter='<paramname>',
value=2.718,
equipment_id='<id>',
description='descriptive data',
command='<cmd>',
arguments='<arguments>',
nonce='<nonce>',
errorclass='InternalError',
errorinfo='nix'))
print(e)
print(self.decode(e))
print()
print("---- Testing decoding -----")
for msgtype, _ in sorted(self.DECODEMAP.items()):
msg = '%s a:b 3' % msgtype
if msgtype == EVENT:
msg = '%s a:b [3,{"t":193868}]' % msgtype
print(msg)
d = self.decode(msg)
print(d)
print(self.encode(d))
print()
print("---- Testing done -----")
ENCODERS = {
'secop': SECoPEncoder,
}
__ALL__ = ['ENCODERS']

View File

@ -25,13 +25,14 @@
class SECOPError(RuntimeError):
def __init__(self, *args, **kwds):
RuntimeError.__init__(self)
self.args = args
for k, v in kwds.items():
for k, v in list(kwds.items()):
setattr(self, k, v)
def __repr__(self):
args = ', '.join(map(repr, self.args))
kwds = ', '.join(['%s=%r' % i for i in self.__dict__.items()])
kwds = ', '.join(['%s=%r' % i for i in list(self.__dict__.items())])
res = []
if args:
res.append(args)
@ -45,14 +46,13 @@ class SECOPError(RuntimeError):
class InternalError(SECOPError):
pass
name = 'InternalError'
class ProtocolError(SECOPError):
pass
name = 'SyntaxError'
# XXX: unifiy NoSuch...Error ?
class NoSuchModuleError(SECOPError):
pass
@ -77,17 +77,42 @@ class CommandFailedError(SECOPError):
pass
class InvalidParamValueError(SECOPError):
class CommandRunningError(SECOPError):
pass
class CommunicationFailedError(SECOPError):
pass
class IsBusyError(SECOPError):
pass
class IsErrorError(SECOPError):
pass
class DisabledError(SECOPError):
pass
EXCEPTIONS = dict(
Internal=InternalError,
Protocol=ProtocolError,
NoSuchModule=NoSuchModuleError,
NoSuchParam=NoSuchParamError,
NoSuchCommand=NoSuchCommandError,
BadValue=BadValueError,
Readonly=ReadonlyError,
CommandFailed=CommandFailedError,
InvalidParam=InvalidParamValueError, )
CommandRunning=CommandRunningError,
Readonly=ReadonlyError,
BadValue=BadValueError,
CommunicationFailed=CommunicationFailedError,
IsBusy=IsBusyError,
IsError=IsErrorError,
Disabled=DisabledError,
SyntaxError=ProtocolError,
InternalError=InternalError,
# internal short versions (candidates for spec)
Protocol=ProtocolError,
Internal=InternalError,
)

View File

@ -1,60 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# 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 2 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, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Encoding/decoding Frames"""
# Base class
class Framer(object):
"""Frames and unframes an encoded message
also transforms the encoded message to the 'wire-format' (and vise-versa)
note: not all MessageEncoders can use all Framers,
but the intention is to have this for as many as possible.
"""
def encode(self, *frames):
"""return the wire-data for the given messageframes"""
raise NotImplemented
def decode(self, data):
"""return a list of messageframes found in data"""
raise NotImplemented
def reset(self):
"""resets the de/encoding stage (clears internal information)"""
raise NotImplemented
# now some Implementations
from .null import NullFramer
from .eol import EOLFramer
from .rle import RLEFramer
from .demo import DemoFramer
FRAMERS = {
'null': NullFramer,
'eol': EOLFramer,
'rle': RLEFramer,
'demo': DemoFramer,
}
__ALL__ = ['FRAMERS']

View File

@ -1,81 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# 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 2 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, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Encoding/decoding Frames"""
from secop.protocol.framing import Framer
class DemoFramer(Framer):
"""Text based message framer
frmes are delimited by '\n'
messages are delimited by '\n\n'
'\r' is ignored
"""
def __init__(self):
self.data = b''
self.decoded = []
def encode(self, frames):
"""add transport layer encapsulation/framing of messages"""
if isinstance(frames, (tuple, list)):
return b'\n'.join(frames) + b'\n\n'
return b'%s\n\n' % frames
def decode(self, data):
"""remove transport layer encapsulation/framing of messages
returns a list of messageframes which got decoded from data!
"""
self.data += data
res = []
while b'\n' in self.data:
frame, self.data = self.data.split(b'\n', 1)
if frame.endswith('\r'):
frame = frame[:-1]
if self.data.startswith('\r'):
self.data = self.data[1:]
res.append(frame)
return res
def decode2(self, data):
"""remove transport layer encapsulation/framing of messages
returns a _list_ of messageframes which got decoded from data!
"""
self.data += data.replace(b'\r', '')
while b'\n' in self.data:
frame, self.data = self.data.split(b'\n', 1)
if frame:
# not an empty line -> belongs to this set of messages
self.decoded.append(frame)
else:
# empty line -> our set of messages is finished decoding
res = self.decoded
self.decoded = []
return res
return None
def reset(self):
self.data = b''
self.decoded = []

View File

@ -1,56 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# 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 2 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, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Encoding/decoding Frames"""
from secop.protocol.framing import Framer
class EOLFramer(Framer):
"""Text based message framer
messages are delimited by '\r\n'
upon reception the end of a message is detected by '\r\n','\n' or '\n\r'
"""
data = b''
def encode(self, *frames):
"""add transport layer encapsulation/framing of messages"""
return b'%s\r\n' % b'\r\n'.join(frames)
def decode(self, data):
"""remove transport layer encapsulation/framing of messages
returns a list of messageframes which got decoded from data!
"""
self.data += data
res = []
while b'\n' in self.data:
frame, self.data = self.data.split(b'\n', 1)
if frame.endswith('\r'):
frame = frame[:-1]
if self.data.startswith('\r'):
self.data = self.data[1:]
res.append(frame)
return res
def reset(self):
self.data = b''

View File

@ -1,39 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# 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 2 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, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Encoding/decoding Frames"""
from secop.protocol.framing import Framer
class NullFramer(Framer):
"""do-nothing-framer
assumes messages are framed by themselfs or the interface does it already.
"""
def encode(self, *frames):
"""add transport layer encapsulation/framing of messages"""
return ''.join(frames)
def decode(self, data):
"""remove transport layer encapsulation/framing of messages"""
return [data]

View File

@ -1,71 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# 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 2 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, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Encoding/decoding Frames"""
from secop.protocol.framing import Framer
class RLEFramer(Framer):
data = b''
frames_to_go = 0
def encode(self, *frames):
"""add transport layer encapsulation/framing of messages"""
# format is 'number of frames:[framelengt:frme]*N'
frdata = ['%d:%s' % (len(frame), frame) for frame in frames]
return b'%d:' + b''.join(frdata)
def decode(self, data):
"""remove transport layer encapsulation/framing of messages
returns a list of messageframes which got decoded from data!
"""
self.data += data
res = []
while self.data:
if frames_to_go == 0:
if ':' in self.data:
# scan for and decode 'number of frames'
frnum, self.data = self.data.split(':', 1)
try:
self.frames_to_go = int(frnum)
except ValueError:
# can not recover, complain!
raise FramingError('invalid start of message found!')
else:
# not enough data to decode number of frames,
# return what we have
return res
while self.frames_to_go:
# there are still some (partial) frames stuck inside self.data
frlen, self.data = self.data.split(':', 1)
if len(self.data) >= frlen:
res.append(self.data[:frlen])
self.data = self.data[frlen:]
self.frames_to_go -= 1
else:
# not enough data for this frame, return what we have
return res
def reset(self):
self.data = b''
self.frames_to_go = 0

View File

@ -20,8 +20,9 @@
#
# *****************************************************************************
"""provide server interfaces to be used by clients"""
from __future__ import absolute_import
from tcp import TCPServer
from .tcp import TCPServer
INTERFACES = {'tcp': TCPServer, }

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under
@ -20,27 +19,81 @@
#
# *****************************************************************************
"""provides tcp interface to the SECoP Server"""
from __future__ import print_function
import os
import socket
import collections
import SocketServer
try:
import socketserver # py3
except ImportError:
import SocketServer as socketserver # py2
from secop.lib import formatExtendedStack, formatException
from secop.protocol.messages import HELPREPLY, Message, HelpMessage
DEF_PORT = 10767
MAX_MESSAGE_SIZE = 1024
from secop.protocol.encoding import ENCODERS
from secop.protocol.framing import FRAMERS
from secop.protocol.messages import HelpMessage
EOL = b'\n'
CR = b'\r'
SPACE = b' '
class TCPRequestHandler(SocketServer.BaseRequestHandler):
def encode_msg_frame(action, specifier=None, data=None):
""" encode a msg_tripel into an msg_frame, ready to be sent
action (and optional specifier) are unicode strings,
data may be an json-yfied python object"""
action = action.encode('utf-8')
if specifier is None:
# implicit: data is None
return b''.join((action, EOL))
specifier = specifier.encode('utf-8')
if data:
data = data.encode('utf-8')
return b''.join((action, SPACE, specifier, SPACE, data, EOL))
return b''.join((action, SPACE, specifier, EOL))
def get_msg(_bytes):
"""try to deframe the next msg in (binary) input
always return a tupel (msg, remaining_input)
msg may also be None
"""
if EOL not in _bytes:
return None, _bytes
return _bytes.split(EOL, 1)
def decode_msg(msg):
"""decode the (binary) msg into a (unicode) msg_tripel"""
# check for leading/trailing CR and remove it
if msg and msg[0] == CR:
msg = msg[1:]
if msg and msg[-1] == CR:
msg = msg[:-1]
res = msg.split(b' ', 2)
action = res[0].decode('utf-8')
if len(res) == 1:
return action, None, None
specifier = res[1].decode('utf-8')
if len(res) == 2:
return action, specifier, None
data = res[2].decode('utf-8')
return action, specifier, data
class TCPRequestHandler(socketserver.BaseRequestHandler):
def setup(self):
self.log = self.server.log
# Queue of msgObjects to send
self._queue = collections.deque(maxlen=100)
self.framing = self.server.framingCLS()
self.encoding = self.server.encodingCLS()
# self.framing = self.server.framingCLS()
# self.encoding = self.server.encodingCLS()
def handle(self):
"""handle a new tcp-connection"""
@ -49,6 +102,7 @@ class TCPRequestHandler(SocketServer.BaseRequestHandler):
clientaddr = self.client_address
serverobj = self.server
self.log.info("handling new connection from %s:%d" % clientaddr)
data = b''
# notify dispatcher of us
serverobj.dispatcher.add_connection(self)
@ -57,14 +111,16 @@ class TCPRequestHandler(SocketServer.BaseRequestHandler):
# mysocket.setblocking(False)
# start serving
while True:
# send replys fist, then listen for requests, timing out after 0.1s
# send replys first, then listen for requests, timing out after 0.1s
while self._queue:
# put message into encoder to get frame(s)
# put frame(s) into framer to get bytestring
# send bytestring
outmsg = self._queue.popleft()
outframes = self.encoding.encode(outmsg)
outdata = self.framing.encode(outframes)
#outmsg.mkreply()
outdata = encode_msg_frame(*outmsg.serialize())
# outframes = self.encoding.encode(outmsg)
# outdata = self.framing.encode(outframes)
try:
mysocket.sendall(outdata)
except Exception:
@ -72,9 +128,12 @@ class TCPRequestHandler(SocketServer.BaseRequestHandler):
# XXX: improve: use polling/select here?
try:
data = mysocket.recv(MAX_MESSAGE_SIZE)
except (socket.timeout, socket.error) as e:
data = data + mysocket.recv(MAX_MESSAGE_SIZE)
except socket.timeout as e:
continue
except socket.error as e:
self.log.exception(e)
return
# XXX: should use select instead of busy polling
if not data:
continue
@ -82,24 +141,49 @@ class TCPRequestHandler(SocketServer.BaseRequestHandler):
# put frames into (de-) coder and if a message appear,
# call dispatcher.handle_request(self, message)
# dispatcher will queue the reply before returning
frames = self.framing.decode(data)
if frames is not None:
if not frames: # empty list
self.queue_reply(HelpMessage(MSGTYPE=reply))
for frame in frames:
reply = None
msg = self.encoding.decode(frame)
if msg:
serverobj.dispatcher.handle_request(self, msg)
while True:
origin, data = get_msg(data)
if origin is None:
break # no more messages to process
if not origin: # empty string -> send help message
for idx, line in enumerate(HelpMessage.splitlines()):
msg = Message(HELPREPLY, specifier='%d' % idx)
msg.data = line
self.queue_async_reply(msg)
continue
msg = decode_msg(origin)
# construct msgObj from msg
try:
msgObj = Message(*msg)
msgObj.origin = origin.decode('latin-1')
msgObj = serverobj.dispatcher.handle_request(self, msgObj)
except Exception as err:
# create Error Obj instead
msgObj.set_error(u'Internal', str(err), {'exception': formatException(),
'traceback':formatExtendedStack()})
print('--------------------')
print(formatException())
print('--------------------')
print(formatExtendedStack())
print('====================')
if msgObj:
self.queue_reply(msgObj)
def queue_async_reply(self, data):
"""called by dispatcher for async data units"""
self._queue.append(data)
if data:
self._queue.append(data)
else:
self.log.error('should asynq_queue %s' % data)
def queue_reply(self, data):
"""called by dispatcher to queue (sync) replies"""
# sync replies go first!
self._queue.appendleft(data)
if data:
self._queue.appendleft(data)
else:
self.log.error('should queue %s' % data)
def finish(self):
"""called when handle() terminates, i.e. the socket closed"""
@ -115,7 +199,7 @@ class TCPRequestHandler(SocketServer.BaseRequestHandler):
self.request.close()
class TCPServer(SocketServer.ThreadingTCPServer):
class TCPServer(socketserver.ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
@ -129,12 +213,14 @@ class TCPServer(SocketServer.ThreadingTCPServer):
portnum = int(_port)
# tcp is a byte stream, so we need Framers (to get frames)
# and encoders (to en/decode messages from frames)
self.framingCLS = FRAMERS[interfaceopts.pop('framing', 'none')]
self.encodingCLS = ENCODERS[interfaceopts.pop('encoding', 'pickle')]
interfaceopts.pop('framing') # HACK
interfaceopts.pop('encoding') # HACK
# self.framingCLS = FRAMERS[interfaceopts.pop('framing', 'none')]
# self.encodingCLS = ENCODERS[interfaceopts.pop('encoding', 'pickle')]
self.log.info("TCPServer binding to %s:%d" % (bindto, portnum))
self.log.debug("TCPServer using framing=%s" % self.framingCLS.__name__)
self.log.debug("TCPServer using encoding=%s" %
self.encodingCLS.__name__)
SocketServer.ThreadingTCPServer.__init__(
# self.log.debug("TCPServer using framing=%s" % self.framingCLS.__name__)
# self.log.debug("TCPServer using encoding=%s" % self.encodingCLS.__name__)
socketserver.ThreadingTCPServer.__init__(
self, (bindto, portnum), TCPRequestHandler, bind_and_activate=True)
self.log.info("TCPServer initiated")

View File

@ -20,178 +20,185 @@
#
# *****************************************************************************
"""Define SECoP Messages"""
from __future__ import print_function
import json
from secop.protocol.errors import EXCEPTIONS
# allowed actions:
IDENTREQUEST = u'*IDN?' # literal
# literal! first part is fixed!
IDENTREPLY = u'SINE2020&ISSE,SECoP,V2018-02-13,rc2'
DESCRIPTIONREQUEST = u'describe' # literal
DESCRIPTIONREPLY = u'describing' # +<id> +json
ENABLEEVENTSREQUEST = u'activate' # literal + optional spec
ENABLEEVENTSREPLY = u'active' # literal + optional spec, is end-of-initial-data-transfer
DISABLEEVENTSREQUEST = u'deactivate' # literal + optional spec
DISABLEEVENTSREPLY = u'inactive' # literal + optional spec
COMMANDREQUEST = u'do' # +module:command +json args (if needed)
# +module:command +json args (if needed) # send after the command finished !
COMMANDREPLY = u'done'
# +module[:parameter] +json_value -> NO direct reply, calls POLL internally
WRITEREQUEST = u'change'
# +module[:parameter] +json_value # send with the read back value
WRITEREPLY = u'changed'
# +module[:parameter] -> NO direct reply, calls POLL internally!
POLLREQUEST = u'read'
EVENTREPLY = u'update' # +module[:parameter] +json_value (value, qualifiers_as_dict)
HEARTBEATREQUEST = u'ping' # +nonce_without_space
HEARTBEATREPLY = u'pong' # +nonce_without_space
ERRORREPLY = u'error' # +errorclass +json_extended_info
HELPREQUEST = u'help' # literal
HELPREPLY = u'helping' # +line number +json_text
# helper mapping to find the REPLY for a REQUEST
REQUEST2REPLY = {
IDENTREQUEST: IDENTREPLY,
DESCRIPTIONREQUEST: DESCRIPTIONREPLY,
ENABLEEVENTSREQUEST: ENABLEEVENTSREPLY,
DISABLEEVENTSREQUEST: DISABLEEVENTSREPLY,
COMMANDREQUEST: COMMANDREPLY,
WRITEREQUEST: WRITEREPLY,
POLLREQUEST: EVENTREPLY,
HEARTBEATREQUEST: HEARTBEATREPLY,
HELPREQUEST: HELPREPLY,
}
class Message(object):
"""base class for messages"""
is_request = False
is_reply = False
is_error = False
qualifiers = {}
origin = "<unknown source>"
origin = u'<unknown source>'
action = u'<unknown message type>'
specifier = None
data = None
def __init__(self, **kwds):
self.ARGS = set()
# cooked versions
module = None
parameter = None
command = None
args = None
# if set, these are used for generating the reply
qualifiers = None # will be rectified to dict() in __init__
value = None # also the result of a command
# if set, these are used for generating the error msg
errorclass = '' # -> specifier
errordescription = '' # -> data[1] (data[0] is origin)
errorinfo = {} # -> data[2]
def __init__(self, action, specifier=None, data=None, **kwds):
self.qualifiers = {}
self.action = action
if data:
data = json.loads(data)
if specifier:
self.module = specifier
self.specifier = specifier
if ':' in specifier:
self.module, p = specifier.split(':',1)
if action in (COMMANDREQUEST, COMMANDREPLY):
self.command = p
# XXX: extract args?
self.args = data
else:
self.parameter = p
if data is not None:
self.data = data
elif data is not None:
self.data = data
# record extra values
self.__arguments = set()
for k, v in kwds.items():
self.setvalue(k, v)
def setvalue(self, key, value):
setattr(self, key, value)
self.ARGS.add(key)
self.__arguments.add(key)
def setqualifier(self, key, value):
self.qualifiers[key] = value
def __repr__(self):
return self.__class__.__name__ + '(' + \
', '.join('%s=%s' % (k, repr(getattr(self, k)))
for k in sorted(self.ARGS)) + ')'
return u'Message(%r' % self.action + \
u', '.join('%s=%s' % (k, repr(getattr(self, k)))
for k in sorted(self.__arguments)) + u')'
def as_dict(self):
"""returns set parameters as dict"""
return dict(map(lambda k: (k, getattr(self, k)), self.ARGS))
def serialize(self):
"""return <action>,<specifier>,<jsonyfied_data> triple"""
if self.errorclass:
for k in self.__arguments:
if k in (u'origin', u'errorclass', u'errorinfo', u'errordescription'):
if k in self.errorinfo:
del self.errorinfo[k]
continue
self.errorinfo[k] = getattr(self, k)
data = [self.origin, self.errordescription, self.errorinfo]
print(repr(data))
return ERRORREPLY, self.errorclass, json.dumps(data)
elif self.value or self.qualifiers:
data = [self.value, self.qualifiers]
else:
data = self.data
try:
data = json.dumps(data) if data else u''
except TypeError:
print('Can not serialze: %s' % repr(data))
data = u'none'
if self.specifier:
specifier = self.specifier
else:
specifier = self.module
if self.parameter:
specifier = u'%s:%s' %(self.module, self.parameter)
if self.command:
specifier = u'%s:%s' %(self.module, self.command)
return self.action, specifier, data
def mkreply(self):
self.action = REQUEST2REPLY.get(self.action, self.action)
def set_error(self, errorclass, errordescription, errorinfo):
if errorclass not in EXCEPTIONS:
errordescription = '%s is not an official errorclass!\n%s' % (errorclass, errordescription)
errorclass = u'Internal'
# used to mark thes as an error message
# XXX: check errorclass for allowed values !
self.setvalue(u'errorclass', errorclass) # a str
self.setvalue(u'errordescription', errordescription) # a str
self.setvalue(u'errorinfo', errorinfo) # a dict
self.action = ERRORREPLY
def set_result(self, value, qualifiers):
# used to mark thes as an result reply message
self.setvalue(u'value', value)
self.qualifiers.update(qualifiers)
self.__arguments.add(u'qualifier')
class Value(object):
def __init__(self,
module,
parameter=None,
command=None,
value=Ellipsis,
**qualifiers):
self.module = module
self.parameter = parameter
self.command = command
self.value = value
self.qualifiers = qualifiers
self.msgtype = 'update' # 'changed' or 'done'
def __repr__(self):
devspec = self.module
if self.parameter:
devspec = '%s:%s' % (devspec, self.parameter)
elif self.command:
devspec = '%s:%s()' % (devspec, self.command)
return '%s:Value(%s)' % (devspec, ', '.join([repr(self.value)] + [
'%s=%s' % (k, format_time(v) if k == "timestamp" else repr(v))
for k, v in self.qualifiers.items()
]))
class Request(Message):
is_request = True
def get_reply(self):
"""returns a Reply object prefilled with the attributes from this request."""
m = Message()
m.is_request = False
m.is_reply = True
m.is_error = False
m.qualifiers = self.qualifiers
m.origin = self.origin
for k in self.ARGS:
m.setvalue(k, self.__dict__[k])
return m
def get_error(self, errorclass, errorinfo):
"""returns a Reply object prefilled with the attributes from this request."""
m = ErrorMessage()
m.qualifiers = self.qualifiers
m.origin = self.origin
for k in self.ARGS:
m.setvalue(k, self.__dict__[k])
m.setvalue("errorclass", errorclass[:-5]
if errorclass.endswith('rror') else errorclass)
m.setvalue("errorinfo", errorinfo)
return m
class IdentifyRequest(Request):
pass
class IdentifyReply(Message):
is_reply = True
version_string = None
class DescribeRequest(Request):
pass
class DescribeReply(Message):
is_reply = True
equipment_id = None
description = None
class ActivateRequest(Request):
pass
class ActivateReply(Message):
is_reply = True
class DeactivateRequest(Request):
pass
class DeactivateReply(Message):
is_reply = True
class CommandRequest(Request):
command = ''
arguments = []
class CommandReply(Message):
is_reply = True
command = ''
result = None
class WriteRequest(Request):
module = None
parameter = None
value = None
class WriteReply(Message):
is_reply = True
module = None
parameter = None
value = None
class PollRequest(Request):
is_request = True
module = None
parameter = None
class HeartbeatRequest(Request):
nonce = 'alive'
class HeartbeatReply(Message):
is_reply = True
nonce = 'undefined'
class EventMessage(Message):
# use Value directly for Replies !
is_reply = True
module = None
parameter = None
command = None
value = None # Value object ! (includes qualifiers!)
class ErrorMessage(Message):
is_error = True
errorclass = 'InternalError'
errorinfo = None
class HelpMessage(Request):
is_reply = True # !sic!
HelpMessage = u"""Try one of the following:
'%s' to query protocol version
'%s' to read the description
'%s <module>[:<parameter>]' to request reading a value
'%s <module>[:<parameter>] value' to request changing a value
'%s <module>[:<command>]' to execute a command
'%s <nonce>' to request a heartbeat response
'%s' to activate async updates
'%s' to deactivate updates
""" % (IDENTREQUEST, DESCRIPTIONREQUEST, POLLREQUEST,
WRITEREQUEST, COMMANDREQUEST, HEARTBEATREQUEST,
ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST)

View File

@ -1,235 +0,0 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# 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 2 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, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Define SECoP Messages"""
# Request Types
REQUEST = 'request'
REPLY = 'reply'
ERROR = 'error'
# Message types ('actions') hint: fetch is list+read
LIST = 'list'
READ = 'read'
WRITE = 'write'
COMMAND = 'command'
POLL = 'poll'
SUBSCRIBE = 'subscribe'
UNSUBSCRIBE = 'unsubscribe'
TRIGGER = 'trigger'
EVENT = 'event'
ERROR = 'error'
HELP = 'help'
# base class for messages
class Message(object):
MSGTYPE = 'Undefined'
devs = None
pars = None
props = None
result = None
error = None
ARGS = None
errortype = None
def __init__(self, **kwds):
self.devs = []
self.pars = []
self.props = []
self.result = []
self.ARGS = set()
for k, v in kwds.items():
self.setvalue(k, v)
def setvalue(self, key, value):
setattr(self, key, value)
self.ARGS.add(key)
@property
def NAME(self):
# generate sensible name
r = 'Message'
if self.props:
r = 'Property' if self.props != ['*'] else 'Properties'
elif self.pars:
r = 'Parameter' if self.pars != ['*'] else 'Parameters'
elif self.devs:
r = 'Module' if self.devs != ['*'] else 'Modules'
t = ''
if self.MSGTYPE in [
LIST, READ, WRITE, COMMAND, POLL, SUBSCRIBE, UNSUBSCRIBE, HELP
]:
t = 'Request' if not self.result else 'Reply'
if self.errortype is None:
return self.MSGTYPE.title() + r + t
else:
return self.errortype + 'Error'
def __repr__(self):
return self.NAME + '(' + \
', '.join('%s=%r' % (k, getattr(self, k))
for k in self.ARGS if getattr(self, k) is not None) + ')'
class Value(object):
def __init__(self, value=Ellipsis, qualifiers=None, **kwds):
self.dev = ''
self.param = ''
self.prop = ''
self.value = value
self.qualifiers = qualifiers or dict()
self.__dict__.update(kwds)
def __repr__(self):
devspec = self.dev
if self.param:
devspec = '%s:%s' % (devspec, self.param)
if self.prop:
devspec = '%s:%s' % (devspec, self.prop)
return '%s:Value(%s)' % (
devspec,
', '.join([repr(self.value)] +
['%s=%r' % (k, v) for k, v in self.qualifiers.items()]))
class ListMessage(Message):
MSGTYPE = LIST
class ReadMessage(Message):
MSGTYPE = READ # return cached value
class WriteMessage(Message):
MSGTYPE = WRITE # write value to some spec
target = None # actually float or string
class CommandMessage(Message):
MSGTYPE = COMMAND
cmd = '' # always string
args = []
result = []
class PollMessage(Message):
MSGTYPE = POLL # read HW and return hw_value
class SubscribeMessage(Message):
MSGTYPE = SUBSCRIBE
class UnsubscribeMessage(Message):
MSGTYPE = UNSUBSCRIBE
class TriggerMessage(Message):
MSGTYPE = TRIGGER
class EventMessage(Message):
MSGTYPE = EVENT
class ErrorMessage(Message):
MSGTYPE = ERROR
errorstring = 'an unhandled error occured'
errortype = 'UnknownError'
class HelpMessage(Message):
MSGTYPE = HELP
class NoSuchModuleError(ErrorMessage):
def __init__(self, *devs):
ErrorMessage.__init__(
self,
devs=devs,
errorstring="Module %r does not exist" % devs[0],
errortype='NoSuchModule')
class NoSuchParamError(ErrorMessage):
def __init__(self, dev, *params):
ErrorMessage.__init__(
self,
devs=(dev, ),
params=params,
errorstring="Module %r has no parameter %r" % (dev, params[0]),
errortype='NoSuchParam')
class ParamReadonlyError(ErrorMessage):
def __init__(self, dev, *params):
ErrorMessage.__init__(
self,
devs=(dev, ),
params=params,
errorstring="Module %r, parameter %r is not writeable!" %
(dev, params[0]),
errortype='ParamReadOnly')
class InvalidParamValueError(ErrorMessage):
def __init__(self, dev, param, value, e):
ErrorMessage.__init__(
self,
devs=(dev, ),
params=params,
values=(value),
errorstring=str(e),
errortype='InvalidParamValueError')
class InternalError(ErrorMessage):
def __init__(self, err, **kwds):
ErrorMessage.__init__(
self, errorstring=str(err), errortype='InternalError', **kwds)
MESSAGE = dict((cls.MSGTYPE, cls)
for cls in [
HelpMessage, ErrorMessage, EventMessage, TriggerMessage,
UnsubscribeMessage, SubscribeMessage, PollMessage,
CommandMessage, WriteMessage, ReadMessage, ListMessage
])
if __name__ == '__main__':
print("Minimal testing of messages....")
m = Message(MSGTYPE='test', a=1, b=2, c='x')
print m
print ReadMessage(devs=['a'], result=[Value(12.3)])
print "OK"
print

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under