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 ...@@ -179,6 +179,17 @@ CONFIG_PRESET['server_default_port'] = 8888
CONFIG_PRESET['debug_compiler'] = False 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 # testing framework configuration
......
...@@ -62,6 +62,7 @@ from DHParser.toolkit import is_filename, escape_control_characters, GLOBALS ...@@ -62,6 +62,7 @@ from DHParser.toolkit import is_filename, escape_control_characters, GLOBALS
__all__ = ('log_dir', __all__ = ('log_dir',
'logging', 'logging',
'is_logging', 'is_logging',
'create_log',
'append_log', 'append_log',
'clear_logs', 'clear_logs',
'HistoryRecord', 'HistoryRecord',
...@@ -121,6 +122,7 @@ def log_dir() -> Union[str, bool]: ...@@ -121,6 +122,7 @@ def log_dir() -> Union[str, bool]:
return dirname return dirname
#TODO: Remove this context manager, not really useful...s
@contextlib.contextmanager @contextlib.contextmanager
def logging(dirname="LOGS"): def logging(dirname="LOGS"):
"""Context manager. Log files within this context will be stored in """Context manager. Log files within this context will be stored in
...@@ -151,17 +153,34 @@ def is_logging() -> bool: ...@@ -151,17 +153,34 @@ def is_logging() -> bool:
return False return False
def append_log(log_name: str, text: str) -> None: def create_log(log_name: str) -> str:
"""Appends text to the log-file with the name 'log_name' if logging is on. """
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() ldir = log_dir()
if ldir: if ldir:
with open(os.path.join(ldir, log_name), 'a') as f: 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'])): 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-directory if it is empty.
""" """
log_dirname = log_dir() log_dirname = log_dir()
......
...@@ -54,7 +54,7 @@ from typing import Callable, Coroutine, Optional, Union, Dict, List, Tuple, Sequ ...@@ -54,7 +54,7 @@ from typing import Callable, Coroutine, Optional, Union, Dict, List, Tuple, Sequ
cast cast
from DHParser.syntaxtree import DHParser_JSONEncoder 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.toolkit import get_config_value, GLOBALS, re
from DHParser.versionnumber import __version__ from DHParser.versionnumber import __version__
...@@ -208,7 +208,10 @@ class Server: ...@@ -208,7 +208,10 @@ class Server:
self.pp_executor = None # type: Optional[ProcessPoolExecutor] self.pp_executor = None # type: Optional[ProcessPoolExecutor]
self.tp_executor = None # type: Optional[ThreadPoolExecutor] 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... self.loop = None # just for python 3.5 compatibility...
...@@ -268,6 +271,15 @@ class Server: ...@@ -268,6 +271,15 @@ class Server:
self.pp_executor = ProcessPoolExecutor() self.pp_executor = ProcessPoolExecutor()
await execute(self.pp_executor, method, params) 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'): if data.startswith(b'GET'):
# HTTP request # HTTP request
m = re.match(RE_GREP_URL, data) m = re.match(RE_GREP_URL, data)
...@@ -275,7 +287,7 @@ class Server: ...@@ -275,7 +287,7 @@ class Server:
if m: if m:
func_name, argument = m.group(1).decode().strip('/').split('/', 1) + [None] func_name, argument = m.group(1).decode().strip('/').split('/', 1) + [None]
if func_name.encode() == STOP_SERVER_REQUEST: 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 kill_switch = True
else: else:
func = self.rpc_table.get(func_name, func = self.rpc_table.get(func_name,
...@@ -284,20 +296,20 @@ class Server: ...@@ -284,20 +296,20 @@ class Server:
await run(func.__name__, func, (argument,) if argument else ()) await run(func.__name__, func, (argument,) if argument else ())
if rpc_error is None: if rpc_error is None:
if isinstance(result, str): if isinstance(result, str):
writer.write(http_response(result)) respond(http_response(result))
else: else:
writer.write(http_response( respond(http_response(
json.dumps(result, indent=2, cls=DHParser_JSONEncoder))) json.dumps(result, indent=2, cls=DHParser_JSONEncoder)))
else: else:
writer.write(http_response(rpc_error[1])) respond(http_response(rpc_error[1]))
elif not re.match(RE_IS_JSONRPC, data): elif not re.match(RE_IS_JSONRPC, data):
# plain data # plain data
if oversized: 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))) % (self.max_source_size // (1024 ** 2)))
elif data.startswith(STOP_SERVER_REQUEST): elif data.startswith(STOP_SERVER_REQUEST):
writer.write(self.stop_response.encode()) respond(self.stop_response.encode())
kill_switch = True kill_switch = True
else: else:
m = re.match(RE_FUNCTION_CALL, data) m = re.match(RE_FUNCTION_CALL, data)
...@@ -316,11 +328,11 @@ class Server: ...@@ -316,11 +328,11 @@ class Server:
await run(func_name, func, argument) await run(func_name, func, argument)
if rpc_error is None: if rpc_error is None:
if isinstance(result, str): if isinstance(result, str):
writer.write(result.encode()) respond(result.encode())
else: else:
writer.write(json.dumps(result, cls=DHParser_JSONEncoder).encode()) respond(json.dumps(result, cls=DHParser_JSONEncoder).encode())
else: else:
writer.write(rpc_error[1].encode()) respond(rpc_error[1].encode())
else: else:
# JSON RPC # JSON RPC
...@@ -367,9 +379,9 @@ class Server: ...@@ -367,9 +379,9 @@ class Server:
if rpc_error is None: if rpc_error is None:
if json_id is not None: if json_id is not None:
json_result = {"jsonrpc": "2.0", "result": result, "id": json_id} 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: 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()) % (rpc_error[0], rpc_error[1], json_id)).encode())
await writer.drain() await writer.drain()
if kill_switch: if kill_switch:
......
...@@ -30,8 +30,13 @@ import time ...@@ -30,8 +30,13 @@ import time
sys.path.extend(['../', './']) sys.path.extend(['../', './'])
from DHParser.server import Server, STOP_SERVER_REQUEST, IDENTIFY_REQUEST, SERVER_OFFLINE, asyncio_run 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__)) scriptdir = os.path.dirname(os.path.realpath(__file__))
...@@ -39,6 +44,18 @@ 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]: # def compiler_dummy(src: str, log_dir: str='') -> Tuple[str, str]:
# return src # 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: class TestServer:
# def test_server(self): # def test_server(self):
...@@ -46,6 +63,7 @@ class TestServer: ...@@ -46,6 +63,7 @@ class TestServer:
# cs.run_server() # cs.run_server()
def setup(self): def setup(self):
stop_server()
self.windows = sys.platform.lower().find('win') >= 0 self.windows = sys.platform.lower().find('win') >= 0
def compiler_dummy(self, src: str) -> str: def compiler_dummy(self, src: str) -> str:
...@@ -191,7 +209,7 @@ def dummy(s: str) -> str: ...@@ -191,7 +209,7 @@ def dummy(s: str) -> str:
def run_server(host, port): def run_server(host, port):
from DHParser.server import Server 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 = Server(dummy)
server.run_server(host, port) server.run_server(host, port)
...@@ -203,24 +221,13 @@ if __name__ == '__main__': ...@@ -203,24 +221,13 @@ if __name__ == '__main__':
class TestSpawning: class TestSpawning:
"""Tests spawning a server by starting a script via subprocess.Popen.""" """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): def setup(self):
self.tmpdir = 'tmp_' + concurrent_ident() self.tmpdir = 'tmp_' + concurrent_ident()
os.mkdir(self.tmpdir) os.mkdir(self.tmpdir)
self.stop_server() stop_server()
def teardown(self): def teardown(self):
self.stop_server() stop_server()
for fname in os.listdir(self.tmpdir): for fname in os.listdir(self.tmpdir):
os.remove(os.path.join(self.tmpdir, fname)) os.remove(os.path.join(self.tmpdir, fname))
os.rmdir(self.tmpdir) os.rmdir(self.tmpdir)
...@@ -239,7 +246,7 @@ class TestSpawning: ...@@ -239,7 +246,7 @@ class TestSpawning:
while countdown > 0: while countdown > 0:
try: try:
reader, writer = await asyncio.open_connection('127.0.0.1', 8888) reader, writer = await asyncio.open_connection('127.0.0.1', 8888)
print(countdown) # print(countdown)
countdown = 0 countdown = 0
connected = True connected = True
except ConnectionRefusedError: except ConnectionRefusedError:
...@@ -254,7 +261,7 @@ class TestSpawning: ...@@ -254,7 +261,7 @@ class TestSpawning:
return '' return ''
result = asyncio.run(identify()) result = asyncio.run(identify())
print(result) # print(result)
if __name__ == "__main__": 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