mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
fix processEvents()
reentrancy bug in progress manager window handling (#3030)
* fix progress manager close window race condition * use monotonic clock for time deltas in progress manager * restructure progress manager finish logic
This commit is contained in:
parent
60f8399aed
commit
7784321b90
1 changed files with 48 additions and 33 deletions
|
@ -3,6 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from concurrent.futures import Future
|
||||
from dataclasses import dataclass
|
||||
|
||||
import aqt.forms
|
||||
|
@ -162,7 +163,7 @@ class ProgressManager:
|
|||
self._counter = min
|
||||
self._min = min
|
||||
self._max = max
|
||||
self._firstTime = time.time()
|
||||
self._firstTime = time.monotonic()
|
||||
self._show_timer = QTimer(self.mw)
|
||||
self._show_timer.setSingleShot(True)
|
||||
self._show_timer.start(immediate and 100 or 600)
|
||||
|
@ -205,7 +206,7 @@ class ProgressManager:
|
|||
maybeShow: bool = True,
|
||||
max: int | None = None,
|
||||
) -> None:
|
||||
# print self._min, self._counter, self._max, label, time.time() - self._lastTime
|
||||
# print self._min, self._counter, self._max, label, time.monotonic() - self._lastTime
|
||||
if not self.mw.inMainThread():
|
||||
print("progress.update() called on wrong thread")
|
||||
return
|
||||
|
@ -223,25 +224,49 @@ class ProgressManager:
|
|||
self._win.form.progressBar.setValue(self._counter)
|
||||
|
||||
def finish(self) -> None:
|
||||
# we must latch the levels update and perform it after cleanup has occured or we expose ourselves to a race
|
||||
# condition where a second progress could see levels == 0 and wrongly assume everything is in a clean state
|
||||
next_levels = self._levels - 1
|
||||
next_levels = max(0, next_levels)
|
||||
if next_levels == 0:
|
||||
if self._win:
|
||||
self._closeWin()
|
||||
if self._busy_cursor_timer:
|
||||
self._busy_cursor_timer.stop()
|
||||
self._busy_cursor_timer = None
|
||||
self._restore_cursor()
|
||||
if self._show_timer:
|
||||
self._show_timer.stop()
|
||||
self._show_timer = None
|
||||
if self._backend_timer:
|
||||
self._backend_timer.stop()
|
||||
self._backend_timer.deleteLater()
|
||||
self._backend_timer = None
|
||||
self._levels = next_levels
|
||||
def do_window_cleanup(future: Future | None = None):
|
||||
# this method can be called in an async fashion from taskman where a future
|
||||
# is passed or in synchronous manner from the main thread
|
||||
if future is not None:
|
||||
future.result()
|
||||
|
||||
next_levels = self._levels - 1
|
||||
next_levels = max(0, next_levels)
|
||||
if next_levels == 0:
|
||||
if self._win:
|
||||
self._closeWin()
|
||||
if self._busy_cursor_timer:
|
||||
self._busy_cursor_timer.stop()
|
||||
self._busy_cursor_timer = None
|
||||
self._restore_cursor()
|
||||
if self._show_timer:
|
||||
self._show_timer.stop()
|
||||
self._show_timer = None
|
||||
if self._backend_timer:
|
||||
self._backend_timer.stop()
|
||||
self._backend_timer.deleteLater()
|
||||
self._backend_timer = None
|
||||
self._levels = next_levels
|
||||
|
||||
# if the window is not currently shown, we can do cleanup immediately, if it is
|
||||
# currently shown then we need to give the window system a half-second to
|
||||
# present the window before we close it again - fixes progress window getting
|
||||
# stuck, especially on ubuntu 16.10+
|
||||
elapsed_time = time.monotonic() - self._shown
|
||||
time_to_wait = 0.5 - elapsed_time
|
||||
# NOTE: we must not yield control if the window is not shown since we don't want
|
||||
# to expose ourselves to the possibility of something showing the window in the
|
||||
# meantime
|
||||
if (not self._shown) or (time_to_wait <= 0):
|
||||
do_window_cleanup()
|
||||
else:
|
||||
# NOTE: we can't use self.single_shot here since it won't call the callback
|
||||
# until _levels = 0, but if we are in this branch then _levels > 0
|
||||
self.mw.taskman.run_in_background(
|
||||
lambda time_to_wait=time_to_wait: time.sleep(time_to_wait),
|
||||
do_window_cleanup,
|
||||
uses_collection=False,
|
||||
)
|
||||
|
||||
def clear(self) -> None:
|
||||
"Restore the interface after an error."
|
||||
|
@ -254,25 +279,15 @@ class ProgressManager:
|
|||
return
|
||||
if self._shown:
|
||||
return
|
||||
delta = time.time() - self._firstTime
|
||||
delta = time.monotonic() - self._firstTime
|
||||
if delta > 0.5:
|
||||
self._showWin()
|
||||
|
||||
def _showWin(self) -> None:
|
||||
self._shown = time.time()
|
||||
self._shown = time.monotonic()
|
||||
self._win.show()
|
||||
|
||||
def _closeWin(self) -> None:
|
||||
if self._shown:
|
||||
while True:
|
||||
# give the window system a second to present
|
||||
# window before we close it again - fixes
|
||||
# progress window getting stuck, especially
|
||||
# on ubuntu 16.10+
|
||||
elap = time.time() - self._shown
|
||||
if elap >= 0.5:
|
||||
break
|
||||
self.app.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents) # type: ignore #possibly related to https://github.com/python/mypy/issues/6910
|
||||
# if the parent window has been deleted, the progress dialog may have
|
||||
# already been dropped; delete it if it hasn't been
|
||||
if not sip.isdeleted(self._win):
|
||||
|
|
Loading…
Reference in a new issue