Repository: ultrabug/py3status

Issue:
usbguard is not working as expected
@cyrinux and I ended up using some old version of `usbguard` because what got in `master` is behaving weirdly (or we don't know how to configure it). This issue is to consider what to do about usbguard, from fixing a bug (if this is a bug) to reverting to an original "simple" version of `usbguard`. 

@cyrinux has promised to look into this 😛 

Marking as 3.16 milestone.


Discussion:
>behaving weirdly

Behaving weirdly how? Tbh, this sounds like https://github.com/ultrabug/py3status/pull/1376#issuecomment-449652973 / https://github.com/ultrabug/py3status/pull/1376#issuecomment-419310706. 
sorry for the absence of description, we discussed this in irc back when 3.15 was released, and Ultrabug reminded me to create an issue. I'd need to repeat the tests, if I remember correctly it was not detecting newly plugged devices.
> > behaving weirdly
> 
> Behaving weirdly how? Tbh, this sounds like [#1376 (comment)](https://github.com/ultrabug/py3status/pull/1376#issuecomment-449652973) / [#1376 (comment)](https://github.com/ultrabug/py3status/pull/1376#issuecomment-419310706).

@lasers yes it is this, when I check to valid or not the PR I missed I was using version in my home directory at this moment.
I gave another test round for the module, my impressions as a user are below. They are intentionally a bit emotionally exaggerated because I'm playing a role of a user who doesn't know the codebase and is discovering this module for the first time. 

1. When the module has no custom configuration, it looks like this:

    ![image](https://user-images.githubusercontent.com/1177900/51500616-59022e00-1dd7-11e9-95e5-6a3ee2e6af76.png)

1. It took me a while to understand what I'm seeing, lots of colors, and I even have no unknown devices connected at this time. Now I know that I'm seeing previously approved devices, but I don't think this view is a good default, I think by default I should only see newly plugged devices that need my approval.

1. I see `1/5` and I understand that I probably need to scroll to see more. I hover my mouse over the module, scroll the mouse wheel down, and my mouse stops working. Why? Apparently because I was hovering the "Block" word and by scrolling I blocked my mouse 😞 

1. Knowing that I have to be pixel-precise not to accidentally approve or reject something, I put my cursor over `1/5` and scroll up, I'm getting `0/5`. The device is not shown, is this an out of index bug or do I have some internal device with no name? In any case I vote for not having zero-based indexing.

    ![image](https://user-images.githubusercontent.com/1177900/51500833-34f31c80-1dd8-11e9-8d3f-4dc35bf9e101.png)

1. I plug a usb stick in, nothing happens to the module, I plug it out and nothing happens again. At first I thought the module is unable to recognize new devices, but after testing turns out I need to wait around 5 seconds for the module to be updated. This has to be instant.

1. Because of this huge delay, it is possible to approve a device that has already been plugged out. The module both throws an error and mistakenly reports as if the device was successfully approved:

    ![image](https://user-images.githubusercontent.com/1177900/51501096-3cff8c00-1dd9-11e9-8ee0-49adac215892.png)

-------------------------

Guys we need a decision on what to do about this module. On the one hand it's a pity to throw away work, on the other hand nobody is actually using this functionality and there are bugs that we don't know about; also maintaining this module is hard, the code is quite complicated to understand.

In practice, what cyrinux and I need is a name of newly plugged device and two buttons, "approve" and "reject". Everything else like scrolling, filters, "permanently", we both disabled with a custom config (we also turned up to be using an older version of this module, that recognizes new devices instantly, but this is minor detail).

If we choose to keep the code as it is, I suggest we change the defaults to a much simpler view, only showing new devices that need approval.

Alternatively, we can try to significantly simplify the module, at a risk that 2 weeks later we get a feature request asking for functionality that was just removed.

Thoughts?
>On the one hand it's a pity to throw away work

I am fine with throwing away my work if it does not satisfy your need. I was addressing issues such as multiple devices wanting an approval and trying to approve only the one you want... and not having to use up buttons (I think it was something like 1 to accept, 2 to block, 3 to reject?).

Also, @cyrinux had problems and not feeling like he can keep working on this module so I stepped in to help him out. To hopefully let him understand more about composite indexes and how we pass it through `event` data.

Whatever you and @cyrinux want to do, go ahead (imho). 

Thanks guys for your patience and understand, @maximbaz has promised he will try to re-implement it based our discussion and tries. 
Goal is to keep it very simple, reactive and secure.
>1. When the module has no custom configuration, it looks like this:

What is the exact issue? Your colors decolorized `good` Allow too. 

>2. I think by default I should only see newly plugged devices that need my approval.

Change `filter` to `block` by default may help. It's not there by default to allow users to play with all filters before settling on a filter (or no filter).

>3. I was hovering the "Block" word and by scrolling I blocked my mouse disappointed

See if we can add a button to listen to `1` clicks for certain placeholders.

>4. The device is not shown.

I think it's there to hide device. Without this, we always have to see devices. This allows users to have a filter and still hide devices until we do something with them. By default, it's zero to avoid annoying users with taking up precious bar space. It's not for `block` filter only. This should allow you to ignore block devices (by leaving it in `0/5` instead of always seeing that blocked device name).

>5. I need to wait around 5 seconds for the module to be updated. This has to be instant.

Bug relating to daemon / Event. Sorry. 

>6. The module both throws an error and mistakenly reports as if the device was successfully approved.

Fixing `5.` will fix this. 


Wait for #1547 first, me think.

Resolving patch:
diff --git a/py3status/modules/usbguard.py b/py3status/modules/usbguard.py
--- a/py3status/modules/usbguard.py
+++ b/py3status/modules/usbguard.py
@@ -1,130 +1,55 @@
 # -*- coding: utf-8 -*-
 """
-Allow, Block, and Reject USB devices.
-
-USBGuard is a software framework for implementing USB device authorization
-policies. For more information, see https://usbguard.github.io/
+Allow or Reject newly plugged USB devices using USBGuard.
 
 Configuration parameters:
-    allow_urgent: display urgency on unread messages (default False)
-    button_next: mouse button to switch next device (default 4)
-    button_previous: mouse button to switch previous device (default 5)
-    filters: specify a list of filters to use:
-        (default ['None', 'any', 'allow', 'block'])
     format: display format for this module
-        *(default '[{format_device_list} ]{format_button_device} '
-        '{format_button_filter}[\?color=darkgray&show \|]'
-        '{format_button_permanent}')*
-    format_button_allow: display format for allow button
-        (default '[\?if=!policy=allow&color=good Allow]')
-    format_button_block: display format for block button
-        (default '[\?if=!policy=block&color=degraded Block]')
-    format_button_device: display format for device button
-        (default '[\?color=deepskyblue&show Device] {device}/{devices}')
-    format_button_filter: display format for filter button
-        *(default '[\?color=deepskyblue&show Filter ]'
-        '[\?if=filter=allow Allow|[\?if=filter=block '
-        'Block|[\?if=filter=None None|Any]]]')*
-    format_button_permanent: display format for permanent button
-        *(default '[\?if=permanent&color=white Permanently'
-        '|\?color=darkgray Permanently]')*
-    format_button_reject: display format for reject button
-        (default '\?color=bad Reject')
+        (default '{format_device}')
+    format_button_allow: display format for allow button filter
+        (default '\[Allow\]')
+    format_button_reject: display format for reject button filter
+        (default '\[Reject\]')
     format_device: display format for USB devices
-        *(default '[{name}|Unknown {id} [\?color=darkgray {usb_id}]]'
-        '[ {format_button_allow}][ {format_button_block}]')*
+        (default '{format_button_reject} [{name}|{usb_id}] {format_button_allow}')
     format_device_separator: show separator if more than one (default ' ')
-    format_notification_message: specify notification message to use
-        (default 'USB ID: {usb_id}\\nName: {name}\\nPort: {port}')
-    format_notification_title: specify notification title to use
-        *(default 'USB Device [\?if=policy=allow Allowed]'
-        '[\?if=policy=block Blocked][\?if=policy=reject Rejected]')*
-    permanent: specify behavior to use, otherwise auto False (default None)
 
 Format placeholders:
     {device}                  number of USB devices
-    {format_button_filter}    button to toggle USB device display filters
-    {format_button_permanent} button to toggle permanent states
     {format_device}           format for USB devices
 
-format_button_filter:
-    {filter}      USB device filter, eg None, any, allow, block
-
-format_button_permanent:
-    {permanent}   permanent boolean, eg False, True
-
 format_device:
-    {id}                   eg 1, 2, 5, 6, 7, 22, 23, 33
-    {policy}               eg allow, block
-    {usb_id}               eg 054c:0268
-    {name}                 eg Poker II, PLAYSTATION(R)3 Controller
-    {serial}               eg 0000:00:00.0
-    {port}                 eg usb1, usb2, usb3, 1-1, 4-1.2.1
-    {interface}            eg 00:00:00:00 00:00:00 00:00:00
-    {hash}                 eg ihYz60+8pxZBi/cm+Q/4Ibrsyyzq/iZ9xtMDAh53sng
-    {parent_hash}          eg npSDT1xuEIOSLNt2RT2EbFrE8XRZoV29t1n7kg6GxXg
-    {format_button_allow}  format for allow button
-    {format_button_block}  format for block button
-    {format_button_reject} format for reject button
-
-format_notification_*:
-    See `format_device`
+    {format_button_allow}     button to allow the device
+    {format_button_reject}    button to reject the device
+    {id}                      eg 1, 2, 5, 6, 7, 22, 23, 33
+    {policy}                  eg allow, block, reject
+    {usb_id}                  eg 054c:0268
+    {name}                    eg Poker II, PLAYSTATION(R)3 Controller
+    {serial}                  eg 0000:00:00.0
+    {port}                    eg usb1, usb2, usb3, 1-1, 4-1.2.1
+    {interface}               eg 00:00:00:00 00:00:00 00:00:00
+    {hash}                    eg ihYz60+8pxZBi/cm+Q/4Ibrsyyzq/iZ9xtMDAh53sng
+    {parent_hash}             eg npSDT1xuEIOSLNt2RT2EbFrE8XRZoV29t1n7kg6GxXg
 
 Requires:
     python-gobject: Python Bindings for GLib/GObject/GIO/GTK+
     usbguard: USB device authorization policy framework
 
-Examples:
-```
-# specify a list of filters: None, any, allow, block
-usbguard {
-    * None: show any devices with any options
-    * any: show any devices
-    * allow: show authorized devices
-    * block: show unauthorized devices
-}
-
-# show block device only, hide button
-usbguard {
-    format_button_filter = ''
-    filters = ['block']
-}
-```
-
-@author Cyril Levis (@cyrinux)
+@author @cyrinux, @maximbaz
 @license BSD
 
 SAMPLE OUTPUT
 [
-    {'full_text': 'Poker II '},
-    {'full_text': 'Allow ', 'color': '#00ff00'},
-    {'full_text': 'Block ', 'color': '#ffff00'},
-    {'full_text': 'Reject', 'color': '#ff0000'},
-]
-
-filters
-[
-    {'full_text': 'PLAYSTATION(R)3 Controller '},
-    {'full_text': 'Allow ', 'color': '#00ff00'},
-    {'full_text': 'Block', 'color': '#ffff00'},
-]
-
-unknown
-[
-    {'full_text': 'Unknown 12 '},
-    {'full_text': '8087:0024 ', 'color': '#a9a9a9'},
-    {'full_text': 'Allow ', 'color': '#00ff00'},
-    {'full_text': 'Block', 'color': '#ffff00'},
+    {'full_text': '[Reject] ', 'urgent': True},
+    {'full_text': 'USB Flash Drive ', 'urgent': True},
+    {'full_text': '[Allow]', 'urgent': True}
 ]
 """
 
-from copy import deepcopy
 from threading import Thread
-from gi.repository import Gio
+from gi.repository import GLib, Gio
 import re
 
 STRING_USBGUARD_DBUS = "start usbguard-dbus.service"
-STRING_INVALID_FILTER = "invalid filters `{}`"
 
 
 class Py3status:
@@ -132,64 +57,21 @@ class Py3status:
     """
 
     # available configuration parameters
-    allow_urgent = False
-    button_next = 4
-    button_previous = 5
-    filters = ["None", "any", "allow", "block"]
-    format = (
-        "[{format_device_list} ]{format_button_device} {format_button_filter}"
-        "[\?color=darkgray&show \|]{format_button_permanent}"
-    )
-    format_button_allow = "[\?if=!policy=allow&color=good Allow]"
-    format_button_block = "[\?if=!policy=block&color=degraded Block]"
-    format_button_device = "[\?color=deepskyblue&show Device] {device}/{devices}"
-    format_button_filter = (
-        "[\?color=deepskyblue&show Filter ]"
-        "[\?if=filter=allow Allow|[\?if=filter=block "
-        "Block|[\?if=filter=None None|Any]]]"
-    )
-    format_button_permanent = (
-        "[\?if=permanent&color=white Permanently" "|\?color=darkgray Permanently]"
-    )
-    format_button_reject = "\?color=bad Reject"
-    format_device = (
-        "[{name}|Unknown {id} [\?color=darkgray {usb_id}]]"
-        "[ {format_button_allow}][ {format_button_block}]"
-    )
+    format = "{format_device}"
+    format_button_allow = "\[Allow\]"
+    format_button_reject = "\[Reject\]"
+    format_device = "{format_button_reject} [{name}|{usb_id}] {format_button_allow}"
     format_device_separator = " "
-    format_notification_message = "USB ID: {usb_id}\nName: {name}\nPort: {port}"
-    format_notification_title = (
-        "USB Device [\?if=policy=allow Allowed]"
-        "[\?if=policy=block Blocked]"
-        "[\?if=policy=reject Rejected]"
-    )
-    permanent = None
-
-    # playground
 
     def post_config_hook(self):
-        for _filter in self.filters:
-            if _filter not in ["None", "any", "allow", "block"]:
-                raise Exception(STRING_INVALID_FILTER.format(_filter))
-
         self.init = {
             "format_button": self.py3.get_placeholders_list(
                 self.format_device, "format_button_*"
             ),
-            "notifications": (
-                self.format_notification_title or self.format_notification_message
-            ),
-            "permanent": self.permanent is None,
-            "target": {"allow": 0, "block": 1, "reject": 2},
+            "target": {"allow": 0, "reject": 2},
         }
 
-        self.filter_index = 0
-        self.filter_length = len(self.filters)
-        self.device_index = 1
-        self.device_length = 0
-        self.permanent = self.permanent or False
-        self.cache_devices = {}
-        self.device_keys = [
+        self.keys = [
             ("serial", re.compile(r"\S*serial \"(\S+)\"\S*")),
             ("policy", re.compile(r"^(\S+)")),
             ("usb_id", re.compile(r"id (\S+)")),
@@ -200,182 +82,92 @@ def post_config_hook(self):
             ("interface", re.compile(r"with-interface \{ (.*) \}$")),
         ]
 
-        try:
-            self.bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
-            self.proxy = Gio.DBusProxy.new_sync(
-                self.bus,
-                Gio.DBusProxyFlags.NONE,
+        self._init_dbus()
+
+    def _init_dbus(self):
+        self.bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
+        self.proxy = Gio.DBusProxy.new_sync(
+            self.bus,
+            Gio.DBusProxyFlags.NONE,
+            None,
+            "org.usbguard",
+            "/org/usbguard/Devices",
+            "org.usbguard.Devices",
+            None,
+        )
+        for signal in ["DevicePolicyChanged", "DevicePresenceChanged"]:
+            self.bus.signal_subscribe(
                 None,
-                "org.usbguard",
-                "/org/usbguard/Devices",
                 "org.usbguard.Devices",
+                signal,
                 None,
+                None,
+                0,
+                lambda *args: self.py3.update(),
             )
-            for signal in ["DevicePolicyChanged", "DevicePresenceChanged"]:
-                self.bus.signal_subscribe(
-                    None, "org.usbguard.Devices", signal, None, None, 0, self.py3.update
-                )
-        except Exception:
-            raise Exception(STRING_USBGUARD_DBUS)
-
-        self.thread = Thread(target=self._start_loop)
-        self.thread.daemon = True
-        # self.thread.start()
 
-    def _start_loop(self):
-        pass
+        thread = Thread(target=lambda: GLib.MainLoop().run())
+        thread.daemon = True
+        thread.start()
 
-    def _get_devices(self, device_filter):
-        if device_filter in ["any", "None"]:
-            device_filter = "match"
-
-        devices = self.proxy.listDevices("(s)", device_filter)
-        new_devices = []
-
-        for device_id, device_string in devices:
-            try:
-                device = self.cache_devices[device_string]
-            except KeyError:
-                device = {"id": device_id}
-                for name, regex in self.device_keys:
-                    value = regex.findall(device_string) or None
-                    if value:
-                        value = (
-                            value[0]
-                            .encode("latin-1")
-                            .decode("unicode_escape")
-                            .encode("latin-1")
-                            .decode()
-                        )
-                    device[name] = value
-                self.cache_devices[device_string] = device
-            new_devices.append(device)
-
-        return deepcopy(new_devices)
+    def _get_devices(self):
+        try:
+            raw_devices = self.proxy.listDevices("(s)", "block")
+        except Exception:
+            raise Exception(STRING_USBGUARD_DBUS)
 
-    def _manipulate_devices(self, devices, device_filter):
-        new_device = []
-        format_device_list = None
-        self.device_length = len(devices)
-        for index, device in enumerate(devices, 1):
-            if device_filter == "any":
-                pass
-            elif device_filter == "None":
-                device["policy"] = None
-            elif device["policy"] != device_filter:
-                continue
-            for x in self.init["format_button"]:
-                composite = self.py3.safe_format(getattr(self, x), device)
-                device[x] = self.py3.composite_update(
-                    composite, {"index": "{}/{}".format(device["id"], x.split("_")[-1])}
+        devices = []
+        for device_id, string in raw_devices:
+            device = {"id": device_id}
+            string = string.encode("latin-1").decode("unicode_escape")
+            string = string.encode("latin-1").decode("utf-8")
+            for name, regex in self.keys:
+                value = regex.findall(string) or None
+                if value:
+                    value = value[0]
+                device[name] = value
+            devices.append(device)
+
+        return devices
+
+    def _format_device(self, devices):
+        device_info = []
+        for device in devices:
+            for btn in self.init["format_button"]:
+                composite = self.py3.safe_format(getattr(self, btn), device)
+                device[btn] = self.py3.composite_update(
+                    composite,
+                    {"index": "{}/{}".format(device["id"], btn.split("_")[-1])},
                 )
 
-            device_composite = self.py3.safe_format(self.format_device, device)
-            if index == self.device_index:
-                format_device_list = device_composite
-
-            new_device.append(device_composite)
+            device_info.append(self.py3.safe_format(self.format_device, device))
 
         format_device_separator = self.py3.safe_format(self.format_device_separator)
-        format_device = self.py3.composite_join(format_device_separator, new_device)
-
-        return format_device, format_device_list
+        format_device = self.py3.composite_join(format_device_separator, device_info)
 
-    def _notify_user(self, device):
-        format_notification_message = self.py3.safe_format(
-            self.format_notification_message, device
-        )
-        format_notification_title = self.py3.safe_format(
-            self.format_notification_title, device
-        )
-        self.py3.notify_user(
-            msg=format_notification_message,
-            title=format_notification_title,
-            icon="/usr/share/icons/hicolor/scalable/apps/usbguard-icon.svg",
-        )
+        return format_device
 
     def usbguard(self):
-        filter_name = self.filters[self.filter_index]
-        device_data = self._get_devices(filter_name)
-        format_device, format_device_list = self._manipulate_devices(
-            device_data, filter_name
-        )
-
-        if self.device_index > self.device_length:
-            self.device_index = self.device_length
-
-        format_button_device = self.py3.safe_format(
-            self.format_button_device,
-            {"device": self.device_index, "devices": self.device_length},
-        )
-        self.py3.composite_update(format_button_device, {"index": "button_device"})
-
-        format_button_filter = self.py3.safe_format(
-            self.format_button_filter, {"filter": filter_name}
-        )
-        self.py3.composite_update(format_button_filter, {"index": "button_filter"})
-        format_button_permanent = self.py3.safe_format(self.format_button_permanent)
-        self.py3.composite_update(
-            format_button_permanent, {"index": "button_permanent"}
-        )
+        devices = self._get_devices()
 
         usbguard_data = {
-            "device": len(device_data),
-            "format_button_filter": format_button_filter,
-            "format_button_permanent": format_button_permanent,
-            "format_device": format_device,
-            "format_button_device": format_button_device,
-            "format_device_list": format_device_list,
+            "device": len(devices),
+            "format_device": self._format_device(devices),
         }
 
-        response = {
-            "cached_until": self.py3.time_in(10),
+        return {
+            "cached_until": self.py3.CACHE_FOREVER,
             "full_text": self.py3.safe_format(self.format, usbguard_data),
+            "urgent": True,
         }
 
-        if self.allow_urgent:
-            response["urgent"] = True
-
-        self.device_data = device_data
-        return response
-
     def on_click(self, event):
-        index = event["index"]
-        button = event["button"]
-        if isinstance(index, int):
+        if isinstance(event["index"], int):
             return
-        elif index == "button_device":
-            if button == self.button_next:
-                self.device_index += 1
-                self.device_index %= self.device_length + 1
-            elif button == self.button_previous:
-                self.device_index -= 1
-                self.device_index %= self.device_length + 1
-        elif index == "button_filter":
-            if button == self.button_next:
-                self.filter_index += 1
-                self.filter_index %= self.filter_length
-            elif button == self.button_previous:
-                self.filter_index -= 1
-                self.filter_index %= self.filter_length
-        elif index == "button_permanent":
-            self.permanent = not self.permanent
-        else:
-            device_id, policy_name = index.split("/")
-            device_id = int(device_id)
-
-            if self.init["notifications"]:
-                for device in self.device_data:
-                    if device["id"] == device_id:
-                        device["policy"] = policy_name
-                        self._notify_user(device)
-                        break
 
-            policy = self.init["target"][policy_name]
-            self.proxy.applyDevicePolicy("(uub)", device_id, policy, self.permanent)
-            if self.init["permanent"]:
-                self.permanent = False
-            self.device_index = 0
+        device_id, policy_name = event["index"].split("/")
+        policy = self.init["target"][policy_name]
+        self.proxy.applyDevicePolicy("(uub)", int(device_id), policy, False)
 
 
 if __name__ == "__main__":


Test patch: