improve error handling & logging

This commit is contained in:
fdev31 2023-07-29 14:33:54 +02:00
parent b50f202f1a
commit 9d0dc6df79
7 changed files with 170 additions and 55 deletions

View file

@ -11,6 +11,7 @@ Check the [wiki](https://github.com/hyprland-community/pyprland/wiki) for more i
- Add `expose` addon
- scratchpad: add "lazy" option
- fix `scratchpads`'s position on monitors using scaling
- improve error handling & logging, enable debug logs with `--debug <filename>`
## 1.3.1

View file

@ -5,11 +5,10 @@ import sys
import os
import importlib
import itertools
import traceback
from .ipc import get_event_stream
from .common import DEBUG
from .ipc import get_event_stream, init as ipc_init
from .common import init_logger, get_logger, PyprError
from .plugins.interface import Plugin
CONTROL = f'/tmp/hypr/{ os.environ["HYPRLAND_INSTANCE_SIGNATURE"] }/.pyprland.sock'
@ -25,11 +24,19 @@ class Pyprland:
def __init__(self):
self.plugins: dict[str, Plugin] = {}
self.log = get_logger()
async def load_config(self, init=True):
self.config = json.loads(
open(os.path.expanduser(CONFIG_FILE), encoding="utf-8").read()
)
try:
self.config = json.loads(
open(os.path.expanduser(CONFIG_FILE), encoding="utf-8").read()
)
except FileNotFoundError as e:
self.log.critical(
"No config file found, create one at ~/.config/hypr/pyprland.json with a valid pyprland.plugins list"
)
raise PyprError()
for name in self.config["pyprland"]["plugins"]:
if name not in self.plugins:
modname = name if "." in name else f"pyprland.plugins.{name}"
@ -39,11 +46,16 @@ class Pyprland:
await plug.init()
self.plugins[name] = plug
except Exception as e:
print(f"Error loading plugin {name}: {e}")
if DEBUG:
traceback.print_exc()
self.log.error(f"Error loading plugin {name}:", exc_info=True)
raise PyprError()
if init:
await self.plugins[name].load_config(self.config)
try:
await self.plugins[name].load_config(self.config)
except PyprError:
raise
except Exception as e:
self.log.error(f"Error initializing plugin {name}:", exc_info=True)
raise PyprError()
async def _callHandler(self, full_name, *params):
for plugin in [self] + list(self.plugins.values()):
@ -51,26 +63,25 @@ class Pyprland:
try:
await getattr(plugin, full_name)(*params)
except Exception as e:
print(f"{plugin.name}::{full_name}({params}) failed:")
traceback.print_exc()
self.log.warn(f"{plugin.name}::{full_name}({params}) failed:")
self.log.exception(e)
async def read_events_loop(self):
while not self.stopped:
data = (await self.event_reader.readline()).decode()
if not data:
print("Reader starved")
self.log.critical("Reader starved")
return
cmd, params = data.split(">>")
full_name = f"event_{cmd}"
if DEBUG:
print(f"EVT {full_name}({params.strip()})")
self.log.debug(f"EVT {full_name}({params.strip()})")
await self._callHandler(full_name, params)
async def read_command(self, reader, writer) -> None:
data = (await reader.readline()).decode()
if not data:
print("Server starved")
self.log.critical("Server starved")
return
if data == "exit\n":
self.stopped = True
@ -91,8 +102,7 @@ class Pyprland:
# run mako for notifications & uncomment this
# os.system(f"notify-send '{data}'")
if DEBUG:
print(f"CMD: {full_name}({args})")
self.log.debug(f"CMD: {full_name}({args})")
await self._callHandler(full_name, *args)
@ -116,22 +126,31 @@ async def run_daemon():
manager = Pyprland()
err_count = itertools.count()
manager.server = await asyncio.start_unix_server(manager.read_command, CONTROL)
try:
events_reader, events_writer = await get_event_stream()
except Exception as e:
print("Failed to get event stream: %s" % e)
if next(err_count) > 10:
raise
await asyncio.sleep(1)
max_retry = 10
while True:
attempt = next(err_count)
try:
events_reader, events_writer = await get_event_stream()
except Exception as e:
if attempt > max_retry:
manager.log.critical(f"Failed to open hyprland event stream: {e}.")
raise PyprError()
else:
manager.log.warn(
f"Failed to get event stream: {e}, retry {attempt}/{max_retry}..."
)
await asyncio.sleep(1)
else:
break
manager.event_reader = events_reader
try:
await manager.load_config() # ensure sockets are connected first
except FileNotFoundError:
print(
f"No config file found, create one at {CONFIG_FILE} with a valid pyprland.plugins list"
)
except PyprError:
raise SystemExit(1)
except Exception as e:
manager.log.critical(f"Failed to load config.")
raise SystemExit(1)
try:
@ -139,7 +158,7 @@ async def run_daemon():
except KeyboardInterrupt:
print("Interrupted")
except asyncio.CancelledError:
print("Bye!")
manager.log.critical("cancelled")
finally:
events_writer.close()
await events_writer.wait_closed()
@ -148,8 +167,8 @@ async def run_daemon():
async def run_client():
if sys.argv[1] in ("--help", "-h"):
manager = Pyprland()
manager = Pyprland()
if sys.argv[1] in ("--help", "-h", "help"):
await manager.load_config(init=False)
print(
"""Syntax: pypr [command]
@ -171,18 +190,37 @@ Commands:
return
_, writer = await asyncio.open_unix_connection(CONTROL)
writer.write((" ".join(sys.argv[1:])).encode())
await writer.drain()
writer.close()
await writer.wait_closed()
try:
_, writer = await asyncio.open_unix_connection(CONTROL)
except FileNotFoundError:
manager.log.critical("Failed to open control socket, is pypr daemon running ?")
raise PyprError()
else:
writer.write((" ".join(sys.argv[1:])).encode())
await writer.drain()
writer.close()
await writer.wait_closed()
def main():
if "--debug" in sys.argv:
i = sys.argv.index("--debug")
init_logger(filename=sys.argv[i + 1], force_debug=True)
del sys.argv[i : i + 2]
else:
init_logger()
ipc_init()
log = get_logger("startup")
try:
asyncio.run(run_daemon() if len(sys.argv) <= 1 else run_client())
except KeyboardInterrupt:
pass
except PyprError as e:
log.critical(f"Command failed.")
except json.decoder.JSONDecodeError as e:
log.critical(f"Invalid JSON syntax in the config file: {e.args[0]}")
except Exception as e:
log.critical("Unhandled exception:", exc_info=True)
if __name__ == "__main__":

View file

@ -1,3 +1,64 @@
import os
import logging
__all__ = ["DEBUG", "get_logger", "init_logger"]
DEBUG = os.environ.get("DEBUG", False)
class PyprError(Exception):
pass
class LogObjects:
handlers: list[logging.Handler] = []
def init_logger(filename=None, force_debug=False):
global DEBUG
if force_debug:
DEBUG = True
class ScreenLogFormatter(logging.Formatter):
LOG_FORMAT = (
r"%(levelname)s:%(name)s - %(message)s // %(filename)s:%(lineno)d"
if DEBUG
else r"%(levelname)s: %(message)s"
)
RESET_ANSI = "\x1b[0m"
FORMATTERS = {
logging.DEBUG: logging.Formatter(LOG_FORMAT + RESET_ANSI),
logging.INFO: logging.Formatter(LOG_FORMAT + RESET_ANSI),
logging.WARNING: logging.Formatter("\x1b[33;20m" + LOG_FORMAT + RESET_ANSI),
logging.ERROR: logging.Formatter("\x1b[31;20m" + LOG_FORMAT + RESET_ANSI),
logging.CRITICAL: logging.Formatter("\x1b[31;1m" + LOG_FORMAT + RESET_ANSI),
}
def format(self, record):
return self.FORMATTERS[record.levelno].format(record)
logging.basicConfig()
if filename:
handler = logging.FileHandler(filename)
handler.setFormatter(
logging.Formatter(
fmt=r"%(asctime)s [%(levelname)s] %(name)s :: %(message)s :: %(filename)s:%(lineno)d"
)
)
LogObjects.handlers.append(handler)
handler = logging.StreamHandler()
handler.setFormatter(ScreenLogFormatter())
LogObjects.handlers.append(handler)
def get_logger(name="pypr", level=None):
logger = logging.getLogger(name)
if level is None:
logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
else:
logger.setLevel(level)
logger.propagate = False
for handler in LogObjects.handlers:
logger.addHandler(handler)
return logger

View file

@ -1,11 +1,13 @@
#!/bin/env python
import asyncio
from logging import Logger
from typing import Any
import json
import os
from .common import DEBUG
from .common import get_logger, PyprError
log: Logger = None
HYPRCTL = f'/tmp/hypr/{ os.environ["HYPRLAND_INSTANCE_SIGNATURE"] }/.socket.sock'
EVENTS = f'/tmp/hypr/{ os.environ["HYPRLAND_INSTANCE_SIGNATURE"] }/.socket2.sock'
@ -17,9 +19,12 @@ async def get_event_stream():
async def hyprctlJSON(command) -> list[dict[str, Any]] | dict[str, Any]:
"""Run an IPC command and return the JSON output."""
if DEBUG:
print("(JS)>>>", command)
ctl_reader, ctl_writer = await asyncio.open_unix_connection(HYPRCTL)
log.debug(f"JS>> {command}")
try:
ctl_reader, ctl_writer = await asyncio.open_unix_connection(HYPRCTL)
except FileNotFoundError:
log.critical("hyprctl socket not found! is it running ?")
raise PyprError()
ctl_writer.write(f"-j/{command}".encode())
await ctl_writer.drain()
resp = await ctl_reader.read()
@ -40,9 +45,13 @@ def _format_command(command_list, default_base_command):
async def hyprctl(command, base_command="dispatch") -> bool:
"""Run an IPC command. Returns success value."""
if DEBUG:
print(">>>", command)
ctl_reader, ctl_writer = await asyncio.open_unix_connection(HYPRCTL)
log.debug(f"JS>> {command}")
try:
ctl_reader, ctl_writer = await asyncio.open_unix_connection(HYPRCTL)
except FileNotFoundError:
log.critical("hyprctl socket not found! is it running ?")
raise PyprError()
if isinstance(command, list):
ctl_writer.write(
f"[[BATCH]] {' ; '.join(_format_command(command, base_command))}".encode()
@ -53,11 +62,10 @@ async def hyprctl(command, base_command="dispatch") -> bool:
resp = await ctl_reader.read(100)
ctl_writer.close()
await ctl_writer.wait_closed()
if DEBUG:
print("<<<", resp)
log.debug(f"<<JS {resp}")
r: bool = resp == b"ok" * (len(resp) // 2)
if DEBUG and not r:
print(f"FAILED {resp}")
if not r:
log.error(f"FAILED {resp}")
return r
@ -67,3 +75,8 @@ async def get_focused_monitor_props() -> dict[str, Any]:
if monitor.get("focused") == True:
return monitor
raise RuntimeError("no focused monitor")
def init():
global log
log = get_logger("ipc")

View file

@ -1,9 +1,11 @@
from typing import Any
from ..common import get_logger
class Plugin:
def __init__(self, name: str):
self.name = name
self.log = get_logger(name)
async def init(self):
pass

View file

@ -55,7 +55,7 @@ class Extension(Plugin):
mon_name = mon["description"]
break
else:
print(f"Monitor {screenid} not found")
self.log.info(f"Monitor {screenid} not found")
return
mon_by_name = {m["name"]: m for m in monitors}

View file

@ -207,7 +207,7 @@ class Extension(Plugin):
uid = uid.strip()
item = self.scratches.get(uid)
if not item:
print(f"{uid} is not configured")
self.log.warn(f"{uid} is not configured")
return
if item.visible:
await self.run_hide(uid)
@ -238,10 +238,10 @@ class Extension(Plugin):
uid = uid.strip()
item = self.scratches.get(uid)
if not item:
print(f"{uid} is not configured")
self.log.warn(f"{uid} is not configured")
return
if not item.visible and not force:
print(f"{uid} is already hidden")
self.log.warn(f"{uid} is already hidden")
return
item.visible = False
addr = "address:0x" + item.address
@ -287,15 +287,15 @@ class Extension(Plugin):
self.focused_window_tracking[uid] = await hyprctlJSON("activewindow")
if not item:
print(f"{uid} is not configured")
self.log.warn(f"{uid} is not configured")
return
if item.visible and not force:
print(f"{uid} is already visible")
self.log.warn(f"{uid} is already visible")
return
if not item.isAlive():
print(f"{uid} is not running, restarting...")
self.log.info(f"{uid} is not running, restarting...")
if uid in self.procs:
self.procs[uid].kill()
if item.pid in self.scratches_by_pid: