summaryrefslogtreecommitdiff
path: root/server/handlers.py
blob: e9cf7c88ef62c0ee958b70d9b7b56ea470a65d97 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
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