]> git.llucax.com Git - software/sercom-old.git/blob - src/sc_test
Se limpia el intento luego de las pruebas, se baja la probabilidad de RC entre que...
[software/sercom-old.git] / src / sc_test
1 #!/usr/bin/env python2.4
2 # -*- encoding: iso-8859-1 -*-
3 # vim: set et sw=4 sts=4 :
4
5 # Módulos estándar
6 import os
7 import pwd
8 import time
9 import signal
10 import glob
11 import shutil
12 import datetime
13 import subprocess
14 # Módulos locales
15 import sercom
16 from sercom.dbo import *
17
18 class secure_process:
19     def __init__(self, chroot, uid, gid, cpu):
20         self.chroot = chroot
21         self.uid = uid
22         self.gid = gid
23         self.cpu = cpu
24     def __call__(self):
25         from os import chroot, setuid, setgid
26         import resource
27         chroot(self.chroot)
28         setgid(self.gid)
29         setuid(self.uid)
30         x2 = lambda val: (val, val) # Devuelve una tupla con val 2 veces
31         resource.setrlimit(resource.RLIMIT_AS, x2(20*1024*1024))
32         resource.setrlimit(resource.RLIMIT_CORE, x2(0))
33         resource.setrlimit(resource.RLIMIT_CPU, x2(self.cpu))
34         resource.setrlimit(resource.RLIMIT_DATA, x2(20*1024*1024))
35         resource.setrlimit(resource.RLIMIT_FSIZE, x2(20*1024*1024)) #XXX Obtener de archivos esperados?
36         #resource.setrlimit(resource.RLIMIT_LOCKS, x2(100)) XXX NO EXISTE EN python
37         resource.setrlimit(resource.RLIMIT_MEMLOCK, x2(100))
38         resource.setrlimit(resource.RLIMIT_NOFILE, x2(100))
39         resource.setrlimit(resource.RLIMIT_NPROC, x2(0))
40         # Tratamos de forzar un sync para que entre al sleep del padre
41         time.sleep(0)
42
43 def sigchld(signum, frame):
44     """Signal handler para SIGCHILD."""
45     global hijo_muerto
46     global log
47     log.debug('Murió el hijo')
48     hijo_muerto = True
49
50 def sigterm(signum, frame):
51     """Signal handler para SIGTERM y SIGINT."""
52     global continuar, log
53     continuar = False
54     log.debug('Señal %d recibida', signum)
55
56 def compilar(intento, data_dir, ejercicio_dir, intento_dir, log):
57     # Busco makefile
58     makefile = os.path.join(ejercicio_dir, 'Makefile')
59     if not os.path.exists(makefile):
60         makefile = os.path.join(data_dir, 'Makefile')
61     # Compilo
62     log.debug('Ejecutando: make -f %s', makefile)
63     intento.inicioCompila = datetime.datetime.now()
64     make = subprocess.Popen(('make', '-f', makefile), stdout=subprocess.PIPE,
65         stderr=subprocess.PIPE, cwd=intento_dir)
66     make.wait()
67     intento.finCompila = datetime.datetime.now()
68     log.debug('Fin del comando: make -f %s', makefile)
69     # Verifico compilación
70     if make.returncode:
71         log.debug('Error al compilar, código de retorno: %d, salida estándar: '
72             '%s, salida de error: %s)', make.returncode, make.stdout.read(),
73             make.stderr.read())
74         intento.compila = False
75         #TODO enviar_respuesta(R_ERR, $mail, "ERROR AL COMPILAR!\n\n$err\n\nCódigo de retorno: $ret\n", $intento);
76         return False
77     log.debug('Compilado OK')
78     intento.compila = True
79     #TODO mail acumulativo
80     return True
81
82 def preparar(intento_dir, chroot_dir):
83     # Creo chroot - TODO copiarlo de algún lado donde ande el valgrind?
84     ejecutable = os.path.join(chroot_dir, 'tp')
85     os.mkdir(chroot_dir)
86     shutil.move(os.path.join(intento_dir, 'tp'), ejecutable)
87
88 def probar(intento, caso_de_prueba, uid, gid, intento_dir, ejercicio_dir, chroot_dir, conf, conn, log):
89     global hijo_muerto # Viene del SIGCHLD
90     hijo_muerto = False # Reseteo variable de SIGCHLD
91     # Obtengo datos útiles del caso de prueba
92     tiempo_cpu = caso_de_prueba.tiempoCpu
93     if tiempo_cpu is None:
94         tiempo_cpu = conf.get('general', 'tiempo_cpu')
95     tiempo_cpu = int(tiempo_cpu)
96     # Creo prueba nueva
97     prueba = Prueba(intento=intento, casoDePrueba=caso_de_prueba,
98         inicio=datetime.datetime.now(), connection = conn)
99     log.debug('Prueba: %s', prueba)
100     # Abro archivos para fds básicos
101     #XXX sacar nombres de archivos de prueba????
102     stdin = file(os.path.join(ejercicio_dir, 'casos_de_prueba', caso_de_prueba.nombre, 'stdin'), 'r')
103     stdout = file(os.path.join(intento_dir, 'stdout'), 'w')
104     stderr = file(os.path.join(intento_dir, 'stderr'), 'w')
105     # Ejecuto programa
106     params = ['/tp']
107     if caso_de_prueba.parametros:
108         params += params2seq(caso_de_prueba.parametros)
109     log.debug('Ejecutando %s', ' '.join(params))
110     os.seteuid(0) # Dios! (para chroot)
111     try:
112         proc = subprocess.Popen(params, stdout=stdout, stderr=stderr, stdin=stdin,
113             cwd=chroot_dir, close_fds=True, preexec_fn=secure_process(chroot_dir, uid, gid, tiempo_cpu))
114     except Exception, e: # FIXME poner en el manejo de exceptiones estandar
115         try:
116             print e.child_traceback
117             raise
118         except:
119             raise
120     os.seteuid(uid) # Mortal de nuevo
121     if not hijo_muerto: # Recibido por el sigchld, para saber si murió
122         time.sleep(tiempo_cpu)
123     # Si el proceso sigue andando lo tenemos que matar
124     if not hijo_muerto:
125         log.debug('La prueba tardó más del tiempo permitido (%d segundos)',
126             tiempo_cpu)
127         os.seteuid(0) # Dios! (corre como Dios, hay que matarlo como Dios)
128         os.kill(proc.pid, signal.SIGKILL)
129         os.seteuid(uid) # Mortal de nuevo
130         log.debug('Prueba cancelada (kill)')
131         proc.wait()
132         prueba.observaciones = 'Excedió el límite de tiempo de ejecución ' \
133             '(%d seg)' % tiempo_cpu
134         prueba.fin = datetime.datetime.now()
135         prueba.pasada = False
136         return prueba
137     proc.wait() # Para que no queden zombies
138     prueba.fin = str(datetime.datetime.now())
139     # Salió con una señal?
140     if proc.returncode < 0:
141         sig = -proc.returncode
142         log.debug('El programa salió con la señal %d', sig)
143         prueba.pasada = False
144         #TODO otras señales conocidas
145         if sig == signal.SIGXCPU:
146             prueba.observaciones = 'Excedió el límite de tiempo de CPU ' \
147                 '(%d seg)' % tiempo_cpu
148         else:
149             prueba.observaciones = 'Salió con la señal %d' % sig
150         return prueba
151     # Si tenemos que verificar el código de retorno
152     if caso_de_prueba.codigoRetorno is not None:
153         #FIXME trucho lo de 256
154         # Si el código de error esperado es 256 => el código de error debe ser != 0
155         # Si no el código de error esperado debe ser igual al obtenido
156         if caso_de_prueba.codigoRetorno != proc.returncode \
157                 or caso_de_prueba.codigoRetorno == 256 \
158                     and proc.returncode != 0:
159             log.debug('Código de retorno incorrecto (debía ser %d y se obtuvo %d)',
160                 caso_de_prueba.codigoRetorno, proc.returncode)
161             prueba.pasada = False
162             prueba.observaciones = 'Código de retorno incorrecto (debía ' \
163                 'ser %d y se obtuvo %d)' % (caso_de_prueba.codigoRetorno,
164                 proc.returncode)
165     #TODO verificar salidas, hacer diff
166     log.debug('Fin de ejecución de caso de prueba (hijo: %d, ret: %d)',
167         proc.pid, proc.returncode)
168     prueba.pasada = True
169     log.debug('Prueba OK: %s', prueba)
170     return prueba
171
172 def params2seq(params):
173     r"""Parsea un string de forma similar al bash, separando por espacios y
174     teniendo en cuenta comillas simples y dobles para agrupar. Para poner
175     comillas se puede usar el \ como caracter de escape (\' y \") y también
176     interpreta \n y \t. Devuelve una lista con los parámetros encontrados."""
177     # Constantes
178     SEP, TOKEN, DQUOTE, SQUOTE = ' ', None, '"', "'"
179     seq = []
180     buff = ''
181     escape = False
182     state = SEP
183     for c in params:
184         # Es un caracter escapado
185         if escape:
186             if c == 'n':
187                 buff += '\n'
188             elif c == 't':
189                 buff += '\t'
190             else:
191                 buff += c
192             escape = False
193             continue
194         # Es una secuencia de escape
195         if c == '\\':
196             escape = True
197             continue
198         # Si está buscando espacios
199         if state == SEP:
200             if c == SEP:
201                 continue
202             else:
203                 state = TOKEN # Encontró
204         if state == TOKEN:
205             if c == DQUOTE:
206                 state = DQUOTE
207                 continue
208             if c == SQUOTE:
209                 state = SQUOTE
210                 continue
211             if c == SEP:
212                 state = SEP
213                 seq.append(buff)
214                 buff = ''
215                 continue
216             buff += c
217             continue
218         if state == DQUOTE:
219             if c == DQUOTE:
220                 state = TOKEN
221                 continue
222             buff += c
223             continue
224         if state == SQUOTE:
225             if c == SQUOTE:
226                 state = TOKEN
227                 continue
228             buff += c
229             continue
230         raise Exception, 'No tiene sentido'
231     if state == DQUOTE or state == SQUOTE:
232         raise Exception, 'Parse error, falta cerrar comilla (%s)' % state
233     if buff:
234         seq.append(buff)
235     return seq
236
237 # Conecto señales
238 signal.signal(signal.SIGTERM, sigterm)
239 signal.signal(signal.SIGINT, sigterm)
240 signal.signal(signal.SIGCHLD, sigchld)
241 hijo_muerto = False
242
243 # Inicializo
244 conf, conn, log =  sercom.init('test')
245 log.info('Iniciado')
246
247 # Obtengo id de usuario con el cual ejecutar las pruebas
248 uid, gid = pwd.getpwnam(conf.get('general', 'user'))[2:4]
249
250 # Cambio UID efectivo
251 os.seteuid(uid)
252
253 # Algunas variables de configuración útiles
254 data_dir = conf.get('general', 'data_dir')
255 intervalo = float(conf.get('general', 'intervalo'))
256
257 # Hasta que nos maten
258 continuar = True # Cambia con una señal
259 while continuar:
260     # Busco intento a probar
261     intento = Intento.getProximoAProbar(conn)
262     if not intento:
263         log.debug('No hay intento para probar')
264         time.sleep(intervalo)
265         continue
266     log.info('Nuevo intento a probar (%s)', intento)
267     # Obtengo paths
268     intento_dir = os.path.join(data_dir, intento.path('intentos'))
269     ejercicio_dir = os.path.join(data_dir, 'ejercicios', str(intento.entrega.ejercicioID))
270     chroot_dir = os.path.join(intento_dir, 'chroot')
271     # Compila
272     if not compilar(intento, data_dir, ejercicio_dir, intento_dir, log):
273         #TODO mandar mail
274         continue
275     # Prepara archivos
276     preparar(intento_dir, chroot_dir)
277     # Ejecución de casos de prueba
278     intento.inicioPruebas = datetime.datetime.now()
279     pruebas = []
280     for caso_de_prueba in intento.entrega.ejercicio.casosDePrueba:
281         pruebas.append(probar(intento, caso_de_prueba, uid, gid, intento_dir, ejercicio_dir, chroot_dir, conf, conn, log))
282     intento.finPruebas = datetime.datetime.now()
283     # Limpio directorio
284     log.debug('Borrando chroot')
285     shutil.rmtree(chroot_dir)
286     log.debug('Borrando código objeto (*.o)')
287     [os.remove(obj) for obj in glob.glob(os.path.join(intento_dir, '*.o'))]
288     #TODO Armar mail de respuesta al alumno
289     for prueba in pruebas:
290         #TODO Si es publica, veo si se hizo ok o no y voy creando mail
291         pass
292     time.sleep(intervalo)
293