123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684 |
- ########################################################################
- # Searx-Qt - Lightweight desktop application for Searx.
- # Copyright (C) 2020-2022 CYBERDEViL
- #
- # This file is part of Searx-Qt.
- #
- # Searx-Qt is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # Searx-Qt is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <https://www.gnu.org/licenses/>.
- #
- ########################################################################
- from requests.status_codes import _codes as StatusCodes
- from PyQt5.QtWidgets import (
- QAbstractItemView,
- QCheckBox,
- QComboBox,
- QDialog,
- QFormLayout,
- QHBoxLayout,
- QHeaderView,
- QLabel,
- QMessageBox,
- QMenu,
- QSpinBox,
- QTableWidget,
- QTableWidgetItem,
- QTabWidget,
- QVBoxLayout,
- QWidget
- )
- from PyQt5.QtGui import QGuiApplication
- from PyQt5.QtCore import Qt, QVariant, QItemSelectionModel
- from searxqt.core.requests import ErrorType as RequestsErrorTypes
- from searxqt.core.requests import ErrorTypeStr as RequestsErrorTypeStr
- from searxqt.core.guard import ConsequenceType, ConsequenceTypeStr
- from searxqt.widgets.buttons import Button
- from searxqt.translations import _, timeToString, durationMinutesToString
- class GuardSettings(QWidget):
- def __init__(self, guard, parent=None):
- QWidget.__init__(self, parent=parent)
- self.__guard = guard
- layout = QVBoxLayout(self)
- # Title label
- layout.addWidget(QLabel(f"<h2>{_('Guard')}</h2>", self))
- # Info label
- infoLabel = QLabel(
- _("Guard can put failing instances on the blacklist or on a"
- " timeout based on set rules below."),
- self
- )
- infoLabel.setWordWrap(True)
- layout.addWidget(infoLabel)
- # Enable checkbox
- self.__enableCheck = QCheckBox(_("Enable guard"), self)
- layout.addWidget(self.__enableCheck)
- # Store log checkbox
- self.__storeLogCheck = QCheckBox(_("Store log"), self)
- layout.addWidget(self.__storeLogCheck)
- # Max log period
- self.__maxLogPeriod = QSpinBox(self)
- self.__maxLogPeriod.setSuffix(_(" days"))
- self.__maxLogPeriod.setMinimum(1)
- self.__maxLogPeriod.setMaximum(999)
- self.__maxLogPeriod.setEnabled(False)
- layout.addWidget(self.__maxLogPeriod, 0, Qt.AlignLeft)
- # Tab widget
- self.__tabWidget = QTabWidget(self)
- self.__tabWidget.setTabShape(QTabWidget.Triangular)
- self.__tabWidget.setTabPosition(QTabWidget.West)
- layout.addWidget(self.__tabWidget)
- # Rule editor
- self.__ruleEditor = GuardRuleEditor(guard, self)
- self.__tabWidget.addTab(self.__ruleEditor, _("Rules"))
- # Log viewer
- self.__logViewer = GuardLogViewer(guard, self)
- self.__tabWidget.addTab(self.__logViewer, _("Log"))
- # Initial state
- self.__enableCheck.setChecked(self.__guard.isEnabled())
- self.__storeLogCheck.setChecked(self.__guard.doesStoreLog())
- self.__maxLogPeriod.setEnabled(self.__guard.doesStoreLog())
- self.__maxLogPeriod.setValue(self.__guard.maxLogPeriod())
- # Connections
- self.__enableCheck.stateChanged.connect(self.__enableCheckStateChange)
- self.__storeLogCheck.stateChanged.connect(self.__storeLogStateChange)
- self.__maxLogPeriod.valueChanged.connect(self.__logPeriodChanged)
- def __enableCheckStateChange(self, state):
- self.__guard.setEnabled(bool(state))
- if not state:
- # Guard disabled; ask to clear the log (when any present)
- if self.__guard.log():
- answer = QMessageBox.question(
- self,
- _("Clear log?"),
- _("You've disabled Guard but there are currently logs"
- " present. Do you want to clear the log?"),
- QMessageBox.Yes | QMessageBox.No
- )
- if answer == QMessageBox.Yes:
- self.__logViewer.clearLog()
- def __storeLogStateChange(self, state):
- state = bool(state)
- self.__guard.setStoreLog(state)
- self.__maxLogPeriod.setEnabled(state)
- def __logPeriodChanged(self, value):
- self.__guard.setMaxLogPeriod(value)
- class GuardRuleEditor(QWidget):
- def __init__(self, guard, parent=None):
- QWidget.__init__(self, parent=parent)
- self.__guard = guard
- layout = QVBoxLayout(self)
- toolBar = QWidget(self)
- toolBarLayout = QHBoxLayout(toolBar)
- self.__addButton = Button(_("Add"), self)
- self.__editButton = Button(_("Edit"), self)
- self.__editButton.setEnabled(False)
- self.__delButton = Button(_("Del"), self)
- self.__delButton.setEnabled(False)
- self.__upButton = Button("▲", self)
- self.__upButton.setToolTip(_("Move rule up."))
- self.__downButton = Button("▼", self)
- self.__downButton.setToolTip(_("Move rule down."))
- toolBarLayout.addWidget(self.__addButton, 0, Qt.AlignLeft)
- toolBarLayout.addWidget(self.__editButton, 0, Qt.AlignLeft)
- toolBarLayout.addWidget(self.__delButton, 0, Qt.AlignLeft)
- toolBarLayout.addWidget(self.__upButton, 0, Qt.AlignLeft)
- toolBarLayout.addWidget(self.__downButton, 1, Qt.AlignLeft)
- self.__rulesTable = QTableWidget(self)
- self.__rulesTable.setEditTriggers(QAbstractItemView.NoEditTriggers)
- self.__rulesTable.setSelectionBehavior(QAbstractItemView.SelectRows)
- self.__rulesTable.setSelectionMode(QAbstractItemView.SingleSelection)
- layout.addWidget(toolBar)
- layout.addWidget(self.__rulesTable)
- # Fill the table
- self.updateTable()
- # Connections
- self.__addButton.clicked.connect(self.__addRuleDialog)
- self.__editButton.clicked.connect(self.__editSelectedRule)
- self.__delButton.clicked.connect(self.__delSelectedRule)
- self.__upButton.clicked.connect(self.moveSelectedUp)
- self.__downButton.clicked.connect(self.moveSelectedDown)
- self.__rulesTable.itemSelectionChanged.connect(self.__selectionChanged)
- def moveSelected(self, amount=-1):
- """
- @param amount: index offset to move.
- @type amount: int
- """
- # move the selected rule.
- index = self.__rulesTable.currentIndex().row()
- toIndex = index + amount
- self.__guard.moveRule(index, toIndex)
- # update the view.
- self.updateTable()
- # re-select the selected rule on new index.
- item = self.__rulesTable.item(toIndex, 0)
- self.__rulesTable.setCurrentItem(
- item,
- QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent
- )
- def moveSelectedUp(self):
- self.moveSelected(-1)
- def moveSelectedDown(self):
- self.moveSelected(1)
- def updateTable(self):
- self.__rulesTable.clear()
- # Columns
- # - errorType
- # - amount
- # - period
- # - statusCode
- # - destinationType
- # - duration
- self.__rulesTable.setColumnCount(6)
- self.__rulesTable.setHorizontalHeaderLabels([
- "Error Type",
- "Amount",
- "Timeframe",
- "Status",
- "Destination",
- "Duration"
- ])
- rules = self.__guard.rules()
- self.__rulesTable.setRowCount(len(rules))
- index = 0
- for rule in rules:
- self.updateRuleIndex(
- index,
- rule.condition.errorType,
- rule.condition.amount,
- rule.condition.period,
- rule.condition.status,
- rule.consequence.type,
- rule.consequence.duration
- )
- index += 1
- def __delSelectedRule(self):
- index = self.__rulesTable.currentRow()
- self.__guard.delRule(index)
- self.__rulesTable.removeRow(index)
- def __editSelectedRule(self):
- # Get data of selected rule.
- rowIndex = self.__rulesTable.currentRow()
- errorType = self.__rulesTable.item(rowIndex, 0).data(Qt.UserRole)
- amount = self.__rulesTable.item(rowIndex, 1).data(Qt.UserRole)
- period = self.__rulesTable.item(rowIndex, 2).data(Qt.UserRole)
- statusCode = self.__rulesTable.item(rowIndex, 3).data(Qt.UserRole)
- destinationType = self.__rulesTable.item(rowIndex, 4).data(Qt.UserRole)
- duration = self.__rulesTable.item(rowIndex, 5).data(Qt.UserRole)
- # Open edit dialog with data from the selected rule.
- dialog = GuardEditRuleDialog(
- errorType=errorType,
- amount=amount,
- period=period,
- statusCode=statusCode,
- destinationType=destinationType,
- duration=duration
- )
- if dialog.exec():
- # Accept button is pressed; update the data.
- rule = self.__guard.getRule(rowIndex)
- rule.condition.errorType = dialog.errorType()
- rule.condition.amount = dialog.amount()
- rule.condition.period = dialog.period()
- rule.condition.status = dialog.statusCode()
- rule.consequence.type = dialog.destinationType()
- rule.consequence.duration = dialog.duration()
- self.updateTable() # Re-generate the table.
- def __selectionChanged(self):
- # When no rule is selected disable the edit and del buttons.
- if self.__rulesTable.currentItem():
- self.__editButton.setEnabled(True)
- self.__delButton.setEnabled(True)
- # Only enable up when the item can go up (index > 0)
- currentIndex = self.__rulesTable.currentIndex().row()
- if currentIndex:
- self.__upButton.setEnabled(True)
- else:
- self.__upButton.setEnabled(False)
- # Only enable down when the item can go down
- if currentIndex < self.__rulesTable.rowCount() - 1:
- self.__downButton.setEnabled(True)
- else:
- self.__downButton.setEnabled(False)
- else:
- self.__editButton.setEnabled(False)
- self.__delButton.setEnabled(False)
- self.__upButton.setEnabled(False)
- self.__downButton.setEnabled(False)
- def updateRuleIndex(
- self, index, errorType, amount,
- period, statusCode, destinationType, duration
- ):
- # errorType
- tableItem = QTableWidgetItem(RequestsErrorTypeStr[errorType])
- tableItem.setData(Qt.UserRole, QVariant(errorType))
- self.__rulesTable.setItem(index, 0, tableItem)
- # amount
- str_ = str(amount) + "x"
- tableItem = QTableWidgetItem(str_)
- tableItem.setData(Qt.UserRole, QVariant(amount))
- self.__rulesTable.setItem(index, 1, tableItem)
- # period
- if period:
- str_ = durationMinutesToString(period)
- else:
- str_ = _("Always")
- tableItem = QTableWidgetItem(str_)
- tableItem.setData(Qt.UserRole, QVariant(period))
- self.__rulesTable.setItem(index, 2, tableItem)
- # statusCode
- if statusCode:
- str_ = str(statusCode)
- else:
- str_ = "-"
- tableItem = QTableWidgetItem(str_)
- tableItem.setData(Qt.UserRole, QVariant(statusCode))
- self.__rulesTable.setItem(index, 3, tableItem)
- # destinationType
- tableItem = QTableWidgetItem(ConsequenceTypeStr[destinationType])
- tableItem.setData(Qt.UserRole, QVariant(destinationType))
- self.__rulesTable.setItem(index, 4, tableItem)
- # duration
- if duration:
- str_ = durationMinutesToString(duration)
- elif destinationType == ConsequenceType.Blacklist:
- str_ = "-"
- else:
- str_ = _("Until restart")
- tableItem = QTableWidgetItem(str_)
- tableItem.setData(Qt.UserRole, QVariant(duration))
- self.__rulesTable.setItem(index, 5, tableItem)
- def __addRuleDialog(self):
- dialog = GuardEditRuleDialog()
- if dialog.exec():
- # errorType
- # amount
- # period
- # statusCode
- # destinationType
- # duration
- rowIndex = self.__rulesTable.rowCount()
- self.__rulesTable.setRowCount(rowIndex + 1)
- # Add view
- self.updateRuleIndex(
- rowIndex,
- dialog.errorType(),
- dialog.amount(),
- dialog.period(),
- dialog.statusCode(),
- dialog.destinationType(),
- dialog.duration()
- )
- # Add the rule itself to Guard
- self.__guard.addRule(
- dialog.errorType(),
- dialog.destinationType(),
- amount=dialog.amount(),
- period=dialog.period(),
- status=dialog.statusCode(),
- duration=dialog.duration()
- )
- class GuardEditRuleDialog(QDialog):
- def __init__(
- self,
- errorType=RequestsErrorTypes.WrongStatus,
- amount=0,
- period=0,
- statusCode=None,
- destinationType=ConsequenceType.Timeout,
- duration=0,
- parent=None
- ):
- QDialog.__init__(self, parent=parent)
- self.setWindowTitle(_("Guard rule editor"))
- layout = QFormLayout(self)
- # errorType QComboBox
- # amount QSpinBox
- # period QSpinBox
- # status QComboBox
- # destination QComboBox
- # duration QSpinBox
- # Error type
- self.__errorTypeCombo = QComboBox(self)
- for errType, errTypeStr in RequestsErrorTypeStr.items():
- # Skip Success and ProxyError
- if (errType == RequestsErrorTypes.Success or
- errType == RequestsErrorTypes.ProxyError):
- continue
- self.__errorTypeCombo.addItem(errTypeStr, QVariant(errType))
- index = self.__errorTypeCombo.findData(QVariant(errorType))
- self.__errorTypeCombo.setCurrentIndex(index)
- layout.addRow(QLabel(_("Error type") + ":"), self.__errorTypeCombo)
- # Amount of failes to trigger.
- self.__amountSpin = QSpinBox(self)
- self.__amountSpin.setRange(0, 64)
- self.__amountSpin.setValue(amount)
- label = QLabel(_("Amount of failes to trigger") + ":")
- label.setWordWrap(True)
- layout.addRow(label, self.__amountSpin)
- # Timeframe in minutes where the amount of failes have to occur in.
- self.__periodSpin = QSpinBox(self)
- self.__periodSpin.setSuffix(_("min"))
- self.__periodSpin.setRange(0, 6000)
- self.__periodSpin.setValue(period)
- label = QLabel(
- _("Timeframe in minutes where the amount of failes have to occur"
- " in") + ":"
- )
- label.setWordWrap(True)
- layout.addRow(label, self.__periodSpin)
- # Status code
- self.__statusCodeCombo = QComboBox(self)
- self.__statusCodeCombo.addItem(_("All"), QVariant(None))
- for code, statusStr in StatusCodes.items():
- if code >= 400:
- self.__statusCodeCombo.addItem(
- str(code) + " - " + statusStr[0],
- QVariant(code)
- )
- index = self.__statusCodeCombo.findData(QVariant(statusCode))
- self.__statusCodeCombo.setCurrentIndex(index)
- label = QLabel(_("Response status code") + ":")
- label.setWordWrap(True)
- layout.addRow(label, self.__statusCodeCombo)
- # Destination
- self.__destinationCombo = QComboBox(self)
- for destType, destStr in ConsequenceTypeStr.items():
- self.__destinationCombo.addItem(destStr, QVariant(destType))
- index = self.__destinationCombo.findData(QVariant(destinationType))
- self.__destinationCombo.setCurrentIndex(index)
- label = QLabel(_("Destination") + ":")
- label.setWordWrap(True)
- layout.addRow(label, self.__destinationCombo)
- # Duration
- self.__durationSpin = QSpinBox(self)
- self.__durationSpin.setSuffix(_("min"))
- self.__durationSpin.setRange(0, 6000)
- self.__durationSpin.setValue(duration)
- label = QLabel(_("Duration of the timeout") + ":")
- label.setWordWrap(True)
- layout.addRow(label, self.__durationSpin)
- # Cancel/Save button
- cancelButton = Button(_("Cancel"), self)
- saveButton = Button(_("Save"), self)
- layout.addRow(cancelButton, saveButton)
- # Initial enable / disable errorType/destination combo
- self.__errorTypeChanged(self.__errorTypeCombo.currentIndex())
- self.__destinationChanged(self.__destinationCombo.currentIndex())
- # Connections
- self.__errorTypeCombo.currentIndexChanged.connect(
- self.__errorTypeChanged
- )
- self.__destinationCombo.currentIndexChanged.connect(
- self.__destinationChanged
- )
- cancelButton.clicked.connect(self.reject)
- saveButton.clicked.connect(self.accept)
- def errorType(self):
- return self.__errorTypeCombo.currentData()
- def amount(self):
- return self.__amountSpin.value()
- def period(self):
- return self.__periodSpin.value()
- def statusCode(self):
- errorType = self.__errorTypeCombo.currentData()
- if errorType == RequestsErrorTypes.WrongStatus:
- return self.__statusCodeCombo.currentData()
- return None
- def destinationType(self):
- return self.__destinationCombo.currentData()
- def duration(self):
- destType = self.__destinationCombo.currentData()
- if destType == ConsequenceType.Timeout:
- return self.__durationSpin.value()
- return 0
- def __errorTypeChanged(self, index):
- # The status code combobox should only be enabled when the WrongStatus
- # errorType is selected.
- errorType = self.__errorTypeCombo.itemData(index)
- if errorType == RequestsErrorTypes.WrongStatus:
- self.__statusCodeCombo.setEnabled(True)
- else:
- self.__statusCodeCombo.setEnabled(False)
- def __destinationChanged(self, index):
- # The duration spinbox should only be enabled when the destinationType
- # is Timeout.
- destType = self.__destinationCombo.itemData(index)
- if destType == ConsequenceType.Timeout:
- self.__durationSpin.setEnabled(True)
- else:
- self.__durationSpin.setEnabled(False)
- class GuardLogViewer(QWidget):
- def __init__(self, guard, parent=None):
- QWidget.__init__(self, parent=parent)
- self.__guard = guard
- layout = QVBoxLayout(self)
- layout.addWidget(QLabel("<h2>Log</h2>", self), 0, Qt.AlignTop)
- # Toolbar
- buttonLayout = QHBoxLayout()
- self.__refreshButton = Button(_("Refresh"), self)
- buttonLayout.addWidget(self.__refreshButton, 0, Qt.AlignLeft)
- self.__clearButton = Button(_("Clear"), self)
- buttonLayout.addWidget(self.__clearButton, 1, Qt.AlignLeft)
- layout.addLayout(buttonLayout)
- # Table
- self.__logTable = QTableWidget(self)
- self.__logTable.setEditTriggers(QAbstractItemView.NoEditTriggers)
- self.__logTable.setSelectionBehavior(QAbstractItemView.SelectRows)
- self.__logTable.setContextMenuPolicy(Qt.CustomContextMenu)
- layout.addWidget(self.__logTable)
- self.__refreshButton.clicked.connect(self.updateTable)
- self.__clearButton.clicked.connect(self.__clearLogPressed)
- self.__logTable.customContextMenuRequested.connect(self.__contextMenu)
- self.updateTable()
- def __contextMenu(self, pos):
- selectionModel = self.__logTable.selectionModel()
- if selectionModel.hasSelection():
- indexes = selectionModel.selectedRows(column=1)
- menu = QMenu(self)
- # Copy URL
- copyUrlAction = menu.addAction(_("Copy URL"))
- # Clear log entries for instance(s)
- removeInstanceEntriesAction = menu.addAction(
- _("Clear log for selected instance(s)")
- )
- # Exec the menu
- action = menu.exec_(self.__logTable.mapToGlobal(pos))
- if action == copyUrlAction:
- index = indexes[0]
- item = self.__logTable.item(index.row(), index.column())
- url = item.data(Qt.UserRole)
- clipboard = QGuiApplication.clipboard()
- clipboard.setText(url)
- elif action == removeInstanceEntriesAction:
- removedInstances = []
- for modelIndex in indexes:
- item = self.__logTable.item(
- modelIndex.row(), modelIndex.column()
- )
- url = item.data(Qt.UserRole)
- if url in removedInstances:
- continue
- self.__guard.clearInstanceLog(url)
- removedInstances.append(url)
- self.updateTable()
- def __clearLogPressed(self):
- answer = QMessageBox.question(
- self,
- _("Confirm clear log"),
- _("Clear log?"),
- QMessageBox.Yes | QMessageBox.No
- )
- if answer == QMessageBox.Yes:
- self.clearLog()
- def clearLog(self):
- self.__guard.clearLog()
- self.updateTable()
- def updateTable(self):
- # Disable sorting, else it will mess up when already sorted.
- self.__logTable.setSortingEnabled(False)
- self.__logTable.clear()
- self.__logTable.setColumnCount(5)
- self.__logTable.setHorizontalHeaderLabels([
- "datetime",
- "instance",
- "error",
- "status",
- "content"
- ])
- self.__logTable.horizontalHeader().setStretchLastSection(True)
- self.__logTable.horizontalHeader().setSectionResizeMode(
- QHeaderView.ResizeToContents
- )
- # Calc row count
- rowCount = 0
- log = self.__guard.log()
- for url in log:
- for errType in log[url]:
- rowCount += len(log[url][errType])
- self.__logTable.setRowCount(rowCount)
- index = 0
- for url in log:
- for errType in log[url]:
- errCount = len(log[url][errType])
- for logTime, status, content in log[url][errType]:
- # Datetime
- tableItem = QTableWidgetItem(
- timeToString(logTime * 60, fmt="%D %H:%M:%S")
- )
- tableItem.setData(Qt.UserRole, QVariant(logTime))
- self.__logTable.setItem(index, 0, tableItem)
- # Url
- tableItem = QTableWidgetItem(url)
- tableItem.setData(Qt.UserRole, QVariant(url))
- tableItem.setToolTip(str(errCount))
- self.__logTable.setItem(index, 1, tableItem)
- # Error
- tableItem = QTableWidgetItem(RequestsErrorTypeStr[errType])
- self.__logTable.setItem(index, 2, tableItem)
- # Status
- tableItem = QTableWidgetItem(str(status))
- if status:
- tableItem.setToolTip(str(StatusCodes[status]))
- self.__logTable.setItem(index, 3, tableItem)
- # Content
- tableItem = QTableWidgetItem(str(content))
- self.__logTable.setItem(index, 4, tableItem)
- index += 1
- # Enable sorting again (restore the set sorting.)
- self.__logTable.setSortingEnabled(True)
|