src/eric7/UI/UserInterface.py

branch
eric7
changeset 10828
fc1310995b98
parent 10823
13542803c1b1
child 10837
40cdaa4a847d
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.

eric ide

mercurial