Config and logging
This commit is contained in:
parent
8e59db2ad6
commit
faf9ccffbc
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -129,4 +129,10 @@ dmypy.json
|
|||
.pyre/
|
||||
|
||||
# VSC
|
||||
/.vscode
|
||||
/.vscode
|
||||
|
||||
# Ignore ini files
|
||||
*.ini
|
||||
|
||||
# Ignore logs directory
|
||||
logs/
|
||||
|
|
|
@ -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))
|
||||
|
|
123
sampy/config.py
123
sampy/config.py
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in New Issue
Block a user