diff options
author | Aleš Smodiš <aless@guru.si> | 2015-09-14 14:49:54 +0200 |
---|---|---|
committer | Aleš Smodiš <aless@guru.si> | 2015-09-14 14:49:54 +0200 |
commit | afdb3c0b28715b3d9a0982e4e0504a0cbcf11e70 (patch) | |
tree | d052125c6fc4fb4803b10dd20d3c24fc3a2711f9 /server/handlers.py | |
parent | d82013c214021d6e5480d18105760fa70cfc708b (diff) |
Reimplemented communication with the client side.
* Implemented a node web server supporting asynchronous websocket and long-polling communication with clients.
* Implemented TCP communication between python middleware and node web server.
Diffstat (limited to 'server/handlers.py')
-rw-r--r-- | server/handlers.py | 302 |
1 files changed, 302 insertions, 0 deletions
diff --git a/server/handlers.py b/server/handlers.py new file mode 100644 index 0000000..e9cf7c8 --- /dev/null +++ b/server/handlers.py @@ -0,0 +1,302 @@ +# coding=utf-8 + +from concurrent.futures import ThreadPoolExecutor +import traceback +from errors.session import * +import server + + +class CodeqService(object): + """Base class for all CodeQ services. + """ + session_is_optional = False + + def process(self, request): + pass + + +class ProblemList(CodeqService): + """List all available problems to the client. + """ + session_is_optional = True + + def process(self, request): + request.reply({'code': 0, 'message': 'ok', 'problems': server.problems.list_problems()}) + + +class Login(CodeqService): + """Logs in a client, creating a new session. + """ + session_is_optional = True + + def process(self, request): + js = request.data + username = js.get('username') + password = js.get('password') + if username is None: + request.reply({'code': 1, 'message': 'Username was not provided'}) + elif password is None: + request.reply({'code': 2, 'message': 'Password was not provided'}) + else: + try: + session = server.user_session.authenticate_and_create_session(username, password) + except AuthenticationFailed: + request.reply({'code': 3, 'message': 'Username or password do not match'}) + else: + if request.session: + request.session.destroy() + request.reply({'code': 0, 'message': 'OK', 'sid':session.get_sid()}) + + +class Activity(CodeqService): + def process(self, request): + js = request.data + trace = js.get('trace') + solution = js.get('solution') + problem_id = js.get('problem_id') + if (trace is not None) or (solution is not None): + # we have something to do + if problem_id is None: + request.reply({'code': 1, 'message': 'Problem ID is missing'}) + else: + request.session.update_solution(problem_id, trace, solution) + request.end() # no feedback, just acknowledge the reception + + +class Query(CodeqService): + def process(self, request): + js = request.data + step = js.get('step') + if step is None: + request.reply({'code': 1, 'message': '"step" is not set'}) + else: + problem_id = js.get('problem_id') + if problem_id is None: + request.reply({'code': 4, 'message': 'Problem ID not given'}) + else: + session = request.session + trace = js.get('trace') + prolog = session.get_prolog() + program = None + if step == 'run': + program = js.get('program') + query = js.get('query') + if program is None: + result = {'code': 2, 'message': 'No program specified'} + elif query is None: + result = {'code': 3, 'message': 'No query specified'} + else: + messages, status, have_more = prolog.run_for_user(session.get_uid(), problem_id, program, query) + result = {'code': 0, 'message': 'ok', 'terminal': {'messages': messages, 'status': status, 'have_more': have_more}} + elif step == 'next': + messages, status, have_more = prolog.step() + result = {'code': 0, 'message': 'ok', 'terminal': {'messages': messages, 'status': status, 'have_more': have_more}} + elif step == 'end': + messages, status, have_more = prolog.end() + result = {'code': 0, 'message': 'ok', 'terminal': {'messages': messages, 'status': status, 'have_more': have_more}} + else: + result = {'code': 5, 'message': 'Unknown prolog step: {0}'.format(step)} + if program or trace: + session.update_solution(problem_id, trace, program) + request.reply(result) + + +# Pull stdout/stderr from the session's Python interpreter. TODO: convert to async handling +class PythonPull(CodeqService): + def process(self, request): + python = request.session.get_python() + output = python.pull() + request.reply({'code': 0, 'message': 'ok', 'terminal': {'text': output if output else ''}}) + + +# Push stdin to the session's Python interpreter. TODO: convert to async handling +class PythonPush(CodeqService): + def process(self, request): + text = request.data.get('text') + if text is None: + request.reply({'code': 1, 'message': 'No input specified'}) + else: + python = request.session.get_python() + python.push(text) + request.reply({'code': 0, 'message': 'ok'}) + + +class Hint(CodeqService): + def process(self, request): + js = request.data + language = js.get('language') + problem_id = js.get('problem_id') + program = js.get('program') + + if problem_id is None: + request.reply({'code': 1, 'message': 'No problem ID specified'}) + elif program is None: + request.reply({'code': 2, 'message': 'No program specified'}) + else: + session = request.session + lang_session = None + if language == 'prolog': + lang_session = session.get_prolog() + elif language == 'python': + lang_session = session.get_python() + + if lang_session is None: + request.reply({'code': 3, 'message': 'Unknown language specified'}) + else: + hints = lang_session.hint(session.get_sid(), problem_id, program) + request.reply({'code': 0, 'message': 'ok', 'hints': hints}) + + +class Test(CodeqService): + def process(self, request): + js = request.data + language = js.get('language') + problem_id = js.get('problem_id') + program = js.get('program') + + if problem_id is None: + request.reply({'code': 1, 'message': 'No problem ID specified'}) + elif program is None: + request.reply({'code': 2, 'message': 'No program specified'}) + else: + session = request.session + lang_session = None + if language == 'prolog': + lang_session = session.get_prolog() + elif language == 'python': + lang_session = session.get_python() + + if lang_session is None: + request.reply({'code': 3, 'message': 'Unknown language specified'}) + else: + hints = lang_session.test(session.get_sid(), problem_id, program) + request.reply({'code': 0, 'message': 'ok', 'hints': hints}) + + +class GetProblem(CodeqService): + def process(self, request): + js = request.data + language = js.get('language') + problem_group = js.get('problem_group') + problem = js.get('problem') + if language is None: + request.reply({'code': 1, 'message': 'Language identifier not given'}) + elif problem_group is None: + request.reply({'code': 2, 'message': 'Problem group identifier not given'}) + elif problem is None: + request.reply({'code': 3, 'message': 'Problem identifier not given'}) + else: + request.reply({'code': 0, 'message': 'ok', 'data': request.session.get_problem_data(language, problem_group, problem)}) + + +# maps actions to their handlers +incoming_handlers = { + 'list_problems': ProblemList(), + 'login': Login(), + 'get_problem': GetProblem(), + 'logout': None, + 'activity': Activity(), + 'query': Query(), + 'hint': Hint(), + 'test': Test() +} + + +class Request(object): + def __init__(self, tid, original_sid, session, data): + """Creates a new request + + :param tid: communicator-level transaction ID (global relative to the specific communicator where it originated) + :param original_sid: session ID, optional + :param session: the actual session with the original_sid, if it exists; the processor may swap it for a new session + :param data: the request data from the client + :return: new instance + """ + self._tid = tid + self._original_sid = original_sid + self.session = session + self.data = data + self.is_finished = False + + def reply(self, data): + """Reply to this request. + + :param data: the dictionary representing the reply, that will be converted to JSON + :return: None + """ + if data is None: + self.end() + if self._original_sid is not None: + sid = data.get('sid') + if sid is None: + data['sid'] = self._original_sid + elif sid != self._original_sid: + data['sid'] = self._original_sid + data['new_sid'] = sid + # it is important to reply with the same tid and sid parameters as were in the request, so message accounting doesn't get confused + send(self._tid, self._original_sid, data) + self.is_finished = True + + def end(self): + """Conclude the request, without sending a response. + + This is to acknowledge that the response has been received. + :return: None + """ + send(self._tid, self._original_sid, None) + self.is_finished = True + + +########## low-level machinery, subject to change to support more than the single socket communicator ########## + +_executor = ThreadPoolExecutor(max_workers=100) + +def _invoke_handler(handler, request): + try: + print('Worker thread processing data={}'.format(str(request.data))) + handler.process(request) + if not request.is_finished: + print('ERROR: the request was not concluded!') + request.reply({'code': -1, 'message': 'Request processing did not provide a reply'}) + print('Processing finished') + except Exception as e: + print('ERROR: data processing failed: ' + str(e)) + traceback.print_exc() + request.reply({'code': -1, 'message': 'Internal error: ' + str(e)}) + +def serve_request(json_obj): + if not isinstance(json_obj, dict): + raise RequestProcessingError('Require a request represented as a dict, instead got: ' + str(type(json_obj))) + tid = json_obj.get('tid') # session ID and transaction ID uniquely identify a transaction + sid = json_obj.get('sid') + action = json_obj.get('action') + if action is None: + raise RequestProcessingError('Request does not contain an action') + if not isinstance(action, str): + raise RequestProcessingError('Requested action must be a string, got: ' + str(type(action))) + handler = incoming_handlers.get(action) + if handler is None: + raise RequestProcessingError('No handler for ' + action) + print("Attempting to serve action={}".format(action)) + session = None + if sid is None: + if not handler.session_is_optional: + raise RequestProcessingError('Request is missing a session-ID') + else: + del json_obj['sid'] + try: + session = server.user_session.get_session_by_id(sid) + except NoSuchSession: + if not handler.session_is_optional: + raise RequestProcessingError('This user session has expired. Please log-in again.') + _executor.submit(_invoke_handler, handler, Request(tid, sid, session, json_obj)) + +def send(tid, sid, json_obj): + # just a proxy function for now + print('Sending reply: {}'.format(str(json_obj))) + server.socket.sendPacket(tid, sid, json_obj) + +def stop(): + global _executor + _executor.shutdown() + _executor = None |