improve error handling & logging
This commit is contained in:
parent
b50f202f1a
commit
9d0dc6df79
7 changed files with 170 additions and 55 deletions
|
@ -11,6 +11,7 @@ Check the [wiki](https://github.com/hyprland-community/pyprland/wiki) for more i
|
||||||
- Add `expose` addon
|
- Add `expose` addon
|
||||||
- scratchpad: add "lazy" option
|
- scratchpad: add "lazy" option
|
||||||
- fix `scratchpads`'s position on monitors using scaling
|
- fix `scratchpads`'s position on monitors using scaling
|
||||||
|
- improve error handling & logging, enable debug logs with `--debug <filename>`
|
||||||
|
|
||||||
## 1.3.1
|
## 1.3.1
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,10 @@ import sys
|
||||||
import os
|
import os
|
||||||
import importlib
|
import importlib
|
||||||
import itertools
|
import itertools
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
from .ipc import get_event_stream
|
from .ipc import get_event_stream, init as ipc_init
|
||||||
from .common import DEBUG
|
from .common import init_logger, get_logger, PyprError
|
||||||
from .plugins.interface import Plugin
|
from .plugins.interface import Plugin
|
||||||
|
|
||||||
CONTROL = f'/tmp/hypr/{ os.environ["HYPRLAND_INSTANCE_SIGNATURE"] }/.pyprland.sock'
|
CONTROL = f'/tmp/hypr/{ os.environ["HYPRLAND_INSTANCE_SIGNATURE"] }/.pyprland.sock'
|
||||||
|
@ -25,11 +24,19 @@ class Pyprland:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.plugins: dict[str, Plugin] = {}
|
self.plugins: dict[str, Plugin] = {}
|
||||||
|
self.log = get_logger()
|
||||||
|
|
||||||
async def load_config(self, init=True):
|
async def load_config(self, init=True):
|
||||||
|
try:
|
||||||
self.config = json.loads(
|
self.config = json.loads(
|
||||||
open(os.path.expanduser(CONFIG_FILE), encoding="utf-8").read()
|
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"]:
|
for name in self.config["pyprland"]["plugins"]:
|
||||||
if name not in self.plugins:
|
if name not in self.plugins:
|
||||||
modname = name if "." in name else f"pyprland.plugins.{name}"
|
modname = name if "." in name else f"pyprland.plugins.{name}"
|
||||||
|
@ -39,11 +46,16 @@ class Pyprland:
|
||||||
await plug.init()
|
await plug.init()
|
||||||
self.plugins[name] = plug
|
self.plugins[name] = plug
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading plugin {name}: {e}")
|
self.log.error(f"Error loading plugin {name}:", exc_info=True)
|
||||||
if DEBUG:
|
raise PyprError()
|
||||||
traceback.print_exc()
|
|
||||||
if init:
|
if init:
|
||||||
|
try:
|
||||||
await self.plugins[name].load_config(self.config)
|
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):
|
async def _callHandler(self, full_name, *params):
|
||||||
for plugin in [self] + list(self.plugins.values()):
|
for plugin in [self] + list(self.plugins.values()):
|
||||||
|
@ -51,26 +63,25 @@ class Pyprland:
|
||||||
try:
|
try:
|
||||||
await getattr(plugin, full_name)(*params)
|
await getattr(plugin, full_name)(*params)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{plugin.name}::{full_name}({params}) failed:")
|
self.log.warn(f"{plugin.name}::{full_name}({params}) failed:")
|
||||||
traceback.print_exc()
|
self.log.exception(e)
|
||||||
|
|
||||||
async def read_events_loop(self):
|
async def read_events_loop(self):
|
||||||
while not self.stopped:
|
while not self.stopped:
|
||||||
data = (await self.event_reader.readline()).decode()
|
data = (await self.event_reader.readline()).decode()
|
||||||
if not data:
|
if not data:
|
||||||
print("Reader starved")
|
self.log.critical("Reader starved")
|
||||||
return
|
return
|
||||||
cmd, params = data.split(">>")
|
cmd, params = data.split(">>")
|
||||||
full_name = f"event_{cmd}"
|
full_name = f"event_{cmd}"
|
||||||
|
|
||||||
if DEBUG:
|
self.log.debug(f"EVT {full_name}({params.strip()})")
|
||||||
print(f"EVT {full_name}({params.strip()})")
|
|
||||||
await self._callHandler(full_name, params)
|
await self._callHandler(full_name, params)
|
||||||
|
|
||||||
async def read_command(self, reader, writer) -> None:
|
async def read_command(self, reader, writer) -> None:
|
||||||
data = (await reader.readline()).decode()
|
data = (await reader.readline()).decode()
|
||||||
if not data:
|
if not data:
|
||||||
print("Server starved")
|
self.log.critical("Server starved")
|
||||||
return
|
return
|
||||||
if data == "exit\n":
|
if data == "exit\n":
|
||||||
self.stopped = True
|
self.stopped = True
|
||||||
|
@ -91,8 +102,7 @@ class Pyprland:
|
||||||
# run mako for notifications & uncomment this
|
# run mako for notifications & uncomment this
|
||||||
# os.system(f"notify-send '{data}'")
|
# os.system(f"notify-send '{data}'")
|
||||||
|
|
||||||
if DEBUG:
|
self.log.debug(f"CMD: {full_name}({args})")
|
||||||
print(f"CMD: {full_name}({args})")
|
|
||||||
|
|
||||||
await self._callHandler(full_name, *args)
|
await self._callHandler(full_name, *args)
|
||||||
|
|
||||||
|
@ -116,22 +126,31 @@ async def run_daemon():
|
||||||
manager = Pyprland()
|
manager = Pyprland()
|
||||||
err_count = itertools.count()
|
err_count = itertools.count()
|
||||||
manager.server = await asyncio.start_unix_server(manager.read_command, CONTROL)
|
manager.server = await asyncio.start_unix_server(manager.read_command, CONTROL)
|
||||||
|
max_retry = 10
|
||||||
|
while True:
|
||||||
|
attempt = next(err_count)
|
||||||
try:
|
try:
|
||||||
events_reader, events_writer = await get_event_stream()
|
events_reader, events_writer = await get_event_stream()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Failed to get event stream: %s" % e)
|
if attempt > max_retry:
|
||||||
if next(err_count) > 10:
|
manager.log.critical(f"Failed to open hyprland event stream: {e}.")
|
||||||
raise
|
raise PyprError()
|
||||||
|
else:
|
||||||
|
manager.log.warn(
|
||||||
|
f"Failed to get event stream: {e}, retry {attempt}/{max_retry}..."
|
||||||
|
)
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
manager.event_reader = events_reader
|
manager.event_reader = events_reader
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await manager.load_config() # ensure sockets are connected first
|
await manager.load_config() # ensure sockets are connected first
|
||||||
except FileNotFoundError:
|
except PyprError:
|
||||||
print(
|
raise SystemExit(1)
|
||||||
f"No config file found, create one at {CONFIG_FILE} with a valid pyprland.plugins list"
|
except Exception as e:
|
||||||
)
|
manager.log.critical(f"Failed to load config.")
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -139,7 +158,7 @@ async def run_daemon():
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("Interrupted")
|
print("Interrupted")
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
print("Bye!")
|
manager.log.critical("cancelled")
|
||||||
finally:
|
finally:
|
||||||
events_writer.close()
|
events_writer.close()
|
||||||
await events_writer.wait_closed()
|
await events_writer.wait_closed()
|
||||||
|
@ -148,8 +167,8 @@ async def run_daemon():
|
||||||
|
|
||||||
|
|
||||||
async def run_client():
|
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)
|
await manager.load_config(init=False)
|
||||||
print(
|
print(
|
||||||
"""Syntax: pypr [command]
|
"""Syntax: pypr [command]
|
||||||
|
@ -171,7 +190,12 @@ Commands:
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
_, writer = await asyncio.open_unix_connection(CONTROL)
|
_, 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())
|
writer.write((" ".join(sys.argv[1:])).encode())
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
writer.close()
|
writer.close()
|
||||||
|
@ -179,10 +203,24 @@ Commands:
|
||||||
|
|
||||||
|
|
||||||
def main():
|
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:
|
try:
|
||||||
asyncio.run(run_daemon() if len(sys.argv) <= 1 else run_client())
|
asyncio.run(run_daemon() if len(sys.argv) <= 1 else run_client())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -1,3 +1,64 @@
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
__all__ = ["DEBUG", "get_logger", "init_logger"]
|
||||||
|
|
||||||
DEBUG = os.environ.get("DEBUG", False)
|
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
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
#!/bin/env python
|
#!/bin/env python
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from logging import Logger
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import json
|
import json
|
||||||
import os
|
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'
|
HYPRCTL = f'/tmp/hypr/{ os.environ["HYPRLAND_INSTANCE_SIGNATURE"] }/.socket.sock'
|
||||||
EVENTS = f'/tmp/hypr/{ os.environ["HYPRLAND_INSTANCE_SIGNATURE"] }/.socket2.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]:
|
async def hyprctlJSON(command) -> list[dict[str, Any]] | dict[str, Any]:
|
||||||
"""Run an IPC command and return the JSON output."""
|
"""Run an IPC command and return the JSON output."""
|
||||||
if DEBUG:
|
log.debug(f"JS>> {command}")
|
||||||
print("(JS)>>>", command)
|
try:
|
||||||
ctl_reader, ctl_writer = await asyncio.open_unix_connection(HYPRCTL)
|
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())
|
ctl_writer.write(f"-j/{command}".encode())
|
||||||
await ctl_writer.drain()
|
await ctl_writer.drain()
|
||||||
resp = await ctl_reader.read()
|
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:
|
async def hyprctl(command, base_command="dispatch") -> bool:
|
||||||
"""Run an IPC command. Returns success value."""
|
"""Run an IPC command. Returns success value."""
|
||||||
if DEBUG:
|
log.debug(f"JS>> {command}")
|
||||||
print(">>>", command)
|
try:
|
||||||
ctl_reader, ctl_writer = await asyncio.open_unix_connection(HYPRCTL)
|
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):
|
if isinstance(command, list):
|
||||||
ctl_writer.write(
|
ctl_writer.write(
|
||||||
f"[[BATCH]] {' ; '.join(_format_command(command, base_command))}".encode()
|
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)
|
resp = await ctl_reader.read(100)
|
||||||
ctl_writer.close()
|
ctl_writer.close()
|
||||||
await ctl_writer.wait_closed()
|
await ctl_writer.wait_closed()
|
||||||
if DEBUG:
|
log.debug(f"<<JS {resp}")
|
||||||
print("<<<", resp)
|
|
||||||
r: bool = resp == b"ok" * (len(resp) // 2)
|
r: bool = resp == b"ok" * (len(resp) // 2)
|
||||||
if DEBUG and not r:
|
if not r:
|
||||||
print(f"FAILED {resp}")
|
log.error(f"FAILED {resp}")
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,3 +75,8 @@ async def get_focused_monitor_props() -> dict[str, Any]:
|
||||||
if monitor.get("focused") == True:
|
if monitor.get("focused") == True:
|
||||||
return monitor
|
return monitor
|
||||||
raise RuntimeError("no focused monitor")
|
raise RuntimeError("no focused monitor")
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
global log
|
||||||
|
log = get_logger("ipc")
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from ..common import get_logger
|
||||||
|
|
||||||
|
|
||||||
class Plugin:
|
class Plugin:
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.log = get_logger(name)
|
||||||
|
|
||||||
async def init(self):
|
async def init(self):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -55,7 +55,7 @@ class Extension(Plugin):
|
||||||
mon_name = mon["description"]
|
mon_name = mon["description"]
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print(f"Monitor {screenid} not found")
|
self.log.info(f"Monitor {screenid} not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
mon_by_name = {m["name"]: m for m in monitors}
|
mon_by_name = {m["name"]: m for m in monitors}
|
||||||
|
|
|
@ -207,7 +207,7 @@ class Extension(Plugin):
|
||||||
uid = uid.strip()
|
uid = uid.strip()
|
||||||
item = self.scratches.get(uid)
|
item = self.scratches.get(uid)
|
||||||
if not item:
|
if not item:
|
||||||
print(f"{uid} is not configured")
|
self.log.warn(f"{uid} is not configured")
|
||||||
return
|
return
|
||||||
if item.visible:
|
if item.visible:
|
||||||
await self.run_hide(uid)
|
await self.run_hide(uid)
|
||||||
|
@ -238,10 +238,10 @@ class Extension(Plugin):
|
||||||
uid = uid.strip()
|
uid = uid.strip()
|
||||||
item = self.scratches.get(uid)
|
item = self.scratches.get(uid)
|
||||||
if not item:
|
if not item:
|
||||||
print(f"{uid} is not configured")
|
self.log.warn(f"{uid} is not configured")
|
||||||
return
|
return
|
||||||
if not item.visible and not force:
|
if not item.visible and not force:
|
||||||
print(f"{uid} is already hidden")
|
self.log.warn(f"{uid} is already hidden")
|
||||||
return
|
return
|
||||||
item.visible = False
|
item.visible = False
|
||||||
addr = "address:0x" + item.address
|
addr = "address:0x" + item.address
|
||||||
|
@ -287,15 +287,15 @@ class Extension(Plugin):
|
||||||
self.focused_window_tracking[uid] = await hyprctlJSON("activewindow")
|
self.focused_window_tracking[uid] = await hyprctlJSON("activewindow")
|
||||||
|
|
||||||
if not item:
|
if not item:
|
||||||
print(f"{uid} is not configured")
|
self.log.warn(f"{uid} is not configured")
|
||||||
return
|
return
|
||||||
|
|
||||||
if item.visible and not force:
|
if item.visible and not force:
|
||||||
print(f"{uid} is already visible")
|
self.log.warn(f"{uid} is already visible")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not item.isAlive():
|
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:
|
if uid in self.procs:
|
||||||
self.procs[uid].kill()
|
self.procs[uid].kill()
|
||||||
if item.pid in self.scratches_by_pid:
|
if item.pid in self.scratches_by_pid:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue