Config and logging

This commit is contained in:
Emily 2023-03-19 01:43:14 +01:00
parent 8e59db2ad6
commit faf9ccffbc
5 changed files with 206 additions and 26 deletions

6
.gitignore vendored
View File

@ -130,3 +130,9 @@ dmypy.json
# VSC # VSC
/.vscode /.vscode
# Ignore ini files
*.ini
# Ignore logs directory
logs/

View File

@ -1,13 +1,18 @@
import argparse import argparse
import asyncio import asyncio
import logging
import textwrap import textwrap
def main(args: argparse.Namespace) -> int: def main(args: argparse.Namespace) -> int:
from sampy.config import Config
from sampy.network.protocol import Protocol from sampy.network.protocol import Protocol
from sampy.server import InteractiveServer from sampy.server import InteractiveServer
server = InteractiveServer(Protocol) server = InteractiveServer(
Protocol,
config=Config(*args.config, logging_level=args.log),
)
server.start() server.start()
asyncio.get_event_loop().run_forever() asyncio.get_event_loop().run_forever()
@ -23,25 +28,83 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="sampy", prog="sampy",
description="A SAMP server made in python", description=textwrap.dedent(
epilog=textwrap.dedent(
""" """
A SAMP server made in python
SAMP (or SA-MP) is a free multiplayer mod for the PC port of GTA: San Andreas. SAMP (or SA-MP) is a free multiplayer mod for the PC port of GTA: San Andreas.
GTA: San Andreas was developed by Rockstar North and released in 2005. GTA: San Andreas was developed by Rockstar North and released in 2005.
SA-MP is an unofficial multiplayer mod made by the `SA-MP.com` team released @[sa-mp.com](https://www.sa-mp.com/) SA-MP is an unofficial multiplayer mod made by the `SA-MP.com` team released @[sa-mp.com](https://www.sa-mp.com/)
""" """
), ),
epilog=textwrap.dedent(
"""
example:
%(prog)s --config default.ini secret.ini race.ini
This will first load default.ini configuration,
secret.ini might override the rcon- and server-password,
race.ini might change the hostname and rules.
example default.ini:
[sampy]
hostname = My SAMP Server
password =
rcon_password = VerySecretPassword
[sampy.rules]
lagcomp = off
[logging.loggers]
keys = root
[logging.handlers]
keys = console, file
[logging.formatters]
keys = simple, color
[logging.logger_root]
level = DEBUG
handlers = console, file
[logging.handler_console]
class = StreamHandler
formatter = color
args = (sys.stdout,)
[logging.handler_file]
class = logging.handlers.TimedRotatingFileHandler
formatter = simple
args = ("logs/sampy.log", "d", 1, 7,) # Note that the logs folder has to exist
[logging.formatter_simple]
format = %%(asctime)s.%%(msecs)03d | %%(levelname)-8s | %%(message)s
datefmt = %%Y-%%m-%%d %%H:%%M:%%S
[logging.formatter_color]
class = sampy.config.ColorFormatter
format = §6%%(asctime)s.%%(msecs)03d §1| %%(levelname)-8s §1|§r %%(message)s
datefmt = %%Y-%%m-%%d %%H:%%M:%%S
"""
),
formatter_class=Formatter, formatter_class=Formatter,
) )
parser.add_argument("--host", type=str, default="0.0.0.0", help="Server host ip")
parser.add_argument("-p", "--port", type=int, default=7777, help="Server port")
parser.add_argument( parser.add_argument(
"-v", "--config",
"--version",
type=str, type=str,
default="latest", nargs="+",
help="Game version", default=[],
choices=["latest"], help="Config filenames",
) )
parser.add_argument(
"--log",
type=str,
nargs="?",
help="Global logging level (Overrides any config)",
choices=logging._nameToLevel.keys(),
)
args = parser.parse_args() args = parser.parse_args()
raise SystemExit(main(args)) raise SystemExit(main(args))

View File

@ -1,8 +1,9 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import logging.config
from configparser import ConfigParser from configparser import ConfigParser
from typing import Dict, Mapping, Union from typing import Any, Dict, Mapping, Optional, Union
class Config(ConfigParser): class Config(ConfigParser):
@ -20,27 +21,67 @@ class Config(ConfigParser):
"sampy.rules": { "sampy.rules": {
"weburl": "https://git.osufx.com/Sunpy/sampy", "weburl": "https://git.osufx.com/Sunpy/sampy",
}, },
"logging": { "logging.loggers": {
"filename": "", "keys": "root",
},
"logging.handlers": {
"keys": "console",
},
"logging.formatters": {
"keys": "simple",
},
"logging.logger_root": {
"level": "INFO", "level": "INFO",
"datefmt": r"%%d-%%b-%%y %%H:%%M:%%S", "handlers": "console",
},
"logging.handler_console": {
"class": "StreamHandler",
"formatter": "simple",
"args": "(sys.stdout,)",
},
"logging.formatter_simple": {
"format": "%(levelname)s - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
}, },
} }
def __init__(self, *filenames): def __init__(
super().__init__() self,
*filenames,
dictionary: Mapping[str, Mapping[str, Union[str, int]]] = {},
logging_level: Optional[int] = None,
):
super().__init__(interpolation=None)
if logging_level is not None:
logging.root.setLevel(logging_level)
self.read_dict(self.DEFAULTS) self.read_dict(self.DEFAULTS)
self.read_dict(dictionary)
found = self.read(filenames) found = self.read(filenames, encoding="utf-8-sig")
missing = set(filenames) - set(found) missing = set(filenames) - set(found)
if len(missing): if len(missing):
logging.warn("Config files not found: %s" % missing) logging.warn("Config files not found: %s" % missing)
def save(self, path: str): logging_config = self.get_logging_config()
with open(path, "w") as f: if logging_config:
self.write(f) logging.config.fileConfig(logging_config)
if logging_level is not None:
logging.root.setLevel(logging_level)
logging.debug("Logging module has been configured")
else:
logging.warn("Logging module was not configured")
def get_logging_config(self) -> ConfigParser:
config = ConfigParser(interpolation=None)
for section in self.sections():
if not section.startswith("logging."):
continue
config[section.replace("logging.", "")] = self[section]
return config
@property @property
def host(self) -> str: def host(self) -> str:
@ -77,3 +118,65 @@ class Config(ConfigParser):
@property @property
def rules(self) -> Dict[str, str]: def rules(self) -> Dict[str, str]:
return self["sampy.rules"] return self["sampy.rules"]
class LogRecordProxy:
def __init__(self, record: logging.LogRecord):
self._record = record
def __getattribute__(self, name: str) -> Any:
attr = {
k: v
for k, v in object.__getattribute__(self, "__dict__").items()
if k != "_record"
}
if name in attr:
return attr[name]
elif name == "__dict__": # Combine dicts
return {**object.__getattribute__(self, "_record").__dict__, **attr}
return object.__getattribute__(self, "_record").__getattribute__(name)
class ColorFormatter(logging.Formatter):
COLORS: Dict[str, str] = {
"0": "30", # Black
"1": "34", # Blue
"2": "32", # Green
"3": "36", # Cyan
"4": "31", # Red,
"5": "35", # Purple/Magenta
"6": "33", # Yellow/Gold
"7": "37", # White/Light Gray
"8": "30;1", # Dark Gray
"9": "34;1", # Light Blue
"a": "32;1", # Light Green
"b": "36;1", # Light Cyan
"c": "31;1", # Light Red
"d": "35;1", # Light Purple/Magenta
"e": "33;1", # Yellow
"f": "37;1", # White
"r": "0", # Reset
"l": "1", # Bold
"n": "4", # Underline
}
LEVEL_COLOR = {
logging.CRITICAL: "31",
logging.ERROR: "31",
logging.WARNING: "33",
logging.INFO: "32",
logging.DEBUG: "35",
logging.NOTSET: "37",
}
def format(self, record: logging.LogRecord) -> str:
record = LogRecordProxy(record)
level_color = ColorFormatter.LEVEL_COLOR.get(record.levelno, None)
if level_color is not None:
record.levelname = "\x1b[%sm%s\x1b[0m" % (level_color, record.levelname)
message = super().format(record)
for k, v in ColorFormatter.COLORS.items():
message = message.replace("§%s" % k, "\x1b[%sm" % v)
return message

View File

@ -1,7 +1,8 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from typing import TYPE_CHECKING, Tuple, Type import logging
from typing import TYPE_CHECKING, Optional, Tuple, Type
from sampy.client.player import Player from sampy.client.player import Player
from sampy.config import Config from sampy.config import Config
@ -19,6 +20,7 @@ class UDPProtocol:
def start(self): def start(self):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
logging.debug("Creating datagram endpoint")
connect = loop.create_datagram_endpoint( connect = loop.create_datagram_endpoint(
lambda: self, lambda: self,
local_addr=self.local_addr, local_addr=self.local_addr,
@ -29,13 +31,15 @@ class UDPProtocol:
if self.transport is None: if self.transport is None:
raise Exception("Cannot stop a server that hasn't been started") raise Exception("Cannot stop a server that hasn't been started")
logging.debug("Shutting down")
self.transport.close() self.transport.close()
def connection_made(self, transport: asyncio.transports.DatagramTransport): def connection_made(self, transport: asyncio.transports.DatagramTransport):
logging.debug("UDP Protocol: connection_made")
self.transport = transport self.transport = transport
def connection_lost(self, exc: Exception | None): def connection_lost(self, exc: Exception | None):
pass logging.debug("UDP Protocol: connection_lost")
def datagram_received(self, data: bytes, addr: Tuple[str, int]): def datagram_received(self, data: bytes, addr: Tuple[str, int]):
raise NotImplementedError raise NotImplementedError
@ -47,7 +51,10 @@ class UDPProtocol:
class Server(UDPProtocol): class Server(UDPProtocol):
config: Config config: Config
def __init__(self, protocol: Type[Protocol], config: Config = Config()): def __init__(self, protocol: Type[Protocol], config: Optional[Config] = None):
if config is None:
config = Config()
logging.warn("Server was initialized with default config")
super().__init__( super().__init__(
protocol=protocol(), protocol=protocol(),
local_addr=( local_addr=(
@ -55,6 +62,7 @@ class Server(UDPProtocol):
config.getint("sampy", "port"), config.getint("sampy", "port"),
), ),
) )
self.config = config self.config = config
def datagram_received(self, data: bytes, addr: Tuple[str, int]): def datagram_received(self, data: bytes, addr: Tuple[str, int]):
@ -67,7 +75,7 @@ class Server(UDPProtocol):
class InteractiveServer(Server): class InteractiveServer(Server):
def __init__(self, protocol: Type[Protocol], config: Config = Config()): def __init__(self, protocol: Type[Protocol], config: Optional[Config] = None):
super().__init__(protocol=protocol, config=config) super().__init__(protocol=protocol, config=config)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.create_task(self.run_input_loop()) loop.create_task(self.run_input_loop())