pythonpyqt5pyudev

Linux: Display names of all currently attached USB sticks on PyQt5 GUI


The following code shows the name of a new inserted USB stick in a console (as replacement for PyQt5 GUI) on Linux.

Unfortunately, a pyudev.device._errors.DeviceNotFoundAtPathError appears in console as soon as you unplug the USB stick without properly ejecting it.

What need to be changed to fix this error?

main.py:

from functools import partial
import os
import sys

import pyudev

from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QSocketNotifier, QObject, pyqtSignal


class MainWindow():

    def __init__(self, parent=None):
        super().__init__()
        # GUI code
        pass

    def print_name(self, name):
        print(name)


class LinuxDeviceMonitor(QObject):
    devices_changed = pyqtSignal(list)

    def __init__(self):
        super().__init__()
        self._context = pyudev.Context()

        self._monitor = pyudev.Monitor.from_netlink(self._context)
        self._monitor.start()

        self._devices = set()

        self._process_devices(self._context.list_devices(), action="add")

    def fileno(self):
        return self._monitor.fileno()

    @property
    def device_names(self):
        return [pyudev.Devices.from_path(self._context, device).get("DEVNAME") for device in self._devices]

    def process_incoming(self):
        read_device = partial(pyudev._util.eintr_retry_call, self._monitor.poll, timeout=0)
        self._process_devices(iter(read_device, None))
        self.devices_changed.emit(self.device_names)

    def _process_devices(self, devices, action=None):
        for device in devices:
            action = device.action if action is None else action

            if action in ("add", "change") and self._is_usb_mass_storage_device(device):
                self._devices.add(device.sys_path)
            elif action == "remove" and device.sys_path in self._devices:
                self._devices.remove(device.sys_path)

    @classmethod
    def _read_device_flag(self, device, name):
        path = os.path.join(device.sys_path, name)
        try:
            with open(path) as data:
                return bool(int(data.read()))
        except (IOError, ValueError):
            return False

    def _is_usb_mass_storage_device(self, device):
        is_removable = self._read_device_flag(device, "removable")
        has_size = self._read_device_flag(device, "size")
        has_usb = device.get("ID_BUS") == "usb"
        has_no_disc = device.get("ID_CDROM") is None
        return is_removable and has_size and has_usb and has_no_disc


def main():
    app = QApplication(sys.argv)

    main_window = MainWindow()

    linux_device_monitor = LinuxDeviceMonitor()

    notifier = QSocketNotifier(linux_device_monitor.fileno(), QSocketNotifier.Read)
    notifier.activated.connect(linux_device_monitor.process_incoming)

    linux_device_monitor.devices_changed.connect(main_window.print_name)

    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

Solution

  • There is nothing wrong with my first answer but you can overwrite a function with your own. Below is an example of this for your problem. The main changes are:

    Added:

    from pyudev._util import ensure_byte_string
    

    and overwrote the from_sys_path function:

            pyudev.Devices.from_sys_path = self.from_sys_path
    
        def from_sys_path(self, context, sys_path):
            device = context._libudev.udev_device_new_from_syspath(
                context, ensure_byte_string(sys_path))
            if not device:
                return None
            return pyudev.Device(context, device)
    

    Changed:

        @property
        def device_names(self):
            return [pyudev.Devices.from_path(self._context, device).get("DEVNAME") for device in self._devices]
    

    to:

        def device_names(self):
            devices = []
            for device in self._devices:
                dev = pyudev.Devices.from_path(self._context, device)
                if dev is not None:
                    devices.append(dev.get("DEVNAME"))
            return devices
    

    and

            self.devices_changed.emit(self.device_names)
    

    to

            self.devices_changed.emit(self.device_names())
    

    The whole code looks like this:

    from functools import partial
    import os
    import sys
    
    import pyudev
    
    from PyQt5.QtWidgets import QApplication
    from PyQt5.QtCore import QSocketNotifier, QObject, pyqtSignal
    from pyudev._util import ensure_byte_string
    
    
    class MainWindow():
    
        def __init__(self, parent=None):
            super().__init__()
            # GUI code
            pass
    
        def print_name(self, name):
            print(name)
    
    
    class LinuxDeviceMonitor(QObject):
        devices_changed = pyqtSignal(list)
    
        def __init__(self):
            super().__init__()
            self._context = pyudev.Context()
    
            self._monitor = pyudev.Monitor.from_netlink(self._context)
            self._monitor.start()
    
            self._devices = set()
    
            self._process_devices(self._context.list_devices(), action="add")
            pyudev.Devices.from_sys_path = self.from_sys_path
    
        def from_sys_path(self, context, sys_path):
            device = context._libudev.udev_device_new_from_syspath(
                context, ensure_byte_string(sys_path))
            if not device:
                return None
            return pyudev.Device(context, device)
    
        def fileno(self):
            return self._monitor.fileno()
    
        def device_names(self):
            devices = []
            for device in self._devices:
                dev = pyudev.Devices.from_path(self._context, device)
                if dev is not None:
                    devices.append(dev.get("DEVNAME"))
            return devices
    
        def process_incoming(self):
            read_device = partial(pyudev._util.eintr_retry_call, self._monitor.poll, timeout=0)
            self._process_devices(iter(read_device, None))
            self.devices_changed.emit(self.device_names())
    
        def _process_devices(self, devices, action=None):
            for device in devices:
                action = device.action if action is None else action
    
                if action in ("add", "change") and self._is_usb_mass_storage_device(device):
                    self._devices.add(device.sys_path)
                elif action == "remove" and device.sys_path in self._devices:
                    self._devices.remove(device.sys_path)
    
        @classmethod
        def _read_device_flag(self, device, name):
            path = os.path.join(device.sys_path, name)
            try:
                with open(path) as data:
                    return bool(int(data.read()))
            except (IOError, ValueError):
                return False
    
        def _is_usb_mass_storage_device(self, device):
            is_removable = self._read_device_flag(device, "removable")
            has_size = self._read_device_flag(device, "size")
            has_usb = device.get("ID_BUS") == "usb"
            has_no_disc = device.get("ID_CDROM") is None
            return is_removable and has_size and has_usb and has_no_disc
    
    
    def main():
        app = QApplication(sys.argv)
    
        main_window = MainWindow()
    
        linux_device_monitor = LinuxDeviceMonitor()
    
        notifier = QSocketNotifier(linux_device_monitor.fileno(), QSocketNotifier.Read)
        notifier.activated.connect(linux_device_monitor.process_incoming)
    
        linux_device_monitor.devices_changed.connect(main_window.print_name)
    
        sys.exit(app.exec_())
    
    
    if __name__ == '__main__':
        main()
    

    Your IDE may complain that you are accessing a protected member of a module (pyudev._util) but it will still work.