refactor profile and collection loading/unloading

- unloadCollection() now waits for all collection windows to
indicate they've closed, and calls a callback when it's done
- autosync runs when the collection is unloaded, and is no longer
responsible for reloading it
- make sure backup thread runs until completion
- ensure we return to profile manager when collection can't be loaded
- don't run the profile manager with exec_(), or opening+closing a
broken profile ends up nesting runloops
- warn if a window wasn't cleaned up as part of collection unloading
This commit is contained in:
Damien Elmes 2017-08-16 14:38:55 +10:00
parent b3a569ed57
commit 489d16ed14

View file

@ -105,13 +105,17 @@ class AnkiQt(QMainWindow):
self.loadProfile() self.loadProfile()
def showProfileManager(self): def showProfileManager(self):
self.pm.profile = None
self.state = "profileManager" self.state = "profileManager"
d = self.profileDiag = QDialog() d = self.profileDiag = QDialog()
f = self.profileForm = aqt.forms.profiles.Ui_Dialog() f = self.profileForm = aqt.forms.profiles.Ui_Dialog()
f.setupUi(d) f.setupUi(d)
f.login.clicked.connect(self.onOpenProfile) f.login.clicked.connect(self.onOpenProfile)
f.profiles.itemDoubleClicked.connect(self.onOpenProfile) f.profiles.itemDoubleClicked.connect(self.onOpenProfile)
f.quit.clicked.connect(self.cleanupAndExit) def onQuit():
d.close()
self.cleanupAndExit()
f.quit.clicked.connect(onQuit)
f.add.clicked.connect(self.onAddProfile) f.add.clicked.connect(self.onAddProfile)
f.rename.clicked.connect(self.onRenameProfile) f.rename.clicked.connect(self.onRenameProfile)
f.delete_2.clicked.connect(self.onRemProfile) f.delete_2.clicked.connect(self.onRemProfile)
@ -120,9 +124,6 @@ class AnkiQt(QMainWindow):
self.refreshProfilesList() self.refreshProfilesList()
# raise first, for osx testing # raise first, for osx testing
d.show() d.show()
d.activateWindow()
d.raise_()
d.exec_()
def refreshProfilesList(self): def refreshProfilesList(self):
f = self.profileForm f = self.profileForm
@ -154,8 +155,8 @@ class AnkiQt(QMainWindow):
if not self.openProfile(): if not self.openProfile():
showWarning(_("Invalid password.")) showWarning(_("Invalid password."))
return return
self.loadProfile()
self.profileDiag.close() self.profileDiag.close()
self.loadProfile()
return True return True
def profileNameOk(self, str): def profileNameOk(self, str):
@ -204,6 +205,11 @@ Are you sure?""")):
self.refreshProfilesList() self.refreshProfilesList()
def loadProfile(self): def loadProfile(self):
self.maybeAutoSync()
if not self.loadCollection():
return
# show main window # show main window
if self.pm.profile['mainWindowState']: if self.pm.profile['mainWindowState']:
restoreGeom(self, "mainWindow") restoreGeom(self, "mainWindow")
@ -214,13 +220,7 @@ Are you sure?""")):
self.show() self.show()
self.activateWindow() self.activateWindow()
self.raise_() self.raise_()
# maybe sync (will load DB)
if self.pendingImport and os.path.basename(
self.pendingImport).startswith("backup-"):
# skip sync when importing a backup
self.loadCollection()
else:
self.onSync(auto=True)
# import pending? # import pending?
if self.pendingImport: if self.pendingImport:
if self.pm.profile['key']: if self.pm.profile['key']:
@ -232,24 +232,37 @@ To import into a password protected profile, please open the profile before atte
self.pendingImport = None self.pendingImport = None
runHook("profileLoaded") runHook("profileLoaded")
def unloadProfile(self, browser=True): def unloadProfile(self, onsuccess):
if not self.pm.profile: def callback():
# already unloaded self._unloadProfile()
return onsuccess()
runHook("unloadProfile") runHook("unloadProfile")
if not self.unloadCollection(): self.unloadCollection(callback)
return
self.state = "profileManager" def _unloadProfile(self):
self.onSync(auto=True, reload=False)
self.pm.profile['mainWindowGeom'] = self.saveGeometry() self.pm.profile['mainWindowGeom'] = self.saveGeometry()
self.pm.profile['mainWindowState'] = self.saveState() self.pm.profile['mainWindowState'] = self.saveState()
self.pm.save() self.pm.save()
self.pm.profile = None
self.hide() self.hide()
if browser:
self.showProfileManager() # at this point there should be no windows left
else: self._checkForUnclosedWidgets()
self.cleanupAndExit()
self.maybeAutoSync()
self.pm.profile = None
def _checkForUnclosedWidgets(self):
for w in self.app.topLevelWidgets():
if w.isVisible():
showWarning(f"Window should have been closed: {w}")
def unloadProfileAndExit(self):
self.unloadProfile(self.cleanupAndExit)
def unloadProfileAndShowProfileManager(self):
self.unloadProfile(self.showProfileManager)
def cleanupAndExit(self): def cleanupAndExit(self):
self.errorHandler.unload() self.errorHandler.unload()
@ -263,62 +276,51 @@ To import into a password protected profile, please open the profile before atte
cpath = self.pm.collectionPath() cpath = self.pm.collectionPath()
try: try:
self.col = Collection(cpath, log=True) self.col = Collection(cpath, log=True)
except anki.db.Error: except Exception as e:
# warn user
showWarning(_("""\ showWarning(_("""\
Your collection is corrupt. Please create a new profile, then \ Anki was unable to open your collection file. If problems persist after \
see the manual for how to restore from an automatic backup. restarting your computer, please see the manual for how to restore from \
an automatic backup.
Debug info: Debug info:
""")+traceback.format_exc()) """)+traceback.format_exc())
self.unloadProfile() self.showProfileManager()
except Exception as e: return False
# the custom exception handler won't catch this if we immediately
# unload, so we have to manually handle it
if "invalidTempFolder" in repr(str(e)):
showWarning(self.errorHandler.tempFolderMsg())
self.unloadProfile()
return
self.unloadProfile()
raise
self.progress.setupDB(self.col.db) self.progress.setupDB(self.col.db)
self.maybeEnableUndo() self.maybeEnableUndo()
self.moveToState("deckBrowser") self.moveToState("deckBrowser")
return True
def unloadCollection(self): def unloadCollection(self, onsuccess):
""" def callback():
Unload the collection. self._unloadCollection()
onsuccess()
This unloads a collection if there is one and returns True if self.closeAllWindows(callback)
there is no collection after the call. (Because the unload
worked or because there was no collection to start with.) def _unloadCollection(self):
""" self.progress.start(label=_("Backing Up..."), immediate=True)
if self.col: corrupt = False
if not self.closeAllCollectionWindows(): try:
return self.maybeOptimize()
self.progress.start(immediate=True) if not devMode:
corrupt = False corrupt = self.col.db.scalar("pragma integrity_check") != "ok"
try: except:
self.maybeOptimize() corrupt = True
except: if corrupt:
corrupt = True showWarning(_("Your collection file appears to be corrupt. \
if not corrupt:
if devMode:
corrupt = False
else:
corrupt = self.col.db.scalar("pragma integrity_check") != "ok"
if corrupt:
showWarning(_("Your collection file appears to be corrupt. \
This can happen when the file is copied or moved while Anki is open, or \ This can happen when the file is copied or moved while Anki is open, or \
when the collection is stored on a network or cloud drive. Please see \ when the collection is stored on a network or cloud drive. Please see \
the manual for information on how to restore from an automatic backup.")) the manual for information on how to restore from an automatic backup."))
try:
self.col.close() self.col.close()
self.col = None except:
if not corrupt: pass
self.backup() self.col = None
self.progress.finish() if not corrupt:
return True self.backup()
self.progress.finish()
# Backup and auto-optimize # Backup and auto-optimize
########################################################################## ##########################################################################
@ -328,6 +330,8 @@ the manual for information on how to restore from an automatic backup."))
Thread.__init__(self) Thread.__init__(self)
self.path = path self.path = path
self.data = data self.data = data
# make sure we complete before exiting the program
self.setDaemon(True)
# create the file in calling thread to ensure the same # create the file in calling thread to ensure the same
# file is not created twice # file is not created twice
open(self.path, "wb").close() open(self.path, "wb").close()
@ -529,9 +533,8 @@ title="%s" %s>%s</button>''' % (
self.mainLayout.addWidget(sweb) self.mainLayout.addWidget(sweb)
self.form.centralwidget.setLayout(self.mainLayout) self.form.centralwidget.setLayout(self.mainLayout)
def closeAllCollectionWindows(self): def closeAllWindows(self, onsuccess):
aqt.dialogs.closeAll(lambda: 1) return aqt.dialogs.closeAll(onsuccess)
return True
# Components # Components
########################################################################## ##########################################################################
@ -579,21 +582,34 @@ title="%s" %s>%s</button>''' % (
# Syncing # Syncing
########################################################################## ##########################################################################
def onSync(self, auto=False, reload=True): # expects a current profile and a loaded collection; reloads
if not auto or (self.pm.profile['syncKey'] and # collection after sync completes
self.pm.profile['autoSync'] and def onSync(self):
not self.safeMode): self.unloadCollection(self._onSync)
from aqt.sync import SyncManager
if not self.unloadCollection(): def _onSync(self):
return self._sync()
# set a sync state so the refresh timer doesn't fire while deck if not self.loadCollection():
# unloaded return
self.state = "sync"
self.syncer = SyncManager(self, self.pm) # expects a current profile, but no collection loaded
self.syncer.sync() def maybeAutoSync(self):
if reload: if (not self.pm.profile['syncKey']
if not self.col: or not self.pm.profile['autoSync']
self.loadCollection() or self.safeMode):
return
if self.pendingImport and os.path.basename(
self.pendingImport).startswith("backup-"):
return
# ok to sync
self._sync()
def _sync(self):
from aqt.sync import SyncManager
self.state = "sync"
self.syncer = SyncManager(self, self.pm)
self.syncer.sync()
# Tools # Tools
########################################################################## ##########################################################################
@ -654,16 +670,9 @@ title="%s" %s>%s</button>''' % (
def closeEvent(self, event): def closeEvent(self, event):
"User hit the X button, etc." "User hit the X button, etc."
event.accept() # ignore the event for now, as we need time to clean up
self.onClose(force=True) event.ignore()
self.unloadProfileAndExit()
def onClose(self, force=False):
"Called from a shortcut key. Close current active window."
aw = self.app.activeWindow()
if not aw or aw == self or force:
self.unloadProfile(browser=False)
else:
aw.close()
# Undo & autosave # Undo & autosave
########################################################################## ##########################################################################
@ -797,7 +806,8 @@ title="%s" %s>%s</button>''' % (
def setupMenus(self): def setupMenus(self):
m = self.form m = self.form
m.actionSwitchProfile.triggered.connect(lambda b: self.unloadProfile()) m.actionSwitchProfile.triggered.connect(
self.unloadProfileAndShowProfileManager)
m.actionImport.triggered.connect(self.onImport) m.actionImport.triggered.connect(self.onImport)
m.actionExport.triggered.connect(self.onExport) m.actionExport.triggered.connect(self.onExport)
m.actionExit.triggered.connect(self.close) m.actionExit.triggered.connect(self.close)