Repository: qutebrowser/qutebrowser

Issue:
Check if the tab is current in HintManager._start_cb
Fixes #3892.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/qutebrowser/qutebrowser/4763)
<!-- Reviewable:end -->

:follow-hint with self._context being None
Not sure what's happening:

```
16:39:39 DEBUG    commands   command:run:486 command called: follow-hint ['-s', 'd']
16:39:39 DEBUG    commands   command:run:501 Calling qutebrowser.browser.hints.HintManager.follow_hint(<qutebrowser.browser.hints.HintManager object at 0x7f70c01191f8>, True, 'd')
16:39:39 ERROR    misc       crashsignal:exception_hook:216 Uncaught exception
Traceback (most recent call last):
  File "/usr/lib64/python3.5/site-packages/qutebrowser/app.py", line 933, in eventFilter
    return handler(event)
  File "/usr/lib64/python3.5/site-packages/qutebrowser/app.py", line 906, in _handle_key_event
    return man.handle_event(event)
  File "/usr/lib64/python3.5/site-packages/qutebrowser/keyinput/modeman.py", line 335, in handle_event
    return handler(event)
  File "/usr/lib64/python3.5/site-packages/qutebrowser/keyinput/modeman.py", line 168, in _handle_keypress
    match = parser.handle(event, dry_run=dry_run)
  File "/usr/lib64/python3.5/site-packages/qutebrowser/keyinput/modeparsers.py", line 271, in handle
    match = super().handle(e)
  File "/usr/lib64/python3.5/site-packages/qutebrowser/keyinput/basekeyparser.py", line 182, in handle
    self.execute(binding, count)
  File "/usr/lib64/python3.5/site-packages/qutebrowser/keyinput/modeparsers.py", line 56, in execute
    self._commandrunner.run(cmdstr, count)
  File "/usr/lib64/python3.5/site-packages/qutebrowser/commands/runners.py", line 304, in run
    result.cmd.run(self._win_id, args, count=count)
  File "/usr/lib64/python3.5/site-packages/qutebrowser/commands/command.py", line 502, in run
    self.handler(*posargs, **kwargs)
  File "/usr/lib64/python3.5/site-packages/qutebrowser/browser/hints.py", line 926, in follow_hint
    elif keystring not in self._context.labels:
AttributeError: 'NoneType' object has no attribute 'labels'
```

Reports:

- https://crashes.qutebrowser.org/view/3d25e562
- https://crashes.qutebrowser.org/view/b19fb2c8
- https://crashes.qutebrowser.org/view/3436ffc2
- https://crashes.qutebrowser.org/view/36936f59 ("I had just closed a tab, when qutebrowser crashed.")
- https://crashes.qutebrowser.org/view/53ec91da
- https://crashes.qutebrowser.org/view/482b485a
- https://crashes.qutebrowser.org/view/b24478c6 ("Tried to switch tabs, didn't react. After a long while, qutebrowser did switch tab but crashed immediately.")
- https://crashes.qutebrowser.org/view/5fa90b29
- https://crashes.qutebrowser.org/view/8a9b99c8


Discussion:
I guess that helps, but I wonder if #3892 can't also happen in scenarios other than switching tabs/windows...
Info: According to a simple test on the linked crash reports that include debug log, there's a line with `Current tab changed` between the last line with `_start_cb` .. `hints:` and the last line with `Calling qutebrowser.browser.hints.HintManager.start` before it.
So it's very likely that every crash of this type encountered so far is caused by tab switching. If another crash report is received, the issue can be reopened.

Note: because of how the mock objects in the test is designed, it doesn't work.
> Note: because of how the mock objects in the test is designed, it doesn't work.

You should be able to turn the reproduction steps in #3892 into an end2end test in `tests/end2end/features/hint.feature` quite easily, no?
I mean the broken tests. The tabbed browser is not registered into objreg.
Is it okay to register them in the tests, or should I do it in the `tabbed_browser` function?
It turns out that they're already registered in `tabbed_browser_stubs`, so the problem is somewhere else.
Wait - I messed the commits up. Temporarily close.
One pattern I see in almost all crash reports is that the users report switching/closing tabs. In other words, they don't actually seem to be hinting links?
Here's a possible way to reproduce the problem:

1. Open at least 2 tabs
2. Make sure the current tab is hintable (has a link)
3. Run `hint ;; tab-next`
4. Type some valid hint keystring (for example: "a")

Reason:

The hint mode is entered after the `tab-next` command attempt to leave it, so it's still entered while the context variable is `None`. However, the other tab is not "prepared" for handling key sequence in hint mode, so it crashes.
Reproduced! Relevant logs:

```
15:57:28 DEBUG    commands   command:run:535 command called: hint
15:57:28 DEBUG    commands   command:run:550 Calling qutebrowser.browser.hints.HintManager.start(<qutebrowser.browser.hints.HintManager object at 0x7f82a07c9d38>, 'all', <Target.normal: 1>, mode=None, add_history=False, rapid=False, first=False)
15:57:28 DEBUG    commands   command:run:535 command called: tab-next
15:57:28 DEBUG    commands   command:run:550 Calling qutebrowser.browser.commands.CommandDispatcher.tab_next(<qutebrowser.browser.commands.CommandDispatcher>, 1)
15:57:28 DEBUG    misc       app:on_focus_object_changed:839 Focus object changed: <PyQt5.QtWidgets.QWidget object at 0x7f82801f23a8>
15:57:28 DEBUG    modes      tabbedbrowser:on_current_changed:701 Current tab changed, focusing <qutebrowser.browser.webengine.webenginetab.WebEngineTab tab_id=2 url='about:blank'>
15:57:28 DEBUG    modes      tabbedbrowser:on_current_changed:709 Mode before tab change: normal (mode_on_change = normal)
15:57:28 DEBUG    modes      modeman:leave:307 Ignoring leave request for KeyMode.hint (reason tab changed) as we're in mode KeyMode.normal
15:57:28 DEBUG    modes      modeman:leave:307 Ignoring leave request for KeyMode.caret (reason tab changed) as we're in mode KeyMode.normal
15:57:28 DEBUG    modes      modeman:leave:307 Ignoring leave request for KeyMode.insert (reason tab changed) as we're in mode KeyMode.normal
15:57:28 DEBUG    modes      modeman:leave:307 Ignoring leave request for KeyMode.passthrough (reason tab changed) as we're in mode KeyMode.normal
15:57:28 DEBUG    modes      tabbedbrowser:on_current_changed:721 Mode after tab change: normal (mode_on_change = normal)
15:57:28 DEBUG    modes      modeman:_handle_keypress:199 match: 2, forward_unbound_keys: auto, passthrough: True, is_non_alnum: True, dry_run: False --> filter: True (focused: <PyQt5.QtWidgets.QWidget object at 0x7f82801f23a8>)
15:57:28 DEBUG    hints      hints:_start_cb:601 hints: a, s, d, f, g
15:57:28 DEBUG    message    message:set_text:259 Follow hint...
15:57:28 DEBUG    statusbar  text:set_text:57 Setting normal text to 'Follow hint...'.
15:57:28 DEBUG    modes      modeman:enter:250 Entering mode KeyMode.hint (reason: HintManager.start)
15:57:29 DEBUG    modes      modeman:_handle_keyrelease:219 filter: True
15:57:32 DEBUG    modes      modeman:_handle_keypress:172 got keypress in mode KeyMode.hint - delegating to <qutebrowser.keyinput.modeparsers.HintKeyParser supports_count=False>
15:57:32 DEBUG    keyboard   basekeyparser:_debug_log:91 Got key: 0x41 / modifiers: 0x0 / text: 'a' / dry_run True
15:57:32 DEBUG    modes      modeman:_handle_keypress:199 match: 2, forward_unbound_keys: auto, passthrough: False, is_non_alnum: False, dry_run: True --> filter: True (focused: <PyQt5.QtWidgets.QWidget object at 0x7f82801f23a8>)
15:57:32 DEBUG    modes      modeman:_handle_keypress:172 got keypress in mode KeyMode.hint - delegating to <qutebrowser.keyinput.modeparsers.HintKeyParser supports_count=False>
15:57:32 DEBUG    keyboard   basekeyparser:_debug_log:91 Got key: 0x41 / modifiers: 0x0 / text: 'a' / dry_run False
15:57:32 DEBUG    keyboard   basekeyparser:_debug_log:91 Definitive match for 'a'.
15:57:32 DEBUG    keyboard   basekeyparser:_debug_log:91 Clearing keystring (was: a).
15:57:32 DEBUG    hints      hints:handle_partial_key:803 Got key without context!
15:57:32 DEBUG    commands   command:run:535 command called: follow-hint ['-s', 'a']
15:57:32 DEBUG    commands   command:run:550 Calling qutebrowser.browser.hints.HintManager.follow_hint(<qutebrowser.browser.hints.HintManager object at 0x7f828016dc18>, True, 'a')
15:57:32 ERROR    misc       crashsignal:exception_hook:214 Uncaught exception
Traceback (most recent call last):
  File "/home/florian/proj/qutebrowser/git/qutebrowser/app.py", line 932, in eventFilter
    return handler(event)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/app.py", line 905, in _handle_key_event
    return man.handle_event(event)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/keyinput/modeman.py", line 353, in handle_event
    return handler(event)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/keyinput/modeman.py", line 173, in _handle_keypress
    match = parser.handle(event, dry_run=dry_run)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/keyinput/modeparsers.py", line 254, in handle
    match = super().handle(e)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/keyinput/basekeyparser.py", line 203, in handle
    self.execute(binding, count)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/keyinput/modeparsers.py", line 56, in execute
    self._commandrunner.run(cmdstr, count)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/commands/runners.py", line 347, in run
    result.cmd.run(self._win_id, args, count=count)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/commands/command.py", line 551, in run
    self.handler(*posargs, **kwargs)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/browser/hints.py", line 961, in follow_hint
    elif keystring not in self._context.labels:
AttributeError: 'NoneType' object has no attribute 'labels'
```

Not sure what the best fix would be. A possible "proper" fix would be exposing a "this command is finished" signal which is used for `;;`, also see #3007.

Another possible solution is entering the hint mode in `HintManager.start` instead of `_start_cb` already, but then we need to take care of leaving it again if there are no elements to hint and all that stuff.

Resolving patch:
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -591,6 +591,18 @@ def _start_cb(self, elems):
             message.error("No elements found.")
             return
 
+        # Because _start_cb is called asynchronously, it's possible that the
+        # user switched to another tab or closed the tab/window. In that case
+        # we should not start hinting.
+        tabbed_browser = objreg.get('tabbed-browser', default=None,
+                                    scope='window', window=self._win_id)
+        tab = tabbed_browser.widget.currentWidget()
+        if tab.tab_id != self._tab_id:
+            log.hints.debug(
+                "Current tab changed ({} -> {}) before _start_cb is run."
+                .format(self._tab_id, tab.tab_id))
+            return
+
         strings = self._hint_strings(elems)
         log.hints.debug("hints: {}".format(', '.join(strings)))
 


Test patch:
diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature
--- a/tests/end2end/features/hints.feature
+++ b/tests/end2end/features/hints.feature
@@ -19,6 +19,16 @@ Feature: Using hints
         When I run :hint --mode=foobar
         Then the error "Invalid mode: Invalid value 'foobar' - valid values: number, letter, word" should be shown
 
+    Scenario: Switching tab between :hint and start_cb (issue 3892)
+        When I open data/hints/html/simple.html
+        And I open data/hints/html/simple.html in a new tab
+        And I run :hint ;; tab-prev
+        And I wait for regex "hints: .*|Current tab changed \(\d* -> \d*\) before _start_cb is run\." in the log
+        # 'hints: .*' is logged when _start_cb is called before tab-prev (on
+        # qtwebkit, _start_cb is called synchronously)
+        And I run :follow-hint a
+        Then the error "follow-hint: This command is only allowed in hint mode, not normal." should be shown
+
     ### Opening in current or new tab
 
     Scenario: Following a hint and force to open in current tab.
diff --git a/tests/unit/browser/test_hints.py b/tests/unit/browser/test_hints.py
--- a/tests/unit/browser/test_hints.py
+++ b/tests/unit/browser/test_hints.py
@@ -54,7 +54,8 @@ def test_show_benchmark(benchmark, tabbed_browser, qtbot, message_bridge,
     with qtbot.wait_signal(tab.load_finished):
         tab.load_url(QUrl('qute://testdata/data/hints/benchmark.html'))
 
-    manager = qutebrowser.browser.hints.HintManager(0, 0)
+    manager = qutebrowser.browser.hints.HintManager(
+        win_id=0, tab_id=tab.tab_id)
 
     def bench():
         with qtbot.wait_signal(mode_manager.entered):
@@ -75,7 +76,8 @@ def test_match_benchmark(benchmark, tabbed_browser, qtbot, message_bridge,
         tab.load_url(QUrl('qute://testdata/data/hints/benchmark.html'))
 
     config_stub.val.hints.scatter = False
-    manager = qutebrowser.browser.hints.HintManager(0, 0)
+    manager = qutebrowser.browser.hints.HintManager(
+        win_id=0, tab_id=tab.tab_id)
 
     with qtbot.wait_signal(mode_manager.entered):
         manager.start()