diff -r 54e6de23c546 -r fc1310995b98 src/eric7/UI/UserInterface.py --- a/src/eric7/UI/UserInterface.py Fri Jul 05 10:36:19 2024 +0200 +++ b/src/eric7/UI/UserInterface.py Sat Jul 06 17:17:07 2024 +0200 @@ -12,6 +12,7 @@ import enum import functools import getpass +import glob import json import logging import os @@ -19,6 +20,8 @@ import shutil import sys +import psutil + from PyQt6 import sip from PyQt6.Qsci import QSCINTILLA_VERSION_STR from PyQt6.QtCore import ( @@ -45,6 +48,7 @@ ) from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkProxyFactory from PyQt6.QtWidgets import ( + QAbstractItemView, QApplication, QDialog, QDockWidget, @@ -75,6 +79,7 @@ from eric7.EricWidgets import EricErrorMessage, EricFileDialog, EricMessageBox from eric7.EricWidgets.EricApplication import ericApp from eric7.EricWidgets.EricClickableLabel import EricClickableLabel +from eric7.EricWidgets.EricListSelectionDialog import EricListSelectionDialog from eric7.EricWidgets.EricMainWindow import EricMainWindow from eric7.EricWidgets.EricSingleApplication import EricSingleApplicationServer from eric7.EricWidgets.EricToolBarManager import EricToolBarManager @@ -1948,6 +1953,44 @@ self.loadSessionAct.triggered.connect(self.__loadSessionFromFile) self.actions.append(self.loadSessionAct) + self.loadCrashSessionAct = EricAction( + self.tr("Load crash session"), + self.tr("Load crash session..."), + 0, + 0, + self, + "load_crash_session", + ) + self.loadCrashSessionAct.setStatusTip(self.tr("Load crash session")) + self.loadCrashSessionAct.setWhatsThis( + self.tr( + """<b>Load crash session...</b>""" + """<p>This presents a list of available crash session files""" + """ to select from and loads the selected one.</p>""" + ) + ) + self.loadCrashSessionAct.triggered.connect(self.__loadCrashSession) + self.actions.append(self.loadCrashSessionAct) + + self.cleanCrashSessionsAct = EricAction( + self.tr("Clean crash sessions"), + self.tr("Clean crash sessions..."), + 0, + 0, + self, + "clean_crash_sessions", + ) + self.cleanCrashSessionsAct.setStatusTip(self.tr("Clean crash sessions")) + self.cleanCrashSessionsAct.setWhatsThis( + self.tr( + """<b>Clean crash sessions...</b>""" + """<p>This asks for confirmation and deletes all stale crash session""" + """ files.</p>""" + ) + ) + self.cleanCrashSessionsAct.triggered.connect(self.__cleanCrashSessions) + self.actions.append(self.cleanCrashSessionsAct) + self.newWindowAct = EricAction( self.tr("New Window"), EricPixmapCache.getIcon("newWindow"), @@ -3819,14 +3862,24 @@ self.__menus["server"] = self.__ericServerInterface.initMenu() ############################################################## + ## Sessions menu + ############################################################## + + self.__menus["sessions"] = QMenu(self.tr("Sessions")) + self.__menus["sessions"].addAction(self.saveSessionAct) + self.__menus["sessions"].addAction(self.loadSessionAct) + self.__menus["sessions"].addSeparator() + self.__menus["sessions"].addAction(self.loadCrashSessionAct) + self.__menus["sessions"].addAction(self.cleanCrashSessionsAct) + + ############################################################## ## File menu ############################################################## self.__menus["file"] = self.viewmanager.initFileMenu() mb.addMenu(self.__menus["file"]) self.__menus["file"].addSeparator() - self.__menus["file"].addAction(self.saveSessionAct) - self.__menus["file"].addAction(self.loadSessionAct) + self.__menus["file"].addMenu(self.__menus["sessions"]) self.__menus["file"].addSeparator() self.__menus["file"].addAction(self.restartAct) self.__menus["file"].addAction(self.exitAct) @@ -7694,23 +7747,20 @@ if os.path.exists(fn): self.__tasksFile.readFile(fn) - def __writeSession(self, filename="", crashSession=False): + def __writeSession(self, filename=""): """ Private slot to write the session data to a JSON file (.esj). @param filename name of a session file to write @type str - @param crashSession flag indicating to write a crash session file - @type bool @return flag indicating success @rtype bool """ - if filename: - fn = filename - elif crashSession: - fn = os.path.join(Globals.getConfigDir(), "eric7_crash_session.esj") - else: - fn = os.path.join(Globals.getConfigDir(), "eric7session.esj") + fn = ( + filename + if filename + else os.path.join(Globals.getConfigDir(), "eric7session.esj") + ) return self.__sessionFile.writeFile(fn) @@ -7787,11 +7837,83 @@ self.__readSession(filename=sessionFile) + def __crashSessionFilePath(self, globPattern=False): + """ + Private method to generate a path name for a unique crash session file. + + @param globPattern flag indicating to get the glob pattern for crash + session files (defaults to False) + @type bool (optional) + @return crash session file path + @rtype str + """ + if globPattern: + return os.path.join(Globals.getConfigDir(), "eric7_crash_session_*.esj") + else: + return os.path.join( + Globals.getConfigDir(), f"eric7_crash_session_{os.getpid()}.esj" + ) + + def __getCrashedSessions(self): + """ + Private method to get a list of crash session file paths of crashed sessions. + + Note: Crashed sessions are those, whose PID does not exist anymore. + + @return list of crashed session file paths + @rtype list of str + """ + crashedSessionsList = [] + + crashSessionPattern = self.__crashSessionFilePath(globPattern=True) + crashSessionPatternParts = crashSessionPattern.split("*", 1) + # crashSessionPatternParts is used to extract the PID from the crash session + # file path + crashSessionsList = glob.glob(crashSessionPattern) + if crashSessionsList: + for crashSession in crashSessionsList: + pid = crashSession.replace(crashSessionPatternParts[0], "").replace( + crashSessionPatternParts[1], "" + ) + if not psutil.pid_exists(int(pid)): + # it is a real crash session + crashedSessionsList.append(crashSession) + + return crashedSessionsList + + def __checkCrashSessionExists(self): + """ + Private method to check for the existence of crash session files and + select the one to open. + + @return file path of the crash session file to open. An empty string indicates + that no crash session file should be opened or exists. + @rtype str + """ + selectedCrashSessionFile = "" + crashedSessionsList = self.__getCrashedSessions() + if crashedSessionsList: + dlg = EricListSelectionDialog( + sorted(crashedSessionsList), + selectionMode=QAbstractItemView.SelectionMode.SingleSelection, + title=self.tr("Found Crash Sessions"), + message=self.tr( + "These crash session files were found. Select the one to" + " open. Select 'Cancel' to not open a crash session." + ), + doubleClickOk=True, + parent=self, + ) + if dlg.exec() == QDialog.DialogCode.Accepted: + selectedCrashSessionFile = dlg.getSelection()[0] + + return selectedCrashSessionFile + def __deleteCrashSession(self): """ Private slot to delete the crash session file. """ - fn = os.path.join(Globals.getConfigDir(), "eric7_crash_session.esj") + fn = self.__crashSessionFilePath() if os.path.exists(fn): with contextlib.suppress(OSError): os.remove(fn) @@ -7805,7 +7927,7 @@ and not self.__disableCrashSession and Preferences.getUI("CrashSessionEnabled") ): - self.__writeSession(crashSession=True) + self.__writeSession(filename=self.__crashSessionFilePath()) def __readCrashSession(self): """ @@ -7820,21 +7942,42 @@ and not self.__noCrashOpenAtStartup and Preferences.getUI("OpenCrashSessionOnStartup") ): - fn = os.path.join(Globals.getConfigDir(), "eric7_crash_session.esj") - if os.path.exists(fn): - yes = EricMessageBox.yesNo( - self, - self.tr("Crash Session found!"), - self.tr( - """A session file of a crashed session was""" - """ found. Shall this session be restored?""" - ), - ) - if yes: - res = self.__readSession(filename=fn) + fn = self.__checkCrashSessionExists() + if fn: + res = self.__readSession(filename=fn) return res + @pyqtSlot() + def __loadCrashSession(self): + """ + Private slot to load a crash session. + """ + fn = self.__checkCrashSessionExists() + if fn: + self.__readSession(filename=fn) + + @pyqtSlot() + def __cleanCrashSessions(self): + """ + Private slot to clean all stale crash sessions. + """ + from .DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog + + crashedSessionsList = self.__getCrashedSessions() + if crashedSessionsList: + dlg = DeleteFilesConfirmationDialog( + parent=self, + caption=self.tr("Clean stale crash sessions"), + message=self.tr( + "Do you really want to delete these stale crash session files?" + ), + files=sorted(crashedSessionsList), + ) + if dlg.exec() == QDialog.DialogCode.Accepted: + for crashSession in crashedSessionsList: + os.remove(crashSession) + def showFindFileByNameDialog(self): """ Public slot to show the Find File by Name dialog.