NixonInnes
NixonInnes

Reputation: 360

Client timeout with asyncio.Protocol

I've been writing a MUD in python, using asyncio.Protocol, however I have a problem with when users close their client (typically a terminal, since you connect via telnet) without disconnecting properly.

The server doesn't recognize the user as disconnected, and they remain in the game.

The problem only occurs when the client is connected remotely (for some reason, maybe someone can explain...) it doesn't happen when connecting from the localhost.

Is there a neat way to check a user is still actually connected (without additional software client-side), or on failing that how do I incorporate a timeout?

My Protocol looks something like this currently:

class User(Protocol):

    def connection_made(self, transport):
        self.transport = transport
        self.addr = transport.get_extra_info('peername')
        self.authd = False
        self.name = None
        self.admin = False
        self.room = None
        self.table = None
        self.db = None

        self.flags = []

        print("Connected: {}".format(self.addr))
        server.connected.append(self)

        actions['help'](self, ['welcome'])
        self.get_prompt()

    def data_received(self, data):
        msg = data.decode().strip()
        args = msg.split()

        if self.authd is False:
            actions['login'](self, args)
            return

        if msg:
            if args[0] in self.db.aliases:
                args[0] = str(self.db.aliases[args[0]])
                msg = ' '.join(args)
                args = msg.split()

            if msg[0] in server.channels:
                ch = db.session.query(db.models.Channel).get(msg[0])
                if msg[1] =='@':
                    channels.send_to_channel(self, ch, msg[2:], do_emote=True)
                else:
                    channels.send_to_channel(self, ch, msg[1:])
                self.get_prompt()
                return

            if args[0] in actions:
                if self.is_frozen():
                    self.send_to_self("You're frozen solid!")
                else:
                    actions[args[0]](self, args[1:] if len(args) > 1 else None)
                self.get_prompt()
                return
            self.send_to_self("Huh?")
        else:
            if self.table is not None:
                actions['table'](self, None)
            elif self.room is not None:
                actions['look'](self, None)

    def send_to_self(self, msg):
        msg = "\r\n" + msg
        msg = colourify(msg)
        self.transport.write(msg.encode())

    @staticmethod
    def send_to_user(user, msg):
        msg = "\r\n"+msg
        msg = colourify(msg)
        user.transport.write(msg.encode())

    @staticmethod
    def send_to_users(users, msg):
        msg = "\r\n"+msg
        msg = colourify(msg)
        for user in users:
            user.transport.write(msg.encode())

    def connection_lost(self, ex):
        print("Disconnected: {}".format(self.addr))
        server.connected.remove(self)

        if self.authd:
            self.save()
            server.users.remove(self)
            self.room.occupants.remove(self)

Note: I've chopped a lot of superfluous stuff out. If you want the full code, it's here.

Upvotes: 2

Views: 1245

Answers (1)

Andrew Svetlov
Andrew Svetlov

Reputation: 17356

You may schedule a new timeout handler on every data_received() call (with cancelling previous timeout handler, sure). I found the approach too cumbersome.

Or, as an option, switch to asyncio streams -- you may use asyncio.wait_for or brand new not released yet asyncio.timeout.

Upvotes: 1

Related Questions