Repository: vispy/vispy

Issue:
nosetests stalled processes
I've been trying to update some packages on my machine and I end up breaking qt installation which is no more available. Now (I think this is the only change), the nosetests always stalls:

```
Failure: RuntimeError (the sip module implements API v11.0 but the PyQt4.QtCore module requires API v10.1) ... ERROR
Test that all events seem to be emitted. ... ok
Test that the keymap contains all keys supported by vispy. ... ok
Test that all _vispy_x methods are there. ... ok
Test that all events seem to be emitted. ... ok
Test that the keymap contains all keys supported by vispy. ... ok
Test that all _vispy_x methods are there. ... ok
Test that all events seem to be emitted. ... ok
Test that the keymap contains all keys supported by vispy. ... ok
Test that all _vispy_x methods are there. ... ok
Failure: SkipTest (Skipping test: __init__: Requires QT) ... SKIP: Skipping test: __init__: Requires QT
Failure: SkipTest (Skipping test: __init__: Requires QT) ... SKIP: Skipping test: __init__: Requires QT
Failure: SkipTest (Skipping test: __init__: Requires QT) ... SKIP: Skipping test: __init__: Requires QT
Test that all events seem to be emitted. ... ok
Test that the keymap contains all keys supported by vispy. ... ok
Test that all _vispy_x methods are there. ... ok
Embed Canvas via Qt Designer ... SKIP: Skipping test: test_qt_designer: Requires QT with UIC
Test desktop GL backend for basic functionality. ... 
```

At this point I have an opened vispy window (that I cannot close) and I have to issue a `ctrl-C` for the process to go on. Then I get `FAILED (SKIP=4, errors=1)`



Discussion:
I also have this. It's actually different from the error that's happening in Almar's PR, which has something to do with threading/timers, I think. This is more likely to do with Pyglet. Is Pyglet your backend? (`vispy.sys_info()` should say)

I can confirm that if I change the order preference to put `pyglet` in first ahead of Qt on my system, I get the same hangup, but _not_ if Qt is first (default) or `glfw`. So I'm pretty sure it's a pyglet-related error.

```
  File "/home/larsoner/custombuilds/vispy/vispy/gloo/gl/tests/test_basics.py", line 28, in test_basics_desktop
    _test_basics('desktop')
  File "/home/larsoner/custombuilds/vispy/vispy/gloo/gl/tests/test_basics.py", line 64, in _test_basics
    context.wait()
  File "/home/larsoner/custombuilds/vispy/vispy/app/_util.py", line 98, in wait
    self.app.process_events()
  File "/home/larsoner/custombuilds/vispy/vispy/app/application.py", line 68, in process_events
    return self._backend._vispy_process_events()
  File "/home/larsoner/custombuilds/vispy/vispy/app/backends/_pyglet.py", line 91, in _vispy_process_events
    return pyglet.app.platform_event_loop.step(0.0)
  File "/home/larsoner/.local/lib/python2.7/site-packages/pyglet/app/xlib.py", line 109, in step
    if device.poll():
  File "/home/larsoner/.local/lib/python2.7/site-packages/pyglet/app/xlib.py", line 91, in poll
    def poll(self):
KeyboardInterrupt
```

That's where the error is if I ctrl-C.

Weird

Here is the minimal code to reproduce the hang:

```
from vispy.app import app_opengl_context

with app_opengl_context(backend='pyglet') as context:
    context.wait()
```

What is this wait function doing ?

https://github.com/vispy/vispy/blob/master/vispy/app/_util.py#L92

Looks like the timer is never actually running. Does Pyglet need to be in the `app.run()` mode for the timer to work or something?

I checked, and it's stuck in that `while` loop.

But why do we start a timer in the first place if we don't use it ?

It looks like it's to make sure the event loop is running. But I think only `Qt` really has a functional event loop unless `app.run()` is called...

@rougier we should talk to @almarklein once he's back on Gitter about this, it'll maybe be easier to sort it out.

Ok.

As I was saying in the chat; both glut and pyglet are not very suited for interactive use. I have seen that running `app.process_events` for pyglet does not really process events at all. Till recently this has not been a problem for the tests, but apparently now it has.

We now have this `requires_non_glut` decorator. Maybe we should create a `requires_interactive_backend` decorator instead, which is not glut or pyglet.

But pyglet and glut can process events one by one so the problem may be elsewhere.

pyglet seems to be able to, but does not. I don't know about glut.


Resolving patch:
diff --git a/vispy/app/backends/_glfw.py b/vispy/app/backends/_glfw.py
--- a/vispy/app/backends/_glfw.py
+++ b/vispy/app/backends/_glfw.py
@@ -80,7 +80,7 @@
              }
 
 _VP_GLFW_ALL_WINDOWS = []
-_VP_GLFW_DO_DRAW = False
+_VP_GLFW_DO_DRAW = []
 
 MOD_KEYS = [keys.SHIFT, keys.ALT, keys.CONTROL, keys.META]
 
@@ -100,6 +100,7 @@ class ApplicationBackend(BaseApplicationBackend):
     def __init__(self):
         BaseApplicationBackend.__init__(self)
         self._timers = list()
+        self._running = False
 
     def _add_timer(self, timer):
         if timer not in self._timers:
@@ -109,20 +110,27 @@ def _vispy_get_backend_name(self):
         return 'Glfw'
 
     def _vispy_process_events(self):
-        wins = _get_glfw_windows()
-        for win in wins:
-            glfw.glfwPollEvents(win._id)
-
+        #wins = _get_glfw_windows()
+        #for win in wins:
+        #    glfw.glfwPollEvents(win._id)
+        glfw.glfwPollEvents()
+        while _VP_GLFW_DO_DRAW:
+            win = _VP_GLFW_DO_DRAW.pop(0)
+            win._on_draw()
+    
     def _vispy_run(self):
         win = _get_glfw_windows(check=True)[0]
-        global _VP_GLFW_DO_DRAW
-        while win._id is not None and not glfw.glfwWindowShouldClose(win._id):
-            if _VP_GLFW_DO_DRAW:
-                win._on_draw()
-            glfw.glfwPollEvents()
-        self._vispy_quit()
+        self._running = True
+        while (self._running and 
+               win._id is not None and 
+               not glfw.glfwWindowShouldClose(win._id)):
+            self._vispy_process_events()
+        self._vispy_quit()  # to clean up
 
     def _vispy_quit(self):
+        # Mark as quit
+        self._running = False
+        # Close windows
         wins = _get_glfw_windows()
         for win in wins:
             win._vispy_close()
@@ -227,10 +235,9 @@ def _vispy_update(self):
         # Invoke a redraw, passing it on to the canvas
         if self._vispy_canvas is None or self._id is None:
             return
-        # XXX HACKISH SOLUTION
-        global _VP_GLFW_DO_DRAW
-        _VP_GLFW_DO_DRAW = True
-        #self._on_draw(self._id)
+        # Mark that this window wants to be painted on the next loop iter
+        if self not in _VP_GLFW_DO_DRAW:
+            _VP_GLFW_DO_DRAW.append(self)
 
     def _vispy_close(self):
         # Force the window or widget to shut down
diff --git a/vispy/app/backends/_glut.py b/vispy/app/backends/_glut.py
--- a/vispy/app/backends/_glut.py
+++ b/vispy/app/backends/_glut.py
@@ -86,16 +86,29 @@ def _vispy_get_backend_name(self):
         return 'Glut'
 
     def _vispy_process_events(self):
-        pass  # not possible?
-
+        # Determine what function to use, if any
+        if hasattr(glut, 'glutMainLoopEvent') and bool(glut.glutMainLoopEvent):
+            func = glut.glutMainLoopEvent
+        elif hasattr(glut, 'glutCheckLoop') and bool(glut.glutCheckLoop):
+            func = glut.glutCheckLoop  # Darwin
+        else:
+            self._vispy_process_events = lambda: None
+            raise RuntimeError('Your implementation of GLUT does not allow ' +
+                               'interactivity. Consider installing freeglut.')
+        # Set for future use, and call!
+        self._vispy_process_events = func
+        func()
+    
     def _vispy_run(self):
         self._vispy_get_native_app()  # Force exist
         return glut.glutMainLoop()
 
     def _vispy_quit(self):
-        global _VP_GLUT_ALL_WINDOWS
-        for win in _VP_GLUT_ALL_WINDOWS:
-            win._vispy_close()
+        if hasattr(glut, 'glutLeaveMainLoop') and bool(glut.glutLeaveMainLoop):
+            glut.glutLeaveMainLoop()
+        else:
+            for win in _VP_GLUT_ALL_WINDOWS:
+                win._vispy_close()
 
     def _vispy_get_native_app(self):
         # HiDPI support for retina display
@@ -121,6 +134,12 @@ def _vispy_get_native_app(self):
                                      glut.GLUT_DOUBLE |
                                      glut.GLUT_STENCIL |
                                      glut.GLUT_DEPTH)
+            # Prevent exit when closing window
+            try:
+                glut.glutSetOption(glut.GLUT_ACTION_ON_WINDOW_CLOSE, 
+                                   glut.GLUT_ACTION_CONTINUE_EXECUTION)
+            except Exception:
+                pass
             self._initialized = True
         return glut
 
@@ -137,7 +156,8 @@ def __init__(self, name='glut window', *args, **kwargs):
 
         # Cache of modifiers so we can send modifiers along with mouse motion
         self._modifiers_cache = ()
-
+        self._closed = False  # Keep track whether the widget is closed
+        
         # Note: this seems to cause the canvas to ignore calls to show()
         # about half of the time.
         # glut.glutHideWindow()  # Start hidden, like the other backends
@@ -221,6 +241,8 @@ def _vispy_update(self):
 
     def _vispy_close(self):
         # Force the window or widget to shut down
+        if self._closed:
+            return
         glut.glutDestroyWindow(self._id)
 
     def _vispy_get_size(self):
@@ -243,8 +265,9 @@ def on_resize(self, w, h):
     def on_close(self):
         if self._vispy_canvas is None:
             return
+        self._closed = True
         self._vispy_canvas.events.close()
-
+        
     def on_draw(self, dummy=None):
         if self._vispy_canvas is None:
             return
diff --git a/vispy/app/backends/_pyglet.py b/vispy/app/backends/_pyglet.py
--- a/vispy/app/backends/_pyglet.py
+++ b/vispy/app/backends/_pyglet.py
@@ -87,8 +87,12 @@ def _vispy_get_backend_name(self):
         return 'Pyglet'
 
     def _vispy_process_events(self):
-        # todo: note that this does not actually process paint events :(
-        return pyglet.app.platform_event_loop.step(0.0)
+        # pyglet.app.platform_event_loop.step(0.0)
+        pyglet.clock.tick()
+        for window in pyglet.app.windows:
+            window.switch_to()
+            window.dispatch_events()
+            window.dispatch_event('on_draw')
 
     def _vispy_run(self):
         return pyglet.app.run()


Test patch: