#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# mesycontrol - Remote control for mesytec devices.
# Copyright (C) 2015-2016 mesytec GmbH & Co. KG <info@mesytec.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
__author__ = 'Florian Lüke'
__email__ = 'f.lueke@mesytec.com'
from qt import QtCore
from qt import pyqtSignal
import errno
from future import Future
from future import progress_forwarder
from tcp_client import MCTCPClient
import proto
import server_process
import util
[docs]class IsConnecting(Exception):
pass
[docs]class IsConnected(Exception):
pass
[docs]class AbstractConnection(QtCore.QObject):
connected = pyqtSignal() #: connected and ready to send requests
disconnected = pyqtSignal() #: disconnected; not ready to handle requests
connecting = pyqtSignal(object) #: Establishing the connection. The argument is a
#: Future instance which fullfills once the connection
#: is established or an error occurs.
connection_error = pyqtSignal(object) #: error object
request_queued = pyqtSignal(object, object) #: request, Future
request_sent = pyqtSignal(object, object) #: request, Future
message_received = pyqtSignal(object) #: Message
response_received = pyqtSignal(object, object, object) #: request, response, Future
notification_received = pyqtSignal(object) #: Message
error_message_received = pyqtSignal(object) #: Message
queue_empty = pyqtSignal()
queue_size_changed = pyqtSignal(int)
def __init__(self, parent=None):
super(AbstractConnection, self).__init__(parent)
[docs] def connect(self):
raise NotImplementedError()
[docs] def disconnect(self):
raise NotImplementedError()
[docs] def is_connected(self):
raise NotImplementedError()
[docs] def is_connecting(self):
raise NotImplementedError()
[docs] def is_disconnected(self):
return not self.is_connected() and not self.is_connecting()
[docs] def queue_request(self, request):
raise NotImplementedError()
[docs] def get_queue_size(self):
raise NotImplementedError()
[docs] def get_url(self):
raise NotImplementedError()
url = property(lambda self: self.get_url())
[docs]class MRCConnection(AbstractConnection):
def __init__(self, host, port, parent=None):
super(MRCConnection, self).__init__(parent)
self.log = util.make_logging_source_adapter(__name__, self)
self.host = host
self.port = port
self.client = MCTCPClient()
self.client.disconnected.connect(self._on_client_disconnected)
self.client.socket_error.connect(self._on_client_socket_error)
self.client.request_queued.connect(self.request_queued)
self.client.request_sent.connect(self.request_sent)
self.client.message_received.connect(self.message_received)
self.client.response_received.connect(self.response_received)
self.client.notification_received.connect(self.notification_received)
self.client.notification_received.connect(self._on_client_notification_received)
self.client.error_received.connect(self.error_message_received)
self.client.queue_empty.connect(self.queue_empty)
self.client.queue_size_changed.connect(self.queue_size_changed)
self._is_connecting = False
self._is_connected = False
[docs] def connect(self):
self.log.debug("connect() is_connecting=%s, is_connected=%s",
self.is_connecting(), self.is_connected())
if self.is_connecting():
raise IsConnecting()
if self.is_connected():
raise IsConnected()
self._is_connecting = True
ret = self._connecting_future = Future()
def on_client_connect_done(f):
#self.log.debug("connect: on_client_connect_done: ret.done()=%s", ret.done())
if self._connecting_future is not None and f.exception() is not None:
self._connecting_future.set_exception(f.exception())
#else:
# ret.set_progress_text("Connected to %s:%d" % (self.host, self.port))
self.client.connect(self.host, self.port).add_done_callback(on_client_connect_done)
# FIXME: emitting this causes the gui to see it twice
#self.log.debug("connect: emitting connecting")
#self.connecting.emit(ret)
return ret
def _on_client_notification_received(self, msg):
if self.is_connecting() and msg.type == proto.Message.NOTIFY_MRC_STATUS:
if msg.mrc_status.code == proto.MRCStatus.RUNNING:
self._is_connecting = False
self._is_connected = True
self._connecting_future.set_result(True)
self._connecting_future = None
self.connected.emit()
self.log.debug("%s: connected & running", self.url)
else:
self._connecting_future.set_progress_text("MRC status: %s%s%s" % (
proto.MRCStatus.StatusCode.Name(msg.mrc_status.code),
" - " if len(msg.mrc_status.info) else str(),
msg.mrc_status.info
))
def _on_client_disconnected(self):
self.log.debug("_on_client_disconnected: connecting_future=%s", self._connecting_future)
self._is_connected = False
self._is_connecting = False
if self._connecting_future is not None:
self._connecting_future.set_exception(RuntimeError("Socket disconnected"))
self._connecting_future = None
self.disconnected.emit()
def _on_client_socket_error(self, error):
self.log.debug("_on_client_socket_error: error=%s, connecting_future=%s", error, self._connecting_future)
self._is_connected = False
self._is_connecting = False
if self._connecting_future is not None:
self._connecting_future.set_exception(error)
self._connecting_future = None
self.connection_error.emit(error)
[docs] def disconnect(self):
self.log.debug("disconnect")
return self.client.disconnect()
[docs] def is_connected(self):
return self._is_connected
[docs] def is_connecting(self):
return self._is_connecting
[docs] def queue_request(self, request):
return self.client.queue_request(request)
[docs] def get_queue_size(self):
return self.client.get_queue_size()
[docs] def get_url(self):
return util.build_connection_url(mc_host=self.host, mc_port=self.port)
[docs]class LocalMRCConnection(AbstractConnection):
connect_delay_ms = 1000 #: delay between server startup and connection attempt
def __init__(self, server_options=dict(), parent=None):
super(LocalMRCConnection, self).__init__(parent)
self.log = util.make_logging_source_adapter(__name__, self)
self.server = server_process.pool.create_process(server_options)
self.connection = MRCConnection(self.server.listen_address, self.server.listen_port)
self.connection.connected.connect(self.connected)
self.connection.disconnected.connect(self.disconnected)
self.connection.connection_error.connect(self.connection_error)
self.connection.request_queued.connect(self.request_queued)
self.connection.request_sent.connect(self.request_sent)
self.connection.message_received.connect(self.message_received)
self.connection.notification_received.connect(self.notification_received)
self.connection.notification_received.connect(self._on_connection_notification_received)
self.connection.error_message_received.connect(self.error_message_received)
self.connection.queue_empty.connect(self.queue_empty)
self.connection.queue_size_changed.connect(self.queue_size_changed)
self._is_connecting = False
self._is_connected = False
[docs] def connect(self):
# Start server, wait for connect_delay_ms, connect to server, done
self._connecting_future = ret = Future()
def on_connection_connected(f):
try:
self._is_connecting = False
ret.set_result(f.result())
self._is_connected = True
self._connecting_future = None
self.log.debug("Connected to %s", self.url)
except Exception as e:
self.log.error("connect result: %s, f=%s, ret=%s", e, f, ret)
ret.set_exception(e)
def on_connect_timer_expired():
self.connection.host = self.server.listen_address
self.connection.port = self.server.listen_port
f = self.connection.connect().add_done_callback(on_connection_connected)
progress_forwarder(f, ret)
def on_server_started(f):
if f.exception() is None:
self._connect_timer = QtCore.QTimer()
self._connect_timer.setSingleShot(True)
self._connect_timer.setInterval(LocalMRCConnection.connect_delay_ms)
self._connect_timer.timeout.connect(on_connect_timer_expired)
self._connect_timer.start()
else:
self._is_connecting = False
ret.set_exception(f.exception())
self.connection_error.emit(f.exception())
self._is_connected = False
self._is_connecting = True
f = self.server.start().add_done_callback(on_server_started)
progress_forwarder(f, ret)
return ret
[docs] def disconnect(self):
self.log.debug("disconnect")
ret = Future()
def on_server_stopped(f):
self.log.debug("disconnect: on_server_stopped")
try:
ret.set_result(f.result())
except Exception as e:
ret.set_exception(e)
def on_connection_disconnected(f):
self.log.debug("disconnect: on_connection_disconnected")
self._is_connected = False
self._is_connecting = False
if self.server.is_running():
self.server.stop().add_done_callback(on_server_stopped)
else:
ret.set_result(True)
self.connection.disconnect().add_done_callback(on_connection_disconnected)
return ret
[docs] def is_connected(self):
return self._is_connected
[docs] def is_connecting(self):
return self._is_connecting
[docs] def queue_request(self, request):
return self.connection.queue_request(request)
[docs] def get_queue_size(self):
return self.connection.get_queue_size()
[docs] def get_url(self):
d = dict(serial_port=self.server.serial_port, baud_rate=self.server.baud_rate,
host=self.server.tcp_host, port=self.server.tcp_port)
return util.build_connection_url(**d)
def _on_connection_notification_received(self, msg):
if (self.is_connecting()
and msg.type == proto.Message.NOTIFY_MRC_STATUS
and msg.mrc_status.reason == errno.EACCES
and self.server.serial_port is not None):
try:
import os, pwd, grp
ser = self.server.serial_port
fs = os.stat(ser)
o = pwd.getpwuid(fs.st_uid)[0]
g = grp.getgrgid(fs.st_gid)[0]
p = oct(fs.st_mode & 0777)
txt = "No write permission on %s (owner=%s,group=%s,perms=%s)!" % (
ser, o, g, p)
self._connecting_future.set_progress_text(txt)
return
except ImportError:
self.log.exception()
[docs]def factory(**kwargs):
"""Connection factory.
Supported keyword arguments in order of priority:
- config:
MRCConnectionConfig instance specifying the details of the connection.
- url:
A string that is passed to util.parse_connection_url(). The resulting
dictionary will then be used to create the connection.
- mc_host, mc_port:
Creates a MRCConnection to the given host and port.
- serial_port, baud_rate:
Creates a LocalMRCConnection using the given serial port and
baud rate.
- host, port:
Creates a LocalMRCConnection connecting to the MRC on the
given host and port.
Additionally 'parent' may specify a parent QObject for the resulting
connection.
"""
config = kwargs.get('config', None)
url = kwargs.get('url', None)
parent = kwargs.get('parent', None)
if config is not None:
ret = None
if config.is_mesycontrol_connection():
ret = MRCConnection(host=config.get_mesycontrol_host(),
port=config.get_mesycontrol_port(), parent=parent)
elif config.is_local_connection():
ret = LocalMRCConnection(server_options=config.get_server_options(),
parent=parent)
else:
raise RuntimeError("Could not create connection from %s" % str(config))
return ret
elif url is not None:
return factory(**util.parse_connection_url(url))
else:
mc_host = kwargs.get('mc_host', None)
mc_port = kwargs.get('mc_port', None)
serial_port = kwargs.get('serial_port', None)
baud_rate = kwargs.get('baud_rate', None)
tcp_host = kwargs.get('host', None)
tcp_port = kwargs.get('port', None)
if None not in (mc_host, mc_port):
return MRCConnection(host=mc_host, port=mc_port, parent=parent)
elif None not in (serial_port, baud_rate):
return LocalMRCConnection(
server_options={
'serial_port': serial_port,
'baud_rate': baud_rate},
parent=parent)
elif None not in (tcp_host, tcp_port):
return LocalMRCConnection(
server_options={
'tcp_host': tcp_host,
'tcp_port': tcp_port},
parent=parent)
else:
raise RuntimeError("Could not create connection from given arguments")