<?php // This file is part of VPL for Moodle - http://vpl.dis.ulpgc.es/ // // VPL for Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // VPL for Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with VPL for Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Compilation and Execution of submission class definition * * @package mod_vpl * @copyright 2013 onwards Juan Carlos RodrÃguez-del-Pino * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Juan Carlos RodrÃguez-del-Pino <jcrodriguez@dis.ulpgc.es> */ defined('MOODLE_INTERNAL') || die(); require_once(dirname(__FILE__).'/../../lib/gradelib.php'); require_once(dirname(__FILE__).'/vpl_submission.class.php'); require_once(dirname(__FILE__).'/jail/jailserver_manager.class.php'); require_once(dirname(__FILE__).'/jail/running_processes.class.php'); class mod_vpl_submission_CE extends mod_vpl_submission { private static $languageext = array ( 'ada' => 'ada', 'adb' => 'ada', 'ads' => 'ada', 'all' => 'all', 'asm' => 'asm', 'c' => 'c', 'cc' => 'cpp', 'cpp' => 'cpp', 'C' => 'cpp', 'clj' => 'clojure', 'cs' => 'csharp', 'd' => 'd', 'erl' => 'erlang', 'go' => 'go', 'java' => 'java', 'js' => 'javascript', 'scala' => 'scala', 'sql' => 'sql', 'scm' => 'scheme', 's' => 'scheme', 'lisp' => 'lisp', 'lsp' => 'lisp', 'lua' => 'lua', 'mod' => 'opl', 'sh' => 'shell', 'pas' => 'pascal', 'p' => 'pascal', 'f77' => 'fortran', 'f' => 'fortran', 'pl' => 'prolog', 'pro' => 'prolog', 'htm' => 'html', 'html' => 'html', 'hs' => 'haskell', 'm' => 'matlab', 'perl' => 'perl', 'prl' => 'perl', 'php' => 'php', 'py' => 'python', 'v' => 'verilog', 'vhd' => 'vhdl', 'vhdl' => 'vhdl', 'r' => 'r', 'rb' => 'ruby', 'ruby' => 'ruby' ); private static $scriptname = array ( 'vpl_run.sh' => 'run', 'vpl_debug.sh' => 'debug', 'vpl_evaluate.sh' => 'evaluate' ); private static $scripttype = array ( 'vpl_run.sh' => 0, 'vpl_debug.sh' => 1, 'vpl_evaluate.sh' => 2, 'vpl_evaluate.cases' => 2 ); private static $scriptlist = array ( 0 => 'vpl_run.sh', 1 => 'vpl_debug.sh', 2 => 'vpl_evaluate.sh' ); const TRUN = 0; const TDEBUG = 1; const TEVALUATE = 2; /** * Return the programming language name based on submitted files extensions * * @param $filelist array * of files submitted to check type * @return string programming language name */ public function get_pln($filelist) { foreach ($filelist as $checkfilename) { $ext = pathinfo( $checkfilename, PATHINFO_EXTENSION ); if (isset( self::$languageext [$ext] )) { return self::$languageext [$ext]; } } return 'default'; } /** * Return the default script to manage the action and detected language * * @param $script string 'vpl_run.sh','vpl_debug.sh' o 'vpl_evaluate.sh' * @param $pln string Programming Language Name * @param $data object execution data * * @return array key=>filename value =>filedata */ public function get_default_script($script, $pln, $data) { $vplinstance = $this->vpl->get_instance(); $ret = array (); $path = dirname( __FILE__ ) . '/jail/default_scripts/'; $scripttype = self::$scriptname [$script]; $field = $scripttype . 'script'; if ( $data->$field > '' ) { $pln = $vplinstance->$field; } $filename = $path . $pln . '_' . $scripttype . '.sh'; if (file_exists( $filename )) { $ret [$script] = file_get_contents( $filename ); } else { $filename = $path . 'default' . '_' . $scripttype . '.sh'; if (file_exists( $filename )) { $ret [$script] = file_get_contents( $filename ); } else { $ret [$script] = file_get_contents( $path . 'default.sh' ); } } if ($script == 'vpl_evaluate.sh') { $ret ['vpl_evaluate.cpp'] = file_get_contents( $path . 'vpl_evaluate.cpp' ); } if ($pln == 'all' && $this->vpl->has_capability( VPL_MANAGE_CAPABILITY )) { // Test all scripts. $dirpath = dirname( __FILE__ ) . '/jail/default_scripts'; if (file_exists( $dirpath )) { $dirlst = opendir( $dirpath ); while ( false !== ($filename = readdir( $dirlst )) ) { if ($filename == "." || $filename == "..") { continue; } if (substr( $filename, - 7 ) == '_run.sh' || substr( $filename, - 9 ) == '_hello.sh' || substr( $filename, - 9 ) == '_debug.sh' ) { $ret [$filename] = file_get_contents( $path . $filename ); } } closedir( $dirlst ); } } return $ret; } /** * Recopile execution data to be send to the jail * * @param array $already=array(). * List of based on instances, usefull to avoid infinite recursion * @param mod_vpl $vpl. VPl instance to process. Default = null * @return object with files, limits, interactive and other info */ public function prepare_execution($type, &$already = array(), $vpl = null) { global $CFG, $DB; $plugincfg = get_config('mod_vpl'); if ($vpl == null) { $vpl = $this->vpl; } $vplinstance = $vpl->get_instance(); if (isset( $already [$vplinstance->id] )) { print_error( 'Recursive basedon vpl definition' ); } $call = count( $already ); $already [$vplinstance->id] = true; // Load basedon files if needed. if ($vplinstance->basedon) { $basedon = new mod_vpl( null, $vplinstance->basedon ); $data = $this->prepare_execution( $type, $already, $basedon ); } else { $data = new stdClass(); $data->files = array (); $data->filestodelete = array (); $data->maxtime = ( int ) $plugincfg->defaultexetime; $data->maxfilesize = ( int ) $plugincfg->defaultexefilesize; $data->maxmemory = ( int ) $plugincfg->defaultexememory; $data->maxprocesses = ( int ) $plugincfg->defaultexeprocesses; $data->jailservers = ''; $data->runscript = $vplinstance->runscript; $data->debugscript = $vplinstance->debugscript; } // Execution files. $sfg = $vpl->get_fgm('execution'); $list = $sfg->getFileList(); foreach ($list as $filename) { // Skip unneeded script. if (isset( self::$scripttype [$filename] ) && self::$scripttype [$filename] > $type) { continue; } if (isset( $data->files [$filename] ) && isset( self::$scriptname [$filename] )) { $data->files [$filename] .= "\n" . $sfg->getFileData( $filename ); } else { $data->files [$filename] = $sfg->getFileData( $filename ); } $data->filestodelete [$filename] = 1; } $deletelist = $sfg->getFileKeepList(); foreach ($deletelist as $filename) { unset( $data->filestodelete [$filename] ); } if ($vplinstance->maxexetime) { $data->maxtime = ( int ) $vplinstance->maxexetime; } if ($vplinstance->maxexememory) { $data->maxmemory = ( int ) $vplinstance->maxexememory; } if ($vplinstance->maxexefilesize) { $data->maxfilesize = ( int ) $vplinstance->maxexefilesize; } if ($vplinstance->maxexeprocesses) { $data->maxprocesses = ( int ) $vplinstance->maxexeprocesses; } // Add jailserver list. if ($vpl->get_instance()->jailservers > '') { $data->jailservers .= "\n" . $vpl->get_instance()->jailservers; } if ( $vplinstance->runscript > '' ) { $data->runscript = $vplinstance->runscript; } if ( $vplinstance->debugscript > '' ) { $data->debugscript = $vplinstance->debugscript; } if ($call > 0) { // Stop if at recursive call. return $data; } // Submitted files. $sfg = $this->get_submitted_fgm(); $list = $sfg->getFileList(); // Var $submittedlist is $list but removing the files overwrited by teacher's one. $submittedlist = array (); foreach ($list as $filename) { if (! isset( $data->files [$filename] )) { $data->files [$filename] = $sfg->getFileData( $filename ); } $submittedlist [] = $filename; } // Get programming language. $pln = $this->get_pln( $list ); // Adapt Java and HTML memory limit. if ($pln == 'java' || $pln == 'html') { $javaoffset = 128 * 1024 * 1024; // Checked at Ubuntu 12.04 64 and CentOS 6.5 64. if ($data->maxmemory + $javaoffset > $data->maxmemory) { $data->maxmemory += $javaoffset; } else { $data->maxmemory = ( int ) PHP_INT_MAX; } } // Limit resource to maximum. $data->maxtime = min( $data->maxtime, ( int ) $plugincfg->maxexetime ); $data->maxfilesize = min( $data->maxfilesize, ( int ) $plugincfg->maxexefilesize ); $data->maxmemory = min( $data->maxmemory, ( int ) $plugincfg->maxexememory ); $data->maxprocesses = min( $data->maxprocesses, ( int ) $plugincfg->maxexeprocesses ); // Info send with script. $info = "#!/bin/bash\n"; $info .= vpl_bash_export( 'VPL_LANG', vpl_get_lang( true ) ); $subinstance = $this->get_instance(); if ($user = $DB->get_record( 'user', array ( 'id' => $subinstance->userid ) )) { //$info .= vpl_bash_export( 'MOODLE_USER_NAME', $vpl->fullname( $user, false ) ); $info .= vpl_bash_export( 'MOODLE_USER_NAME', $user->username ); } $info .= vpl_bash_export( 'MOODLE_USER_ID', $subinstance->userid ); if ($type == 2) { // If evaluation add information. $info .= vpl_bash_export( 'VPL_MAXTIME', $data->maxtime ); $info .= vpl_bash_export( 'VPL_MAXMEMORY', $data->maxmemory ); $info .= vpl_bash_export( 'VPL_MAXFILESIZE', $data->maxfilesize ); $info .= vpl_bash_export( 'VPL_MAXPROCESSES', $data->maxprocesses ); $gradesetting = $vpl->get_grade_info(); if ($gradesetting !== false) { $info .= vpl_bash_export( 'VPL_GRADEMIN', $gradesetting->grademin ); $info .= vpl_bash_export( 'VPL_GRADEMAX', $gradesetting->grademax ); } $info .= vpl_bash_export( 'VPL_COMPILATIONFAILED', get_string( 'VPL_COMPILATIONFAILED', VPL ) ); } $filenames = ''; $num = 0; foreach ($submittedlist as $filename) { $filenames .= $filename . ' '; $info .= vpl_bash_export( 'VPL_SUBFILE' . $num, $filename ); $num ++; } $info .= 'export VPL_SUBFILES="' . $filenames . "\"\n"; // Add identifications of variations if exist. $info .= vpl_bash_export( 'VPL_VARIATION', '' ); $varids = $vpl->get_variation_identification( $this->instance->userid ); foreach ($varids as $id => $varid) { $info .= vpl_bash_export( 'VPL_VARIATION' . $id, $varid ); $info .= vpl_bash_export( 'VPL_VARIATION', $varid ); } for ($i = 0; $i <= $type; $i ++) { $script = self::$scriptlist [$i]; if (isset( $data->files [$script] ) && trim( $data->files [$script] ) > '') { if (substr( $data->files [$script], 0, 2 ) != '#!') { // No shebang => add bash. $data->files [$script] = "#!/bin/bash\n" . $data->files [$script]; } } else { $filesadded = $this->get_default_script( $script, $pln, $data ); foreach ($filesadded as $filename => $filedata) { if (trim( $filedata ) > '') { $data->files [$filename] = $filedata; $data->filestodelete [$filename] = 1; } } } } // Add script file with VPL environment information. $data->files ['vpl_environment.sh'] = $info; $data->files ['common_script.sh'] = file_get_contents( dirname( __FILE__ ) . '/jail/default_scripts/common_script.sh' ); // TODO change jail server to avoid this patch. if (count( $data->filestodelete ) == 0) { // If keeping all files => add dummy. $data->filestodelete ['__vpl_to_delete__'] = 1; } // Info to log who/what. $data->userid = $this->instance->userid; $data->activityid = $this->vpl->get_instance()->id; return $data; } public function jailaction($server, $action, $data) { if (! function_exists( 'xmlrpc_encode_request' )) { throw new Exception( 'Inernal server error: PHP XMLRPC requiered' ); } $request = xmlrpc_encode_request( $action, $data, array ( 'encoding' => 'UTF-8' ) ); $response = vpl_jailserver_manager::get_response( $server, $request, $error ); if ($response === false) { $manager = $this->vpl->has_capability( VPL_MANAGE_CAPABILITY ); if ($manager) { throw new Exception( get_string( 'serverexecutionerror', VPL ) . "\n" . $error ); } throw new Exception( get_string( 'serverexecutionerror', VPL ) ); } return $response; } public function jailrequestaction($data, $maxmemory, $localservers, &$server) { $error = ''; $server = vpl_jailserver_manager::get_server( $maxmemory, $localservers, $error ); if ($server == '') { $manager = $this->vpl->has_capability( VPL_MANAGE_CAPABILITY ); $men = get_string( 'nojailavailable', VPL ); if ($manager) { $men .= ": " . $error; } throw new Exception( $men ); } return $this->jailaction( $server, 'request', $data ); } public function jailreaction($action, $processinfo = false) { if ($processinfo === false) { $processinfo = vpl_running_processes::get( $this->get_instance()->userid ); } if ($processinfo === false) { throw new Exception( 'Process not found' ); } $server = $processinfo->server; $data = new stdClass(); $data->adminticket = $processinfo->adminticket; return $this->jailaction( $server, $action, $data ); } /** * Run, debug, evaluate * * @param int $type * (0=run, 1=debug, evaluate=2) */ public function run($type, $options = array()) { // Stop current task if one. $this->cancelprocess(); $options = ( array ) $options; $plugincfg = get_config('mod_vpl'); $executescripts = array ( 0 => 'vpl_run.sh', 1 => 'vpl_debug.sh', 2 => 'vpl_evaluate.sh' ); $data = $this->prepare_execution( $type ); $data->execute = $executescripts [$type]; $data->interactive = $type < 2 ? 1 : 0; $data->lang = vpl_get_lang( true ); if (isset( $options ['XGEOMETRY'] )) { // TODO refactor to a better solution. $data->files ['vpl_environment.sh'] .= "\n".vpl_bash_export( 'VPL_XGEOMETRY', $options ['XGEOMETRY'] ); } if (isset( $options ['COMMANDARGS'] )) { $data->commandargs = $options ['COMMANDARGS']; } $localservers = $data->jailservers; $maxmemory = $data->maxmemory; // Remove jailservers field. unset( $data->jailservers ); // Adapt files to send binary as base64. $fileencoding = array(); $encodefiles = array (); foreach ($data->files as $filename => $filedata) { if (vpl_is_binary( $filename )) { $encodefiles [$filename . '.b64'] = base64_encode( $filedata ); $fileencoding [$filename . '.b64'] = 1; $data->filestodelete [$filename . '.b64'] = 1; } else { $fileencoding [$filename] = 0; $encodefiles [$filename] = $filedata; } $data->files [$filename] = ''; } $data->files = $encodefiles; $data->fileencoding = $fileencoding; $jailserver = ''; $jailresponse = $this->jailrequestaction( $data, $maxmemory, $localservers, $jailserver ); $parsed = parse_url( $jailserver ); // Fix jail server port. if (! isset( $parsed ['port'] ) && $parsed ['scheme'] == 'http') { $parsed ['port'] = 80; } if (! isset( $jailresponse ['port'] )) { // Try to fix old jail servers that don't return port. $jailresponse ['port'] = $parsed ['port']; } $response = new stdClass(); $response->server = $parsed ['host']; $response->monitorPath = $jailresponse ['monitorticket'] . '/monitor'; $response->executionPath = $jailresponse ['executionticket'] . '/execute'; $response->port = $jailresponse ['port']; $response->securePort = $jailresponse ['secureport']; $response->wsProtocol = $plugincfg->websocket_protocol; $response->VNCpassword = substr( $jailresponse ['executionticket'], 0, 8 ); $instance = $this->get_instance(); vpl_running_processes::set( $instance->userid, $jailserver, $instance->vpl, $jailresponse ['adminticket'] ); return $response; } public function retrieveresult() { $response = $this->jailreaction( 'getresult' ); if ($response === false) { throw new Exception( get_string( 'serverexecutionerror', VPL ) ); } if ($response ['interactive'] == 0) { $this->saveCE( $response ); if ($response ['executed'] > 0) { // If automatic grading. if ($this->vpl->get_instance()->automaticgrading) { $data = new StdClass(); $data->grade = $this->proposedGrade( $response ['execution'] ); $data->comments = $this->proposedComment( $response ['execution'] ); $this->set_grade( $data, true ); } } } return $this->get_CE_for_editor( $response ); } public function isrunning() { try { $response = $this->jailreaction( 'running' ); } catch ( Exception $e ) { return false; } return $response ['running'] > 0; } public function cancelprocess() { $processinfo = vpl_running_processes::get( $this->get_instance()->userid ); if ($processinfo == null) { // No process to cancel. return; } try { $this->jailreaction( 'stop', $processinfo ); } catch ( Exception $e ) { // No matter, consider that the process stopped. debugging( "Process in execution server not sttoped or not found", DEBUG_DEVELOPER ); } vpl_running_processes::delete( $this->get_instance()->userid ); } }