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