Repository: spyder-ide/spyder

Issue:
TimeoutError when starting a kernel still poping out
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

I started Spyder and hovered over one of those red "X" icons in the editor margin to see the tooltip, and this dialog box appeared

### Traceback
```python-traceback
  File "C:\Users\AlexStoll\Anaconda3\lib\site-packages\spyder\plugins\ipythonconsole\plugin.py", line 1083, in set_spyder_breakpoints
    cl.shellwidget.set_spyder_breakpoints()
  File "C:\Users\AlexStoll\Anaconda3\lib\site-packages\spyder\plugins\ipythonconsole\widgets\debugging.py", line 143, in set_spyder_breakpoints
    CONF.get('run', 'breakpoints', {}))
  File "C:\Users\AlexStoll\Anaconda3\lib\site-packages\spyder_kernels\comms\commbase.py", line 538, in __call__
    call_dict, call_data, self._comm_id)
  File "C:\Users\AlexStoll\Anaconda3\lib\site-packages\spyder\plugins\ipythonconsole\comms\kernelcomm.py", line 130, in _get_call_return_value
    with self.comm_channel_manager(comm_id, queue_message=not interrupt):
  File "C:\Users\AlexStoll\Anaconda3\lib\contextlib.py", line 81, in __enter__
    return next(self.gen)
  File "C:\Users\AlexStoll\Anaconda3\lib\site-packages\spyder\plugins\ipythonconsole\comms\kernelcomm.py", line 87, in comm_channel_manager
    timeout)
  File "C:\Users\AlexStoll\Anaconda3\lib\site-packages\spyder\plugins\ipythonconsole\comms\kernelcomm.py", line 168, in _wait
    raise TimeoutError(timeout_msg)
TimeoutError: Timeout while waiting for comm port.
```

## Versions

* Spyder version: 4.1.1 
* Python version: 3.6.8
* Qt version: 5.9.6
* PyQt5 version: 5.9.2
* Operating System: Windows 10

### Dependencies

```

# Mandatory:
atomicwrites >=1.2.0           :  1.3.0 (OK)
chardet >=2.0.0                :  3.0.4 (OK)
cloudpickle >=0.5.0            :  1.3.0 (OK)
diff_match_patch >=20181111    :  20181111 (OK)
intervaltree                   :  None (OK)
IPython >=4.0                  :  7.13.0 (OK)
jedi =0.15.2                   :  0.15.2 (OK)
nbconvert >=4.0                :  5.6.1 (OK)
numpydoc >=0.6.0               :  0.9.2 (OK)
paramiko >=2.4.0               :  2.7.1 (OK)
parso =0.5.2                   :  0.5.2 (OK)
pexpect >=4.4.0                :  4.8.0 (OK)
pickleshare >=0.4              :  0.7.5 (OK)
psutil >=5.3                   :  5.7.0 (OK)
pygments >=2.0                 :  2.6.1 (OK)
pylint >=0.25                  :  2.4.4 (OK)
pyls >=0.31.9;<0.32.0          :  0.31.9 (OK)
qdarkstyle >=2.8               :  2.8 (OK)
qtawesome >=0.5.7              :  0.7.0 (OK)
qtconsole >=4.6.0              :  4.7.1 (OK)
qtpy >=1.5.0                   :  1.9.0 (OK)
rtree >=0.8.3                  :  0.9.3 (OK)
sphinx >=0.6.6                 :  2.4.0 (OK)
spyder_kernels >=1.9.0;<1.10.0 :  1.9.0 (OK)
watchdog                       :  None (OK)
zmq >=17                       :  18.1.1 (OK)

# Optional:
cython >=0.21                  :  None (OK)
matplotlib >=2.0.0             :  None (OK)
numpy >=1.7                    :  1.18.1 (OK)
pandas >=0.13.1                :  None (OK)
scipy >=0.17.0                 :  1.4.1 (OK)
sympy >=0.7.3                  :  None (OK)
```



Discussion:
I tried a lot things to reproduce this but I couldn't. Let's wait to see if more people reports it.
From issue #12037: steps to reproduce:

1. Launch python -m spyder_kernels.console on remote host
2. Conect to remote kernel in spyder using ssh creds and kernel json.

I experienced the same issue Spyder 4.1 / Python 3.7 

 File "E:\DEV\envs\LORA\lib\site-packages\spyder\plugins\ipythonconsole\plugin.py", line 569, in update_path
    shell.update_syspath(path_dict, new_path_dict)
  File "E:\DEV\envs\LORA\lib\site-packages\spyder\plugins\ipythonconsole\widgets\shell.py", line 223, in update_syspath
    blocking=False).update_syspath(path_dict, new_path_dict)
  File "E:\DEV\envs\LORA\lib\site-packages\spyder_kernels\comms\commbase.py", line 538, in __call__
    call_dict, call_data, self._comm_id)
  File "E:\DEV\envs\LORA\lib\site-packages\spyder\plugins\ipythonconsole\comms\kernelcomm.py", line 130, in _get_call_return_value
    with self.comm_channel_manager(comm_id, queue_message=not interrupt):
  File "E:\DEV\envs\LORA\lib\contextlib.py", line 112, in __enter__
    return next(self.gen)
  File "E:\DEV\envs\LORA\lib\site-packages\spyder\plugins\ipythonconsole\comms\kernelcomm.py", line 87, in comm_channel_manager
    timeout)
  File "E:\DEV\envs\LORA\lib\site-packages\spyder\plugins\ipythonconsole\comms\kernelcomm.py", line 168, in _wait
    raise TimeoutError(timeout_msg)
TimeoutError: Timeout while waiting for comm port.

@impact27, we really need your help with this one. I don't understand why it's still happening.
I don't have access to a remote host now to try it but if it happens consistently I think it means the port used for comms is blocked by the firewall. This shouldn't happen in a local host.
As I said in the other issue this is happening for me when running slow tests locally all the time and it happens randomly on CI systems so there is something going on beyond "your system is too slow". 


@impact27, I think we need to show a kernel error message when this happens in the console, instead of raising a TimeoutError that people is compelled to report here.
> As I said in the other issue this is happening for me when running slow tests locally all the time and it happens randomly on CI systems so there is something going on beyond "your system is too slow".

Yes the system would need to be very slow for the kernel to not launch in 45 seconds. There are no steps to reproduce so it is a bit hard for me to do a better guess. The local tests are passing on my machine.

Issue #12037: I think this might be related to the firewall blocking the port used for comms.

Issue #12247: I think this is unrelated to comms and happens because the kernel is not launching somehow (Not in 45 seconds anyway). The line where the tests are crashing is already a line that was added to handle slow-starting kernels in tests. It would be useful if you could confirm that by looking at the failing test and checking if the kernel comes up in a reasonable amount of time for the failing test. In practice I would record the screen to avoid having to look at the computer during the entire test procedure. As far as I know the tests only fail during the setups of the tests in `test_mainwindow.py`. Can you:
1 - Confirm whether the local tests only fail during the test setup in `test_mainwindow.py`?
2 - Send a video of a failing test setup if that is the case?


> @impact27, I think we need to show a kernel error message when this happens in the console, instead of raising a TimeoutError that people is compelled to report here.

We could have a message saying: 
"The communication with the kernel is unresponsive. Try restarting the kernel if this persists."
Will do late today! Thanks. The tests that fails is not always the same, it just changes everytime I run the slow test suite.
> Will do late today! Thanks. The tests that fails is not always the same, it just changes everytime I run the slow test suite.

Yes I see but I think that what is failing is the setup for the tests, So any test in `test_mainwindow.py` might fail, but always during the setup.
>might fail, but always during the setup.

Will check and let you know ;-) 


Resolving patch:
diff --git a/spyder/plugins/ipythonconsole/comms/kernelcomm.py b/spyder/plugins/ipythonconsole/comms/kernelcomm.py
--- a/spyder/plugins/ipythonconsole/comms/kernelcomm.py
+++ b/spyder/plugins/ipythonconsole/comms/kernelcomm.py
@@ -124,12 +124,30 @@ def _get_call_return_value(self, call_dict, call_data, comm_id):
         """
         Interupt the kernel if needed.
         """
+        settings = call_dict['settings']
+        blocking = 'blocking' in settings and settings['blocking']
+
+        if not self.kernel_client.is_alive():
+            if blocking:
+                raise RuntimeError("Kernel is dead")
+            else:
+                # The user has other problems
+                return
+
         settings = call_dict['settings']
         interrupt = 'interrupt' in settings and settings['interrupt']
 
-        with self.comm_channel_manager(comm_id, queue_message=not interrupt):
-            return super(KernelComm, self)._get_call_return_value(
-                call_dict, call_data, comm_id)
+        try:
+            with self.comm_channel_manager(
+                    comm_id, queue_message=not interrupt):
+                return super(KernelComm, self)._get_call_return_value(
+                    call_dict, call_data, comm_id)
+        except RuntimeError:
+            if blocking:
+                raise
+            else:
+                # The user has other problems
+                return
 
     def _wait_reply(self, call_id, call_name, timeout):
         """Wait for the other side reply."""
@@ -149,28 +167,40 @@ def _wait(self, condition, signal, timeout_msg, timeout):
         timeout_msg: Message to display in case of a timeout.
         timeout: time in seconds before a timeout
         """
+        # Exit if condition is fulfilled or the kernel is dead.
         if condition():
             return
+        if not self.kernel_client.is_alive():
+            raise RuntimeError("Kernel is dead")
 
         # Create event loop to wait with
         wait_loop = QEventLoop()
-        signal.connect(wait_loop.quit)
         wait_timeout = QTimer()
         wait_timeout.setSingleShot(True)
+
+        # Connect signals to stop kernel loop
         wait_timeout.timeout.connect(wait_loop.quit)
+        self.kernel_client.hb_channel.kernel_died.connect(wait_loop.quit)
+        signal.connect(wait_loop.quit)
 
         # Wait until the kernel returns the value
         wait_timeout.start(timeout * 1000)
         while not condition():
             if not wait_timeout.isActive():
                 signal.disconnect(wait_loop.quit)
-                if not condition():
-                    raise TimeoutError(timeout_msg)
-                return
+                self.kernel_client.hb_channel.kernel_died.disconnect(
+                    wait_loop.quit)
+                if condition():
+                    return
+                if not self.kernel_client.is_alive():
+                    raise RuntimeError("Kernel is dead")
+                raise TimeoutError(timeout_msg)
             wait_loop.exec_()
 
         wait_timeout.stop()
         signal.disconnect(wait_loop.quit)
+        self.kernel_client.hb_channel.kernel_died.disconnect(
+            wait_loop.quit)
 
     def _handle_remote_call_reply(self, msg_dict, buffer):
         """
diff --git a/spyder/plugins/ipythonconsole/widgets/help.py b/spyder/plugins/ipythonconsole/widgets/help.py
--- a/spyder/plugins/ipythonconsole/widgets/help.py
+++ b/spyder/plugins/ipythonconsole/widgets/help.py
@@ -133,7 +133,7 @@ def is_defined(self, objtxt, force_import=False):
             return self.call_kernel(
                 interrupt=True, blocking=True
                 ).is_defined(objtxt, force_import=force_import)
-        except (TimeoutError, UnpicklingError):
+        except (TimeoutError, UnpicklingError, RuntimeError):
             return None
 
     def get_doc(self, objtxt):
@@ -141,7 +141,7 @@ def get_doc(self, objtxt):
         try:
             return self.call_kernel(interrupt=True, blocking=True
                                     ).get_doc(objtxt)
-        except (TimeoutError, UnpicklingError):
+        except (TimeoutError, UnpicklingError, RuntimeError):
             return None
 
     def get_source(self, objtxt):
@@ -149,7 +149,7 @@ def get_source(self, objtxt):
         try:
             return self.call_kernel(interrupt=True, blocking=True
                                     ).get_source(objtxt)
-        except (TimeoutError, UnpicklingError):
+        except (TimeoutError, UnpicklingError, RuntimeError):
             return None
 
     #---- Private methods (overrode by us) ---------------------------------
diff --git a/spyder/plugins/ipythonconsole/widgets/namespacebrowser.py b/spyder/plugins/ipythonconsole/widgets/namespacebrowser.py
--- a/spyder/plugins/ipythonconsole/widgets/namespacebrowser.py
+++ b/spyder/plugins/ipythonconsole/widgets/namespacebrowser.py
@@ -89,6 +89,7 @@ def get_value(self, name):
         """Ask kernel for a value"""
         reason_big = _("The variable is too big to be retrieved")
         reason_not_picklable = _("The variable is not picklable")
+        reason_dead = _("The kernel is dead")
         msg = _("%s.<br><br>"
                 "Note: Please don't report this problem on Github, "
                 "there's nothing to do about it.")
@@ -101,6 +102,8 @@ def get_value(self, name):
             raise ValueError(msg % reason_big)
         except (PicklingError, UnpicklingError):
             raise ValueError(msg % reason_not_picklable)
+        except RuntimeError:
+            raise ValueError(msg % reason_dead)
 
     def set_value(self, name, value):
         """Set value for a variable"""
@@ -137,7 +140,7 @@ def load_data(self, filename, ext):
         except TimeoutError:
             msg = _("Data is too big to be loaded")
             return msg
-        except UnpicklingError:
+        except (UnpicklingError, RuntimeError):
             return None
 
     def save_namespace(self, filename):
@@ -149,7 +152,7 @@ def save_namespace(self, filename):
         except TimeoutError:
             msg = _("Data is too big to be saved")
             return msg
-        except UnpicklingError:
+        except (UnpicklingError, RuntimeError):
             return None
 
     # ---- Private API (overrode by us) ----------------------------


Test patch: