Stav 23.06.2026
This commit is contained in:
@@ -0,0 +1,646 @@
|
||||
"""
|
||||
Kivy Logging
|
||||
============
|
||||
|
||||
By default, Kivy provides a logging system based on the standard Python
|
||||
`logging <https://docs.python.org/3/library/logging.html>`_ module with
|
||||
several additional features designed to be more convenient. These features
|
||||
include:
|
||||
|
||||
* simplied usage (single instance, simple configuration, works by default)
|
||||
* color-coded output on supported terminals
|
||||
* output to ``stderr`` by default
|
||||
* message categorization via colon separation
|
||||
* access to log history even if logging is disabled
|
||||
* built-in handling of various cross-platform considerations
|
||||
* any stray output written to ``sys.stderr`` is captured, and stored in the log
|
||||
file as a warning.
|
||||
|
||||
These features are configurable via the Config file or environment variables -
|
||||
including falling back to only using the standard Python system.
|
||||
|
||||
Logger object
|
||||
=============
|
||||
|
||||
The Kivy ``Logger`` class provides a singleton logging.logger instance.
|
||||
|
||||
As well as the standard logging levels (``debug``, ``info``,
|
||||
``warning``, ``error`` and ``critical``), an additional ``trace`` level is
|
||||
available.
|
||||
|
||||
Example Usage
|
||||
-------------
|
||||
|
||||
Use the ``Logger`` as you would a standard Python logger. ::
|
||||
|
||||
from kivy.logger import Logger
|
||||
|
||||
Logger.info('title: This is a info message.')
|
||||
Logger.debug('title: This is a debug message.')
|
||||
|
||||
try:
|
||||
raise Exception('bleh')
|
||||
except Exception:
|
||||
Logger.exception('Something happened!')
|
||||
|
||||
The message passed to the logger is split into two parts separated by a colon
|
||||
(:). The first part is used as a title and the second part is used as the
|
||||
message. This way, you can "categorize" your messages easily. ::
|
||||
|
||||
Logger.info('Application: This is a test')
|
||||
|
||||
# will appear as
|
||||
|
||||
[INFO ] [Application ] This is a test
|
||||
|
||||
You can change the logging level at any time using the ``setLevel`` method. ::
|
||||
|
||||
from kivy.logger import Logger, LOG_LEVELS
|
||||
|
||||
Logger.setLevel(LOG_LEVELS["debug"])
|
||||
|
||||
.. versionchanged:: 2.2.0
|
||||
|
||||
Interaction with other logging
|
||||
------------------------------
|
||||
|
||||
The Kivy logging system will, by default, present all log messages sent from
|
||||
any logger - e.g. from third-party libraries.
|
||||
|
||||
Additional handlers may be added.
|
||||
|
||||
.. warning:: Handlers that output to ``sys.stderr`` may cause loops, as stderr
|
||||
output is reported as a warning log message.
|
||||
|
||||
Logger Configuration
|
||||
====================
|
||||
|
||||
Kivy Log Mode
|
||||
-------------
|
||||
|
||||
At the highest level, Kivy's logging system is controlled by an environment
|
||||
variable ``KIVY_LOG_MODE``. It may be given any of three values:
|
||||
``KIVY``, ``PYTHON``, ``MIXED``
|
||||
|
||||
.. versionadded: 2.2.0
|
||||
|
||||
KIVY Mode (default)
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In ``KIVY`` mode, all Kivy handlers are attached to the root logger, so all log
|
||||
messages in the system are output to the Kivy log files and to the console. Any
|
||||
stray output to ``sys.stderr`` is logged as a warning.
|
||||
|
||||
If you are writing an entire Kivy app from scratch, this is the most convenient
|
||||
mode.
|
||||
|
||||
PYTHON Mode
|
||||
^^^^^^^^^^^
|
||||
|
||||
In ``PYTHON`` mode, no handlers are added, and ``sys.stderr`` output is not
|
||||
captured. It is left to the client to add appropriate handlers. (If none are
|
||||
added, the ``logging`` module will output them to ``stderr``.)
|
||||
|
||||
Messages logged with ``Logger`` will be propagated to the root logger, from a
|
||||
logger named ``kivy``.
|
||||
|
||||
If the Kivy app is part of a much larger project which has its own logging
|
||||
regimen, this is the mode that gives most control.
|
||||
|
||||
The ``kivy.logger`` file contains a number of ``logging.handler``,
|
||||
``logging.formatter``, and other helper classes to allow
|
||||
users to adopt the features of Kivy logging that they like, including the
|
||||
stderr redirection.
|
||||
|
||||
MIXED Mode
|
||||
^^^^^^^^^^
|
||||
|
||||
In ``MIXED`` mode, handlers are added to the Kivy's ``Logger`` object directly,
|
||||
and propagation is turned off. ``sys.stderr`` is not redirected.
|
||||
|
||||
Messages logged with Kivy's ``Logger`` will appear in the Kivy log file and
|
||||
output to the Console.
|
||||
|
||||
However, messages logged with other Python loggers will not be handled by Kivy
|
||||
handlers. The client will need to add their own.
|
||||
|
||||
If you like the features of Kivy ``Logger``, but are writing a Kivy app that
|
||||
relies on third-party libraries that don't use colon-separation of categorise
|
||||
or depend on the display of the logger name, this mode provides a compromise.
|
||||
|
||||
Again, the ``kivy.logger`` file contains re-usable logging features that can be
|
||||
used to get the best of both systems.
|
||||
|
||||
Config Files
|
||||
------------
|
||||
|
||||
In ``KIVY`` and ``MIXED`` modes, the logger handlers can be controlled via the
|
||||
Kivy configuration file::
|
||||
|
||||
[kivy]
|
||||
log_level = info
|
||||
log_enable = 1
|
||||
log_dir = logs
|
||||
log_name = kivy_%y-%m-%d_%_.txt
|
||||
log_maxfiles = 100
|
||||
|
||||
More information about the allowed values are described in the
|
||||
:mod:`kivy.config` module.
|
||||
|
||||
In addition, the environment variables ``KIVY_NO_FILELOG`` and
|
||||
``KIVY_NO_CONSOLELOG`` can be used to turn off the installation of the
|
||||
corresponding handlers.
|
||||
|
||||
|
||||
Logger History
|
||||
--------------
|
||||
|
||||
Even if the logger is not enabled, you still have access to the last 100
|
||||
LogRecords::
|
||||
|
||||
from kivy.logger import LoggerHistory
|
||||
|
||||
print(LoggerHistory.history)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from functools import partial
|
||||
import pathlib
|
||||
|
||||
import kivy
|
||||
from kivy.utils import platform
|
||||
|
||||
|
||||
__all__ = (
|
||||
"add_kivy_handlers",
|
||||
"ColonSplittingLogRecord",
|
||||
"ColoredLogRecord",
|
||||
"COLORS",
|
||||
"ConsoleHandler",
|
||||
"file_log_handler",
|
||||
"FileHandler",
|
||||
"is_color_terminal",
|
||||
"KivyFormatter",
|
||||
"LOG_LEVELS",
|
||||
"Logger",
|
||||
"LoggerHistory",
|
||||
"ProcessingStream",
|
||||
"UncoloredLogRecord",
|
||||
)
|
||||
|
||||
|
||||
Logger = None
|
||||
|
||||
|
||||
logging.addLevelName(9, 'TRACE')
|
||||
logging.TRACE = 9
|
||||
LOG_LEVELS = {
|
||||
'trace': logging.TRACE,
|
||||
'debug': logging.DEBUG,
|
||||
'info': logging.INFO,
|
||||
'warning': logging.WARNING,
|
||||
'error': logging.ERROR,
|
||||
'critical': logging.CRITICAL}
|
||||
|
||||
|
||||
class FileHandler(logging.Handler):
|
||||
history = []
|
||||
filename = 'log.txt'
|
||||
fd = None
|
||||
log_dir = ''
|
||||
encoding = 'utf-8'
|
||||
|
||||
def purge_logs(self):
|
||||
"""Purge logs which exceed the maximum amount of log files,
|
||||
starting with the oldest creation timestamp (or edit-timestamp on Linux)
|
||||
"""
|
||||
|
||||
if not self.log_dir:
|
||||
return
|
||||
|
||||
from kivy.config import Config
|
||||
maxfiles = Config.getint("kivy", "log_maxfiles")
|
||||
|
||||
# Get path to log directory
|
||||
log_dir = pathlib.Path(self.log_dir)
|
||||
|
||||
if maxfiles < 0: # No log file limit set
|
||||
return
|
||||
|
||||
Logger.info("Logger: Purge log fired. Processing...")
|
||||
|
||||
# Get all files from log directory and corresponding creation timestamps
|
||||
files = [(item, item.stat().st_ctime)
|
||||
for item in log_dir.iterdir() if item.is_file()]
|
||||
# Sort files by ascending timestamp
|
||||
files.sort(key=lambda x: x[1])
|
||||
|
||||
for file, _ in files[:(-maxfiles or len(files))]:
|
||||
# More log files than allowed maximum,
|
||||
# delete files, starting with oldest creation timestamp
|
||||
# (or edit-timestamp on Linux)
|
||||
try:
|
||||
file.unlink()
|
||||
except (PermissionError, FileNotFoundError) as e:
|
||||
Logger.info(f"Logger: Skipped file {file}, {repr(e)}")
|
||||
|
||||
Logger.info("Logger: Purge finished!")
|
||||
|
||||
def _configure(self, *largs, **kwargs):
|
||||
from time import strftime
|
||||
from kivy.config import Config
|
||||
log_dir = Config.get('kivy', 'log_dir')
|
||||
log_name = Config.get('kivy', 'log_name')
|
||||
|
||||
_dir = kivy.kivy_home_dir
|
||||
if log_dir and os.path.isabs(log_dir):
|
||||
_dir = log_dir
|
||||
else:
|
||||
_dir = os.path.join(_dir, log_dir)
|
||||
if not os.path.exists(_dir):
|
||||
os.makedirs(_dir)
|
||||
self.log_dir = _dir
|
||||
|
||||
pattern = log_name.replace('%_', '@@NUMBER@@')
|
||||
pattern = os.path.join(_dir, strftime(pattern))
|
||||
n = 0
|
||||
while True:
|
||||
filename = pattern.replace('@@NUMBER@@', str(n))
|
||||
if not os.path.exists(filename):
|
||||
break
|
||||
n += 1
|
||||
if n > 10000: # prevent maybe flooding ?
|
||||
raise Exception('Too many logfile, remove them')
|
||||
|
||||
if FileHandler.filename == filename and FileHandler.fd is not None:
|
||||
return
|
||||
|
||||
FileHandler.filename = filename
|
||||
if FileHandler.fd not in (None, False):
|
||||
FileHandler.fd.close()
|
||||
FileHandler.fd = open(filename, 'w', encoding=FileHandler.encoding)
|
||||
Logger.info('Logger: Record log in %s' % filename)
|
||||
|
||||
def _write_message(self, record):
|
||||
if FileHandler.fd in (None, False):
|
||||
return
|
||||
|
||||
msg = self.format(record)
|
||||
stream = FileHandler.fd
|
||||
fs = "%s\n"
|
||||
stream.write('[%-7s] ' % record.levelname)
|
||||
stream.write(fs % msg)
|
||||
stream.flush()
|
||||
|
||||
def emit(self, message):
|
||||
# during the startup, store the message in the history
|
||||
if Logger.logfile_activated is None:
|
||||
FileHandler.history += [message]
|
||||
return
|
||||
|
||||
# startup done, if the logfile is not activated, avoid history.
|
||||
if Logger.logfile_activated is False:
|
||||
FileHandler.history = []
|
||||
return
|
||||
|
||||
if FileHandler.fd is None:
|
||||
try:
|
||||
self._configure()
|
||||
from kivy.config import Config
|
||||
Config.add_callback(self._configure, 'kivy', 'log_dir')
|
||||
Config.add_callback(self._configure, 'kivy', 'log_name')
|
||||
except Exception:
|
||||
# deactivate filehandler...
|
||||
if FileHandler.fd not in (None, False):
|
||||
FileHandler.fd.close()
|
||||
FileHandler.fd = False
|
||||
Logger.exception('Error while activating FileHandler logger')
|
||||
return
|
||||
while FileHandler.history:
|
||||
_message = FileHandler.history.pop()
|
||||
self._write_message(_message)
|
||||
|
||||
self._write_message(message)
|
||||
|
||||
|
||||
class LoggerHistory(logging.Handler):
|
||||
|
||||
history = []
|
||||
|
||||
def emit(self, message):
|
||||
LoggerHistory.history = [message] + LoggerHistory.history[:100]
|
||||
|
||||
@classmethod
|
||||
def clear_history(cls):
|
||||
del cls.history[:]
|
||||
|
||||
def flush(self):
|
||||
super(LoggerHistory, self).flush()
|
||||
self.clear_history()
|
||||
|
||||
|
||||
class ConsoleHandler(logging.StreamHandler):
|
||||
"""
|
||||
Emits records to a stream (by default, stderr).
|
||||
|
||||
However, if the msg starts with "stderr:" it is not formatted, but
|
||||
written straight to the stream.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
"""
|
||||
|
||||
def filter(self, record):
|
||||
try:
|
||||
msg = record.msg
|
||||
k = msg.split(':', 1)
|
||||
if k[0] == 'stderr' and len(k) == 2:
|
||||
# This message was scraped from stderr.
|
||||
# Emit it without formatting.
|
||||
self.stream.write(k[1] + '\n')
|
||||
# Don't pass it to the formatted emitter.
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
class ProcessingStream(object):
|
||||
"""
|
||||
Stream-like object that takes each completed line written to it,
|
||||
adds a given prefix, and applies the given function to it.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
"""
|
||||
|
||||
def __init__(self, channel, func):
|
||||
self.buffer = ""
|
||||
self.func = func
|
||||
self.channel = channel
|
||||
self.errors = ""
|
||||
|
||||
def write(self, s):
|
||||
s = self.buffer + s
|
||||
self.flush()
|
||||
f = self.func
|
||||
channel = self.channel
|
||||
lines = s.split('\n')
|
||||
for line in lines[:-1]:
|
||||
f('%s: %s' % (channel, line))
|
||||
self.buffer = lines[-1]
|
||||
|
||||
def flush(self):
|
||||
return
|
||||
|
||||
def isatty(self):
|
||||
return False
|
||||
|
||||
|
||||
def logger_config_update(section, key, value):
|
||||
if KIVY_LOG_MODE != "PYTHON":
|
||||
if LOG_LEVELS.get(value) is None:
|
||||
raise AttributeError('Loglevel {0!r} doesn\'t exists'.format(value))
|
||||
Logger.setLevel(level=LOG_LEVELS.get(value))
|
||||
|
||||
|
||||
class ColonSplittingLogRecord(logging.LogRecord):
|
||||
"""Clones an existing logRecord, but reformats the message field
|
||||
if it contains a colon.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
"""
|
||||
|
||||
def __init__(self, logrecord):
|
||||
try:
|
||||
parts = logrecord.msg.split(":", 1)
|
||||
if len(parts) == 2:
|
||||
new_msg = "[%-12s]%s" % (parts[0], parts[1])
|
||||
else:
|
||||
new_msg = parts[0]
|
||||
except Exception:
|
||||
new_msg = logrecord.msg
|
||||
|
||||
super().__init__(
|
||||
name=logrecord.name,
|
||||
level=logrecord.levelno,
|
||||
pathname=logrecord.pathname,
|
||||
lineno=logrecord.lineno,
|
||||
msg=new_msg,
|
||||
args=logrecord.args,
|
||||
exc_info=logrecord.exc_info,
|
||||
func=logrecord.funcName,
|
||||
sinfo=logrecord.stack_info,
|
||||
)
|
||||
|
||||
|
||||
class ColoredLogRecord(logging.LogRecord):
|
||||
"""Clones an existing logRecord, but reformats the levelname to add
|
||||
color, and the message to add bolding (where indicated by $BOLD
|
||||
and $RESET in the message).
|
||||
|
||||
.. versionadded:: 2.2.0"""
|
||||
|
||||
BLACK = 0
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
YELLOW = 3
|
||||
BLUE = 4
|
||||
MAGENTA = 5
|
||||
CYAN = 6
|
||||
WHITE = 7
|
||||
RESET_SEQ = "\033[0m"
|
||||
COLOR_SEQ = "\033[1;%dm"
|
||||
BOLD_SEQ = "\033[1m"
|
||||
|
||||
LEVEL_COLORS = {
|
||||
"TRACE": MAGENTA,
|
||||
"WARNING": YELLOW,
|
||||
"INFO": GREEN,
|
||||
"DEBUG": CYAN,
|
||||
"CRITICAL": RED,
|
||||
"ERROR": RED,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _format_message(cls, message):
|
||||
return str(message).replace(
|
||||
"$RESET", cls.RESET_SEQ).replace("$BOLD", cls.BOLD_SEQ)
|
||||
|
||||
@classmethod
|
||||
def _format_levelname(cls, levelname):
|
||||
if levelname in cls.LEVEL_COLORS:
|
||||
return (
|
||||
cls.COLOR_SEQ % (30 + cls.LEVEL_COLORS[levelname])
|
||||
+ levelname
|
||||
+ cls.RESET_SEQ
|
||||
)
|
||||
return levelname
|
||||
|
||||
def __init__(self, logrecord):
|
||||
super().__init__(
|
||||
name=logrecord.name,
|
||||
level=logrecord.levelno,
|
||||
pathname=logrecord.pathname,
|
||||
lineno=logrecord.lineno,
|
||||
msg=logrecord.msg,
|
||||
args=logrecord.args,
|
||||
exc_info=logrecord.exc_info,
|
||||
func=logrecord.funcName,
|
||||
sinfo=logrecord.stack_info,
|
||||
)
|
||||
self.levelname = self._format_levelname(self.levelname)
|
||||
self.msg = self._format_message(self.msg)
|
||||
|
||||
|
||||
# Included for backward compatibility only.
|
||||
# Could be used to override colors.
|
||||
COLORS = ColoredLogRecord.LEVEL_COLORS
|
||||
|
||||
|
||||
class UncoloredLogRecord(logging.LogRecord):
|
||||
"""Clones an existing logRecord, but reformats the message
|
||||
to remove $BOLD/$RESET markup.
|
||||
|
||||
.. versionadded:: 2.2.0"""
|
||||
|
||||
@classmethod
|
||||
def _format_message(cls, message):
|
||||
return str(message).replace("$RESET", "").replace("$BOLD", "")
|
||||
|
||||
def __init__(self, logrecord):
|
||||
super().__init__(
|
||||
name=logrecord.name,
|
||||
level=logrecord.levelno,
|
||||
pathname=logrecord.pathname,
|
||||
lineno=logrecord.lineno,
|
||||
msg=logrecord.msg,
|
||||
args=logrecord.args,
|
||||
exc_info=logrecord.exc_info,
|
||||
func=logrecord.funcName,
|
||||
sinfo=logrecord.stack_info,
|
||||
)
|
||||
self.msg = self._format_message(self.msg)
|
||||
|
||||
|
||||
class KivyFormatter(logging.Formatter):
|
||||
"""Split out first field in message marked with a colon,
|
||||
and either apply terminal color codes to the record, or strip
|
||||
out color markup if colored logging is not available.
|
||||
|
||||
.. versionadded:: 2.2.0"""
|
||||
|
||||
def __init__(self, *args, use_color=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._coloring_cls = (
|
||||
ColoredLogRecord if use_color else UncoloredLogRecord)
|
||||
|
||||
def format(self, record):
|
||||
return super().format(
|
||||
self._coloring_cls(ColonSplittingLogRecord(record)))
|
||||
|
||||
|
||||
def is_color_terminal():
|
||||
""" Detect whether the environment supports color codes in output.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
"""
|
||||
|
||||
return (
|
||||
(
|
||||
os.environ.get("WT_SESSION") or
|
||||
os.environ.get("COLORTERM") == 'truecolor' or
|
||||
os.environ.get('PYCHARM_HOSTED') == '1' or
|
||||
os.environ.get('TERM') in (
|
||||
'rxvt',
|
||||
'rxvt-256color',
|
||||
'rxvt-unicode',
|
||||
'rxvt-unicode-256color',
|
||||
'xterm',
|
||||
'xterm-256color',
|
||||
)
|
||||
)
|
||||
and platform not in ('android', 'ios')
|
||||
)
|
||||
|
||||
|
||||
#: Kivy default logger instance
|
||||
# .. versionchanged:: 2.2.0
|
||||
Logger = logging.getLogger('kivy')
|
||||
Logger.logfile_activated = None
|
||||
Logger.trace = partial(Logger.log, logging.TRACE)
|
||||
|
||||
file_log_handler = (
|
||||
FileHandler()
|
||||
if 'KIVY_NO_FILELOG' not in os.environ
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
# Issue #7891 describes an undocumented feature that was since removed
|
||||
# Detect if a client was depending on it.
|
||||
# .. versionchanged:: 2.2.0
|
||||
assert not hasattr(sys, '_kivy_logging_handler'), \
|
||||
"Not supported. Try logging.root.addHandler()"
|
||||
|
||||
|
||||
def add_kivy_handlers(logger):
|
||||
""" Add Kivy-specific handlers to a logger.
|
||||
|
||||
.. versionadded:: 2.2.0
|
||||
"""
|
||||
# add default kivy logger
|
||||
logger.addHandler(LoggerHistory())
|
||||
if file_log_handler:
|
||||
logger.addHandler(file_log_handler)
|
||||
|
||||
# Use the custom handler instead of streaming one.
|
||||
# Don't output to stderr if it is set to None
|
||||
# stderr is set to None by pythonw and pyinstaller 5.7+
|
||||
if sys.stderr and 'KIVY_NO_CONSOLELOG' not in os.environ:
|
||||
use_color = is_color_terminal()
|
||||
if not use_color:
|
||||
# No additional control characters will be inserted inside the
|
||||
# levelname field, 7 chars will fit "WARNING"
|
||||
fmt = "[%(levelname)-7s] %(message)s"
|
||||
else:
|
||||
# levelname field width need to take into account the length of
|
||||
# the color control codes (7+4 chars for bold+color, and reset)
|
||||
fmt = "[%(levelname)-18s] %(message)s"
|
||||
formatter = KivyFormatter(fmt, use_color=use_color)
|
||||
console = ConsoleHandler()
|
||||
console.setFormatter(formatter)
|
||||
logger.addHandler(console)
|
||||
|
||||
|
||||
KIVY_LOG_MODE = os.environ.get("KIVY_LOG_MODE", "KIVY")
|
||||
assert KIVY_LOG_MODE in ("KIVY", "PYTHON", "MIXED"), "Unknown log mode"
|
||||
|
||||
if KIVY_LOG_MODE == "KIVY":
|
||||
# Add the Kivy handlers to the root logger, so they will be used
|
||||
# for all propagated log messages.
|
||||
add_kivy_handlers(logging.root)
|
||||
|
||||
# Root logger defaults to warning. Let Logger be the limiting factor.
|
||||
logging.root.setLevel(logging.NOTSET)
|
||||
|
||||
# install stderr handlers
|
||||
|
||||
# Caution: If any logging handlers output to sys.stderr they should be
|
||||
# configured BEFORE this reconfiguration is done to avoid loops.
|
||||
sys.stderr = ProcessingStream("stderr", Logger.warning)
|
||||
# Sends all messages written to stderr to the Logger, after prefixing it
|
||||
# with "stderr:"
|
||||
elif KIVY_LOG_MODE == "MIXED":
|
||||
# Add the Kivy handlers to the Kivy logger, so they will be used
|
||||
# for all messages sent through Logger, only.
|
||||
add_kivy_handlers(Logger)
|
||||
|
||||
# Don't spread Kivy-related log messages to the root logger.
|
||||
Logger.propagate = False
|
||||
|
||||
# Don't set stderr redirection: it is too likely to cause loops with other
|
||||
# handlers. Client can manually add it, if desired.
|
||||
else: # KIVY_LOG_MODE == "PYTHON"
|
||||
# Don't add handlers or redirect stderr. Client can manually add if desired.
|
||||
pass
|
||||
Reference in New Issue
Block a user