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()
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()