Source code for magboltz_gui.util.process
"""Manage one live Magboltz subprocess connected to the GUI."""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
from PyQt6.QtCore import QProcess
if TYPE_CHECKING:
from magboltz_gui.window.main_window import MagboltzGUI
[docs]
class ProcessManager:
"""Own one ``QProcess`` and bridge it with the main window state."""
def __init__(
self,
main_window: MagboltzGUI,
*,
input_file: Path,
cleanup_input_file: bool = False,
) -> None:
self.main_window = main_window
self.input_file = input_file
self.cleanup_input_file = cleanup_input_file
self._stdout_buffer: list[str] = []
[docs]
def run(self) -> None:
"""Create the process, wire its signals, and launch it."""
self.process = QProcess(self.main_window)
# Connect QProcess signals
self.process.readyReadStandardOutput.connect(self.handle_stdout)
self.process.readyReadStandardError.connect(self.handle_stderr)
self.process.finished.connect(self.process_finished)
self.run_process()
[docs]
def stop(self) -> None:
"""Attempt a graceful stop, then fall back to killing the process."""
try:
self.process.terminate()
except Exception:
try:
self.process.kill()
except Exception:
pass
[docs]
def run_process(self) -> None:
"""Start ``magboltz`` and stream the current input card to stdin."""
if not self.input_file.is_file():
self.main_window.show_error("Error", f"Input file not found:\n{self.input_file}")
self._finish_without_start()
return
cmd = str(self.main_window.magboltzPath) if self.main_window.magboltzPath is not None else "magboltz"
self.process.start(cmd)
self.main_window.consoleOutput.clear()
self.main_window.consoleOutput.append(f"<span style='color:blue;'>{cmd} < {self.input_file}</span>")
self.main_window.consoleOutput.append(f"")
if self.cleanup_input_file:
self.main_window.consoleOutput.append("Input was not saved; running from a temporary input card.")
self.main_window.consoleOutput.append("Starting process...")
with self.input_file.open("r", encoding="utf-8") as f:
for line in f.readlines():
self.process.write(f"{line}\n".encode("utf-8"))
def _finish_without_start(self) -> None:
"""Restore window state when the process cannot be started."""
if self.cleanup_input_file:
self.input_file.unlink(missing_ok=True)
if self in self.main_window.processes:
self.main_window.processes.remove(self)
if not self.main_window.processes:
self.main_window._set_running_state(False)
[docs]
def handle_stdout(self) -> None:
"""Append standard output to the console view and capture buffer."""
data = self.process.readAllStandardOutput().data()
text = data.decode("utf-8")
self._stdout_buffer.append(text)
self.main_window.consoleOutput.append(text)
[docs]
def handle_stderr(self) -> None:
"""Append standard error to the console view."""
data = self.process.readAllStandardError().data()
text = data.decode("utf-8")
self.main_window.consoleOutput.append(f"<span style='color:red;'>{text}</span>")
[docs]
def process_finished(self) -> None:
"""Parse the finished run and update export/plot availability."""
self.main_window.consoleOutput.append("")
self.main_window.consoleOutput.append("Process finished.")
stdout_text = "".join(self._stdout_buffer)
try:
from magboltz_gui.util.output_parser import parse_magboltz_output
input_text = None
input_path = str(self.input_file)
if self.input_file.is_file():
with self.input_file.open("r", encoding="utf-8") as f:
input_text = f.read()
self.main_window._last_run_result = parse_magboltz_output(
stdout_text=stdout_text, input_text=input_text, input_path=input_path
)
self.main_window.actionResultExport.setEnabled(True)
self.main_window.btnResultExport.setEnabled(True)
self.main_window.actionShowPlots.setEnabled(True)
self.main_window.btnResultPlots.setEnabled(True)
except Exception as exc:
self.main_window.show_error("Parse error", f"Failed to parse Magboltz output: {exc}")
finally:
if self.cleanup_input_file:
self.input_file.unlink(missing_ok=True)
self.main_window.processes.remove(self)
if not self.main_window.processes:
self.main_window._set_running_state(False)