class QProcessExecutionManager(ExecutionManager):
    """Class to manage tool instance execution using a PySide2 QProcess."""

    def __init__(self, logger, program="", args=None, silent=False, semisilent=False):
        """Class constructor.

        Args:
            logger (LoggerInterface): a logger instance
            program (str): Path to program to run in the subprocess (e.g. julia.exe)
            args (list, optional): List of argument for the program (e.g. path to script file)
            silent (bool): Whether or not to emit logger msg signals
            semisilent (bool): If True, show Process Log messages
        """
        super().__init__(logger)
        self._program = program
        self._args = args if args is not None else []
        self._silent = silent  # Do not show Event Log nor Process Log messages
        self._semisilent = semisilent  # Do not show Event Log messages but show Process Log messages
        self.process_failed = False
        self.process_failed_to_start = False
        self.user_stopped = False
        self._process = QProcess(self)
        self.process_output = None  # stdout when running silent
        self.process_error = None  # stderr when running silent
        self._out_chunks = []
        self._err_chunks = []

    def program(self):
        """Program getter method."""
        return self._program

    def args(self):
        """Program argument getter method."""
        return self._args

    # noinspection PyUnresolvedReferences
    def start_execution(self, workdir=None):
        """Starts the execution of a command in a QProcess.

        Args:
            workdir (str, optional): Work directory
        """
        if workdir is not None:
            self._process.setWorkingDirectory(workdir)
        self._process.started.connect(self.process_started)
        self._process.finished.connect(self.on_process_finished)
        if not self._silent and not self._semisilent:  # Loud
            self._process.readyReadStandardOutput.connect(self.on_ready_stdout)
            self._process.readyReadStandardError.connect(self.on_ready_stderr)
            self._process.errorOccurred.connect(self.on_process_error)
            self._process.stateChanged.connect(self.on_state_changed)
        elif self._semisilent:  # semi-silent
            self._process.readyReadStandardOutput.connect(self.on_ready_stdout)
            self._process.readyReadStandardError.connect(self.on_ready_stderr)
        self._process.start(self._program, self._args)
        if self._process is not None and not self._process.waitForStarted(msecs=10000):
            self.process_failed = True
            self.process_failed_to_start = True
            self._process.deleteLater()
            self._process = None
            self.execution_finished.emit(-9998)

    def wait_for_process_finished(self, msecs=30000):
        """Wait for subprocess to finish.

        Args:
            msecs (int): Timeout in milliseconds

        Return:
            True if process finished successfully, False otherwise
        """
        if self._process is None:
            return False
        if self.process_failed or self.process_failed_to_start:
            return False
        if not self._process.waitForFinished(msecs):
            self.process_failed = True
            self._process.close()
            self._process = None
            return False
        return True

    @Slot()
    def process_started(self):
        """Run when subprocess has started."""

    @Slot(int)
    def on_state_changed(self, new_state):
        """Runs when QProcess state changes.

        Args:
            new_state (int): Process state number (``QProcess::ProcessState``)
        """
        if new_state == QProcess.Starting:
            self._logger.msg.emit("\tStarting program <b>{0}</b>".format(self._program))
            arg_str = " ".join(self._args)
            self._logger.msg.emit("\tArguments: <b>{0}</b>".format(arg_str))
        elif new_state == QProcess.Running:
            self._logger.msg_warning.emit("\tExecution in progress...")
        elif new_state == QProcess.NotRunning:
            # logging.debug("Process is not running")
            pass
        else:
            self._logger.msg_error.emit("Process is in an unspecified state")
            logging.error("QProcess unspecified state: %s", new_state)

    @Slot(int)
    def on_process_error(self, process_error):
        """Runs if there is an error in the running QProcess.

        Args:
            process_error (int): Process error number (``QProcess::ProcessError``)
        """
        if process_error == QProcess.FailedToStart:
            self.process_failed = True
            self.process_failed_to_start = True
            self._logger.msg_error.emit("Process failed to start")
        elif process_error == QProcess.Timedout:
            self.process_failed = True
            self._logger.msg_error.emit("Timed out")
        elif process_error == QProcess.Crashed:
            self.process_failed = True
            if not self.user_stopped:
                self._logger.msg_error.emit("Process crashed")
        elif process_error == QProcess.WriteError:
            self._logger.msg_error.emit("Process WriteError")
        elif process_error == QProcess.ReadError:
            self._logger.msg_error.emit("Process ReadError")
        elif process_error == QProcess.UnknownError:
            self._logger.msg_error.emit("Unknown error in process")
        else:
            self._logger.msg_error.emit("Unspecified error in process: {0}".format(process_error))
        self.teardown_process()

    def teardown_process(self):
        """Tears down the QProcess in case a QProcess.ProcessError occurred.
        Emits execution_finished signal."""
        if not self._process:
            pass
        else:
            out = str(self._process.readAllStandardOutput().data(), "utf-8", errors="replace")
            errout = str(self._process.readAllStandardError().data(), "utf-8", errors="replace")
            if out is not None:
                self._logger.msg_proc.emit(out.strip())
            if errout is not None:
                self._logger.msg_proc.emit(errout.strip())
            self._process.deleteLater()
            self._process = None
        self.execution_finished.emit(-9998)

    def stop_execution(self):
        """See base class."""
        self.user_stopped = True
        self.process_failed = True
        if not self._process:
            return
        try:
            self._process.kill()
            if not self._process.waitForFinished(5000):
                self._process.finished.emit(-1, -1)
                self._process.deleteLater()
        except Exception as ex:  # pylint: disable=broad-except
            self._logger.msg_error.emit("[{0}] exception when terminating process".format(ex))
            logging.exception("Exception in closing QProcess: %s", ex)
        finally:
            self._process = None

    @Slot(int, int)
    def on_process_finished(self, exit_code, exit_status):
        """Runs when subprocess has finished.

        Args:
            exit_code (int): Return code from external program (only valid for normal exits)
            exit_status (int): Crash or normal exit (``QProcess::ExitStatus``)
        """
        if not self._process:
            return
        if exit_status == QProcess.CrashExit:
            if not self._silent:
                self._logger.msg_error.emit("\tProcess crashed")
            exit_code = -1
        elif exit_status == QProcess.NormalExit:
            pass
        else:
            if not self._silent:
                self._logger.msg_error.emit("Unknown QProcess exit status [{0}]".format(exit_status))
            exit_code = -1
        if not exit_code == 0:
            self.process_failed = True
        if not self.user_stopped:
            out = str(self._process.readAllStandardOutput().data(), "utf-8", errors="replace")
            errout = str(self._process.readAllStandardError().data(), "utf-8", errors="replace")
            if out is not None:
                if not self._silent:
                    self._logger.msg_proc.emit(out.strip())
                else:
                    self.process_output = out.strip()
                    self.process_error = errout.strip()
        else:
            self._logger.msg.emit("*** Terminating process ***")
        # Delete QProcess
        self._process.deleteLater()
        self._process = None
        self.execution_finished.emit(exit_code)

    @Slot()
    def on_ready_stdout(self):
        """Emit data from stdout."""
        if not self._process:
            return
        self._process.setReadChannel(QProcess.StandardOutput)
        chunk = self._process.readLine().data()
        self._out_chunks.append(chunk)
        if not chunk.endswith(b"\n"):
            return
        line = b"".join(self._out_chunks)
        line = str(line, "unicode_escape", errors="replace").strip()
        self._logger.msg_proc.emit(line)
        self._out_chunks.clear()

    @Slot()
    def on_ready_stderr(self):
        """Emit data from stderr."""
        if not self._process:
            return
        self._process.setReadChannel(QProcess.StandardError)
        chunk = self._process.readLine().data()
        self._err_chunks.append(chunk)
        if not chunk.endswith(b"\n"):
            return
        line = b"".join(self._err_chunks)
        line = str(line, "utf-8", errors="replace").strip()
        self._logger.msg_proc_error.emit(line)
        self._err_chunks.clear()
예제 #2
0
class QKonsol(QPlainTextEdit):

    userTextEntry = ""
    commandList = ["cd " + QDir.homePath()]
    length = 0
    history = -1

    def __init__(self, parent=None):
        super().__init__()
        self.setParent(parent)
        self.setWindowTitle(self.tr("Terminal"))
        self.setCursorWidth(7)
        self.setContextMenuPolicy(Qt.NoContextMenu)
        font = self.font()
        font.setFamily("Consolas")
        font.setPointSize(10)
        self.setFont(font)
        self.setUndoRedoEnabled(False)
        palette = QPalette()
        palette.setColor(QPalette.Base, Qt.black)
        palette.setColor(QPalette.Text, Qt.white)
        palette.setColor(QPalette.Highlight, Qt.white)
        palette.setColor(QPalette.HighlightedText, Qt.black)
        self.setFrameShape(QFrame.NoFrame)
        self.setPalette(palette)
        self.resize(720, 480)

        self.process = QProcess(self)
        self.process.setProcessChannelMode(QProcess.MergedChannels)
        self.process.setReadChannel(QProcess.StandardOutput)

        self.process.readyReadStandardOutput.connect(self.readStandartOutput)
        self.process.readyReadStandardError.connect(
            lambda: print(self.readAllStandartError()))
        self.cursorPositionChanged.connect(self.cursorPosition)
        self.textChanged.connect(self.whatText)

        if sys.platform == "win32":
            self.process.start("cmd.exe", [], mode=QProcess.ReadWrite)

        else:
            self.process.start(
                "bash", ["-i"],
                mode=QProcess.ReadWrite)  # bash -i interactive mode

    def readStandartOutput(self):
        if sys.platform == "win32":
            st = self.process.readAllStandardOutput().data().decode(
                str(ctypes.cdll.kernel32.GetConsoleOutputCP()))

        else:
            st = self.process.readAllStandardOutput().data().decode("utf-8")

        # print(repr(st), self.commandList)
        if not st.startswith(self.commandList[-1]):
            self.appendPlainText(st)

    def __line_end(self):
        if sys.platform == "win32":
            return "\r\n"

        elif sys.platform == "linux":
            return "\n"

        elif sys.platform == "darwin":
            return "\r"

    def keyPressEvent(self, event: QKeyEvent):

        if event.key() in (Qt.Key_Enter, Qt.Key_Return):
            if self.commandList[
                    -1] != self.userTextEntry and self.userTextEntry != "":
                self.commandList.append(self.userTextEntry)

            self.length = len(self.userTextEntry + self.__line_end())
            self.process.writeData(self.userTextEntry + self.__line_end(),
                                   self.length)
            self.userTextEntry = ""

        elif event.key() == Qt.Key_Backspace:
            if self.userTextEntry == "":
                return

            else:
                self.userTextEntry = self.userTextEntry[:-1]
                super().keyPressEvent(event)

        elif event.key() == Qt.Key_Up:
            if -len(self.commandList) < self.history:
                self.history -= 1
                print(self.commandList[self.history])
            return

        elif event.key() == Qt.Key_Down:
            if self.history < -1:
                self.history += 1
                print(self.commandList[self.history])
            return

        elif event.key() == Qt.Key_Delete:
            return

        elif event.modifiers() == Qt.ControlModifier:
            super().keyPressEvent(event)

        else:
            super().keyPressEvent(event)
            self.userTextEntry += event.text()

    def cursorPosition(self):
        pass

    # print(self.textCursor().position())

    def whatText(self):
        pass

    # print(self.blockCount())

    def insertFromMimeData(self, source):
        super().insertFromMimeData(source)
        self.userTextEntry += source.text()

    def mouseReleaseEvent(self, event):
        super().mousePressEvent(event)
        cur = self.textCursor()

        if event.button() == Qt.LeftButton:
            cur.movePosition(QTextCursor.End, QTextCursor.MoveAnchor, 1)
            self.setTextCursor(cur)

    def closeEvent(self, event):
        self.process.close()