diff -r 0f9c86561033 -r 4e8e63df7893 src/eric7/UI/UserInterface.py --- a/src/eric7/UI/UserInterface.py Fri Jul 05 10:15:29 2024 +0200 +++ b/src/eric7/UI/UserInterface.py Mon Jul 29 14:43:35 2024 +0200 @@ -12,6 +12,8 @@ import enum import functools import getpass +import glob +import importlib import json import logging import os @@ -19,6 +21,8 @@ import shutil import sys +import psutil + from PyQt6 import sip from PyQt6.Qsci import QSCINTILLA_VERSION_STR from PyQt6.QtCore import ( @@ -45,6 +49,7 @@ ) from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkProxyFactory from PyQt6.QtWidgets import ( + QAbstractItemView, QApplication, QDialog, QDockWidget, @@ -75,6 +80,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 @@ -254,16 +260,15 @@ splash.showMessage(self.tr("Initializing Basic Services...")) logging.getLogger(__name__).debug("Initializing Basic Services...") - # Generate the redirection helpers - self.stdout = EricStdRedirector(False, self) - self.stdout.stdoutString.connect(self.appendToStdout) - self.stderr = EricStdRedirector(True, self) - self.stderr.stderrString.connect(self.appendToStderr) # Redirect sys.stdout and/or sys.stderr if those are None if sys.stdout is None or UserInterface.ReleaseMode: - sys.stdout = self.stdout + self.__stdout = EricStdRedirector(False, self) + self.__stdout.stdoutString.connect(self.appendToStdout) + sys.stdout = self.__stdout if sys.stderr is None or UserInterface.ReleaseMode: - sys.stderr = self.stderr + self.__stderr = EricStdRedirector(True, self) + self.__stderr.stderrString.connect(self.appendToStderr) + sys.stderr = self.__stderr # create the remote server interface logging.getLogger(__name__).debug("Creating 'eric-ide' Server Interface...") @@ -3108,6 +3113,30 @@ self.webBrowserAct.triggered.connect(self.__startWebBrowser) self.actions.append(self.webBrowserAct) + if importlib.util.find_spec("fido2"): + self.securityKeyMgmtAct = EricAction( + self.tr("FIDO2 Security Key Management"), + EricPixmapCache.getIcon("securityKey"), + self.tr("FIDO2 Security Key Management..."), + 0, + 0, + self, + "fido2_security_key_mgmt", + ) + self.securityKeyMgmtAct.setStatusTip( + self.tr("Start the FIDO2 Security Key Management tool") + ) + self.securityKeyMgmtAct.setWhatsThis( + self.tr( + """<b>FIDO2 Security Key Management</b>""" + """<p>Start a tool to manage FIDO2 securit y keys.</p>""" + ) + ) + self.securityKeyMgmtAct.triggered.connect(self.__startFido2SecurityKeyMgmt) + self.actions.append(self.securityKeyMgmtAct) + else: + self.securityKeyMgmtAct = None + self.iconEditorAct = EricAction( self.tr("Icon Editor"), EricPixmapCache.getIcon("iconEditor"), @@ -3820,14 +3849,20 @@ self.__menus["server"] = self.__ericServerInterface.initMenu() ############################################################## + ## Sessions menu + ############################################################## + + self.__menus["sessions"] = QMenu(self.tr("Sessions")) + self.__menus["sessions"].aboutToShow.connect(self.__showSessionsMenu) + + ############################################################## ## 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) @@ -4188,9 +4223,10 @@ if self.snapshotAct is not None: toolstb.addAction(self.snapshotAct) toolstb.addAction(self.pdfViewerAct) - if self.webBrowserAct: - toolstb.addSeparator() - toolstb.addAction(self.webBrowserAct) + toolstb.addSeparator() + toolstb.addAction(self.webBrowserAct) + if self.securityKeyMgmtAct is not None: + toolstb.addAction(self.securityKeyMgmtAct) self.toolbarManager.addToolBar(toolstb, toolstb.windowTitle()) # setup the settings toolbar @@ -5170,8 +5206,9 @@ if self.snapshotAct is not None: btMenu.addAction(self.snapshotAct) btMenu.addAction(self.pdfViewerAct) - if self.webBrowserAct: - btMenu.addAction(self.webBrowserAct) + btMenu.addAction(self.webBrowserAct) + if self.securityKeyMgmtAct is not None: + btMenu.addAction(self.securityKeyMgmtAct) ptMenu = QMenu(self.tr("&Plugin Tools"), self) ptMenu.aboutToShow.connect(self.__showPluginToolsMenu) @@ -6260,6 +6297,16 @@ """ self.launchHelpViewer("") + @pyqtSlot() + def __startFido2SecurityKeyMgmt(self): + """ + Private slot to start the FIDO2 Security Key Management. + """ + fido2Mgmt = os.path.join(os.path.dirname(__file__), "..", "eric7_fido2.py") + QProcess.startDetached( + PythonUtilities.getPythonExecutable(), [fido2Mgmt] + ) + def __customViewer(self, home=None): """ Private slot to start a custom viewer. @@ -7695,23 +7742,39 @@ if os.path.exists(fn): self.__tasksFile.readFile(fn) - def __writeSession(self, filename="", crashSession=False): + @pyqtSlot() + def __showSessionsMenu(self): + """ + Private slot to mofify the state of some session actions. + """ + crashSessionsAvailable = bool(self.__getCrashedSessions()) + + menu = self.__menus["sessions"] + menu.clear() + menu.addAction(self.saveSessionAct) + menu.addAction(self.loadSessionAct) + menu.addSeparator() + act = menu.addAction(self.tr("Load crash session..."), self.__loadCrashSession) + act.setEnabled(crashSessionsAvailable) + act = menu.addAction( + self.tr("Clean crash sessions..."), self.__cleanCrashSessions + ) + act.setEnabled(crashSessionsAvailable) + + 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) @@ -7788,11 +7851,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) @@ -7806,7 +7941,7 @@ and not self.__disableCrashSession and Preferences.getUI("CrashSessionEnabled") ): - self.__writeSession(crashSession=True) + self.__writeSession(filename=self.__crashSessionFilePath()) def __readCrashSession(self): """ @@ -7821,21 +7956,44 @@ 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) + if res and Preferences.getUI("DeleteLoadedCrashSession"): + os.remove(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.