Commit 243472fa authored by Eckhart Arnold's avatar Eckhart Arnold

- server.py/log.py/configuration.py: added optional server logging

parent 72f4531d
......@@ -179,6 +179,17 @@ CONFIG_PRESET['server_default_port'] = 8888
CONFIG_PRESET['debug_compiler'] = False
########################################################################
#
# logging support configuration
#
########################################################################
# Log server traffic (requests and responses)
# Default value: False
CONFIG_PRESET['log_server'] = False
########################################################################
#
# testing framework configuration
......
......@@ -62,6 +62,7 @@ from DHParser.toolkit import is_filename, escape_control_characters, GLOBALS
__all__ = ('log_dir',
'logging',
'is_logging',
'create_log',
'append_log',
'clear_logs',
'HistoryRecord',
......@@ -121,6 +122,7 @@ def log_dir() -> Union[str, bool]:
return dirname
#TODO: Remove this context manager, not really useful...s
@contextlib.contextmanager
def logging(dirname="LOGS"):
"""Context manager. Log files within this context will be stored in
......@@ -151,17 +153,34 @@ def is_logging() -> bool:
return False
def append_log(log_name: str, text: str) -> None:
"""Appends text to the log-file with the name 'log_name' if logging is on.
def create_log(log_name: str) -> str:
"""
Creates a new log file in the log directory. If a file with
the same name already exists, it will be overwritten.
:param log_name: The file name of the log file to be created
:return: the file name of the log file.
"""
ldir = log_dir()
if ldir:
with open(os.path.join(ldir, log_name), 'w') as f:
f.write('LOG-FILE: ' + log_name + '\n\n')
return log_name
def append_log(log_name: str, *strings) -> None:
"""
Appends one or more strings to the log-file with the name 'log_name'.
"""
ldir = log_dir()
if ldir:
with open(os.path.join(ldir, log_name), 'a') as f:
f.write(text)
for text in strings:
f.write(text)
def clear_logs(logfile_types=frozenset(['.cst', '.ast', '.log'])):
"""Removes all logs from the log-directory and removes the
"""
Removes all logs from the log-directory and removes the
log-directory if it is empty.
"""
log_dirname = log_dir()
......
......@@ -54,7 +54,7 @@ from typing import Callable, Coroutine, Optional, Union, Dict, List, Tuple, Sequ
cast
from DHParser.syntaxtree import DHParser_JSONEncoder
from DHParser.log import is_logging
from DHParser.log import create_log, append_log
from DHParser.toolkit import get_config_value, GLOBALS, re
from DHParser.versionnumber import __version__
......@@ -208,7 +208,10 @@ class Server:
self.pp_executor = None # type: Optional[ProcessPoolExecutor]
self.tp_executor = None # type: Optional[ThreadPoolExecutor]
self.log_file = (self.__class__.__name__ + '.log') if is_logging() else '' # type: str
if get_config_value('log_server'):
self.log_file = create_log('%s_%s.log' % (self.__class__.__name__, hex(id(self))[2:])) # type: str
else:
self.log_file = ''
self.loop = None # just for python 3.5 compatibility...
......@@ -268,6 +271,15 @@ class Server:
self.pp_executor = ProcessPoolExecutor()
await execute(self.pp_executor, method, params)
def respond(response: bytes):
nonlocal writer
if self.log_file:
append_log(self.log_file, 'RESPONSE: ', response.decode(), '\n\n')
writer.write(response)
if self.log_file:
append_log(self.log_file, 'RECEIVE: ', data.decode(), '\n')
if data.startswith(b'GET'):
# HTTP request
m = re.match(RE_GREP_URL, data)
......@@ -275,7 +287,7 @@ class Server:
if m:
func_name, argument = m.group(1).decode().strip('/').split('/', 1) + [None]
if func_name.encode() == STOP_SERVER_REQUEST:
writer.write(http_response(ONELINER_HTML.format(line=self.stop_response)))
respond(http_response(ONELINER_HTML.format(line=self.stop_response)))
kill_switch = True
else:
func = self.rpc_table.get(func_name,
......@@ -284,20 +296,20 @@ class Server:
await run(func.__name__, func, (argument,) if argument else ())
if rpc_error is None:
if isinstance(result, str):
writer.write(http_response(result))
respond(http_response(result))
else:
writer.write(http_response(
respond(http_response(
json.dumps(result, indent=2, cls=DHParser_JSONEncoder)))
else:
writer.write(http_response(rpc_error[1]))
respond(http_response(rpc_error[1]))
elif not re.match(RE_IS_JSONRPC, data):
# plain data
if oversized:
writer.write("Source code too large! Only %i MB allowed"
respond("Source code too large! Only %i MB allowed"
% (self.max_source_size // (1024 ** 2)))
elif data.startswith(STOP_SERVER_REQUEST):
writer.write(self.stop_response.encode())
respond(self.stop_response.encode())
kill_switch = True
else:
m = re.match(RE_FUNCTION_CALL, data)
......@@ -316,11 +328,11 @@ class Server:
await run(func_name, func, argument)
if rpc_error is None:
if isinstance(result, str):
writer.write(result.encode())
respond(result.encode())
else:
writer.write(json.dumps(result, cls=DHParser_JSONEncoder).encode())
respond(json.dumps(result, cls=DHParser_JSONEncoder).encode())
else:
writer.write(rpc_error[1].encode())
respond(rpc_error[1].encode())
else:
# JSON RPC
......@@ -367,9 +379,9 @@ class Server:
if rpc_error is None:
if json_id is not None:
json_result = {"jsonrpc": "2.0", "result": result, "id": json_id}
writer.write(json.dumps(json_result, cls=DHParser_JSONEncoder).encode())
respond(json.dumps(json_result, cls=DHParser_JSONEncoder).encode())
else:
writer.write(('{"jsonrpc": "2.0", "error": {"code": %i, "message": "%s"}, "id": %s}'
respond(('{"jsonrpc": "2.0", "error": {"code": %i, "message": "%s"}, "id": %s}'
% (rpc_error[0], rpc_error[1], json_id)).encode())
await writer.drain()
if kill_switch:
......
......@@ -30,8 +30,13 @@ import time
sys.path.extend(['../', './'])
from DHParser.server import Server, STOP_SERVER_REQUEST, IDENTIFY_REQUEST, SERVER_OFFLINE, asyncio_run
from DHParser.toolkit import concurrent_ident
from DHParser.toolkit import concurrent_ident, GLOBALS
# from DHParser.configuration import CONFIG_PRESET
# GLOBALS.LOGGING = 'LOGS'
# if not os.path.exists('LOGS'):
# os.mkdir('LOGS')
# CONFIG_PRESET['log_server'] = True
scriptdir = os.path.dirname(os.path.realpath(__file__))
......@@ -39,6 +44,18 @@ scriptdir = os.path.dirname(os.path.realpath(__file__))
# def compiler_dummy(src: str, log_dir: str='') -> Tuple[str, str]:
# return src
def stop_server():
async def send_stop_server():
try:
reader, writer = await asyncio.open_connection('127.0.0.1', 8888)
writer.write(STOP_SERVER_REQUEST)
_ = await reader.read(1024)
writer.close()
except ConnectionRefusedError:
pass
asyncio.run(send_stop_server())
class TestServer:
# def test_server(self):
......@@ -46,6 +63,7 @@ class TestServer:
# cs.run_server()
def setup(self):
stop_server()
self.windows = sys.platform.lower().find('win') >= 0
def compiler_dummy(self, src: str) -> str:
......@@ -191,7 +209,7 @@ def dummy(s: str) -> str:
def run_server(host, port):
from DHParser.server import Server
print('Starting server on %s:%i' % (host, port))
# print('Starting server on %s:%i' % (host, port))
server = Server(dummy)
server.run_server(host, port)
......@@ -203,24 +221,13 @@ if __name__ == '__main__':
class TestSpawning:
"""Tests spawning a server by starting a script via subprocess.Popen."""
def stop_server(self):
async def send_stop_server():
try:
reader, writer = await asyncio.open_connection('127.0.0.1', 8888)
writer.write(STOP_SERVER_REQUEST)
_ = await reader.read(1024)
writer.close()
except ConnectionRefusedError:
pass
asyncio.run(send_stop_server())
def setup(self):
self.tmpdir = 'tmp_' + concurrent_ident()
os.mkdir(self.tmpdir)
self.stop_server()
stop_server()
def teardown(self):
self.stop_server()
stop_server()
for fname in os.listdir(self.tmpdir):
os.remove(os.path.join(self.tmpdir, fname))
os.rmdir(self.tmpdir)
......@@ -239,7 +246,7 @@ class TestSpawning:
while countdown > 0:
try:
reader, writer = await asyncio.open_connection('127.0.0.1', 8888)
print(countdown)
# print(countdown)
countdown = 0
connected = True
except ConnectionRefusedError:
......@@ -254,7 +261,7 @@ class TestSpawning:
return ''
result = asyncio.run(identify())
print(result)
# print(result)
if __name__ == "__main__":
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment