pythonpython-3.xwebsockettwistedautobahn

How do I send input from python cmd to autobahn websocket client running in the same interpreter?


I'm trying to take input from an interactive prompt via python's cmd library and pass the input to an autobahn websocket client to send up to a websocket server. The cmd loop and the autobahn websocket client loop are running in the same interpreter. I'm trying to use crochet to make that work. The websocket client connects to the server successfully but when I enter something at the cmd prompt to invoke sendMessage, I get the exception shown at the bottom of this post. Any guidance on where I may have messed up would be much appreciated. If there's a better approach to accomplish what I'm trying to do, I'm all ears.

These are the relevant imports and setup:

from cmd import Cmd
from crochet import setup, run_in_reactor, wait_for, retrieve_result, TimeoutError

# Setup crochet before importing twisted
setup()

from twisted.internet import reactor, ssl
from twisted.python import log
from autobahn.twisted.websocket import WebSocketClientFactory, \
    WebSocketClientProtocol, \
    connectWS

This is the websocket client protocol class:

class MyClientProtocol(WebSocketClientProtocol):

    def __init__(self, *args, **kwargs):
        super(MyClientProtocol, self).__init__(*args, **kwargs)

    def onConnect(self, response):
        print("Connected")

    def onMessage(self, payload, isBinary):
        if not isBinary:
            print('Message received: {}'.format(payload.decode('utf8')))

    def sendTask(self, payload):
        payload = json.dumps(payload, ensure_ascii = False).encode('utf8')
        self.sendMessage(payload)

This is the websocket client factory class:

class MyClientFactory(WebSocketClientFactory):

    def __init__(self, *args, **kwargs):
        super(MyClientFactory, self).__init__(*args, **kwargs)

    def buildFactory(self, uri, headers):
        factory = WebSocketClientFactory(uri, headers=headers)
        factory.protocol = MyClientProtocol
        return factory

This is the cmd class that sends input to the websocket client:

class mycmd(Cmd):
    def do_send(self, inp):
        payload = {'task': inp}
        m = MyClientProtocol()
        reactor.callFromThread(m.sendTask, payload)

This is how I'm calling the websocket client and cmd loop:

if __name__ == '__main__':

    @run_in_reactor
    def start_connectWS():
        headers = {'header1': 'value1'}
        f = MyClientFactory()
        connectStatement = f.buildFactory(uri, headers)
        if connectStatement.isSecure:
            contextFactory = ssl.ClientContextFactory()
        else:
            contextFactory = None
        connectWS(connectStatement, contextFactory)

    start_connectWS()
    mycmd().cmdloop()

This is the exception:

Unhandled Error
Traceback (most recent call last):
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/threading.py", line 865, in run
    self._target(*self._args, **self._kwargs)
  File "/Users/tomd/project/lib/python3.7/site-packages/crochet/_eventloop.py", line 412, in <lambda>
    target=lambda: self._reactor.run(installSignalHandlers=False),
  File "/Users/tomd/project/lib/python3.7/site-packages/twisted/internet/base.py", line 1283, in run
    self.mainLoop()
  File "/Users/tomd/project/lib/python3.7/site-packages/twisted/internet/base.py", line 1292, in mainLoop
    self.runUntilCurrent()
--- <exception caught here> ---
  File "/Users/tomd/project/lib/python3.7/site-packages/twisted/internet/base.py", line 886, in runUntilCurrent
    f(*a, **kw)
  File "./client.py", line 62, in sendTask
    self.sendMessage(payload)
  File "/Users/tomd/project/lib/python3.7/site-packages/autobahn/websocket/protocol.py", line 2215, in sendMessage
    if self.state != WebSocketProtocol.STATE_OPEN:
builtins.AttributeError: 'MyClientProtocol' object has no attribute 'state'

Solution

  • Your copmmand class creates a new, unconnected protocol instance and then tries to use it as if it were connected:

    class mycmd(Cmd):
        def do_send(self, inp):
            payload = {'task': inp}
            m = MyClientProtocol()
            reactor.callFromThread(m.sendTask, payload)
    

    Specifically, this creates a new instance of your protocol class:

            m = MyClientProtocol()
    

    And this tries to use it as if it were connected:

            reactor.callFromThread(m.sendTask, payload)
    

    Later on you have code that actually connects a protocol to something:

            connectWS(connectStatement, contextFactory)
    

    However this code is not connected to your command class in any useful way.

    Instead of creating a new MyClientProtocol instance you need to use the connection that results from calling connectWS.

    There are many ways you could go about this which different trade-offs. One way which happens to be easy to explain is to use mutable state shared between the websocket code and the command interpreter code.

    For example, MyClientProtocol.onConnect can set itself as an attribute on the factory instance and your command line code could accept the factory instance as an argument, then read the connected protocol instance from the attribute.

    class MyClientProtocol(...):
        def onConnect(self, response):
            self.factory.connectedProtocol = self
        ...
    
    class mycmd(Cmd):
        # ... __init__ that accepts factory and sets it on self
    
        def do_send(self, inp):
            payload = {'task': inp}
            m = self.factory.connectedProtocol
            if m is None:
                print("No connection")
            else:
                reactor.callFromThread(m.sendTask, payload)