File Coverage

blib/lib/OnlineJudge/Progra.pm
Criterion Covered Total %
statement 152 313 48.5
branch 30 96 31.2
condition 2 6 33.3
subroutine 18 31 58.0
pod 11 24 45.8
total 213 470 45.3


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             package OnlineJudge::Progra;
3              
4 3     3   85849 use File::Spec::Functions qw(catfile catdir);
  3         3178  
  3         289  
5 3     3   3526 use Time::HiRes qw(time);
  3         6131  
  3         90  
6 3     3   3208 use Proc::Killall;
  3         49787  
  3         269  
7 3     3   3146 use File::Copy;
  3         29838  
  3         247  
8 3     3   27 use warnings;
  3         6  
  3         85  
9 3     3   17 use strict;
  3         5  
  3         11797  
10              
11             our $VERSION = '0.023';
12              
13             $0 = 'progra';
14              
15             # these words cannot be in user's source code
16             my @BADWORDS = ();
17              
18             # output and error files (only if running in background mode)
19             my $output_file = 'output.log';
20             my $error_file = 'error.log';
21             my $log_file = 'messages.log';
22              
23             # defines how source codes must be compiled according to its language
24             my $COMPILERS = {
25             'c' => '/usr/bin/gcc _SOURCECODE_ -o _BINARY_ > /dev/null 2>&1',
26             'cpp' => '/usr/bin/g++ _SOURCECODE_ -o _BINARY_ > /dev/null 2>&1',
27             };
28              
29             # defines how programs must be executed according to its language
30             my $EXEC = {
31             'pl' => '/usr/bin/perl _FILE_',
32             'py' => '/usr/bin/python _FILE_',
33             'c' => './_FILE_',
34             'cpp' => './_FILE_',
35             };
36              
37             sub new {
38 2     2 0 31 my $class = shift;
39 2         6 my $self = {};
40              
41 2         7 $self->{'background'} = 1;
42 2         7 $self->{'log'} = 1;
43 2         6 $self->{'verbose'} = undef;
44 2         7 $self->{'pid_file'} = undef;
45 2         6 $self->{'get_sub'} = undef;
46 2         6 $self->{'update_sub'} = undef;
47 2         4 $self->{'time_interval'} = 60; # in seconds
48 2         9 $self->{'home'} = '/tmp';
49 2         4 $self->{'diff_options'} = 'biw';
50              
51 2         6 bless $self, $class;
52              
53 2         7 return $self;
54             }
55              
56             sub set_background {
57 0     0 1 0 my $self = shift;
58 0 0       0 my $value = shift or undef;
59              
60 0 0       0 if ( $value ) { $self->{'background'} = 1; }
  0         0  
61             }
62              
63             # home directory is used for storing log and pid files.
64             sub set_home {
65 0     0 1 0 my $self = shift;
66 0 0       0 my $dir = shift or undef;
67              
68 0 0       0 if ( $dir ) {
69 0 0       0 if ( !(-d $dir) ) {
70 0         0 $self->error("Error: $dir does not exist or is not a directory.");
71             }
72 0         0 $self->{'home'} = $dir;
73             }
74             }
75              
76             # time interval must be in seconds
77             sub set_timeinterval {
78 0     0 1 0 my $self = shift;
79 0 0       0 my $value = shift or undef;
80              
81 0 0       0 if ( $value ) { $self->{'time_interval'} = $value; }
  0         0  
82             }
83              
84             # set a new compiler or replace a previous one
85             sub set_compiler {
86 0     0 1 0 my $self = shift;
87 0         0 my $lang = shift;
88 0         0 my $comp = shift;
89            
90 0         0 $COMPILERS->{ $lang } = $comp;
91             }
92              
93             # set a new way of executing a program or replace a previous one
94             sub set_exec {
95 0     0 1 0 my $self = shift;
96 0         0 my $lang = shift;
97 0         0 my $exec = shift;
98            
99 0         0 $EXEC->{ $lang } = $exec;
100             }
101              
102             # set logging (true by default)
103             sub set_logging {
104 1     1 1 7 my $self = shift;
105 1 50       5 my $value = shift or undef;
106            
107 1 50       6 if ( defined($value) ) { $self->{'log'} = $value; }
  1         8  
108             }
109              
110             # set diff options
111             sub diff_options {
112 0     0 1 0 my $self = shift;
113 0         0 my $options = shift;
114            
115 0 0       0 if ( $options ) { $self->{'diff_options'} = $options; }
  0         0  
116             }
117              
118             sub verbose {
119 0     0 1 0 my $self = shift;
120 0 0       0 my $value = shift or undef;
121            
122 0 0       0 if ( $value ) { $self->{'verbose'} = 1; }
  0         0  
123             }
124              
125             # e.g. system, exec, etc.
126             sub load_badwords {
127 1     1 1 24 my $self = shift;
128 1         3 my $file = shift;
129            
130 1 50       71 open my $F, '<', $file or $self->error("can't open badwords file: $file");
131 1         4 local $/;
132 1         33 my $content = <$F>;
133 1         14 close $F;
134            
135 1         7 $content =~ s/\n//g;
136 1         32 @BADWORDS = split(',', $content);
137             }
138              
139             sub run {
140 0     0 1 0 my $self = shift;
141 0         0 my %args = @_;
142            
143 0         0 my $get_sub = $args{'get_sub'};
144 0         0 my $update_sub = $args{'update_sub'};
145              
146 0 0 0     0 if ( !(defined($get_sub)) or !(defined($update_sub)) ) {
147 0         0 $self->error('missing get and/or update subroutines!');
148             }
149            
150 0         0 $self->{'get_sub'} = $get_sub;
151 0         0 $self->{'update_sub'} = $update_sub;
152            
153 0         0 my $home = $self->{'home'};
154 0         0 my $background = $self->{'background'};
155            
156 0         0 my $output = catfile( $home, $output_file );
157 0         0 my $errors = catfile( $home, $error_file );
158 0         0 $log_file = catfile( $home, $log_file );
159 0         0 my $pid_file;
160            
161 0 0       0 if ( !$background ) {
162 0         0 $pid_file = catfile( $home, $$.'.pid' );
163            
164 0 0       0 open my $F, '>', $pid_file or $self->error('could not create PID file');
165 0         0 print {$F} $$;
  0         0  
166 0         0 close $F;
167              
168             # everything is OK
169 0         0 print ":: progra running with pid $$\n";
170            
171 0         0 $self->{'pid_file'} = $pid_file;
172            
173             # start judging!
174 0         0 $self->judge();
175             }
176             else {
177 0         0 my $pid = fork();
178            
179 0 0       0 if ( $pid ) {
    0          
180             # to stop pogra delete PID file
181 0         0 $pid_file = catfile( $home, $pid.'.pid' );
182            
183 0 0       0 open my $F, '>', $pid_file or $self->error('could not create PID file');
184 0         0 print {$F} $pid;
  0         0  
185 0         0 close $F;
186              
187             # everything is OK
188 0         0 print ":: progra running with pid $pid\n";
189             }
190             elsif ( $pid == 0 ) {
191 0         0 chdir $home;
192              
193 0         0 close STDIN;
194 0         0 open STDOUT, '>>', $output;
195 0         0 open STDERR, '>>', $errors;
196            
197 0         0 $| = 1;
198            
199             # let's just wait a sec
200 0         0 sleep 1;
201 0         0 $pid_file = catfile( $home, $$.'.pid' );
202 0         0 $self->{'pid_file'} = $pid_file;
203            
204             # start judging!
205 0         0 $self->judge();
206             }
207             }
208             }
209              
210             sub judge {
211 0     0 0 0 my $self = shift;
212            
213 0         0 my $get_sub = $self->{'get_sub'};
214 0         0 my $update_sub = $self->{'update_sub'};
215 0         0 my $time_interval = $self->{'time_interval'};
216 0         0 my $pid_file = $self->{'pid_file'};
217            
218 0         0 my $date = $self->get_date();
219 0         0 print ":: progra started - $date\n";
220            
221             # main loop
222 0         0 while (1) {
223 0 0       0 if ( !(-e $pid_file) ) {
224 0         0 $date = $self->get_date();
225 0         0 print ":: progra terminated - $date\n\n";
226 0         0 last;
227             }
228            
229 0         0 my @requests = $get_sub->();
230            
231 0 0       0 if (@requests) {
232 0         0 foreach my $request ( @requests ) {
233             # process each request individually
234 0         0 my $processed = $self->process_request( $request );
235             # and update request information
236 0         0 $update_sub->( $processed );
237             }
238             }
239            
240             # do not stress
241 0         0 sleep $time_interval;
242 0         0 undef @requests;
243             }
244             }
245              
246             sub process_request {
247 5     5 0 5340 my $self = shift;
248 5         9 my $r = shift;
249              
250             # update comment before processing
251 5         18 $r->{'comment'} = 'PC';
252 5         12 $r->{'grade'} = 0;
253 5         32 $r->{'timemarked'} = time();
254             # in case source code fails before being tested
255 5         16 $r->{'executiontime'} = 0;
256              
257             # processing a request is divided in three/four steps:
258             # - check for badwords in source code.
259             # - compile source code (if needed)
260             # - test user's program.
261             # - delete created files.
262 5         29 $self->check($r);
263 5 100       22 if ( $r->{'compile'} ) { $self->compile($r); }
  1         440  
264 5         34 $self->test($r);
265 5         73 $self->clean($r);
266            
267             # build the processed request (original request may have some garbage)
268 5         24 my $processed = {};
269 5         38 $processed->{'rid'} = $r->{'rid'};
270 5         31 $processed->{'grade'} = $r->{'grade'};
271 5         28 $processed->{'executiontime'} = $r->{'executiontime'};
272 5         27 $processed->{'timemarked'} = $r->{'timemarked'};
273 5         13 $processed->{'comment'} = $r->{'comment'};
274              
275 5 50       26 if ( $self->{'verbose'} ) {
276 0         0 print "\nrequest: $processed->{'rid'}\ngrade: $processed->{'grade'}\n";
277 0         0 print "execution time: $processed->{'executiontime'}\ntime marked: ";
278 0         0 print "$processed->{'timemarked'}\ncomment: $processed->{'comment'}\n";
279             }
280            
281 5         69 return $processed;
282             }
283              
284             # this should be complemented with stronger security policies
285             # see TODO in the POD for mode detail
286             sub check {
287 5     5 0 15 my $self = shift;
288 5         9 my $r = shift;
289            
290 5         11 $r->{'executiontime'} = 0;
291            
292 5 50       385 if ( (open my $F, '<', $r->{'sourcecode'}) ) {
293 5         404 my @content = <$F>;
294 5         66 close $F;
295              
296 5         26 foreach my $bw ( @BADWORDS ) {
297 9         188 my $has_badword = grep( /$bw/, @content );
298 9 100       46 if ( $has_badword ) {
299             # request comment is updated
300 1         8 $r->{'comment'} = "BW: $bw";
301 1 50       12 if( $self->{'log'} != 0 ) {
302 0         0 $self->log($r);
303             }
304 1         236 return;
305             }
306             }
307             } else {
308 0         0 $self->warn($r->{'rid'}." - error while reading $r->{'sourcecode'}");
309 0         0 $r->{'comment'} = 'IE ('.$r->{'rid'}.')';
310             }
311             }
312              
313             # get the compile string associated with a given language
314             sub get_compile_string {
315 1     1 0 1 my $self = shift;
316 1         3 my $sourcecode = shift;
317 1         6 my $binary = shift;
318 1         2 my $lang = shift;
319            
320 1         6 my $compile_string = $COMPILERS->{ $lang };
321 1         12 $compile_string =~ s/_SOURCECODE_/$sourcecode/;
322 1         5 $compile_string =~ s/_BINARY_/$binary/;
323            
324 1         3 return $compile_string;
325             }
326              
327             sub compile {
328 1     1 0 11 my $self = shift;
329 1         5 my $r = shift;
330            
331             # if a previous step failed, it does not continue
332 1 50       8 if ( $r->{'comment'} ne 'PC' ) {
333 0         0 return;
334             }
335            
336 1         2 my $binary;
337             # all source codes have extension, right?
338 1 50       24 if ( $r->{'sourcecode'} =~ /.*\/(.+)\.\w+/ ){ $binary = $1; }
  1         8  
339            
340 1         6 my $compile_string = $self->get_compile_string( $r->{'sourcecode'}, $binary, $r->{'lang'} );
341            
342 1         19 chdir $r->{'userpath'};
343 1         107966 system( $compile_string );
344            
345 1 50       69 if ( !(-e $binary) ) { $r->{'comment'} = 'CE'; }
  1         20  
346            
347 1         36 $r->{'binary'} = $binary;
348             }
349              
350             # get the execution string associated with a given language
351             sub get_exec_string {
352 3     3 0 8 my $self = shift;
353 3         12 my $sourcecode = shift;
354 3         7 my $lang = shift;
355            
356 3         15 my $exec_string = $EXEC->{ $lang };
357 3         27 $exec_string =~ s/_FILE_/$sourcecode/;
358            
359 3         13 return $exec_string;
360             }
361              
362             sub test {
363 5     5 1 13 my $self = shift;
364 5         7 my $r = shift;
365            
366             # if a previous step failed, it does not continue
367 5 100       23 if ( $r->{'comment'} ne 'PC' ) {
368 2         6 return;
369             }
370            
371 3         6 my ($inittime, $finaltime, $totaltime) = (0, 0, 0);
372 3         6 my $averagetime;
373            
374 3         12 my $systeminput = $r->{'taskpath'}.'input.';
375 3         10 my $systemoutput = $r->{'taskpath'}.'output.';
376 3         9 my $useroutput = $r->{'userpath'}.'output';
377            
378 3         7 my $score = 0;
379            
380 3         14 for ( my $i = 0; $i < $r->{'testcases'}; $i++ ) {
381            
382 3 50       88 if ( !(-e $systeminput.$i) ) {
383 0         0 $self->warn($r->{'rid'}." - error: system input file does not exist: $systeminput$i");
384 0         0 $r->{'comment'} = 'IE ('.$r->{'rid'}.')';
385 0         0 return;
386             }
387            
388 3 50       69 if ( !(-e $systemoutput.$i) ) {
389 0         0 $self->warn($r->{'rid'}." - error: system output file does not exist: $systemoutput$i");
390 0         0 $r->{'comment'} = 'IE ('.$r->{'rid'}.')';
391 0         0 return;
392             }
393            
394 3         6 my $exec_string;
395 3 50       12 if( $r->{'compile'} ) {
396 0         0 $exec_string = $self->get_exec_string($r->{'binary'}, $r->{'lang'});
397             } else {
398 3         14 $exec_string = $self->get_exec_string($r->{'sourcecode'}, $r->{'lang'});
399             }
400            
401             # child processes are ignored in order to avoid "zombies"
402 3         60 $SIG{'CHLD'} = 'IGNORE';
403              
404 3         10 eval {
405 3     1   42 $SIG{'ALRM'} = sub { die 'time limit exceeded' };
  1         42  
406            
407 3         39 alarm $r->{'timelimit'};
408 3         9 $inittime = time();
409              
410 3         61 chdir $r->{'userpath'};
411            
412             # execute redirecting input and output
413 3         1047213 system($exec_string.' < '.$systeminput.$i.' > '.$useroutput);
414              
415 2         76 $finaltime = time();
416 2         33 alarm 0;
417             };
418            
419 3         109 killall('KILL', $r->{'sourcecode'});
420            
421 3         16757 $totaltime = $finaltime - $inittime;
422 3         31 $totaltime = substr($totaltime, 0, 6);
423             # sometimes time is negative....don't know why
424 3 100       30 $totaltime = 0 if ( $totaltime < 0 );
425            
426 3         7 $averagetime += $totaltime;
427            
428 3 100       21 if ( $@ =~ /time limit exceeded/ ) {
429 1         13 $r->{'comment'} = 'TL';
430 1 50       30 if( $self->{'log'} != 0 ) {
431 0         0 $self->log($r);
432             }
433 1         21 return;
434             }
435              
436 2 100       33 if ( $self->compare( $systemoutput.$i, $useroutput ) ) {
437             # test case passed :-)
438 1         38 $score += $r->{'maxscore'}/$r->{'testcases'};
439             }
440             }
441            
442             # approved!
443 2 100       21 if ( $score == $r->{'maxscore'} ) {
444 1         11 $r->{'comment'} = 'AC';
445             }
446             # keep trying..
447             else {
448 1         17 $r->{'comment'} = 'WA';
449             }
450            
451 2         28 $r->{'executiontime'} = $averagetime/$r->{'testcases'};
452 2         22 $r->{'grade'} = $score;
453             }
454              
455             # returns true if files are equal
456             sub compare {
457 4     4 0 25 my $self = shift;
458 4         14 my $systemoutput = shift;
459 4         9 my $useroutput = shift;
460 4         22 my $options = $self->{'diff_options'};
461              
462             # diff command will return a true value if there are any
463             # differences between the files. The -b argument ignores
464             # extra white spaces, the -w ignores all white spaces,
465             # the -i ignore case differences
466 4         36105 my $diff = `diff -$options $systemoutput $useroutput`;
467            
468 4 100       354 (!$diff) ? return 1 : return 0;
469             }
470              
471             sub clean {
472 5     5 0 20 my $self = shift;
473 5         11 my $r = shift;
474            
475             # remove output file created by user's program
476 5         523 unlink $r->{'userpath'}.'output';
477 5 50 66     69 if( $r->{'compile'} and (-e $r->{'binary'}) ) { unlink $r->{'binary'}; }
  0            
478             }
479              
480             sub warn {
481 0     0 0   my $self = shift;
482 0           my $error = shift;
483 0           my ( $package, $filename, $line, $sub, $dayname );
484              
485 0           my $date = $self->get_date();
486 0           my $errstr = ":: [$date] > ";
487 0           $errstr .= "\"$error\"\n";
488              
489             # we want to know where the error came from
490 0           $filename = ( caller(1) )[1];
491 0           $line = ( caller(1) )[2];
492 0           $sub = ( caller(1) )[3];
493              
494 0           $errstr .= " - called by $sub() in $filename line $line\n";
495 0           $errstr .= "\n";
496            
497 0           warn $errstr;
498             }
499              
500             sub error {
501 0     0 0   my $self = shift;
502 0           my $error = shift;
503 0           my ( $package, $filename, $line, $sub );
504              
505 0           my $date = $self->get_date();
506 0           my $errstr = ":: [$date] > ";
507 0           $errstr .= "\"$error\"\n";
508            
509 0           print ":: progra terminated - $date\n\n";
510              
511             # we want to know where the error came from
512 0           $filename = ( caller(1) )[1];
513 0           $line = ( caller(1) )[2];
514 0           $sub = ( caller(1) )[3];
515              
516 0           $errstr .= " - called by $sub() in $filename line $line\n";
517 0           $errstr .= "\n";
518              
519 0           die $errstr;
520             }
521              
522             # it keeps the logs in home directory
523             sub log {
524 0     0 0   my $self = shift;
525 0           my $r = shift;
526              
527 0           my ($hour, $min) = (0, 0);
528 0           my $home = $self->{'home'};
529 0           my $date = $self->get_date();
530 0           my $msg = ":: [$date] > ";
531 0           $msg .= $r->{'comment'}." on ".$r->{'rid'}."\n";
532            
533 0           print ":: logging - $date\n";
534            
535 0 0         open my $F, '>>', $log_file or error('cannot open messages.log file!');
536 0           print {$F} $msg;
  0            
537 0           close $F;
538            
539             # save the source code that caused logging
540 0 0         if ( $date =~ /.*\s+(\d+):(\d+)/ ) { ($hour, $min) = ($1, $2); }
  0            
541            
542 0 0         if ( !(-d catdir($home, 'logged')) ) {
543 0           mkdir catdir($home, 'logged');
544             }
545            
546 0           my $filename = catfile( $home, catfile( 'logged', $r->{'rid'}.'_'.$hour.$min.'.'.$r->{'lang'} ) );
547 0           copy( $r->{'sourcecode'}, $filename );
548             }
549              
550             sub get_date {
551 0     0 0   my $self = shift;
552            
553 0           my ($sec,$min,$hour,$mday,$mon,$year, $wday) = localtime(time);
554 0           my $days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
555              
556 0           $year += 1900;
557 0           $mon++;
558              
559 0           my $dayname = $days->[$wday];
560            
561 0 0         $hour = '00' if ($hour == 0);
562 0 0         $hour = '0'.$hour if ($hour < 10);
563 0 0         $min = '00' if ($min == 0);
564 0 0         $min = '0'.$min if ($min < 10);
565 0 0         $mon = '0'.$mon if ($mon < 10);
566 0           my $date = "$dayname $year/$mon/$mday $hour:$min";
567            
568 0           return $date;
569             }
570              
571             1;
572              
573             __END__