You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
input-remapper/inputremapper/logger.py

337 lines
11 KiB
Python

# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2023 sezanzeb <proxima@sezanzeb.de>
#
# This file is part of input-remapper.
#
# input-remapper 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.
#
# input-remapper 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 input-remapper. If not, see <https://www.gnu.org/licenses/>.
"""Logging setup for input-remapper."""
Frontend Refactor (#375) * Tests for the GuiEventHandler * Implement GuiEventHandler * tests for data manager * Implemented data_manager * Remove Ellipsis from type hint * workaround for old pydantic version * workaround for old pydantic version * some more tests for data_manager * Updated Data Manager * move DeviceSelection to its own class * Data Manager no longer listens for events * Moved PresetSelection to its own class * MappingListBox and SelectionLable Listen to the EventHandler * DataManager no longer creates its own data objects in the init * removed global reader object * Changed UI startup * created backend Interface * event_handler debug logs show function which emit a event * some cleanup * added target selector to components * created code editor component * adapted autocompletion & some cleanup * black * connected some buttons to the event_handler * tests for data_manager newest_preset and group * cleanup presets and test_presets * migrated confirm delete dialog * backend tests * controller tests * add python3-gi to ci * more dependencies * and more ... * Github-Actions workaround remove this commit * not so many permission denyed errors in test.yml * Fix #404 (hopefully) * revert Github-Actions workaround * More tests * event_handler allows for event supression * more tests * WIP Implement Key recording * Start and Stop Injection * context no longer stores preset * restructured the RelToBtnHandler * Simplified read_loop * Implement async iterator for ipc.pipe * multiple event actions * helper now implements mapping handlers to read inputs all with async * updated and simplified reader the helper uses the mapping handlers, so the reader now can be much simpler * Fixed race condition in tests * implemented DataBus * Fixed a UIMapping bug where the last_error would not be deleted * added a immutable variant of the UIMapping * updated data_manager to use data_bus * Uptdated tests to use the DataBus * Gui uses DataBus * removed EventHandler * Renamed controller methods * Implemented recording toggle * implemented StatusBar * Sending validation errors to status bar * sending injection status to status bar * proper preset renaming * implemented copy preset in the data manager * implemented copy_preset in controller * fixed a bug where a wron selection lable would update * no longer send invalid data over the bus, if the preset or group changes * Implement create and delete mapping * Allow for frontend specific mapping defaults * implemented autoload toggle * cleanup user_interface * removed editor * Docstings renaming and ordering of methods * more simplifications to user_interface * integrated backend into data_manager * removed active preset * transformation tests * controller tests * fix missing uinputs in gui * moved some tests and implemented basic tests for mapping handlers * docstring reformatting Co-authored-by: Tobi <proxima@sezanzeb.de> * allow for empty groups * docstring * fixed TestGroupFromHelper * some work on integration tests * test for annoying import error in tests * testing if test_user_interface works * I feel lucky * not so lucky * some more tests * fixed but where the group_key was used as folder name * Fixed a bug where state=NO_GRAB would never be read from the injector * allow to stop the recorder * working on integration tests * integration tests * fixed more integration tests * updated coveragerc * no longer attempt to record keys when injecting * event_reader cleans up not finished tasks * More integration tests * All tests pass * renamed data_bus * WIP fixing typing issues * more typing fixes * added keyboard+mouse device to tests * cleanup imports * new read loop because the evdev async read loop can not be cancelled * Added field to modify mapping name * created tests for components * even more component tests * do component tests need a screen? * apparently they do :_( * created release_input switch * Don't record relative axis when movement is slow * show delete dialog above main window * wip basic dialog to edit combination * some gui changes to the combination-editor * Simple implementation of CombinationListbox * renamed attach_to_events method and mark as private * shorter str() for UInputsData * moved logic to generate readable event string from combination to event * new mapping parameter force release timeout this helps with the helper when recording multiple relative axis at once * make it possible to rearange the event_combination * more work on the combination editor * tests for DataManager.load_event * simplyfied test_controller * more controller tests * Implement input threshold in gui * greater range for time dependent unit test * implemented a output-axis selector * data_manager now provides injector state * black * mypy * Updated confirm cancel dialog * created release timeout input * implemented transformation graph * Added sliders for gain, expo and deadzone * fix bug where the system_mapping was overridden in each injector thread * updated slider settings * removed debug statement * explicitly checking output code against None (0 is a valid code) * usage * Allow for multiple axis to be activated by same button * readme * only warn about not implemented mapping-handler don't fail to create event-pipelines * More accurate event names * Allow removal of single events from the input-combination * rename callback to notify_callback * rename event message to selected_event * made read_continuisly private * typing for autocompletion * docstrings for message_broker messages * make components methods and propreties private * gui spacings * removed eval * make some controller functions private * move status message generation from data_manager to controller * parse mapping errors in controller for more helpful messages * remove system_mapping from code editor * More component tests * more tests * mypy * make grab_devices less greedy (partial mitigation for #435) only grab one device if there are multiple which can satisfy the same mapping * accumulate more values in test * docstrings * Updated status messages * comments, docstrings, imports Co-authored-by: Tobi <proxima@sezanzeb.de>
2 years ago
import logging
import os
import sys
4 years ago
import time
Frontend Refactor (#375) * Tests for the GuiEventHandler * Implement GuiEventHandler * tests for data manager * Implemented data_manager * Remove Ellipsis from type hint * workaround for old pydantic version * workaround for old pydantic version * some more tests for data_manager * Updated Data Manager * move DeviceSelection to its own class * Data Manager no longer listens for events * Moved PresetSelection to its own class * MappingListBox and SelectionLable Listen to the EventHandler * DataManager no longer creates its own data objects in the init * removed global reader object * Changed UI startup * created backend Interface * event_handler debug logs show function which emit a event * some cleanup * added target selector to components * created code editor component * adapted autocompletion & some cleanup * black * connected some buttons to the event_handler * tests for data_manager newest_preset and group * cleanup presets and test_presets * migrated confirm delete dialog * backend tests * controller tests * add python3-gi to ci * more dependencies * and more ... * Github-Actions workaround remove this commit * not so many permission denyed errors in test.yml * Fix #404 (hopefully) * revert Github-Actions workaround * More tests * event_handler allows for event supression * more tests * WIP Implement Key recording * Start and Stop Injection * context no longer stores preset * restructured the RelToBtnHandler * Simplified read_loop * Implement async iterator for ipc.pipe * multiple event actions * helper now implements mapping handlers to read inputs all with async * updated and simplified reader the helper uses the mapping handlers, so the reader now can be much simpler * Fixed race condition in tests * implemented DataBus * Fixed a UIMapping bug where the last_error would not be deleted * added a immutable variant of the UIMapping * updated data_manager to use data_bus * Uptdated tests to use the DataBus * Gui uses DataBus * removed EventHandler * Renamed controller methods * Implemented recording toggle * implemented StatusBar * Sending validation errors to status bar * sending injection status to status bar * proper preset renaming * implemented copy preset in the data manager * implemented copy_preset in controller * fixed a bug where a wron selection lable would update * no longer send invalid data over the bus, if the preset or group changes * Implement create and delete mapping * Allow for frontend specific mapping defaults * implemented autoload toggle * cleanup user_interface * removed editor * Docstings renaming and ordering of methods * more simplifications to user_interface * integrated backend into data_manager * removed active preset * transformation tests * controller tests * fix missing uinputs in gui * moved some tests and implemented basic tests for mapping handlers * docstring reformatting Co-authored-by: Tobi <proxima@sezanzeb.de> * allow for empty groups * docstring * fixed TestGroupFromHelper * some work on integration tests * test for annoying import error in tests * testing if test_user_interface works * I feel lucky * not so lucky * some more tests * fixed but where the group_key was used as folder name * Fixed a bug where state=NO_GRAB would never be read from the injector * allow to stop the recorder * working on integration tests * integration tests * fixed more integration tests * updated coveragerc * no longer attempt to record keys when injecting * event_reader cleans up not finished tasks * More integration tests * All tests pass * renamed data_bus * WIP fixing typing issues * more typing fixes * added keyboard+mouse device to tests * cleanup imports * new read loop because the evdev async read loop can not be cancelled * Added field to modify mapping name * created tests for components * even more component tests * do component tests need a screen? * apparently they do :_( * created release_input switch * Don't record relative axis when movement is slow * show delete dialog above main window * wip basic dialog to edit combination * some gui changes to the combination-editor * Simple implementation of CombinationListbox * renamed attach_to_events method and mark as private * shorter str() for UInputsData * moved logic to generate readable event string from combination to event * new mapping parameter force release timeout this helps with the helper when recording multiple relative axis at once * make it possible to rearange the event_combination * more work on the combination editor * tests for DataManager.load_event * simplyfied test_controller * more controller tests * Implement input threshold in gui * greater range for time dependent unit test * implemented a output-axis selector * data_manager now provides injector state * black * mypy * Updated confirm cancel dialog * created release timeout input * implemented transformation graph * Added sliders for gain, expo and deadzone * fix bug where the system_mapping was overridden in each injector thread * updated slider settings * removed debug statement * explicitly checking output code against None (0 is a valid code) * usage * Allow for multiple axis to be activated by same button * readme * only warn about not implemented mapping-handler don't fail to create event-pipelines * More accurate event names * Allow removal of single events from the input-combination * rename callback to notify_callback * rename event message to selected_event * made read_continuisly private * typing for autocompletion * docstrings for message_broker messages * make components methods and propreties private * gui spacings * removed eval * make some controller functions private * move status message generation from data_manager to controller * parse mapping errors in controller for more helpful messages * remove system_mapping from code editor * More component tests * more tests * mypy * make grab_devices less greedy (partial mitigation for #435) only grab one device if there are multiple which can satisfy the same mapping * accumulate more values in test * docstrings * Updated status messages * comments, docstrings, imports Co-authored-by: Tobi <proxima@sezanzeb.de>
2 years ago
from datetime import datetime
from typing import cast
try:
from inputremapper.commit_hash import COMMIT_HASH
except ImportError:
3 years ago
COMMIT_HASH = ""
start = time.time()
previous_key_debug_log = None
previous_write_debug_log = None
def parse_mapping_handler(mapping_handler):
indent = 0
lines_and_indent = []
while True:
if isinstance(handler, str):
lines_and_indent.append([mapping_handler, indent])
break
if isinstance(mapping_handler, list):
for sub_handler in mapping_handler:
sub_list = parse_mapping_handler(sub_handler)
for line in sub_list:
line[1] += indent
lines_and_indent.extend(sub_list)
break
lines_and_indent.append([repr(mapping_handler), indent])
try:
mapping_handler = mapping_handler.child
except AttributeError:
break
indent += 1
return lines_and_indent
class Logger(logging.Logger):
def debug_mapping_handler(self, mapping_handler):
"""Parse the structure of a mapping_handler and log it."""
if not self.isEnabledFor(logging.DEBUG):
return
lines_and_indent = parse_mapping_handler(mapping_handler)
for line in lines_and_indent:
indent = " "
msg = indent * line[1] + line[0]
self._log(logging.DEBUG, msg, args=None)
def write(self, key, uinput):
"""Log that an event is being written
Parameters
----------
key
anything that can be string formatted, but usually a tuple of
(type, code, value) tuples
"""
# pylint: disable=protected-access
if not self.isEnabledFor(logging.DEBUG):
return
global previous_write_debug_log
str_key = repr(key)
str_key = str_key.replace(",)", ")")
msg = f'Writing {str_key} to "{uinput.name}"'
if msg == previous_write_debug_log:
# avoid some super spam from EV_ABS events
return
previous_write_debug_log = msg
self._log(logging.DEBUG, msg, args=None)
# https://github.com/python/typeshed/issues/1801
logging.setLoggerClass(Logger)
logger = cast(Logger, logging.getLogger("input-remapper"))
def is_debug():
"""True, if the logger is currently in DEBUG or DEBUG mode."""
return logger.level <= logging.DEBUG
class ColorfulFormatter(logging.Formatter):
"""Overwritten Formatter to print nicer logs.
It colors all logs from the same filename in the same color to visually group them
together. It also adds process name, process id, file, line-number and time.
If debug mode is not active, it will not do any of this.
"""
3 years ago
def __init__(self):
super().__init__()
self.file_color_mapping = {}
# see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
self.allowed_colors = []
for r in range(0, 6):
for g in range(0, 6):
for b in range(0, 6):
# https://stackoverflow.com/a/596243
brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b
if brightness < 1:
# prefer light colors, because most people have a dark
# terminal background
continue
if g + b <= 1:
# red makes it look like it's an error
continue
if abs(g - b) < 2 and abs(b - r) < 2 and abs(r - g) < 2:
# no colors that are too grey
continue
self.allowed_colors.append(self._get_ansi_code(r, g, b))
self.level_based_colors = {
logging.WARNING: 11,
logging.ERROR: 9,
logging.FATAL: 9,
}
def _get_ansi_code(self, r: int, g: int, b: int):
return 16 + b + (6 * g) + (36 * r)
def _word_to_color(self, word: str):
"""Convert a word to a 8bit ansi color code."""
digit_sum = sum([ord(char) for char in word])
index = digit_sum % len(self.allowed_colors)
return self.allowed_colors[index]
def _allocate_debug_log_color(self, record: logging.LogRecord):
"""Get the color that represents the source file of the log."""
if self.file_color_mapping.get(record.filename) is not None:
return self.file_color_mapping[record.filename]
color = self._word_to_color(record.filename)
if self.file_color_mapping.get(record.filename) is None:
# calculate the color for each file only once
self.file_color_mapping[record.filename] = color
return color
def _get_process_name(self):
"""Generate a beaitiful to read name for this process."""
process_path = sys.argv[0]
process_name = process_path.split("/")[-1]
if "input-remapper-" in process_name:
process_name = process_name.replace("input-remapper-", "")
if process_name == "gtk":
process_name = "GUI"
return process_name
def _get_format(self, record: logging.LogRecord):
"""Generate a message format string."""
debug_mode = is_debug()
if record.levelno == logging.INFO and not debug_mode:
# if not launched with --debug, then don't print "INFO:"
return "%(message)s"
if not debug_mode:
color = self.level_based_colors[record.levelno]
return f"\033[38;5;{color}m%(levelname)s\033[0m: %(message)s"
color = self._allocate_debug_log_color(record)
if record.levelno in [logging.ERROR, logging.WARNING, logging.FATAL]:
# underline
style = f"\033[4;38;5;{color}m"
else:
style = f"\033[38;5;{color}m"
process_color = self._word_to_color(f"{os.getpid()}{sys.argv[0]}")
return ( # noqa
f'{datetime.now().strftime("%H:%M:%S.%f")} '
f"\033[38;5;{process_color}m" # color
f"{os.getpid()} "
f"{self._get_process_name()} "
"\033[0m" # end style
f"{style}"
f"%(levelname)s "
f"%(filename)s:%(lineno)d: "
"%(message)s"
"\033[0m" # end style
).replace(" ", " ")
def format(self, record: logging.LogRecord):
4 years ago
"""Overwritten format function."""
# pylint: disable=protected-access
self._style._fmt = self._get_format(record)
return super().format(record)
handler = logging.StreamHandler()
handler.setFormatter(ColorfulFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
3 years ago
logging.getLogger("asyncio").setLevel(logging.WARNING)
# using pkg_resources to figure out the version fails in many cases,
# so we hardcode it instead
9 months ago
VERSION = "2.0.1"
EVDEV_VERSION = None
try:
# pkg_resources very commonly fails/breaks
import pkg_resources
3 years ago
EVDEV_VERSION = pkg_resources.require("evdev")[0].version
except Exception as error:
# there have been pkg_resources.DistributionNotFound and
# pkg_resources.ContextualVersionConflict errors so far.
# We can safely ignore all Exceptions here
3 years ago
logger.info("Could not figure out the version")
logger.debug(error)
2 years ago
# check if the version is something like 1.5.0-beta or 1.5.0-beta.5
IS_BETA = "beta" in VERSION
def log_info(name="input-remapper"):
3 years ago
"""Log version and name to the console."""
logger.info(
"%s %s %s https://github.com/sezanzeb/input-remapper",
name,
VERSION,
COMMIT_HASH,
)
3 years ago
if EVDEV_VERSION:
3 years ago
logger.info("python-evdev %s", EVDEV_VERSION)
if is_debug():
logger.warning(
3 years ago
"Debug level will log all your keystrokes! Do not post this "
"output in the internet if you typed in sensitive or private "
"information with your device!"
)
def update_verbosity(debug):
"""Set the logging verbosity according to the settings object.
Also enable rich tracebacks in debug mode.
"""
3 years ago
# pylint really doesn't like what I'm doing with rich.traceback here
# pylint: disable=broad-except,import-error,import-outside-toplevel
if debug:
logger.setLevel(logging.DEBUG)
try:
from rich.traceback import install
3 years ago
install(show_locals=True)
3 years ago
logger.debug("Using rich.traceback")
except Exception as error:
# since this is optional, just skip all exceptions
if not isinstance(error, ImportError):
3 years ago
logger.debug("Cannot use rich.traceback: %s", error)
else:
logger.setLevel(logging.INFO)
def trim_logfile(log_path):
"""Keep the logfile short."""
if not os.path.exists(log_path):
return
file_size_mb = os.path.getsize(log_path) / 1000 / 1000
if file_size_mb > 100:
# something went terribly wrong here. The service might timeout because
# it takes too long to trim this file. delete it instead. This probably
# only happens when doing funny things while in debug mode.
logger.warning(
"Removing enormous log file of %dMB",
file_size_mb,
)
os.remove(log_path)
return
# the logfile should not be too long to avoid overflowing the storage
try:
with open(log_path, "rb") as file:
binary = file.readlines()[-1000:]
content = [line.decode("utf-8", errors="ignore") for line in binary]
with open(log_path, "w") as file:
file.truncate(0)
file.writelines(content)
except PermissionError:
# let the outermost PermissionError handler handle it
raise
except Exception as exception:
logger.error('Failed to trim logfile: "%s"', str(exception))