Repository: qutebrowser/qutebrowser

Issue:
Some timers accumulate duplicate slots over time
**Version info**:
<!-- Please copy the first block from :version, not just the qutebrowser version -->
qutebrowser v3.0.0
Git commit:
Backend: QtWebEngine 6.5.2, based on Chromium 108.0.5359.220 (from api)
Qt: 6.5.2

**Does the bug happen if you start with `--temp-basedir`?**:
Yes

**Description**
There are two instances where timers accumulate duplicate slots over time. The timers are `qutebrowser/keyinput/modeparsers.py::NormalKeyParser._inhibited_timer` and `qutebrowser/browser/webengine/webenginetab.py::WebEngineAudio._silence_timer`.



**How to reproduce**
<!-- Link to the affected site, or steps to reproduce the issue (if possible/applicable). -->
__Run qutebrowser with the following flags__
```
qutebrowser -l debug --logfilter keyboard -s hints.auto_follow always -s hints.auto_follow_timeout 1
```
__Follow any hint on the page.__ This causes the following call stack
```
...
qutebrowser/browser/hints.py::HintManager._handle_auto_follow()
qutebrowser/keyinput/modeparsers.py::NormalKeyParser.set_inhibited_timeout
```
when `set_inhibited_timeout` is run, `self._inhibited_timer.timeout.connect(self._clear_inhibited)` is run. This adds an additional slot of `self._clear_inhibited` to the timer _every time it is run_. Since `NormalKeyParser._clear_inhibited` produces a debug message of `Releasing inhibition state of normal mode.`, this can easily be tracked from the debug log. This first time, we only see one instance of `Releasing inhibition...` in the debug log.
__Follow another hint on the page.__ This time, we see _2_ instances of `Releasing inhibition...` in the debug log.
__Follow another hint on the page.__ This time, we see _3_ instances of `Releasing inhibition...` in the debug log.
__Follow another hint on the page.__ This time, we see _4_ instances of `Releasing inhibition...` in the debug log.
... and so on.

A similar phenomenon can be observed with command
```
qutebrowser -l debug --logfilter webview
```
but, since `qutebrowser/mainwindow/tabbedbrowser.py::TabbedBrowser._on_audio_changed` has no debug message, one must first add a debug message to be printed on every execution of `TabbedBrowser._on_audio_changed`. One then must repeatedly execute the `:tab-mute` command.


Discussion:

Resolving patch:
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -816,6 +816,8 @@ def _connect_signals(self):
         page.recentlyAudibleChanged.connect(self._delayed_recently_audible_changed)
         self._tab.url_changed.connect(self._on_url_changed)
         config.instance.changed.connect(self._on_config_changed)
+        self._silence_timer.timeout.connect(functools.partial(
+            self.recently_audible_changed.emit, False))
 
     # WORKAROUND for recentlyAudibleChanged being emitted without delay from the moment
     # that audio is dropped.
@@ -831,8 +833,6 @@ def _delayed_recently_audible_changed(self, recently_audible):
             # Ignore all subsequent calls while the tab is muted with an active timer
             if timer.isActive():
                 return
-            timer.timeout.connect(
-                functools.partial(self.recently_audible_changed.emit, recently_audible))
             timer.start()
 
     def set_muted(self, muted: bool, override: bool = False) -> None:
diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py
--- a/qutebrowser/keyinput/modeparsers.py
+++ b/qutebrowser/keyinput/modeparsers.py
@@ -84,6 +84,7 @@ def __init__(self, *, win_id: int,
         self._inhibited = False
         self._inhibited_timer = usertypes.Timer(self, 'normal-inhibited')
         self._inhibited_timer.setSingleShot(True)
+        self._inhibited_timer.timeout.connect(self._clear_inhibited)
 
     def __repr__(self) -> str:
         return utils.get_repr(self)
@@ -113,7 +114,6 @@ def set_inhibited_timeout(self, timeout: int) -> None:
                 timeout))
             self._inhibited = True
             self._inhibited_timer.setInterval(timeout)
-            self._inhibited_timer.timeout.connect(self._clear_inhibited)
             self._inhibited_timer.start()
 
     @pyqtSlot()


Test patch: