22import os
33import random
44from pathlib import Path
5+ from contextlib import redirect_stdout
6+
7+ from multiprocessing import Process , Queue
8+ from pythonbackend import CaptureErrors
9+ from pythonbackend .shell_utils import create
10+ from pythonbackend .tasks import TaskKillProcess , TaskCaptureFullOutput
511
612from pythonwhat .reporter import Reporter
7- from contextlib import redirect_stdout
813
914
10- class StubShell ( object ) :
15+ class StubShell :
1116 def __init__ (self , init_code = None ):
1217 self .user_ns = {}
1318 if init_code :
@@ -17,7 +22,7 @@ def run_code(self, code):
1722 exec (code , self .user_ns )
1823
1924
20- class StubProcess ( object ) :
25+ class StubProcess :
2126 def __init__ (self , init_code = None , pid = None ):
2227 self .shell = StubShell (init_code )
2328 self ._identity = (pid ,) if pid else (random .randint (0 , 1e12 ),)
@@ -26,6 +31,82 @@ def executeTask(self, task):
2631 return task (self .shell )
2732
2833
34+ class TaskCaptureOutput :
35+ def __init__ (self , code ):
36+ self .code = code
37+
38+ def __call__ (self , shell ):
39+ raw_output , error = run_code (shell .run_code , self .code )
40+
41+ return raw_output , error
42+
43+
44+ # todo: merge with pythonbackend
45+ class WorkerProcess (Process ):
46+ instances = []
47+
48+ def __init__ (self , pid = None ):
49+ Process .__init__ (self )
50+ self .task_queue = Queue ()
51+ self .result_queue = Queue ()
52+ self .daemon = (
53+ True
54+ ) # when parent process is killed, sub/childprocess get also killed
55+ self .instances .append (self )
56+ # used to detect single process exercise
57+ self ._identity = (pid ,) if pid else (random .randint (0 , 1e12 ),)
58+
59+ def get_shell (self ):
60+ return create ({})
61+
62+ def run (self ):
63+ shell = self .get_shell ()
64+ while True :
65+ output = []
66+ with CaptureErrors (output ):
67+ next_task = self .task_queue .get ()
68+ answer = next_task (shell )
69+ if len (output ) > 0 : # means backend error happened
70+ answer = output
71+ output = []
72+ with CaptureErrors (output ):
73+ self .result_queue .put_nowait (answer )
74+ if len (output ) > 0 : # means backend error happened
75+ self .result_queue .put_nowait (output )
76+ if isinstance (next_task , TaskKillProcess ):
77+ break # break while loop -> we do not wait upon new task
78+ return
79+
80+ def executeTask (self , task ):
81+ self .task_queue .put_nowait (task )
82+ return self .result_queue .get () # wait and fetches next item in queue
83+
84+ def kill (self ):
85+ try :
86+ if self .is_alive ():
87+ self .executeTask (TaskKillProcess ())
88+ self .terminate ()
89+ self .join (timeout = 3.0 )
90+ if self .is_alive ():
91+ raise Exception
92+ if self in self .instances :
93+ self .instances .remove (self )
94+ finally :
95+ pass
96+ # python 3.7:
97+ # self.close()
98+
99+ @classmethod
100+ def kill_all (cls ):
101+ for instance in list (cls .instances ):
102+ instance .kill ()
103+
104+
105+ class SimpleProcess (WorkerProcess ):
106+ def get_shell (self ):
107+ return StubShell ()
108+
109+
29110class ChDir (object ):
30111 """
31112 Step into a directory temporarily.
@@ -42,31 +123,57 @@ def __exit__(self, *args):
42123 os .chdir (self .old_dir )
43124
44125
45- def run_code (process , code ):
46- output = io .StringIO ()
47- try :
48- with redirect_stdout (output ):
49- process . shell . run_code (code )
50- raw_output = output .getvalue ()
51- error = None
52- except BaseException as e :
53- raw_output = ""
54- error = str (e )
126+ def run_code (executor , code ):
127+ with io .StringIO () as output :
128+ try :
129+ with redirect_stdout (output ):
130+ executor (code )
131+ raw_output = output .getvalue ()
132+ error = None
133+ except BaseException as e :
134+ raw_output = ""
135+ error = str (e )
55136 return raw_output , error
56137
57138
58- def run_single_process (pec , code , pid = None ):
59- process = StubProcess (init_code = pec , pid = pid )
60- raw_stu_output , error = run_code (process , code )
139+ def run_single_process (pec , code , pid = None , mode = "simple" ):
140+ if mode == "stub" :
141+ # no isolation
142+ process = StubProcess (init_code = pec , pid = pid )
143+ raw_stu_output , error = run_code (process .shell .run_code , code )
144+
145+ elif mode == "simple" :
146+ # no advanced functionality
147+ process = SimpleProcess (pid )
148+ process .start ()
149+ _ = process .executeTask (TaskCaptureOutput (pec ))
150+ raw_stu_output , error = process .executeTask (TaskCaptureOutput (code ))
151+
152+ elif mode == "full" :
153+ # slow
154+ process = WorkerProcess (pid )
155+ process .start ()
156+ _ = process .executeTask (
157+ TaskCaptureFullOutput ((pec ,), "<PEC>" , None , silent = True )
158+ )
159+ output , raw_output = process .executeTask (
160+ TaskCaptureFullOutput ((code ,), "script.py" , None , silent = True )
161+ )
162+ raw_stu_output = raw_output ["output_stream" ]
163+ error = raw_output ["error" ]
164+
165+ else :
166+ raise ValueError ("Invalid mode" )
167+
61168 return process , raw_stu_output , error
62169
63170
64- def run_exercise (pec , sol_code , stu_code , pid = None , sol_wd = None , stu_wd = None ):
171+ def run_exercise (pec , sol_code , stu_code , sol_wd = None , stu_wd = None , ** kwargs ):
65172 with ChDir (sol_wd or os .getcwd ()):
66- sol_process , _ , _ = run_single_process (pec , sol_code , pid = pid )
173+ sol_process , _ , _ = run_single_process (pec , sol_code , ** kwargs )
67174
68175 with ChDir (stu_wd or os .getcwd ()):
69- stu_process , raw_stu_output , error = run_single_process (pec , stu_code , pid = pid )
176+ stu_process , raw_stu_output , error = run_single_process (pec , stu_code , ** kwargs )
70177
71178 return sol_process , stu_process , raw_stu_output , error
72179
@@ -135,7 +242,6 @@ def run(state, relative_working_dir="", solution_dir="solution"):
135242 pec = "" ,
136243 sol_code = state .solution_code or "" ,
137244 stu_code = state .student_code ,
138- pid = None ,
139245 sol_wd = sol_wd ,
140246 stu_wd = stu_wd ,
141247 )
0 commit comments