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
/.vscode
# Ignore ini files
*.ini
# Ignore logs directory
logs/

View File

@ -1,13 +1,18 @@
import argparse
import asyncio
import logging
import textwrap
def main(args: argparse.Namespace) -> int:
from sampy.config import Config
from sampy.network.protocol import Protocol
from sampy.server import InteractiveServer
server = InteractiveServer(Protocol)
server = InteractiveServer(
Protocol,
config=Config(*args.config, logging_level=args.log),
)
server.start()
asyncio.get_event_loop().run_forever()
@ -23,25 +28,83 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(
prog="sampy",
description="A SAMP server made in python",
epilog=textwrap.dedent(
description=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.
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/)
"""
),
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,
)
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(
"-v",
"--version",
"--config",
type=str,
default="latest",
help="Game version",
choices=["latest"],
nargs="+",
default=[],
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()
raise SystemExit(main(args))

View File

@ -1,8 +1,9 @@
from __future__ import annotations
import logging
import logging.config
from configparser import ConfigParser
from typing import Dict, Mapping, Union
from typing import Any, Dict, Mapping, Optional, Union
class Config(ConfigParser):
@ -20,27 +21,67 @@ class Config(ConfigParser):
"sampy.rules": {
"weburl": "https://git.osufx.com/Sunpy/sampy",
},
"logging": {
"filename": "",
"logging.loggers": {
"keys": "root",
},
"logging.handlers": {
"keys": "console",
},
"logging.formatters": {
"keys": "simple",
},
"logging.logger_root": {
"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):
super().__init__()
def __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(dictionary)
found = self.read(filenames)
found = self.read(filenames, encoding="utf-8-sig")
missing = set(filenames) - set(found)
if len(missing):
logging.warn("Config files not found: %s" % missing)
def save(self, path: str):
with open(path, "w") as f:
self.write(f)
logging_config = self.get_logging_config()
if logging_config:
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
def host(self) -> str:
@ -77,3 +118,65 @@ class Config(ConfigParser):
@property
def rules(self) -> Dict[str, str]:
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
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.config import Config
@ -19,6 +20,7 @@ class UDPProtocol:
def start(self):
loop = asyncio.get_event_loop()
logging.debug("Creating datagram endpoint")
connect = loop.create_datagram_endpoint(
lambda: self,
local_addr=self.local_addr,
@ -29,13 +31,15 @@ class UDPProtocol:
if self.transport is None:
raise Exception("Cannot stop a server that hasn't been started")
logging.debug("Shutting down")
self.transport.close()
def connection_made(self, transport: asyncio.transports.DatagramTransport):
logging.debug("UDP Protocol: connection_made")
self.transport = transport
def connection_lost(self, exc: Exception | None):
pass
logging.debug("UDP Protocol: connection_lost")
def datagram_received(self, data: bytes, addr: Tuple[str, int]):
raise NotImplementedError
@ -47,7 +51,10 @@ class UDPProtocol:
class Server(UDPProtocol):
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__(
protocol=protocol(),
local_addr=(
@ -55,6 +62,7 @@ class Server(UDPProtocol):
config.getint("sampy", "port"),
),
)
self.config = config
def datagram_received(self, data: bytes, addr: Tuple[str, int]):
@ -67,7 +75,7 @@ class Server(UDPProtocol):
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)
loop = asyncio.get_event_loop()
loop.create_task(self.run_input_loop())

View File

@ -1,4 +1,4 @@
from setuptools import setup
if __name__ == "__main__":
setup()
setup()