164 lines
6.2 KiB
Python
164 lines
6.2 KiB
Python
"""
|
|
Process list viewer — shows running server processes with kill capability.
|
|
Auto-refresh support with configurable interval.
|
|
"""
|
|
from PyQt6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
|
QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox,
|
|
QMessageBox,
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
|
|
|
|
from app.utils.worker import SchemaWorker
|
|
|
|
|
|
class ProcessListPanel(QWidget):
|
|
status_message = pyqtSignal(str)
|
|
|
|
def __init__(self, driver, connection_name: str = "", parent=None):
|
|
super().__init__(parent)
|
|
self._driver = driver
|
|
self._connection_name = connection_name
|
|
self._refresh_timer = QTimer(self)
|
|
self._refresh_timer.timeout.connect(self._refresh)
|
|
self._build_ui()
|
|
self._refresh()
|
|
|
|
# ── UI ────────────────────────────────────────────────────────────────────
|
|
|
|
def _build_ui(self):
|
|
root = QVBoxLayout(self)
|
|
root.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Header bar
|
|
hdr = QHBoxLayout()
|
|
hdr.setContentsMargins(8, 6, 8, 4)
|
|
|
|
title = QLabel(f"⚙️ Process List — {self._connection_name}")
|
|
title.setObjectName("structureTitle")
|
|
f = title.font()
|
|
f.setBold(True)
|
|
f.setPointSize(12)
|
|
title.setFont(f)
|
|
|
|
self._auto_cb = QCheckBox("Auto-refresh (5s)")
|
|
self._auto_cb.toggled.connect(self._toggle_auto_refresh)
|
|
|
|
self._refresh_btn = QPushButton("🔄 Refresh")
|
|
self._refresh_btn.setFixedWidth(90)
|
|
self._refresh_btn.clicked.connect(self._refresh)
|
|
|
|
self._kill_btn = QPushButton("🛑 Kill Process")
|
|
self._kill_btn.setObjectName("deleteBtn")
|
|
self._kill_btn.setEnabled(False)
|
|
self._kill_btn.clicked.connect(self._kill_selected)
|
|
|
|
hdr.addWidget(title)
|
|
hdr.addStretch()
|
|
hdr.addWidget(self._auto_cb)
|
|
hdr.addWidget(self._refresh_btn)
|
|
hdr.addWidget(self._kill_btn)
|
|
root.addLayout(hdr)
|
|
|
|
# Process table
|
|
self._table = QTableWidget(0, 0)
|
|
self._table.setAlternatingRowColors(True)
|
|
self._table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
self._table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
|
self._table.horizontalHeader().setSectionResizeMode(
|
|
QHeaderView.ResizeMode.ResizeToContents
|
|
)
|
|
self._table.horizontalHeader().setStretchLastSection(True)
|
|
self._table.verticalHeader().setDefaultSectionSize(24)
|
|
self._table.verticalHeader().setVisible(False)
|
|
self._table.itemSelectionChanged.connect(self._on_selection_changed)
|
|
root.addWidget(self._table)
|
|
|
|
# ── Data loading ──────────────────────────────────────────────────────────
|
|
|
|
def _refresh(self):
|
|
self._refresh_btn.setEnabled(False)
|
|
w = SchemaWorker(self._driver.get_process_list, parent=self)
|
|
w.result.connect(self._populate)
|
|
w.error.connect(self._on_error)
|
|
w.finished.connect(lambda: self._refresh_btn.setEnabled(True))
|
|
w.start()
|
|
|
|
def _populate(self, data):
|
|
cols, rows = data
|
|
self._table.setColumnCount(len(cols))
|
|
self._table.setHorizontalHeaderLabels([c.upper() for c in cols])
|
|
self._table.setRowCount(0)
|
|
for row in rows:
|
|
r = self._table.rowCount()
|
|
self._table.insertRow(r)
|
|
for c, val in enumerate(row):
|
|
item = QTableWidgetItem("" if val is None else str(val))
|
|
item.setTextAlignment(
|
|
Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft
|
|
)
|
|
self._table.setItem(r, c, item)
|
|
self.status_message.emit(
|
|
f"Process list: {len(rows)} process(es)"
|
|
)
|
|
|
|
def _on_error(self, msg: str):
|
|
self._table.setRowCount(1)
|
|
self._table.setColumnCount(1)
|
|
self._table.setHorizontalHeaderLabels(["Error"])
|
|
self._table.setItem(0, 0, QTableWidgetItem(msg))
|
|
self.status_message.emit(f"Process list error: {msg}")
|
|
|
|
# ── Kill ──────────────────────────────────────────────────────────────────
|
|
|
|
def _on_selection_changed(self):
|
|
self._kill_btn.setEnabled(bool(self._table.selectedItems()))
|
|
|
|
def _kill_selected(self):
|
|
row = self._table.currentRow()
|
|
if row < 0:
|
|
return
|
|
|
|
# Process ID is always the first column
|
|
pid_item = self._table.item(row, 0)
|
|
if not pid_item or not pid_item.text():
|
|
return
|
|
|
|
try:
|
|
pid = int(pid_item.text())
|
|
except ValueError:
|
|
self.status_message.emit("Could not determine process ID.")
|
|
return
|
|
|
|
btn = QMessageBox.warning(
|
|
self, "Kill Process",
|
|
f"Kill process {pid}?\n\nThis will immediately terminate the running query.",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.No,
|
|
)
|
|
if btn != QMessageBox.StandardButton.Yes:
|
|
return
|
|
|
|
w = SchemaWorker(self._driver.kill_process, pid, parent=self)
|
|
w.result.connect(lambda _: (
|
|
self.status_message.emit(f"Process {pid} killed."),
|
|
self._refresh(),
|
|
))
|
|
w.error.connect(
|
|
lambda e: QMessageBox.warning(self, "Kill Error",
|
|
f"Could not kill process {pid}:\n{e}")
|
|
)
|
|
w.start()
|
|
|
|
# ── Auto-refresh ──────────────────────────────────────────────────────────
|
|
|
|
def _toggle_auto_refresh(self, checked: bool):
|
|
if checked:
|
|
self._refresh_timer.start(5000)
|
|
else:
|
|
self._refresh_timer.stop()
|
|
|
|
def closeEvent(self, event):
|
|
self._refresh_timer.stop()
|
|
super().closeEvent(event)
|