123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560 |
- ########################################################################
- # 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/>.
- #
- ########################################################################
- """ Conditionally place failing instances on the blacklist or on a timeout (the
- temporary blacklist).
- """
- from copy import deepcopy
- from searxqt.core.requests import ErrorType as RequestErrorType
- from searxqt.utils.time import nowInMinutes
- from searxqt.translations import _
- class Condition:
- """ This describes the condition of a rule to trigger.
- """
- def __init__(
- self, errorType, amount=0,
- period=60, status=None
- ):
- """
- @param errorType: Error type to meet this conditiion.
- @type errorType: RequestErrorType
- @param amount: Amount of failed searches (in a row) of this errorType
- to meet this condition.
- @type amount: uint
- @param period: The last x minutes where the fails have to occur in.
- Set to 0 for forever.
- @type period: uint
- @param status: Status code of the failed search request. Set to None
- when irrelevant.
- @type status: uint or None
- """
- self.__errorType = errorType
- self.__amount = amount
- self.__period = period
- self.__status = status
- @property
- def errorType(self):
- return self.__errorType
- @errorType.setter
- def errorType(self, errorType):
- self.__errorType = errorType
- @property
- def amount(self):
- return self.__amount
- @amount.setter
- def amount(self, amount):
- self.__amount = amount
- @property
- def period(self):
- return self.__period
- @period.setter
- def period(self, period):
- self.__period = period
- @property
- def status(self):
- return self.__status
- @status.setter
- def status(self, status):
- self.__status = status
- def serialize(self):
- return {
- "errorType": self.__errorType,
- "amount": self.__amount,
- "period": self.__period,
- "status": self.__status
- }
- def deserialize(self, data):
- self.__errorType = data.get('errorType', RequestErrorType.Other)
- self.__amount = data.get('amount', 0)
- self.__period = data.get('period', 0)
- self.__status = data.get('status', None)
- def evaluate(self, instanceLog):
- # First check if our errorType is present in the instanceLog
- if self.__errorType not in instanceLog:
- return False
- amountPeroidCount = 0
- startTime = nowInMinutes() - self.__period
- for logTime, statusCode, errorMsg in instanceLog[self.__errorType]:
- # Response status code
- if self.__status is not None:
- # This rule has defined a specific status code.
- if self.__status != statusCode:
- # Don't count incidents that have different status code.
- continue
- # Count occurrences in time-frame.
- if self.__period:
- if logTime - startTime > 0:
- amountPeroidCount += 1
- elif self.__amount:
- amountPeroidCount += 1
- else:
- break
- return True if amountPeroidCount >= self.__amount else False
- class ConsequenceType:
- Blacklist = 0
- Timeout = 1
- ConsequenceTypeStr = {
- ConsequenceType.Blacklist: _("Blacklist"),
- ConsequenceType.Timeout: _("Timeout")
- }
- class Consequence:
- """ This describes the consequence for a instance of a rule that has it's
- condition met.
- """
- def __init__(self, type_=ConsequenceType.Timeout, duration=0):
- """
- @param consequence: Put the failing instance on the blacklist or
- timeout? See class Consequences
- @type consequence: uint
- @param duration: Only used when consequence == Consequences.Timeout. It
- is the duration in minutes the instance should be on
- timeout.
- @type duration: uint
- """
- self.__type = type_
- self.__duration = duration
- @property
- def type(self):
- return self.__type
- @type.setter
- def type(self, type_):
- self.__type = type_
- @property
- def duration(self):
- return self.__duration
- @duration.setter
- def duration(self, duration):
- self.__duration = duration
- def serialize(self):
- return {
- "type": self.__type,
- "duration": self.__duration
- }
- def deserialize(self, data):
- self.__type = data.get('type', ConsequenceType.Timeout)
- self.__duration = data.get('duration', 0)
- class Rule:
- def __init__(
- self,
- errorType=RequestErrorType.Other,
- consequenceType=ConsequenceType.Timeout,
- amount=0,
- period=0,
- duration=0,
- status=None
- ):
- self.__condition = Condition(
- errorType,
- amount=amount,
- period=period,
- status=status
- )
- self.__consequence = Consequence(consequenceType, duration)
- @property
- def condition(self):
- return self.__condition
- @property
- def consequence(self):
- return self.__consequence
- def meetsConditions(self, instanceLog):
- return self.__condition.evaluate(instanceLog)
- def serialize(self):
- return {
- "condition": self.__condition.serialize(),
- "consequence": self.__consequence.serialize()
- }
- def deserialize(self, data):
- self.__condition.deserialize(data.get('condition', {}))
- self.__consequence.deserialize(data.get('consequence', {}))
- class Guard:
- """ Guard can have rules (condition and consequence) for failing searches.
- When enabled it logs failing instances so from that log can be evaluated
- (by the rules) whether the failing instance should be places on a timeout
- or the blacklist (the consequence).
- Guard itself doesn't handle the consequence itself but the consequence can
- be requested by other objects to handle.
- When enabled, each search response should be reported by calling
- `reportSearchResult()` for Guard to properly handle.
- The fail log of a instance will be cleared when a valid search response is
- reported to Guard, so instances have to fail in a row!
- The order of rules does matter! Rules with a lower index have higher
- priority.
- """
- # Defaults
- Enabled = False
- StoreLog = False
- LogPeriod = 7 # In days
- def __init__(self):
- self.__enabled = Guard.Enabled
- # Store logs on disk when True (bool).
- self.__storeLog = Guard.StoreLog
- # Max log period in days (uint) from now.
- self.__logPeriod = Guard.LogPeriod
- self.__log = {}
- self.__rules = []
- def reset(self):
- """ Reset Guard to default values, this will also clear the log and
- made rules.
- """
- self.__enabled = Guard.Enabled
- self.__storeLog = Guard.StoreLog
- self.clear()
- def clear(self):
- """ Clear the log and rules.
- """
- self.clearLog()
- self.__rules.clear()
- def clearLog(self):
- """ Clear the whole log.
- """
- self.__log.clear()
- def clearInstanceLog(self, instanceUrl):
- """ Clear the log of a specific instance by url.
- @param instanceUrl: Url of the instance
- @type instanceUrl: str
- """
- if instanceUrl in self.__log:
- del self.__log[instanceUrl]
- def doesStoreLog(self):
- """
- @return: Whether the log is stored on disk or not.
- @rtype: bool
- """
- return self.__storeLog
- def setStoreLog(self, state):
- """
- @param state: Store log on disk?
- @type state: bool
- """
- self.__storeLog = state
- def maxLogPeriod(self):
- """ Maximum log period in days.
- """
- return self.__logPeriod
- def setMaxLogPeriod(self, days):
- """ Set the maximum log period in days. This is only relevant when
- doesStoreLog() returns True.
- @param days: For how many days should the logs be stored.
- @type days: uint
- """
- self.__logPeriod = days
- def isEnabled(self):
- """ Returns whether Guard is enabled or not.
- """
- return self.__enabled
- def setEnabled(self, state):
- """ Enable/disable Guard.
- @param state: Enabled or disabled state of Guard as a bool.
- @type state: bool
- """
- self.__enabled = state
- def rules(self):
- """ Returns a list with Rules
- @rtype: list
- """
- return self.__rules
- def log(self):
- """ Returns the log
- @rtype: dict
- """
- return self.__log
- def addRule(
- self,
- errorType,
- consequenceType,
- amount=0,
- period=0,
- duration=0,
- status=None
- ):
- """ Add a new rule.
- A rule will only trigger when searches of a specific instance fails
- `amount` times in the last `period`, the fails have to be from the
- same `errorType`.
- The `consequenceType` defines what to do with the instance when this
- rule gets triggered, for now it can be put on a timeout or on the
- blacklist. When the type is
- `searxqt.core.guard.ConsequenceType.Timeout` a `duration` in minutes
- can be given to define for how long the timeout should last. When the
- `duration` is left to `0` with `Timeout` type it will be put on the
- timeout list until restart/switch-profile or manual removal.
- @param errorType: The search error type for this rule to trigger.
- @type errorType: searxqt.core.requests.ErrorType
- @param consequenceType: The action that will be taken on trigger.
- @type consequenceType: searxqt.core.guard.ConsequenceType
- @param amount: The amount of failures with the set errorType of this
- rule that have to occur in a row to trigger this rule.
- @type amount: uint
- @param period: Period in minutes where the fails have to occur in.
- `0` is always.
- @type period: uint
- """
- rule = Rule(
- errorType,
- consequenceType,
- amount=amount,
- period=period,
- duration=duration,
- status=status
- )
- self.__rules.append(rule)
- def moveRule(self, index, toIndex):
- """ Move a rule from index to a new index.
- @param index: Rule index
- @type index: uint
- @param toIndex: New Rule index
- @type toIndex: uint
- """
- rule = self.__rules.pop(index)
- self.__rules.insert(toIndex, rule)
- def delRule(self, index):
- """ Delete a rule by it's index
- @param index: Rule index
- @type index: uint
- """
- del self.__rules[index]
- def popRule(self, index=0):
- """ Pops a rule
- @param index: Rule index
- @type index: uint
- """
- return self.__rules.pop(index)
- def getRule(self, index):
- """ Get a rule by index
- @param index: Rule index
- @type index: uint
- @return: Guard Rule
- @rtype: searxqt.core.guard.Rule
- """
- return self.__rules[index]
- def serialize(self):
- """ Serialize this object.
- @return: Current data of this object.
- @rtype: dict
- """
- return {
- "rules": [rule.serialize() for rule in self.__rules],
- "log": self.__log if self.doesStoreLog() else {},
- "enabled": self.__enabled,
- "storeLog": self.doesStoreLog(),
- "maxLogDays": self.maxLogPeriod()
- }
- def deserialize(self, data):
- """ Deserialize data into this object.
- @param data: Data to set.
- @type data: dict
- """
- self.reset()
- for ruleData in data.get("rules", []):
- rule = Rule()
- rule.deserialize(ruleData)
- self.__rules.append(rule)
- self.setEnabled(data.get("enabled", Guard.Enabled))
- self.setStoreLog(data.get("storeLog", Guard.StoreLog))
- self.setMaxLogPeriod(data.get("maxLogDays", Guard.LogPeriod))
- if self.doesStoreLog():
- dataLog = deepcopy(data.get("log", {}))
- # Remove old logs
- deltaMax = self.__logPeriod * 24 * 60
- now = nowInMinutes()
- for url in dataLog:
- for errorType in dataLog[url]:
- if not dataLog[url][errorType]:
- # No log entries for this error type.
- continue
- index = len(dataLog[url][errorType]) - 1
- while True:
- logEntry = dataLog[url][errorType][index]
- delta = int(now - logEntry[0])
- if delta > deltaMax:
- # This log enrtry is older then the max set log
- # time, so delete this entry.
- del dataLog[url][errorType][index]
- if not index:
- # Processed last log entry for this errorType.
- break
- index -= 1
- # Update log
- self.__log.update(dataLog)
- def getConsequence(self, instanceUrl):
- """ Get consequence for a instance by url. It will return `None` when
- none of the rules triggered.
- @param instanceUrl: url of the instance.
- @type instanceUrl: str
- @return: Consequence for this instance, should it be put on the
- blacklist, a timeout or should nothing be done?
- @rtype: searxqt.core.guard.Consequence or None
- """
- if instanceUrl in self.__log:
- instanceLog = self.__log[instanceUrl]
- for rule in self.__rules:
- if rule.meetsConditions(instanceLog):
- return rule.consequence
- return None
- def reportSearchResult(self, instanceUrl, searchResult):
- """ Search results (failed or not) should be reported to Guard through
- this method so Guard can evaluate with `getConsequence`. When the
- search succeeded, previous fail logs for this instance will be removed.
- So search fails have to occur in a row. Failed searches will be
- logged.
- @param instanceUrl: url of the instance.
- @type instanceUrl: str
- @param searchResult: Search result
- @type searchResult: searxqt.core.requests.Result
- """
- if bool(searchResult):
- # Clear the log for given instanceUrl when we have a valid result.
- # This will reset the counting of failures for the instance. This
- # also means that search failures have to occur in a row for one of
- # the rules to trigger.
- self.clearInstanceLog(instanceUrl)
- else:
- # Search failed; add the incident to the log.
- self.reportSearchFail(instanceUrl, searchResult)
- def reportSearchFail(self, instanceUrl, searchResult):
- """ Log failed search
- @param instanceUrl: url of the instance.
- @type instanceUrl: str
- @param searchResult: Search result
- @type searchResult: searxqt.core.requests.Result
- """
- if instanceUrl not in self.__log:
- # No previous log for this instance
- self.__log.update({instanceUrl: {}})
- errorType = searchResult.errorType()
- if errorType not in self.__log[instanceUrl]:
- # New errorType for this instance.
- self.__log[instanceUrl].update({errorType: []})
- self.__log[instanceUrl][errorType].append((
- nowInMinutes(),
- searchResult.statusCode(),
- searchResult.error()
- ))
|