input-remapper/inputremapper/logger.py

333 lines
10 KiB
Python
Raw Normal View History

2020-10-26 22:45:22 +00:00
#!/usr/bin/python3
# -*- coding: utf-8 -*-
2022-01-01 12:00:49 +00:00
# input-remapper - GUI for device specific keyboard mappings
2022-01-01 12:52:33 +00:00
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
2020-10-26 22:45:22 +00:00
#
2022-01-01 12:00:49 +00:00
# This file is part of input-remapper.
2020-10-26 22:45:22 +00:00
#
2022-01-01 12:00:49 +00:00
# input-remapper is free software: you can redistribute it and/or modify
2020-10-26 22:45:22 +00:00
# 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.
#
2022-01-01 12:00:49 +00:00
# input-remapper is distributed in the hope that it will be useful,
2020-10-26 22:45:22 +00:00
# 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
2022-01-01 12:00:49 +00:00
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
2020-10-26 22:45:22 +00:00
2022-01-01 12:00:49 +00:00
"""Logging setup for input-remapper."""
2020-10-26 22:45:22 +00:00
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>
2022-07-23 08:53:41 +00:00
import logging
2020-10-26 22:45:22 +00:00
import os
import sys
2020-12-03 19:37:36 +00:00
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>
2022-07-23 08:53:41 +00:00
from datetime import datetime
2022-04-17 10:19:23 +00:00
from typing import cast
2020-10-26 22:45:22 +00:00
import pkg_resources
2021-03-21 18:15:20 +00:00
2021-04-15 19:16:07 +00:00
try:
2022-01-01 12:00:49 +00:00
from inputremapper.commit_hash import COMMIT_HASH
2021-04-15 19:16:07 +00:00
except ImportError:
2021-09-26 10:44:56 +00:00
COMMIT_HASH = ""
2021-04-15 19:16:07 +00:00
2020-10-26 22:45:22 +00:00
2021-01-05 18:33:47 +00:00
start = time.time()
previous_key_debug_log = None
2020-11-18 19:03:37 +00:00
2022-04-17 10:19:23 +00:00
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
2021-01-07 16:15:12 +00:00
2022-04-17 10:19:23 +00:00
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
2022-04-17 10:19:23 +00:00
lines_and_indent.append([mapping_handler.__str__(), 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 an log it"""
2022-04-17 10:19:23 +00:00
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 debug_key(self, key, msg, *args):
"""Log a spam message custom tailored to keycode_mapper.
Parameters
----------
key : tuple of int
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
2022-04-17 10:19:23 +00:00
global previous_key_debug_log
2022-04-17 10:19:23 +00:00
msg = msg % args
str_key = str(key)
str_key = str_key.replace(",)", ")")
spacing = " " + "·" * max(0, 30 - len(msg))
if len(spacing) == 1:
spacing = ""
msg = f"{msg}{spacing} {str_key}"
2022-04-17 10:19:23 +00:00
if msg == previous_key_debug_log:
# avoid some super spam from EV_ABS events
return
2022-04-17 10:19:23 +00:00
previous_key_debug_log = msg
2021-01-05 18:33:47 +00:00
2022-04-17 10:19:23 +00:00
self._log(logging.DEBUG, msg, args=None)
2021-01-05 18:33:47 +00:00
2020-11-18 19:03:37 +00:00
2022-04-17 10:19:23 +00:00
# https://github.com/python/typeshed/issues/1801
logging.setLoggerClass(Logger)
logger = cast(Logger, logging.getLogger("input-remapper"))
2021-08-22 09:44:16 +00:00
def is_debug():
"""True, if the logger is currently in DEBUG or DEBUG mode."""
2021-08-22 09:44:16 +00:00
return logger.level <= logging.DEBUG
2020-11-18 19:03:37 +00:00
2022-01-18 21:34:28 +00:00
class ColorfulFormatter(logging.Formatter):
"""Overwritten Formatter to print nicer logs.
2022-01-18 21:34:28 +00:00
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.
2022-01-18 21:34:28 +00:00
If debug mode is not active, it will not do any of this.
"""
2021-09-26 10:44:56 +00:00
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:
2022-01-18 21:34:28 +00:00
# prefer light colors, because most people have a dark
# terminal background
continue
if g + b <= 1:
2022-01-18 21:34:28 +00:00
# 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
2022-01-18 21:34:28 +00:00
self.allowed_colors.append(self._get_ansi_code(r, g, b))
self.level_based_colors = {
logging.WARNING: 11,
logging.ERROR: 9,
logging.FATAL: 9,
}
2022-01-18 21:34:28 +00:00
def _get_ansi_code(self, r, g, b):
return 16 + b + (6 * g) + (36 * r)
2022-01-18 21:34:28 +00:00
def _word_to_color(self, word):
"""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]
2022-01-18 21:34:28 +00:00
def _allocate_debug_log_color(self, record):
"""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]
2022-01-18 21:34:28 +00:00
color = self._word_to_color(record.filename)
2022-01-18 21:34:28 +00:00
if self.file_color_mapping.get(record.filename) is None:
# calculate the color for each file only once
2022-01-18 21:34:28 +00:00
self.file_color_mapping[record.filename] = color
return color
2022-01-18 21:34:28 +00:00
def _get_process_name(self):
"""Generate a beaitiful to read name for this process."""
name = sys.argv[0].split("/")[-1].split("-")[-1]
return {
"gtk": "GUI",
"helper": "GUI-Helper",
"service": "Service",
"control": "Control",
}.get(name, name)
def _get_format(self, record):
"""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"
2022-01-18 21:34:28 +00:00
color = self._allocate_debug_log_color(record)
if record.levelno in [logging.ERROR, logging.WARNING, logging.FATAL]:
2022-01-18 21:34:28 +00:00
# underline
style = f"\033[4;38;5;{color}m"
else:
2022-01-18 21:34:28 +00:00
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")} '
2022-01-18 21:34:28 +00:00
f"\033[38;5;{process_color}m" # color
f"{os.getpid()} "
2022-01-18 21:34:28 +00:00
f"{self._get_process_name()} "
"\033[0m" # end style
2022-01-18 21:34:28 +00:00
f"{style}"
f"%(levelname)s "
f"%(filename)s:%(lineno)d: "
"%(message)s"
"\033[0m" # end style
2022-01-18 21:34:28 +00:00
).replace(" ", " ")
2020-10-26 22:45:22 +00:00
def format(self, record):
2020-11-22 20:41:29 +00:00
"""Overwritten format function."""
2020-11-26 23:34:45 +00:00
# pylint: disable=protected-access
self._style._fmt = self._get_format(record)
2020-10-26 22:45:22 +00:00
return super().format(record)
handler = logging.StreamHandler()
2022-01-18 21:34:28 +00:00
handler.setFormatter(ColorfulFormatter())
2020-10-26 22:45:22 +00:00
logger.addHandler(handler)
logger.setLevel(logging.INFO)
2021-09-26 10:44:56 +00:00
logging.getLogger("asyncio").setLevel(logging.WARNING)
2020-10-26 22:45:22 +00:00
2021-09-29 18:10:39 +00:00
2022-06-25 10:23:56 +00:00
VERSION = "1.6.0-beta"
2021-09-29 18:10:39 +00:00
EVDEV_VERSION = None
2021-03-21 13:17:34 +00:00
try:
2021-09-26 10:44:56 +00:00
EVDEV_VERSION = pkg_resources.require("evdev")[0].version
2021-03-21 13:17:34 +00:00
except pkg_resources.DistributionNotFound as error:
logger.info("Could not figure out the evdev version")
2021-03-21 13:17:34 +00:00
logger.debug(error)
2022-05-08 08:12:23 +00:00
# check if the version is something like 1.5.0-beta or 1.5.0-beta.5
IS_BETA = "beta" in VERSION
2022-04-17 10:19:23 +00:00
2020-10-26 22:45:22 +00:00
2022-01-01 12:00:49 +00:00
def log_info(name="input-remapper"):
2021-03-28 11:19:44 +00:00
"""Log version and name to the console."""
2021-03-21 13:17:34 +00:00
logger.info(
"%s %s %s https://github.com/sezanzeb/input-remapper",
name,
VERSION,
COMMIT_HASH,
2021-03-21 13:17:34 +00:00
)
2020-12-26 15:46:01 +00:00
2021-03-28 11:19:44 +00:00
if EVDEV_VERSION:
2021-09-26 10:44:56 +00:00
logger.info("python-evdev %s", EVDEV_VERSION)
2020-10-26 22:45:22 +00:00
if is_debug():
logger.warning(
2021-09-26 10:44:56 +00:00
"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!"
)
2020-10-26 22:45:22 +00:00
def update_verbosity(debug):
2020-12-05 10:33:35 +00:00
"""Set the logging verbosity according to the settings object.
Also enable rich tracebacks in debug mode.
"""
2021-02-14 11:34:56 +00:00
# pylint really doesn't like what I'm doing with rich.traceback here
# pylint: disable=broad-except,import-error,import-outside-toplevel
2020-10-26 22:45:22 +00:00
if debug:
logger.setLevel(logging.DEBUG)
2020-12-05 10:33:35 +00:00
try:
from rich.traceback import install
2021-09-26 10:44:56 +00:00
2020-12-05 10:33:35 +00:00
install(show_locals=True)
2021-09-26 10:44:56 +00:00
logger.debug("Using rich.traceback")
2020-12-05 10:33:35 +00:00
except Exception as error:
# since this is optional, just skip all exceptions
if not isinstance(error, ImportError):
2021-09-26 10:44:56 +00:00
logger.debug("Cannot use rich.traceback: %s", error)
2020-10-26 22:45:22 +00:00
else:
logger.setLevel(logging.INFO)
2022-04-15 11:32:54 +00:00
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)
2022-04-17 10:19:23 +00:00
except PermissionError:
# let the outermost PermissionError handler handle it
raise
2022-04-15 11:32:54 +00:00
except Exception as e:
logger.error('Failed to trim logfile: "%s"', str(e))