Repository: spyder-ide/spyder

Issue:
Crash when trying to autocomplete with fallback
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

For some reason, LSP seems to be acting up and won't work intermittently. Since PR #9320, when I try to autocomplete and LSP is not running, spyder crashes.

## Traceback
```
Process:               Python [2291]
Path:                  /usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/Resources/Python.app/Contents/MacOS/Python
Identifier:            org.python.python
Version:               3.7.3 (3.7.3)
Code Type:             X86-64 (Native)
Parent Process:        ??? [1]
Responsible:           Python [2291]
User ID:               502

Date/Time:             2019-06-07 18:52:05.774 +0100
OS Version:            Mac OS X 10.14.5 (18F132)
Report Version:        12
Bridge OS Version:     3.0 (14Y901)
Anonymous UUID:        3106AA5D-92F8-7F2F-88AA-C53A5B0C167B


Time Awake Since Boot: 3500 seconds

System Integrity Protection: enabled

Crashed Thread:        25  FallbackActor

Exception Type:        EXC_BAD_ACCESS (SIGSEGV)
Exception Codes:       KERN_INVALID_ADDRESS at 0x0000000000000028
Exception Note:        EXC_CORPSE_NOTIFY

Termination Signal:    Segmentation fault: 11
Termination Reason:    Namespace SIGNAL, Code 0xb
Terminating Process:   exc handler [2291]
```

## Versions

* Spyder version: 4.0.0.dev0 290c6c7cb
* Python version: 3.7.3
* Qt version: 5.12.3
* PyQt5 version: 5.12.2
* Operating System: Darwin 18.6.0

### Dependencies

```
pygments >=2.0     :  2.3.1 (OK)
qdarkstyle >=2.6.4 :  2.6.5 (OK)
sphinx >=0.6.6     :  1.8.5 (OK)
psutil >=0.3       :  5.6.1 (OK)
pyls >=0.19.0;<0.25:  0.24.0 (OK)
nbconvert >=4.0    :  5.4.1 (OK)
pandas >=0.13.1    :  0.24.2 (OK)
numpy >=1.7        :  1.16.2 (OK)
sympy >=0.7.3      :  1.3 (OK)
cython >=0.21      :  0.29.6 (OK)
qtconsole >=4.2.0  :  4.5.1 (OK)
IPython >=4.0      :  7.3.0 (OK)
matplotlib >=2.0.0 :  3.0.3 (OK)
pylint >=0.25      :  2.3.1 (OK)
```



Discussion:
Stack:
```
Thread 25 Crashed:: FallbackActor
0   QtWidgets                     	0x000000011ef89271 QFocusFramePrivate::update() + 49
1   libqmacstyle.dylib            	0x000000012cc312fa QMacStyle::event(QEvent*) + 506
2   QtWidgets                     	0x000000011ef55626 QStyleSheetStyle::event(QEvent*) + 70
3   QtWidgets                     	0x000000011eea16fd QApplicationPrivate::notify_helper(QObject*, QEvent*) + 269
4   QtWidgets                     	0x000000011eea2b02 QApplication::notify(QObject*, QEvent*) + 594
5   QtWidgets.so                  	0x000000011ea703ca sipQApplication::notify(QObject*, QEvent*) + 234
6   QtCore                        	0x000000011e120e44 QCoreApplication::notifyInternal2(QObject*, QEvent*) + 212
7   QtWidgets                     	0x000000011ee9f554 QApplicationPrivate::setFocusWidget(QWidget*, Qt::FocusReason) + 420
8   QtWidgets                     	0x000000011eed8521 QWidget::setFocus(Qt::FocusReason) + 609
9   QtWidgets.so                  	0x000000011eb7b38e meth_QWidget_setFocus(_object*, _object*) + 158
10  org.python.python             	0x0000000104d8d5ee _PyMethodDef_RawFastCallKeywords + 235
11  org.python.python             	0x0000000104d8cc8e _PyCFunction_FastCallKeywords + 44
12  org.python.python             	0x0000000104e21db2 call_function + 636
13  org.python.python             	0x0000000104e1ab9a _PyEval_EvalFrameDefault + 6439
14  org.python.python             	0x0000000104d8d068 function_code_fastcall + 112
15  org.python.python             	0x0000000104e21e27 call_function + 753
16  org.python.python             	0x0000000104e1ab9a _PyEval_EvalFrameDefault + 6439
17  org.python.python             	0x0000000104d8d068 function_code_fastcall + 112
18  org.python.python             	0x0000000104e21e27 call_function + 753
19  org.python.python             	0x0000000104e1ab9a _PyEval_EvalFrameDefault + 6439
20  org.python.python             	0x0000000104d8d068 function_code_fastcall + 112
21  org.python.python             	0x0000000104e21e27 call_function + 753
22  org.python.python             	0x0000000104e1ab9a _PyEval_EvalFrameDefault + 6439
23  org.python.python             	0x0000000104e226d3 _PyEval_EvalCodeWithName + 1867
24  org.python.python             	0x0000000104d8c8c1 _PyFunction_FastCallDict + 441
25  org.python.python             	0x0000000104d8d9ed _PyObject_Call_Prepend + 150
26  org.python.python             	0x0000000104d8cd9b PyObject_Call + 136
27  sip.so                        	0x000000011e529ede call_method + 76
28  sip.so                        	0x000000011e525cd3 sip_api_call_procedure_method + 152
29  QtCore.so                     	0x000000011dc9ac04 sipQThread::run() + 84
30  QtCore                        	0x000000011df5a073 QThreadPrivate::start(void*) + 323
31  libsystem_pthread.dylib       	0x00007fff669342eb _pthread_body + 126
32  libsystem_pthread.dylib       	0x00007fff66937249 _pthread_start + 66
33  libsystem_pthread.dylib       	0x00007fff6693340d thread_start + 13

```
@impact27, is there no traceback we can take a look at to better understand what's happening?
@impact27, which steps did you take to produce this error?
1 - Open spyder
2 - Notice that LSP is not working for some reason
3 - press tab to get a completion
4 - Spyder crashes because of a segmentation fault

as it is a Segmentation fault, there is no python traceback. This probably mean this is a Qt - related error. The FallbackActor is probably trying to update something that does not exist anymore. 
I am having a look: I got this error:
```
QObject::connect: Cannot queue arguments of type 'QItemSelection'
(Make sure 'QItemSelection' is registered using qRegisterMetaType().)
QObject::connect: Cannot queue arguments of type 'QItemSelection'
(Make sure 'QItemSelection' is registered using qRegisterMetaType().)
QBasicTimer::stop: Failed. Possibly trying to stop from a different thread
QBasicTimer::start: Timers cannot be started from another thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QBasicTimer::start: Timers cannot be started from another thread
```
The last executed line is:
<actor.py l.113> editor.receive_text_tokens(tokens)
<codeeditor.py l.1172> self.process_completion({'params': tokens})
<codeeditor.py l.973> self.completion_widget.show_list(completion_list, position, automatic)
<base.py l.116> self.setFocus()

I am not sure you can do GUI operations on an object that is not owned by the thread. I think `self.completion_widget.show_list(completion_list, position, automatic)` should be called by the GUI thread.


Resolving patch:
diff --git a/spyder/plugins/editor/fallback/actor.py b/spyder/plugins/editor/fallback/actor.py
--- a/spyder/plugins/editor/fallback/actor.py
+++ b/spyder/plugins/editor/fallback/actor.py
@@ -16,10 +16,9 @@
 # Standard library imports
 import re
 import logging
-from queue import Queue
 
 # Qt imports
-from qtpy.QtCore import QThread, QMutex, QMutexLocker, Signal
+from qtpy.QtCore import QObject, QThread, QMutex, QMutexLocker, Signal, Slot
 
 # Other imports
 import pygments
@@ -34,18 +33,24 @@
 logger = logging.getLogger(__name__)
 
 
-class FallbackActor(QThread):
+class FallbackActor(QObject):
     #: Signal emitted when the Thread is ready
     sig_fallback_ready = Signal()
+    sig_set_tokens = Signal(object, list)
+    sig_mailbox = Signal(dict)
 
     def __init__(self, parent):
-        QThread.__init__(self, parent)
-        self.mailbox = Queue()
+        QObject.__init__(self)
         self.stopped = False
         self.daemon = True
         self.mutex = QMutex()
         self.file_tokens = {}
         self.diff_patch = diff_match_patch()
+        self.thread = QThread()
+        self.moveToThread(self.thread)
+
+        self.thread.started.connect(self.started)
+        self.sig_mailbox.connect(self.handle_msg)
 
     def tokenize(self, text, language):
         """
@@ -79,35 +84,38 @@ def tokenize(self, text, language):
     def stop(self):
         """Stop actor."""
         with QMutexLocker(self.mutex):
-            self.stopped = True
+            logger.debug("Fallback plugin stopping...")
+            self.thread.quit()
+
+    def start(self):
+        """Start thread."""
+        self.thread.start()
 
-    def run(self):
-        """Run actor."""
+    def started(self):
+        """Thread started."""
         logger.debug('Fallback plugin starting...')
         self.sig_fallback_ready.emit()
 
-        while True:
-            with QMutexLocker(self.mutex):
-                if self.stopped:
-                    logger.debug("Fallback plugin stopping...")
-            message = self.mailbox.get()
-            msg_type, file, msg, editor = [
-                message[k] for k in ('type', 'file', 'msg', 'editor')]
-            if msg_type == 'update':
-                if file not in self.file_tokens:
-                    self.file_tokens[file] = {
-                        'text': '', 'language': msg['language']}
-                diff = msg['diff']
-                text = self.file_tokens[file]
-                text, _ = self.diff_patch.patch_apply(
-                    diff, text['text'])
-                self.file_tokens[file]['text'] = text
-            elif msg_type == 'close':
-                self.file_tokens.pop(file, {})
-            elif msg_type == 'retrieve':
-                tokens = []
-                if file in self.file_tokens:
-                    text_info = self.file_tokens[file]
-                    tokens = self.tokenize(
-                        text_info['text'], text_info['language'])
-                editor.receive_text_tokens(tokens)
+    @Slot(dict)
+    def handle_msg(self, message):
+        """Handle one message"""
+        msg_type, file, msg, editor = [
+            message[k] for k in ('type', 'file', 'msg', 'editor')]
+        if msg_type == 'update':
+            if file not in self.file_tokens:
+                self.file_tokens[file] = {
+                    'text': '', 'language': msg['language']}
+            diff = msg['diff']
+            text = self.file_tokens[file]
+            text, _ = self.diff_patch.patch_apply(
+                diff, text['text'])
+            self.file_tokens[file]['text'] = text
+        elif msg_type == 'close':
+            self.file_tokens.pop(file, {})
+        elif msg_type == 'retrieve':
+            tokens = []
+            if file in self.file_tokens:
+                text_info = self.file_tokens[file]
+                tokens = self.tokenize(
+                    text_info['text'], text_info['language'])
+            self.sig_set_tokens.emit(editor, tokens)
diff --git a/spyder/plugins/editor/plugin.py b/spyder/plugins/editor/plugin.py
--- a/spyder/plugins/editor/plugin.py
+++ b/spyder/plugins/editor/plugin.py
@@ -237,6 +237,8 @@ def __init__(self, parent, ignore_last_opened_files=False):
         # Know when the fallback completion engine is up
         self.main.fallback_completions.sig_fallback_ready.connect(
             self.fallback_ready)
+        self.main.fallback_completions.sig_set_tokens.connect(
+            self.fallback_set_tokens)
 
     def set_projects(self, projects):
         self.projects = projects
@@ -326,7 +328,7 @@ def send_lsp_request(self, language, request, params):
 
     def send_fallback_request(self, msg):
         """Send request to fallback engine."""
-        self.main.fallback_completions.mailbox.put(msg)
+        self.main.fallback_completions.sig_mailbox.emit(msg)
 
     def fallback_ready(self):
         """Notify all stackeditors about fallback availability."""
@@ -335,6 +337,10 @@ def fallback_ready(self):
         for editorstack in self.editorstacks:
             editorstack.notify_fallback_ready()
 
+    def fallback_set_tokens(self, editor, tokens):
+        """Set the tokend in the editor."""
+        editor.receive_text_tokens(tokens)
+
     #------ SpyderPluginWidget API ---------------------------------------------
     def get_plugin_title(self):
         """Return widget title"""


Test patch:
diff --git a/spyder/plugins/editor/fallback/tests/conftest.py b/spyder/plugins/editor/fallback/tests/conftest.py
--- a/spyder/plugins/editor/fallback/tests/conftest.py
+++ b/spyder/plugins/editor/fallback/tests/conftest.py
@@ -22,4 +22,9 @@ def teardown():
         fallback.stop()
 
     request.addfinalizer(teardown)
+
+    def call_editor(editor, tokens):
+        editor.receive_text_tokens(tokens)
+
+    fallback.sig_set_tokens.connect(call_editor)
     return fallback
diff --git a/spyder/plugins/editor/fallback/tests/test_fallback.py b/spyder/plugins/editor/fallback/tests/test_fallback.py
--- a/spyder/plugins/editor/fallback/tests/test_fallback.py
+++ b/spyder/plugins/editor/fallback/tests/test_fallback.py
@@ -84,7 +84,7 @@ def test_file_open_close(qtbot_module, fallback_editor):
             'diff': diff
         }
     }
-    fallback.mailbox.put(open_request)
+    fallback.sig_mailbox.emit(open_request)
     qtbot_module.wait(1000)
     assert 'test.py' in fallback.file_tokens
 
@@ -94,7 +94,7 @@ def test_file_open_close(qtbot_module, fallback_editor):
         'editor': editor,
         'msg': {}
     }
-    fallback.mailbox.put(close_request)
+    fallback.sig_mailbox.emit(close_request)
     qtbot_module.wait(1000)
     assert 'test.py' not in fallback.file_tokens
 
@@ -115,7 +115,7 @@ def test_tokenize(qtbot_module, fallback_editor, file_fixture):
             'diff': diff
         }
     }
-    fallback.mailbox.put(open_request)
+    fallback.sig_mailbox.emit(open_request)
     qtbot_module.wait(1000)
 
     tokens_request = {
@@ -126,7 +126,7 @@ def test_tokenize(qtbot_module, fallback_editor, file_fixture):
     }
     with qtbot_module.waitSignal(editor.sig_recv_tokens,
                                  timeout=3000) as blocker:
-        fallback.mailbox.put(tokens_request)
+        fallback.sig_mailbox.emit(tokens_request)
     tokens = blocker.args
     tokens = {token['insertText'] for token in tokens[0]}
     assert len(expected_tokens - tokens) == 0
@@ -146,7 +146,7 @@ def test_token_update(qtbot_module, fallback_editor):
             'diff': diff
         }
     }
-    fallback.mailbox.put(open_request)
+    fallback.sig_mailbox.emit(open_request)
     qtbot_module.wait(1000)
 
     tokens_request = {
@@ -157,7 +157,7 @@ def test_token_update(qtbot_module, fallback_editor):
     }
     with qtbot_module.waitSignal(editor.sig_recv_tokens,
                                  timeout=3000) as blocker:
-        fallback.mailbox.put(tokens_request)
+        fallback.sig_mailbox.emit(tokens_request)
     initial_tokens = blocker.args[0]
     initial_tokens = {token['insertText'] for token in initial_tokens}
     assert 'args' not in initial_tokens
@@ -172,11 +172,11 @@ def test_token_update(qtbot_module, fallback_editor):
             'diff': diff
         }
     }
-    fallback.mailbox.put(update_request)
+    fallback.sig_mailbox.emit(update_request)
     qtbot_module.wait(1000)
     with qtbot_module.waitSignal(editor.sig_recv_tokens,
                                  timeout=3000) as blocker:
-        fallback.mailbox.put(tokens_request)
+        fallback.sig_mailbox.emit(tokens_request)
     updated_tokens = blocker.args[0]
     updated_tokens = {token['insertText'] for token in updated_tokens}
     assert 'args' in updated_tokens
diff --git a/spyder/plugins/editor/widgets/tests/conftest.py b/spyder/plugins/editor/widgets/tests/conftest.py
--- a/spyder/plugins/editor/widgets/tests/conftest.py
+++ b/spyder/plugins/editor/widgets/tests/conftest.py
@@ -67,7 +67,7 @@ def fallback_codeeditor(fallback, qtbot_module, request):
     """CodeEditor instance with Fallback enabled."""
 
     def send_fallback_request(request):
-        fallback.mailbox.put(request)
+        fallback.sig_mailbox.emit(request)
 
     # Create a CodeEditor instance
     editor = codeeditor_factory()