Mic92 / python-mpd2

Python library which provides a client interface for the Music Player Daemon.
GNU Lesser General Public License v3.0
352 stars 119 forks source link

asyncio races on responses #195

Closed eprst closed 1 year ago

eprst commented 2 years ago

It is possible to construct a rapid sequence of MPD commands that will confuse asyncio client with response order. Attached example most often makes it parse status response while expecting an idle response, which leads to a crash. Sometimes I saw the reverse, when idle output comes when a client expects status output, but it is pretty rare.

version: python-mpd2 3.0.3

Here's sample output of the script below, with some extra debugging added to the library:

pi@pi3:~/pimpd/pimpd $ /usr/bin/python3 -X dev Test2.py
Connected!
cmd:  status
cmd:  setvol
cmd:  status
cmd:  status
cmd:  setvol
cmd:  status
cmd:  idle
_parse_list:  ['volume: 16', 'repeat: 1', 'random: 0', 'single: 0', 'consume: 0', 'partition: default', 'playlist: 2', 'playlistlength: 1', 'mixrampdb: 0.000000', 'state:
play', 'song: 0', 'songid: 1', 'time: 6575:36000', 'elapsed: 6575.234', 'bitrate: 192', 'duration: 36000.104', 'audio: 44100:24:2', 'nextsong: 0', 'nextsongid: 1']
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/mpd/asyncio.py", line 279, in __run
    await result._feed_from(self)
  File "/usr/lib/python3/dist-packages/mpd/asyncio.py", line 45, in _feed_from
    self._feed_line(line)
  File "/usr/lib/python3/dist-packages/mpd/asyncio.py", line 64, in _feed_line
    self.set_result(self._callback(self.__spooled_lines))
  File "/usr/lib/python3/dist-packages/mpd/asyncio.py", line 273, in <lambda>
    result = CommandResult("idle", subsystems, lambda result: self.__distribute_idle_result(self._parse_list(result)))
  File "/usr/lib/python3/dist-packages/mpd/asyncio.py", line 313, in __distribute_idle_result
    idle_changes = list(result)
  File "/usr/lib/python3/dist-packages/mpd/base.py", line 265, in _parse_list
    raise ProtocolError("Expected key '{}', got '{}'".format(seen, key))
mpd.base.ProtocolError: Expected key 'volume', got 'repeat'

script to reproduce:

import asyncio
import logging

from mpd.asyncio import MPDClient

async def connected(client):
    print("Connected!")
    asyncio.create_task(idle_loop(client))
    while True:
        await asyncio.sleep(0.1)
        st = await client.status()
        volume = max(0, int(st.get('volume', 0)))
        await client.setvol(volume+1)
        await asyncio.sleep(0.09)
        await client.status()
        await asyncio.sleep(0.01)
        await client.status()
        await asyncio.sleep(0.01)
        await client.setvol(volume)
        await asyncio.sleep(0.09)
        await client.status()

async def idle_loop(client):
    async for s in client.idle():
        print("Idle change in", s)

async def main():
    client = MPDClient()
    await client.connect("127.0.0.1", 6600)
    await connected(client)

asyncio.run(main())
Mic92 commented 1 year ago

Has been fixed.