python-vlc icon indicating copy to clipboard operation
python-vlc copied to clipboard

Segfault - how can I best debug this?

Open BackSeat opened this issue 3 years ago • 6 comments

I get intermittent segfaults but I'm not certain what's causing them. There's an example bt below. I don't know whether this is a python-vlc problem or a me problem.

What's the best way of going about debugging this?

python: ../nptl/pthread_mutex_lock.c:428: __pthread_mutex_lock_full: Assertion `e != ESRCH || !robust' failed.

Thread 1 "python" received signal SIGABRT, Aborted.
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50	../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
(gdb) bt
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007ffff7c48537 in __GI_abort () at abort.c:79
#2  0x00007ffff7c4840f in __assert_fail_base (fmt=0x7ffff7db1128 "%s%s%s:%u: %s%sAssertion `%s' failed.\n%n", assertion=0x7ffff7f9c617 "e != ESRCH || !robust", file=0x7ffff7f9c5fa "../nptl/pthread_mutex_lock.c", line=428, function=<optimized out>) at assert.c:92
#3  0x00007ffff7c57662 in __GI___assert_fail (assertion=assertion@entry=0x7ffff7f9c617 "e != ESRCH || !robust", file=file@entry=0x7ffff7f9c5fa "../nptl/pthread_mutex_lock.c", line=line@entry=428, function=function@entry=0x7ffff7f9c770 <__PRETTY_FUNCTION__.1> "__pthread_mutex_lock_full") at assert.c:101
#4  0x00007ffff7f905d4 in __pthread_mutex_lock_full (mutex=0x1c19e00) at ../nptl/pthread_mutex_lock.c:428
#5  0x00007ffff194e536 in input_resource_Terminate () at /lib/x86_64-linux-gnu/libvlccore.so.9
#6  0x00007ffff1a08ec3 in libvlc_media_player_stop () at /lib/x86_64-linux-gnu/libvlc.so.5
#7  0x00007ffff1b85d1d in  () at /lib/x86_64-linux-gnu/libffi.so.7
#8  0x00007ffff1b85289 in  () at /lib/x86_64-linux-gnu/libffi.so.7
#9  0x00007ffff7679360 in  () at /usr/lib/python3.9/lib-dynload/_ctypes.cpython-39-x86_64-linux-gnu.so
#10 0x00007ffff766f8d8 in  () at /usr/lib/python3.9/lib-dynload/_ctypes.cpython-39-x86_64-linux-gnu.so
#11 0x000000000051d89b in _PyObject_MakeTpCall ()
#12 0x00000000005170e4 in _PyEval_EvalFrameDefault ()
#13 0x0000000000528b63 in _PyFunction_Vectorcall ()
#14 0x0000000000511fb5 in _PyEval_EvalFrameDefault ()
#15 0x0000000000528b63 in _PyFunction_Vectorcall ()
#16 0x0000000000512192 in _PyEval_EvalFrameDefault ()
#17 0x00000000005106ed in  ()
#18 0x0000000000528d21 in _PyFunction_Vectorcall ()
#19 0x0000000000512192 in _PyEval_EvalFrameDefault ()
#20 0x0000000000528b63 in _PyFunction_Vectorcall ()
#21 0x000000000053bdb1 in  ()
[...]

Python 3.9.2 python-vlc==3.0.11115

BackSeat avatar Jun 07 '21 20:06 BackSeat

This is tricky to debug, because it is (a) certainly unrelated to python-vlc itself and (b) deeper inside the libvlc library.

As an initial suggestion, double check the comments of the python-vlc functions or methods used in your Python. Several of those are not thread-safe and if used, that may be the root cause.

If that does not clear anything up, debugging libvlc may be the only way to get to the bottom of this.

mrJean1 avatar Jun 08 '21 01:06 mrJean1

It may be related to the bindings, if the ctypes definition is wrong and passes a wrong parameter type/size to the underlying lib. The first thing to debug this is to manage to reproducibly exhibit the problem in a minimal test code. With the stack trace only, we can only speculate. For instance, I see that the libvlc_media_player_stop is involved, so it may well be that you have a MediaPlayerStopped event handler registered, which may cause the issue?

oaubert avatar Jun 08 '21 08:06 oaubert

I have no event handlers registered. I'll include the module that manages VLC below. The locks in that code are designed to protect the non-thread-safe parts of VLC - some of the functions (eg, get_position()) are called from a timer asynchronously.

I've not been able to consistently reproduce the problem, but I'll try again later.

import os
import threading
import vlc

from config import Config
from datetime import datetime
from time import sleep

lock = threading.Lock()


class Music:
    """
    Manage the playing of music tracks
    """

    def __init__(self):
        self.current_track_start_time = None
        self.fading = 0
        self.VLC = vlc.Instance()
        self.player = None
        self.track_path = None
        self.max_volume = Config.VOLUME_VLC_DEFAULT

    def fade(self):
        """
        Fade the currently playing track.

        The actual management of fading runs in its own thread so as not
        to hold up the UI during the fade.
        """

        with lock:
            if not self.player:
                return

            if not self.player.get_position() > 0 and self.player.is_playing():
                return

            self.fading += 1

            thread = threading.Thread(target=self._fade)
            thread.start()

    def _fade(self):
        """
        Implementation of fading the current track in a separate thread.
        """

        # Take a copy of current player to allow another track to be
        # started without interfering here

        p = self.player

        fade_time = Config.FADE_TIME / 1000
        steps = Config.FADE_STEPS
        sleep_time = fade_time / steps

        # We reduce volume by one mesure first, then by two measures,
        # then three, and so on.

        # The sum of the arithmetic sequence 1, 2, 3, ..n is
        # (n**2 + n) / 2
        total_measures_count = (steps**2 + steps) / 2

        measures_to_reduce_by = 0
        for i in range(1, steps + 1):
            measures_to_reduce_by += i
            volume_factor = 1 - (
                measures_to_reduce_by / total_measures_count)
            p.audio_set_volume(int(self.max_volume * volume_factor))
            sleep(sleep_time)

        self.stop(p)
        self.fading -= 1

    def get_playtime(self):
        "Return elapsed play time"

        with lock:
            if not self.player:
                return None

            return self.player.get_time()

    def get_position(self):
        "Return current position"

        with lock:
            return self.player.get_position()

    def play(self, path):
        """
        Start playing the track at path.

        Log and return if path not found.
        """

        if not os.access(path, os.R_OK):
            print(f"play({path}): path not found")
            return

        self.track_path = path

        self.player = self.VLC.media_player_new(path)
        self.player.audio_set_volume(self.max_volume)
        self.player.play()
        self.current_track_start_time = datetime.now()

    def playing(self):
        """
        Return True if currently playing a track, else False

        vlc.is_playing() returns True if track was faded out.
        get_position seems more reliable.
        """

        with lock:
            if self.player:
                if self.player.get_position() > 0 and self.player.is_playing():
                    return True

            # We take a copy of the player when fading, so we could be
            # playing in a fade nowFalse
            return self.fading > 0

    def set_position(self, ms):
        "Set current play time in milliseconds from start"

        with lock:
            return self.player.set_time(ms)

    def set_volume(self, volume):
        "Set maximum volume used for player"

        with lock:
            if not self.player:
                return

            self.max_volume = volume
            self.player.audio_set_volume(volume)

    def stop(self, player=None):
        "Immediately stop playing"

        with lock:
            if not player:
                if not self.player:
                    return
                player = self.player

            position = player.get_position()
            player.stop()
            player.release()
            # Ensure we don't reference player after release
            player = None

            return position

BackSeat avatar Jun 08 '21 12:06 BackSeat

Fading is done from a separate thread and that thread does call libvlc_media_player_stop via self.stop(p) and player.stop(). Try running without fading to see if the segfault occurs then.

Also, try with the following changes to isolate the fading player entirely from the Music instance before fading starts.

    def fade(self):
        ...
        self.fading += 1
        p = self.player  # add
        self.player = None  # add
        thread = threading.Thread(target=self._fade, args=(p,))  # change
        ...


    def _fade(self, p):
         ...
        # p = self.player  remove
        ....
        self.stop(p)
        with lock:  # add
            self.fading -= 1

mrJean1 avatar Jun 08 '21 13:06 mrJean1

Thanks @mrJean1 It certainly works calling from a different thread, but I take your point that this could be the problem. Certainly when I've experienced the segfault, there has often (maybe always) been fading in progress. Maybe I'll try fading in a subprocess rather than a thread.

BackSeat avatar Jun 08 '21 14:06 BackSeat

One other suggestion.

    def stop(self, ...):
        ...
        with lock:
            if not player:
                if not self.player:
                    return
                player = self.player
                self.player = None  # add
        ...
        # player = None  # remove
        return position

mrJean1 avatar Jun 08 '21 14:06 mrJean1