plugin pipe/stream, file transfert: refactoring and improvments:
this is a big patch as things had to be changed at the same time. - changed methods using profile argument to use client instead - move SatFile in a new tools.stream module, has it should be part of core, not a plugin - new IStreamProducer interface, to handler starting a pull producer - new FileStreamObject which create a stream producer/consumer from a SatFile - plugin pipe is no more using unix named pipe, as it complicate the thing, special care need to be taken to not block, and it's generally not necessary. Instead a socket is now used, so the plugin has been renomed to jingle stream. - bad connection/error should be better handler in jingle stream plugin, and code should not block anymore - jp pipe commands have been updated accordingly fix bug 237
This commit is contained in:
parent
ece10e8cd2
commit
60b8c8fa73
1 changed files with 870 additions and 670 deletions
281
src/plugins/plugin_exp_jingle_stream.py
Executable file
281
src/plugins/plugin_exp_jingle_stream.py
Executable file
|
|
@ -0,0 +1,281 @@
|
|||
#!/usr/bin/env python2
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# SAT plugin for managing pipes (experimental)
|
||||
# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from sat.core.i18n import _, D_
|
||||
from sat.core.constants import Const as C
|
||||
from sat.core import exceptions
|
||||
from sat.core.log import getLogger
|
||||
log = getLogger(__name__)
|
||||
from sat.tools import xml_tools
|
||||
from sat.tools import stream
|
||||
from twisted.words.xish import domish
|
||||
from twisted.words.protocols.jabber import jid
|
||||
from twisted.internet import defer
|
||||
from twisted.internet import protocol
|
||||
from twisted.internet import endpoints
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet import error
|
||||
from twisted.internet import interfaces
|
||||
from zope import interface
|
||||
import errno
|
||||
|
||||
NS_STREAM = 'http://salut-a-toi.org/protocol/stream'
|
||||
SECURITY_LIMIT=30
|
||||
START_PORT = 8888
|
||||
|
||||
PLUGIN_INFO = {
|
||||
C.PI_NAME: "Jingle Stream Plugin",
|
||||
C.PI_IMPORT_NAME: "STREAM",
|
||||
C.PI_TYPE: "EXP",
|
||||
C.PI_PROTOCOLS: [],
|
||||
C.PI_DEPENDENCIES: ["XEP-0166"],
|
||||
C.PI_MAIN: "JingleStream",
|
||||
C.PI_HANDLER: "no",
|
||||
C.PI_DESCRIPTION: _("""Jingle Stream plugin""")
|
||||
}
|
||||
|
||||
CONFIRM = D_(u"{peer} wants to send you a stream, do you accept ?")
|
||||
CONFIRM_TITLE = D_(u"Stream Request")
|
||||
|
||||
|
||||
class StreamProtocol(protocol.Protocol):
|
||||
|
||||
def __init__(self):
|
||||
self.pause = False
|
||||
|
||||
def setPause(self, paused):
|
||||
# in Python 2.x, Twisted classes are old style
|
||||
# so we can use property and setter
|
||||
if paused:
|
||||
if not self.pause:
|
||||
self.transport.pauseProducing()
|
||||
self.pause = True
|
||||
else:
|
||||
if self.pause:
|
||||
self.transport.resumeProducing()
|
||||
self.pause = False
|
||||
|
||||
def disconnect(self):
|
||||
self.transport.loseConnection()
|
||||
|
||||
def connectionMade(self):
|
||||
if self.factory.client_conn is not None:
|
||||
self.transport.loseConnection()
|
||||
self.factory.setClientConn(self)
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.factory.writeToConsumer(data)
|
||||
|
||||
def sendData(self, data):
|
||||
self.transport.write(data)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
if self.factory.client_conn != self:
|
||||
# only the first connected client_conn is relevant
|
||||
return
|
||||
|
||||
if reason.type == error.ConnectionDone:
|
||||
self.factory.streamFinished()
|
||||
else:
|
||||
self.factory.streamFailed(reason)
|
||||
|
||||
|
||||
@interface.implementer(stream.IStreamProducer)
|
||||
@interface.implementer(interfaces.IPushProducer)
|
||||
@interface.implementer(interfaces.IConsumer)
|
||||
class StreamFactory(protocol.Factory):
|
||||
protocol = StreamProtocol
|
||||
consumer = None
|
||||
producer = None
|
||||
deferred = None
|
||||
|
||||
def __init__(self):
|
||||
self.client_conn = None
|
||||
|
||||
def setClientConn(self, stream_protocol):
|
||||
# in Python 2.x, Twisted classes are old style
|
||||
# so we can use property and setter
|
||||
assert self.client_conn is None
|
||||
self.client_conn = stream_protocol
|
||||
if self.consumer is None:
|
||||
self.client_conn.setPause(True)
|
||||
|
||||
def startStream(self, consumer):
|
||||
if self.consumer is not None:
|
||||
raise exceptions.InternalError(_(u"stream can't be used with multiple consumers"))
|
||||
assert self.deferred is None
|
||||
self.consumer = consumer
|
||||
consumer.registerProducer(self, True)
|
||||
self.deferred = defer.Deferred()
|
||||
if self.client_conn is not None:
|
||||
self.client_conn.setPause(False)
|
||||
return self.deferred
|
||||
|
||||
def streamFinished(self):
|
||||
self.client_conn = None
|
||||
if self.consumer:
|
||||
self.consumer.unregisterProducer()
|
||||
self.port_listening.stopListening()
|
||||
self.deferred.callback(None)
|
||||
|
||||
def streamFailed(self, failure_):
|
||||
self.client_conn = None
|
||||
if self.consumer:
|
||||
self.consumer.unregisterProducer()
|
||||
self.port_listening.stopListening()
|
||||
self.deferred.errback(failure_)
|
||||
elif self.producer:
|
||||
self.producer.stopProducing()
|
||||
|
||||
def stopStream(self):
|
||||
if self.client_conn is not None:
|
||||
self.client_conn.disconnect()
|
||||
|
||||
def registerProducer(self, producer, streaming):
|
||||
self.producer = producer
|
||||
|
||||
def pauseProducing(self):
|
||||
self.client_conn.setPause(True)
|
||||
|
||||
def resumeProducing(self):
|
||||
self.client_conn.setPause(False)
|
||||
|
||||
def stopProducing(self):
|
||||
if self.client_conn:
|
||||
self.client_conn.disconnect()
|
||||
|
||||
def write(self, data):
|
||||
try:
|
||||
self.client_conn.sendData(data)
|
||||
except AttributeError:
|
||||
log.warning(_(u"No client connected, can't send data"))
|
||||
|
||||
def writeToConsumer(self, data):
|
||||
self.consumer.write(data)
|
||||
|
||||
|
||||
class JingleStream(object):
|
||||
"""This non standard jingle application send byte stream"""
|
||||
|
||||
def __init__(self, host):
|
||||
log.info(_("Plugin Stream initialization"))
|
||||
self.host = host
|
||||
self._j = host.plugins["XEP-0166"] # shortcut to access jingle
|
||||
self._j.registerApplication(NS_STREAM, self)
|
||||
host.bridge.addMethod("streamOut", ".plugin", in_sign='ss', out_sign='s', method=self._streamOut, async=True)
|
||||
# jingle callbacks
|
||||
|
||||
def _streamOut(self, to_jid_s, profile_key):
|
||||
client = self.host.getClient(profile_key)
|
||||
return self.streamOut(client, jid.JID(to_jid_s))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def streamOut(self, client, to_jid):
|
||||
"""send a stream
|
||||
|
||||
@param peer_jid(jid.JID): recipient
|
||||
@return: an unique id to identify the transfer
|
||||
"""
|
||||
port = START_PORT
|
||||
factory = StreamFactory()
|
||||
while True:
|
||||
endpoint = endpoints.TCP4ServerEndpoint(reactor, port)
|
||||
try:
|
||||
port_listening = yield endpoint.listen(factory)
|
||||
except error.CannotListenError as e:
|
||||
if e.socketError.errno == errno.EADDRINUSE:
|
||||
port += 1
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
factory.port_listening = port_listening
|
||||
break
|
||||
self._j.initiate(client,
|
||||
to_jid,
|
||||
[{'app_ns': NS_STREAM,
|
||||
'senders': self._j.ROLE_INITIATOR,
|
||||
'app_kwargs': {'stream_object': factory},
|
||||
}])
|
||||
defer.returnValue(unicode(port))
|
||||
|
||||
def jingleSessionInit(self, client, session, content_name, stream_object):
|
||||
content_data = session['contents'][content_name]
|
||||
application_data = content_data['application_data']
|
||||
assert 'stream_object' not in application_data
|
||||
application_data['stream_object'] = stream_object
|
||||
desc_elt = domish.Element((NS_STREAM, 'description'))
|
||||
return desc_elt
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def jingleRequestConfirmation(self, client, action, session, content_name, desc_elt):
|
||||
"""This method request confirmation for a jingle session"""
|
||||
content_data = session['contents'][content_name]
|
||||
if content_data['senders'] not in (self._j.ROLE_INITIATOR, self._j.ROLE_RESPONDER):
|
||||
log.warning(u"Bad sender, assuming initiator")
|
||||
content_data['senders'] = self._j.ROLE_INITIATOR
|
||||
|
||||
confirm_data = yield xml_tools.deferDialog(self.host,
|
||||
_(CONFIRM).format(peer=session['peer_jid'].full()),
|
||||
_(CONFIRM_TITLE),
|
||||
type_=C.XMLUI_DIALOG_CONFIRM,
|
||||
action_extra={'meta_from_jid': session['peer_jid'].full(),
|
||||
'meta_type': "STREAM",
|
||||
},
|
||||
security_limit=SECURITY_LIMIT,
|
||||
profile=client.profile)
|
||||
|
||||
if not C.bool(confirm_data['answer']):
|
||||
defer.returnValue(False)
|
||||
try:
|
||||
port = int(confirm_data['port'])
|
||||
except (ValueError, KeyError):
|
||||
raise exceptions.DataError(_(u'given port is invalid'))
|
||||
endpoint = endpoints.TCP4ClientEndpoint(reactor, 'localhost', port)
|
||||
factory = StreamFactory()
|
||||
yield endpoint.connect(factory)
|
||||
content_data['stream_object'] = factory
|
||||
finished_d = content_data['finished_d'] = defer.Deferred()
|
||||
args = [client, session, content_name, content_data]
|
||||
finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args)
|
||||
defer.returnValue(True)
|
||||
|
||||
def jingleHandler(self, client, action, session, content_name, desc_elt):
|
||||
content_data = session['contents'][content_name]
|
||||
application_data = content_data['application_data']
|
||||
if action in (self._j.A_ACCEPTED_ACK, self._j.A_SESSION_INITIATE):
|
||||
pass
|
||||
elif action == self._j.A_SESSION_ACCEPT:
|
||||
assert not 'stream_object' in content_data
|
||||
content_data['stream_object'] = application_data['stream_object']
|
||||
finished_d = content_data['finished_d'] = defer.Deferred()
|
||||
args = [client, session, content_name, content_data]
|
||||
finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args)
|
||||
else:
|
||||
log.warning(u"FIXME: unmanaged action {}".format(action))
|
||||
return desc_elt
|
||||
|
||||
def _finishedCb(self, dummy, client, session, content_name, content_data):
|
||||
log.info(u"Pipe transfer completed")
|
||||
self._j.contentTerminate(client, session, content_name)
|
||||
content_data['stream_object'].stopStream()
|
||||
|
||||
def _finishedEb(self, failure, client, session, content_name, content_data):
|
||||
log.warning(u"Error while streaming pipe: {}".format(failure))
|
||||
self._j.contentTerminate(client, session, content_name, reason=self._j.REASON_FAILED_TRANSPORT)
|
||||
content_data['stream_object'].stopStream()
|
||||
Loading…
Reference in a new issue