ProjectFlask/RunServerDialog.py

Sat, 23 Dec 2023 15:48:52 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 23 Dec 2023 15:48:52 +0100
branch
eric7
changeset 83
d8788dc3442f
parent 82
bb14c648099b
child 84
f39230b845e4
permissions
-rw-r--r--

Updated copyright for 2024.

# -*- coding: utf-8 -*-

# Copyright (c) 2020 - 2024 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a dialog to run the Flask server.
"""

import re

from PyQt6.QtCore import QProcess, Qt, QTimer, pyqtSlot
from PyQt6.QtGui import QTextCharFormat
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QMenu

try:
    from eric7.EricGui import EricPixmapCache
except ImportError:
    from UI import PixmapCache as EricPixmapCache

from eric7.EricWidgets import EricMessageBox
from eric7.EricWidgets.EricApplication import ericApp

from . import AnsiTools
from .ServerStartOptionsDialog import ServerStartOptionsDialog
from .Ui_RunServerDialog import Ui_RunServerDialog


# TODO: should this be placed into the sidebar as a sidebar widget?
class RunServerDialog(QDialog, Ui_RunServerDialog):
    """
    Class implementing a dialog to run the Flask server.
    """

    def __init__(self, plugin, project, parent=None):
        """
        Constructor

        @param plugin reference to the plug-in object
        @type PluginProjectFlask
        @param project reference to the project object
        @type Project
        @param parent reference to the parent widget
        @type QWidget
        """
        super().__init__(parent)
        self.setupUi(self)

        self.__plugin = plugin
        self.__project = project

        self.__serverOptions = {"development": False}

        self.__process = None
        self.__serverUrl = ""

        self.__ansiRe = re.compile(r"(\x1b\[\d+m)")

        self.__urlRe = re.compile(r" \* Running on ([^(]+) \(.*")

        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setDefault(True)

        self.__defaultTextFormat = self.outputEdit.currentCharFormat()

        self.__initActionsMenu()

    def __initActionsMenu(self):
        """
        Private method to populate the actions button menu.
        """
        self.__actionsMenu = QMenu()
        self.__actionsMenu.setTearOffEnabled(True)
        self.__actionsMenu.setToolTipsVisible(True)
        self.__actionsMenu.aboutToShow.connect(self.__showActionsMenu)

        # re-start server
        self.__actionsMenu.addAction(self.tr("Re-start Server"), self.__restartServer)
        self.__restartModeAct = self.__actionsMenu.addAction(
            self.tr("Re-start Server"), self.__restartServerDifferentMode
        )
        self.__actionsMenu.addSeparator()
        # re-start server with options
        self.__actionsMenu.addAction(
            self.tr("Re-start Server with Options"), self.__restartServerWithOptions
        )

        self.menuButton.setIcon(EricPixmapCache.getIcon("actionsToolButton"))
        self.menuButton.setMenu(self.__actionsMenu)

    @pyqtSlot()
    def __showActionsMenu(self):
        """
        Private slot handling the actions menu about to be shown.
        """
        if self.__serverOptions["development"]:
            self.__restartModeAct.setText(self.tr("Re-start Server (Production Mode)"))
        else:
            self.__restartModeAct.setText(self.tr("Re-start Server (Development Mode)"))

    def startServer(self, development=False, askForOptions=False):
        """
        Public method to start the Flask server process.

        @param development flag indicating development mode
        @type bool
        @param askForOptions flag indicating to ask for server start options
            first
        @type bool
        @return flag indicating success
        @rtype bool
        """
        self.__serverOptions["development"] = development

        if askForOptions:
            dlg = ServerStartOptionsDialog(self.__serverOptions)
            if dlg.exec() != QDialog.DialogCode.Accepted:
                return False

            self.__serverOptions.update(dlg.getDataDict())

        workdir, env = self.__project.prepareRuntimeEnvironment(
            development=self.__serverOptions["development"]
        )
        if env is not None:
            command = self.__project.getFlaskCommand()

            self.__process = QProcess()
            self.__process.setProcessEnvironment(env)
            self.__process.setWorkingDirectory(workdir)
            self.__process.setProcessChannelMode(
                QProcess.ProcessChannelMode.MergedChannels
            )

            self.__process.readyReadStandardOutput.connect(self.__readStdOut)
            self.__process.finished.connect(self.__processFinished)

            self.outputEdit.clear()

            args = ["run"]
            if "host" in self.__serverOptions and self.__serverOptions["host"]:
                args += ["--host", self.__serverOptions["host"]]
            if "port" in self.__serverOptions and self.__serverOptions["port"]:
                args += ["--port", self.__serverOptions["port"]]
            if "cert" in self.__serverOptions and self.__serverOptions["cert"]:
                args += ["--cert", self.__serverOptions["cert"]]
            if "key" in self.__serverOptions and self.__serverOptions["key"]:
                args += ["--key", self.__serverOptions["key"]]

            self.__process.start(command, args)
            ok = self.__process.waitForStarted(10000)
            if not ok:
                EricMessageBox.critical(
                    None,
                    self.tr("Run Flask Server"),
                    self.tr(
                        """The Flask server process could not be""" """ started."""
                    ),
                )
            else:
                self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(
                    False
                )
                self.stopServerButton.setEnabled(True)
                self.stopServerButton.setDefault(True)
                self.startBrowserButton.setEnabled(True)
        else:
            ok = False

        return ok

    def closeEvent(self, evt):
        """
        Protected method handling a close event.

        @param evt reference to the close event
        @type QCloseEvent
        """
        self.on_stopServerButton_clicked()
        evt.accept()

    @pyqtSlot()
    def __readStdOut(self):
        """
        Private slot to add the server process output to the output pane.
        """
        if self.__process is not None:
            out = str(self.__process.readAllStandardOutput(), "utf-8")
            if not self.__serverUrl:
                urlMatch = self.__urlRe.search(out)
                if urlMatch:
                    self.__serverUrl = urlMatch.group(1)

            for txt in self.__ansiRe.split(out):
                if txt.startswith("\x1b["):
                    color = int(txt[2:-1])  # strip off ANSI command parts
                    if color == 0:
                        self.outputEdit.setCurrentCharFormat(self.__defaultTextFormat)
                    elif 30 <= color <= 37:
                        brush = AnsiTools.getColor(
                            self.__plugin.getPreferences("AnsiColorScheme"), color - 30
                        )
                        if brush is not None:
                            charFormat = QTextCharFormat(self.__defaultTextFormat)
                            charFormat.setForeground(brush)
                            self.outputEdit.setCurrentCharFormat(charFormat)
                else:
                    self.outputEdit.insertPlainText(txt)

    @pyqtSlot()
    def __processFinished(self):
        """
        Private slot handling the finishing of the server process.
        """
        if (
            self.__process is not None
            and self.__process.state() != QProcess.ProcessState.NotRunning
        ):
            self.__process.terminate()
            QTimer.singleShot(2000, self.__process.kill)
            self.__process.waitForFinished(3000)

        self.__process = None

        self.startBrowserButton.setEnabled(False)
        self.stopServerButton.setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setDefault(True)
        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setFocus(
            Qt.FocusReason.OtherFocusReason
        )

    @pyqtSlot()
    def on_stopServerButton_clicked(self):
        """
        Private slot to stop the running server.
        """
        self.__processFinished()

    @pyqtSlot()
    def on_startBrowserButton_clicked(self):
        """
        Private slot to start a web browser with the server URL.
        """
        if self.__plugin.getPreferences("UseExternalBrowser"):
            import webbrowser

            webbrowser.open(self.__serverUrl)
        else:
            ericApp().getObject("UserInterface").launchHelpViewer(self.__serverUrl)

    @pyqtSlot()
    def __restartServer(self):
        """
        Private slot to restart the server process.
        """
        # step 1: stop the current server
        self.on_stopServerButton_clicked()

        # step 2: start a new server
        self.startServer(development=self.__serverOptions["development"])

    @pyqtSlot()
    def __restartServerDifferentMode(self):
        """
        Private slot to restart the server process with the opposite mode.
        """
        # step 1: stop the current server
        self.on_stopServerButton_clicked()

        # step 2: start a new server
        self.startServer(development=not self.__serverOptions["development"])

    @pyqtSlot()
    def __restartServerWithOptions(self):
        """
        Private slot to restart the server asking for start options.
        """
        # step 1: stop the current server
        self.on_stopServerButton_clicked()

        # step 2: start a new server
        self.startServer(
            development=self.__serverOptions["development"], askForOptions=True
        )

eric ide

mercurial