88import queue
99import resource
1010import sys
11+ import traceback
1112from code import InteractiveConsole
1213from collections import defaultdict
1314from datetime import datetime
2526from snoop import snoop
2627
2728from main .text import pages
28- from main .utils import format_exception_string , rows_to_dicts
29+ from main .utils import rows_to_dicts , print_exception
2930from main .workers .communications import AbstractCommunications ,ThreadCommunications
3031
3132log = logging .getLogger (__name__ )
@@ -87,6 +88,14 @@ def __init__(self, *args, **kwargs):
8788console = InteractiveConsole ()
8889
8990
91+ def execute (code_obj ):
92+ try :
93+ # noinspection PyTypeChecker
94+ exec (code_obj ,console .locals )
95+ except Exception :
96+ print_exception ()
97+
98+
9099def runner (code_source ,code ):
91100if code_source == "shell" :
92101console .push (code )
@@ -104,46 +113,41 @@ def runner(code_source, code):
104113
105114try :
106115code_obj = compile (code ,filename ,"exec" )
107- except SyntaxError as e :
108- print ( format_exception_string ( e ), file = sys . stderr )
116+ except SyntaxError :
117+ print_exception ( )
109118return {}
110119
111120birdseye_objects = None
112121
113- try :
114- if code_source == "snoop" :
115- config = snoop .Config (
116- columns = (),
117- out = sys .stdout ,
118- color = True ,
119- )
120- tracer = config .snoop ()
121- tracer .variable_whitelist = set ()
122- for node in ast .walk (ast .parse (code )):
123- if isinstance (node ,ast .Name ):
124- name = node .id
125- tracer .variable_whitelist .add (name )
126- tracer .target_codes .add (code_obj )
127- with tracer :
128- exec (code_obj ,console .locals )
129- elif code_source == "birdseye" :
130- eye = BirdsEye ("sqlite://" )
131- traced_file = eye .compile (code ,filename )
132- eye ._trace ('<module>' ,filename ,traced_file ,traced_file .code ,'module' ,code )
133- try :
134- exec (traced_file .code ,eye ._trace_methods_dict (traced_file ))
135- finally :
136- with eye .db .session_scope ()as session :
137- objects = session .query (eye .db .Call ,eye .db .Function ).all ()
138- calls ,functions = [rows_to_dicts (set (column ))for column in zip (* objects )]
139- birdseye_objects = dict (calls = calls ,functions = functions )
140- else :
141- exec (code_obj ,console .locals )
142-
143- except Exception as e :
144- print (format_exception_string (e ),file = sys .stderr )
122+ if code_source == "snoop" :
123+ config = snoop .Config (
124+ columns = (),
125+ out = sys .stdout ,
126+ color = True ,
127+ )
128+ tracer = config .snoop ()
129+ tracer .variable_whitelist = set ()
130+ for node in ast .walk (ast .parse (code )):
131+ if isinstance (node ,ast .Name ):
132+ name = node .id
133+ tracer .variable_whitelist .add (name )
134+ tracer .target_codes .add (code_obj )
135+ with tracer :
136+ execute (code_obj )
137+ elif code_source == "birdseye" :
138+ eye = BirdsEye ("sqlite://" )
139+ traced_file = eye .compile (code ,filename )
140+ eye ._trace ('<module>' ,filename ,traced_file ,traced_file .code ,'module' ,code )
141+ console .locals = eye ._trace_methods_dict (traced_file )
142+ execute (traced_file .code )
143+ with eye .db .session_scope ()as session :
144+ objects = session .query (eye .db .Call ,eye .db .Function ).all ()
145+ calls ,functions = [rows_to_dicts (set (column ))for column in zip (* objects )]
146+ birdseye_objects = dict (calls = calls ,functions = functions )
147+ else :
148+ execute (code_obj )
145149
146- return dict ( birdseye_objects = birdseye_objects )
150+ return birdseye_objects
147151
148152
149153@lru_cache
@@ -202,8 +206,7 @@ def set_limits():
202206resource .setrlimit (resource .RLIMIT_NOFILE , (0 ,0 ))
203207
204208
205- def put_result (
206- q ,* ,
209+ def make_result (
207210passed = False ,
208211message = '' ,
209212awaiting_input = False ,
@@ -217,16 +220,31 @@ def put_result(
217220if output_parts is None :
218221output_parts = output_buffer .pop ()
219222
220- q . put ( dict (
223+ return dict (
221224passed = passed ,
222225message = message ,
223226awaiting_input = awaiting_input ,
224227output = output ,
225228output_parts = output_parts ,
226229birdseye_objects = birdseye_objects ,
227- ))
230+ )
228231
229- return output
232+
233+ def internal_error_result ():
234+ output = f"""
235+ INTERNAL ERROR IN COURSE:
236+ =========================
237+
238+ { "" .join (traceback .format_exception (* sys .exc_info ()))}
239+
240+ This is an error in our code, not yours.
241+ Consider using the Feedback button in the top-right menu
242+ to explain what led up to this.
243+ """
244+ return make_result (
245+ output = output ,
246+ output_parts = [dict (color = "red" ,text = output )],
247+ )
230248
231249
232250def worker_loop_in_thread (* args ):
@@ -244,15 +262,15 @@ def worker_loop(task_queue, input_queue, result_queue):
244262
245263while True :
246264entry = task_queue .get ()
247- run_code (entry ,input_queue ,result_queue )
265+ try :
266+ run_code (entry ,input_queue ,result_queue )
267+ except Exception :
268+ result_queue .put (internal_error_result ())
248269
249270
250271def run_code (entry ,input_queue ,result_queue ):
251272def readline ():
252- put_result (
253- result_queue ,
254- awaiting_input = True ,
255- )
273+ result_queue .put (make_result (awaiting_input = True ))
256274return input_queue .get ()
257275
258276sys .stdin .readline = readline
@@ -262,7 +280,7 @@ def readline():
262280try :
263281sys .stdout = output_buffer .stdout
264282sys .stderr = output_buffer .stderr
265- runner_result = runner (entry ['source' ],entry ['input' ])
283+ birdseye_objects = runner (entry ['source' ],entry ['input' ])
266284finally :
267285sys .stdout = orig_stdout
268286sys .stderr = orig_stderr
@@ -280,13 +298,12 @@ def readline():
280298else :
281299passed = step_result
282300
283- put_result (
284- result_queue ,
301+ result_queue .put (make_result (
285302passed = passed ,
286303message = message ,
287304output = output ,
288- birdseye_objects = runner_result . get ( " birdseye_objects" ) ,
289- )
305+ birdseye_objects = birdseye_objects ,
306+ ))
290307
291308
292309class UserProcess :
@@ -325,6 +342,14 @@ def handle_entry(self, entry):
325342self .task_queue .put (entry )
326343
327344def await_result (self ,callback ):
345+ try :
346+ result = self ._await_result ()
347+ except Exception :
348+ result = internal_error_result ()
349+ self .awaiting_input = result ["awaiting_input" ]
350+ callback (result )
351+
352+ def _await_result (self ):
328353# TODO cancel if result was cancelled by a newer handle_entry
329354result = None
330355while result is None :
@@ -336,20 +361,15 @@ def await_result(self, callback):
336361if alive :
337362self .process .terminate ()
338363self .start_process ()
339- result = dict (
364+ result = make_result (
340365output_parts = [
341366dict (color = 'red' ,text = 'The process died.\n ' ),
342367dict (color = 'red' ,text = 'Your code probably took too long.\n ' ),
343368dict (color = 'red' ,text = 'Maybe you have an infinite loop?\n ' ),
344369 ],
345- passed = False ,
346- message = '' ,
347370output = 'The process died.' ,
348- awaiting_input = False ,
349- birdseye_objects = None ,
350371 )
351- self .awaiting_input = result ["awaiting_input" ]
352- callback (result )
372+ return result
353373
354374
355375def master_consumer_loop (comms :AbstractCommunications ):
@@ -360,16 +380,19 @@ def master_consumer_loop(comms: AbstractCommunications):
360380while True :
361381entry = comms .recv_entry ()
362382user_id = str (entry ["user_id" ])
363- user_process = user_processes [user_id ]
364- user_process .handle_entry (entry )
365383
366384def callback (result ):
367385comms .send_result (user_id ,result )
368386
369- Thread (
370- target = user_process .await_result ,
371- args = [callback ],
372- ).start ()
387+ try :
388+ user_process = user_processes [user_id ]
389+ user_process .handle_entry (entry )
390+ Thread (
391+ target = user_process .await_result ,
392+ args = [callback ],
393+ ).start ()
394+ except Exception :
395+ callback (internal_error_result ())
373396
374397
375398@lru_cache ()