From fea4eea8d7a31427c6dd06b44f45c90967cabce3 Mon Sep 17 00:00:00 2001 From: Baoshuo Date: Mon, 13 Feb 2023 08:28:33 +0800 Subject: [PATCH] refactor: UOJProblemDataSynchronizer --- web/app/controllers/hack.php | 2 +- web/app/controllers/judge/submit.php | 2 +- web/app/controllers/problem_data_manage.php | 46 +- web/app/libs/uoj-data-lib.php | 487 ----------------- web/app/models/FS.php | 27 +- web/app/models/UOJProblem.php | 27 + web/app/models/UOJProblemConfigure.php | 2 +- web/app/models/UOJProblemDataSynchronizer.php | 503 ++++++++++++++++++ 8 files changed, 563 insertions(+), 533 deletions(-) create mode 100644 web/app/models/UOJProblemDataSynchronizer.php diff --git a/web/app/controllers/hack.php b/web/app/controllers/hack.php index 572a3c2..1d81d73 100644 --- a/web/app/controllers/hack.php +++ b/web/app/controllers/hack.php @@ -38,7 +38,7 @@ if (UOJHack::cur()->userCanReview(Auth::user())) { $new_in = "{$input}_in"; $new_out = "{$input}_out"; $reason = null; - $err = dataAddHackPoint(UOJHack::cur()->problem->info, $new_in, $new_out, $reason, Auth::user()); + $err = UOJHack::cur()->problem->addHackPoint($new_in, $new_out, $reason, Auth::user()); $err === '' || UOJResponse::message($err); unlink($new_in); unlink($new_out); diff --git a/web/app/controllers/judge/submit.php b/web/app/controllers/judge/submit.php index 59e25c0..b8ed8e3 100644 --- a/web/app/controllers/judge/submit.php +++ b/web/app/controllers/judge/submit.php @@ -72,7 +72,7 @@ function hackJudged() { $up_out = $_FILES["std_output"]['tmp_name']; if (!UOJHack::cur()->problem->needToReviewHack()) { - $err = dataAddHackPoint(UOJHack::cur()->problem->info, $up_in, $up_out); + $err = UOJHack::cur()->problem->addHackPoint($up_in, $up_out); if ($err === '') { unlink($input); DB::update([ diff --git a/web/app/controllers/problem_data_manage.php b/web/app/controllers/problem_data_manage.php index 381c768..863f25e 100644 --- a/web/app/controllers/problem_data_manage.php +++ b/web/app/controllers/problem_data_manage.php @@ -70,33 +70,12 @@ if ($_POST['problem_data_file_submit'] == 'submit') { $zip_mime_types = ['application/zip', 'application/x-zip', 'application/x-zip-compressed']; if (in_array($_FILES["problem_data_file"]["type"], $zip_mime_types) || $_FILES["problem_data_file"]["type"] == 'application/octet-stream' && substr($_FILES["problem_data_file"]["name"], -4) == '.zip') { - $zip = new ZipArchive; + $errmsg = UOJProblem::cur()->uploadDataViaZipFile($_FILES["problem_data_file"]["tmp_name"]); - try { - if ($zip->open($_FILES["problem_data_file"]["tmp_name"]) !== true) { - throw new UOJUploadFailedException('压缩文件打开失败'); - } - - if (!$zip->extractTo(UOJProblem::cur()->getUploadFolderPath())) { - throw new UOJUploadFailedException('压缩文件解压失败'); - } - - if (!$zip->close()) { - throw new UOJUploadFailedException('压缩文件关闭失败'); - } - } catch (Exception $e) { - becomeMsgPage('
' . $e->getMessage() . '
返回'); + if ($errmsg !== '') { + UOJResponse::message('
' . $errmsg . '
返回'); } - UOJLocalRun::execAnd([ - ['cd', UOJProblem::cur()->getUploadFolderPath()], - <<<'EOD' - if [ "$(find . -maxdepth 1 -type f)File" = "File" ]; - then for sub_dir in "$(find -maxdepth 1 -type d ! -name .)"; - do mv -f "$sub_dir"/* . && rm -rf "$sub_dir"; done; fi - EOD - ]); - echo ""; } else { becomeMsgPage('
请上传 zip 格式的文件!
返回'); @@ -372,18 +351,17 @@ if (isset($_GET['display_file'])) { } $hackable_form = new UOJForm('hackable'); -$hackable_form->handle = function () use ($problem) { - $problem['hackable'] = !$problem['hackable']; - $ret = dataSyncProblemData($problem); +$hackable_form->handle = function () { + UOJProblem::cur()->info['hackable'] = !UOJProblem::cur()->info['hackable']; + $ret = UOJProblem::cur()->syncData(Auth::user()); if ($ret) { - becomeMsgPage('
' . $ret . '
返回'); + becomeMsgPage('
' . $ret . '
返回'); } - $hackable = $problem['hackable'] ? 1 : 0; DB::update([ "update problems", - "set", ["hackable" => $hackable], - "where", ["id" => $problem['id']] + "set", ["hackable" => UOJProblem::cur()->info['hackable']], + "where", ["id" => UOJProblem::info('id')] ]); }; $hackable_form->config['submit_container']['class'] = ''; @@ -393,11 +371,11 @@ $hackable_form->config['confirm']['smart'] = true; $hackable_form->runAtServer(); $data_form = new UOJForm('data'); -$data_form->handle = function () use ($problem) { +$data_form->handle = function () { set_time_limit(60 * 5); - $ret = dataSyncProblemData($problem, Auth::user()); + $ret = UOJProblem::cur()->syncData(Auth::user()); if ($ret) { - becomeMsgPage('
' . $ret . '
返回'); + becomeMsgPage('
' . $ret . '
返回'); } }; $data_form->config['submit_container']['class'] = ''; diff --git a/web/app/libs/uoj-data-lib.php b/web/app/libs/uoj-data-lib.php index aad2fb7..bb58086 100644 --- a/web/app/libs/uoj-data-lib.php +++ b/web/app/libs/uoj-data-lib.php @@ -25,490 +25,3 @@ function dataClearProblemData($problem) { UOJLocalRun::exec(['rm', "/var/uoj_data/upload/$id", '-r']); dataNewProblem($id); } - -class SyncProblemDataHandler { - private UOJProblem $problem; - private $user; - private int $id; - private string $upload_dir, $data_dir, $prepare_dir; - private $requirement, $problem_extra_config; - private $problem_conf, $final_problem_conf; - private $allow_files; - - public function retryMsg() { - return '请等待上一次数据上传或同步操作结束后重试'; - } - - public function __construct($problem_info, $user = null) { - $this->problem = new UOJProblem($problem_info); - $this->user = $user; - - if (!validateUInt($this->problem->info['id'])) { - UOJLog::error("SyncProblemDataHandler: hacker detected"); - return; - } - $this->id = (int)$this->problem->info['id']; - - $this->data_dir = "/var/uoj_data/{$this->id}"; - $this->prepare_dir = "/var/uoj_data/prepare_{$this->id}"; - $this->upload_dir = "/var/uoj_data/upload/{$this->id}"; - } - - /** - * $type can be either LOCK_SH or LOCK_EX - */ - private function lock($type, $func) { - $ret = FS::lock_file("/var/uoj_data/{$this->id}_lock", $type, $func); - return $ret === false ? $this->retryMsg() : $ret; - } - - private function check_conf_on($name) { - return isset($this->problem_conf[$name]) && $this->problem_conf[$name] == 'on'; - } - - private function create_prepare_folder() { - return mkdir($this->prepare_dir, 0755); - } - private function remove_prepare_folder() { - return UOJLocalRun::exec(['rm', $this->prepare_dir, '-rf']); - } - - private function copy_to_prepare($file_name) { - if (!isset($this->allow_files[$file_name])) { - throw new UOJFileNotFoundException($file_name); - } - - $src = "{$this->upload_dir}/$file_name"; - $dest = "{$this->prepare_dir}/$file_name"; - - if (file_exists($dest)) { - return; - } - - if (isset($this->problem_extra_config['dont_use_formatter']) || !is_file("{$this->upload_dir}/$file_name")) { - $ret = UOJLocalRun::exec(['cp', $src, $dest, '-r']); - } else { - $ret = UOJLocalRun::formatter($src, $dest); - } - - if ($ret === false) { - throw new UOJFileNotFoundException($file_name); - } - } - - private function copy_file_to_prepare($file_name) { - if (!isset($this->allow_files[$file_name]) || !is_file("{$this->upload_dir}/$file_name")) { - throw new UOJFileNotFoundException($file_name); - } - - $this->copy_to_prepare($file_name); - } - - private function copy_source_code_to_prepare($code_name) { // file name without suffix - $src = UOJLang::findSourceCode($code_name, $this->upload_dir); - - if ($src === false) { - throw new UOJFileNotFoundException($code_name); - } - - $this->copy_to_prepare($src['path']); - } - - private function compile_at_prepare($name, $config = []) { - $include_path = UOJLocalRun::$judger_include_path; - - $src = UOJLang::findSourceCode($name, $this->prepare_dir); - - if (isset($config['path'])) { - if (rename("{$this->prepare_dir}/{$src['path']}", "{$this->prepare_dir}/{$config['path']}/{$src['path']}") === false) { - throw new Exception("$name : move failed"); - } - $work_path = "{$this->prepare_dir}/{$config['path']}"; - } else { - $work_path = $this->prepare_dir; - } - - $compile_options = [ - ['custom', UOJLocalRun::$judger_run_path] - ]; - $runp_options = [ - ['in', '/dev/null'], - ['out', 'stderr'], - ['err', "{$this->prepare_dir}/compiler_result.txt"], - ['tl', 60], - ['ml', 512], - ['ol', 64], - ['type', 'compiler'], - ['work-path', $work_path], - ]; - if (!empty($config['need_include_header'])) { - $compile_options[] = ['cinclude', $include_path]; - $runp_options[] = ['add-readable-raw', "{$include_path}/"]; - } - if (!empty($config['implementer'])) { - $compile_options[] = ['impl', $config['implementer']]; - } - $res = UOJLocalRun::compile($name, $compile_options, $runp_options); - $this->final_problem_conf["{$name}_run_type"] = UOJLang::getRunTypeFromLanguage($src['lang']); - $rstype = isset($res['rstype']) ? $res['rstype'] : 7; - - if ($rstype != 0 || $res['exit_code'] != 0) { - if ($rstype == 0) { - throw new Exception("$name : compile error
\n" . HTML::escape(uojFilePreview("{$this->prepare_dir}/compiler_result.txt", 10000)) . "\n
"); - } elseif ($rstype == 7) { - throw new Exception("$name : compile error. No comment"); - } else { - throw new Exception("$name : compile error. Compiler " . judgerCodeStr($rstype)); - } - } - - unlink("{$this->prepare_dir}/compiler_result.txt"); - - if (isset($config['path'])) { - rename("{$this->prepare_dir}/{$config['path']}/{$src['path']}", "{$this->prepare_dir}/{$src['path']}"); - rename("{$this->prepare_dir}/{$config['path']}/$name", "{$this->prepare_dir}/$name"); - } - } - - private function makefile_at_prepare() { - $include_path = UOJLocalRun::$judger_include_path; - - $res = UOJLocalRun::exec(['/usr/bin/make', "INCLUDE_PATH={$include_path}"], [ - ['in', '/dev/null'], - ['out', 'stderr'], - ['err', "{$this->prepare_dir}/makefile_result.txt"], - ['tl', 60], - ['ml', 512], - ['ol', 64], - ['type', 'compiler'], - ['work-path', $this->prepare_dir], - ['add-readable-raw', "{$include_path}/"] - ]); - $rstype = isset($res['rstype']) ? $res['rstype'] : 7; - - if ($rstype != 0 || $res['exit_code'] != 0) { - if ($rstype == 0) { - throw new Exception("Makefile : compile error
\n" . HTML::escape(uojFilePreview("{$this->prepare_dir}/makefile_result.txt", 10000)) . "\n
"); - } elseif ($rstype == 7) { - throw new Exception("Makefile : compile error. No comment"); - } else { - throw new Exception("Makefile : compile error. Compiler " . judgerCodeStr($rstype)); - } - } - - unlink("{$this->prepare_dir}/makefile_result.txt"); - } - - public function _updateProblemConf($new_problem_conf) { - try { - putUOJConf("{$this->upload_dir}/problem.conf", $new_problem_conf); - - $this->_sync(); - return ''; - } catch (Exception $e) { - return $e->getMessage(); - } - } - public function updateProblemConf($new_problem_conf) { - return $this->lock(LOCK_EX, fn () => $this->_updateProblemConf($new_problem_conf)); - } - - private function _addHackPoint($uploaded_input_file, $uploaded_output_file, $reason) { - try { - switch ($this->problem->getExtraConfig('add_hack_as')) { - case 'test': - $key_num = 'n_tests'; - $msg = 'add new test'; - $gen_in_name = 'getUOJProblemInputFileName'; - $gen_out_name = 'getUOJProblemOutputFileName'; - break; - case 'ex_test': - $key_num = 'n_ex_tests'; - $msg = 'add new extra test'; - $gen_in_name = 'getUOJProblemExtraInputFileName'; - $gen_out_name = 'getUOJProblemExtraOutputFileName'; - break; - default: - return 'add hack to data failed: add_hack_as should be either "ex_test" or "test"'; - } - - $new_problem_conf = $this->problem->getProblemConfArray(); - if ($new_problem_conf == -1 || $new_problem_conf == -2) { - return $new_problem_conf; - } - $new_problem_conf[$key_num] = getUOJConfVal($new_problem_conf, $key_num, 0) + 1; - - putUOJConf("{$this->upload_dir}/problem.conf", $new_problem_conf); - - $new_input_name = $gen_in_name($new_problem_conf, $new_problem_conf[$key_num]); - $new_output_name = $gen_out_name($new_problem_conf, $new_problem_conf[$key_num]); - - if (!copy($uploaded_input_file, "{$this->upload_dir}/$new_input_name")) { - return "input file not found"; - } - if (!copy($uploaded_output_file, "{$this->upload_dir}/$new_output_name")) { - return "output file not found"; - } - } catch (Exception $e) { - return $e->getMessage(); - } - - $ret = $this->_sync(); - if ($ret !== '') { - return "hack successfully but sync failed: $ret"; - } - - if (isset($reason['hack_url'])) { - UOJSystemUpdate::updateProblem($this->problem, [ - 'text' => 'Hack 成功,自动添加数据', - 'url' => $reason['hack_url'] - ]); - } - UOJSubmission::rejudgeProblemAC($this->problem, [ - 'reason_text' => $reason['rejudge'], - 'requestor' => '' - ]); - return ''; - } - - public function addHackPoint($uploaded_input_file, $uploaded_output_file, $reason = []) { - return $this->lock(LOCK_EX, fn () => $this->_addHackPoint($uploaded_input_file, $uploaded_output_file, $reason)); - } - - public function fast_hackable_check() { - if (!$this->problem->info['hackable']) { - return; - } - if (!$this->check_conf_on('use_builtin_judger')) { - return; - } - - if ($this->check_conf_on('submit_answer')) { - throw new UOJProblemConfException("提交答案题不可 Hack,请先停用本题的 Hack 功能。"); - } else { - if (UOJLang::findSourceCode('std', $this->upload_dir) === false) { - throw new UOJProblemConfException("找不到本题的 std。请上传 std 代码文件,或停用本题的 Hack 功能。"); - } - if (UOJLang::findSourceCode('val', $this->upload_dir) === false) { - throw new UOJProblemConfException("找不到本题的 val。请上传 val 代码文件,或停用本题的 Hack 功能。"); - } - } - } - - private function _sync() { - try { - if (!$this->create_prepare_folder()) { - throw new UOJSyncFailedException('创建临时文件夹失败'); - } - - $this->requirement = []; - $this->problem_extra_config = $this->problem->getExtraConfig();; - if (!is_file("{$this->upload_dir}/problem.conf")) { - throw new UOJFileNotFoundException("problem.conf"); - } - - $this->problem_conf = getUOJConf("{$this->upload_dir}/problem.conf"); - $this->final_problem_conf = $this->problem_conf; - if ($this->problem_conf === -1) { - throw new UOJFileNotFoundException("problem.conf"); - } elseif ($this->problem_conf === -2) { - throw new UOJProblemConfException("syntax error: duplicate keys"); - } - - $this->allow_files = array_flip(FS::scandir($this->upload_dir)); - - $zip_file = new ZipArchive(); - if ($zip_file->open("{$this->prepare_dir}/download.zip", ZipArchive::CREATE) !== true) { - throw new Exception("download.zip : failed to create the zip file"); - } - - if (isset($this->allow_files['require']) && is_dir("{$this->upload_dir}/require")) { - $this->copy_to_prepare('require'); - } - - if (isset($this->allow_files['testlib.h']) && is_file("{$this->upload_dir}/testlib.h")) { - $this->copy_file_to_prepare('testlib.h'); - } - - $this->fast_hackable_check(); - - if ($this->check_conf_on('use_builtin_judger')) { - $n_tests = getUOJConfVal($this->problem_conf, 'n_tests', 10); - if (!validateUInt($n_tests) || $n_tests <= 0) { - throw new UOJProblemConfException("n_tests must be a positive integer"); - } - for ($num = 1; $num <= $n_tests; $num++) { - $input_file_name = getUOJProblemInputFileName($this->problem_conf, $num); - $output_file_name = getUOJProblemOutputFileName($this->problem_conf, $num); - - $this->copy_file_to_prepare($input_file_name); - $this->copy_file_to_prepare($output_file_name); - } - - if (!$this->check_conf_on('interaction_mode')) { - if (isset($this->problem_conf['use_builtin_checker'])) { - if (!preg_match('/^[a-zA-Z0-9_]{1,20}$/', $this->problem_conf['use_builtin_checker'])) { - throw new Exception("" . HTML::escape($this->problem_conf['use_builtin_checker']) . " is not a valid checker"); - } - } else { - $this->copy_source_code_to_prepare('chk'); - $this->compile_at_prepare('chk', ['need_include_header' => true]); - } - } - - if ($this->check_conf_on('submit_answer')) { - if (!isset($this->problem_extra_config['dont_download_input'])) { - for ($num = 1; $num <= $n_tests; $num++) { - $input_file_name = getUOJProblemInputFileName($this->problem_conf, $num); - $zip_file->addFile("{$this->prepare_dir}/$input_file_name", "$input_file_name"); - } - } - - $n_output_files = 0; - for ($num = 1; $num <= $n_tests; $num++) { - $output_file_id = getUOJConfVal($this->problem_conf, ["output_file_id_{$num}", "output_file_id"], "$num"); - if (!validateUInt($output_file_id) || $output_file_id < 0 || $output_file_id > $n_tests) { - throw new UOJProblemConfException("output_file_id/output_file_id_{$num} must be in [1, n_tests]"); - } - $n_output_files = max($n_output_files, $output_file_id); - } - for ($num = 1; $num <= $n_output_files; $num++) { - $output_file_name = getUOJProblemOutputFileName($this->problem_conf, $num); - $this->requirement[] = ['name' => "output$num", 'type' => 'text', 'file_name' => $output_file_name]; - } - } else { - $n_ex_tests = getUOJConfVal($this->problem_conf, 'n_ex_tests', 0); - if (!validateUInt($n_ex_tests) || $n_ex_tests < 0) { - throw new UOJProblemConfException('n_ex_tests must be a non-negative integer. Current value: ' . HTML::escape($n_ex_tests)); - } - - for ($num = 1; $num <= $n_ex_tests; $num++) { - $input_file_name = getUOJProblemExtraInputFileName($this->problem_conf, $num); - $output_file_name = getUOJProblemExtraOutputFileName($this->problem_conf, $num); - - $this->copy_file_to_prepare($input_file_name); - $this->copy_file_to_prepare($output_file_name); - } - - if ($this->problem->info['hackable']) { - $this->copy_source_code_to_prepare('std'); - if (isset($this->problem_conf['with_implementer']) && $this->problem_conf['with_implementer'] == 'on') { - $this->compile_at_prepare('std', [ - 'implementer' => 'implementer', - 'path' => 'require' - ]); - } else { - $this->compile_at_prepare('std'); - } - $this->copy_source_code_to_prepare('val'); - $this->compile_at_prepare('val', ['need_include_header' => true]); - } - - if ($this->check_conf_on('interaction_mode')) { - $this->copy_source_code_to_prepare('interactor'); - $this->compile_at_prepare('interactor', ['need_include_header' => true]); - } - - $n_sample_tests = getUOJConfVal($this->problem_conf, 'n_sample_tests', $n_tests); - if (!validateUInt($n_sample_tests) || $n_sample_tests < 0) { - throw new UOJProblemConfException('n_sample_tests must be a non-negative integer. Current value: ' . HTML::escape($n_sample_tests)); - } - if ($n_sample_tests > $n_ex_tests) { - throw new UOJProblemConfException("n_sample_tests can't be greater than n_ex_tests"); - } - - if (!isset($this->problem_extra_config['dont_download_sample'])) { - for ($num = 1; $num <= $n_sample_tests; $num++) { - $input_file_name = getUOJProblemExtraInputFileName($this->problem_conf, $num); - $output_file_name = getUOJProblemExtraOutputFileName($this->problem_conf, $num); - $zip_file->addFile("{$this->prepare_dir}/{$input_file_name}", "$input_file_name"); - if (!isset($this->problem_extra_config['dont_download_sample_output'])) { - $zip_file->addFile("{$this->prepare_dir}/{$output_file_name}", "$output_file_name"); - } - } - } - - $this->requirement[] = ['name' => 'answer', 'type' => 'source code', 'file_name' => 'answer.code']; - } - } else { - if (!isSuperUser($this->user)) { - throw new UOJProblemConfException("use_builtin_judger must be on."); - } else { - foreach ($this->allow_files as $file_name => $file_num) { - $this->copy_to_prepare($file_name); - } - $this->makefile_at_prepare(); - - $this->requirement[] = ['name' => 'answer', 'type' => 'source code', 'file_name' => 'answer.code']; - } - } - putUOJConf("{$this->prepare_dir}/problem.conf", $this->final_problem_conf); - - if (isset($this->allow_files['download']) && is_dir("{$this->upload_dir}/download")) { - $download_dir = "{$this->upload_dir}/download"; - foreach (FS::scandir_r($download_dir) as $file_name) { - if (is_file("{$download_dir}/{$file_name}")) { - $zip_file->addFile("{$download_dir}/{$file_name}", $file_name); - } - } - } - - $zip_file->close(); - - $orig_requirement = $this->problem->getSubmissionRequirement(); - if (!$orig_requirement) { - DB::update([ - "update problems", - "set", ["submission_requirement" => json_encode($this->requirement)], - "where", ["id" => $this->id] - ]); - } - - UOJSystemUpdate::updateProblemInternally($this->problem, [ - 'text' => 'sync', - 'requestor' => Auth::check() ? Auth::id() : null - ]); - } catch (Exception $e) { - $this->remove_prepare_folder(); - return $e->getMessage(); - } - - UOJLocalRun::exec(['rm', $this->data_dir, '-r']); - rename($this->prepare_dir, $this->data_dir); - - UOJLocalRun::execAnd([ - ['cd', '/var/uoj_data'], - ['zip', "{$this->id}.next.zip", $this->id, '-r', '-q'], - ['mv', "{$this->id}.next.zip", "{$this->id}.zip", '-f'], - ]); - - return ''; - } - - public function sync() { - return $this->lock(LOCK_EX, fn () => $this->_sync()); - } -} - -function dataSyncProblemData($problem, $user = null) { - return (new SyncProblemDataHandler($problem, $user))->sync(); -} - -function dataAddHackPoint($problem, $uploaded_input_file, $uploaded_output_file, $reason = null, $user = null) { - if ($reason === null) { - if (UOJHack::cur()) { - $reason = [ - 'rejudge' => '自动重测本题所有获得 100 分的提交记录', - 'hack_url' => HTML::url(UOJHack::cur()->getUri()) - ]; - } else { - $reason = []; - } - } - - return (new SyncProblemDataHandler($problem, $user))->addHackPoint($uploaded_input_file, $uploaded_output_file, $reason); -} - -function dataUpdateProblemConf($problem, $new_problem_conf) { - return (new SyncProblemDataHandler($problem))->updateProblemConf($new_problem_conf); -} diff --git a/web/app/models/FS.php b/web/app/models/FS.php index 5fda198..07d52ec 100644 --- a/web/app/models/FS.php +++ b/web/app/models/FS.php @@ -3,11 +3,11 @@ class FS { public static function scandir(string $directory, $cfg = []) { $cfg += [ - 'exclude_dots' => true + 'exclude_dots' => true ]; $entries = scandir($directory); if ($cfg['exclude_dots']) { - $entries = array_filter($entries, fn($name) => $name !== '.' && $name !== '..'); + $entries = array_values(array_filter($entries, fn ($name) => $name !== '.' && $name !== '..')); } return $entries; } @@ -27,17 +27,17 @@ class FS { /** * @param int $type lock type. can be either LOCK_SH or LOCK_EX - */ + */ public static function lock_file(string $path, int $type, callable $func) { $lock_fp = fopen($path, 'c'); - + if (!flock($lock_fp, $type | LOCK_NB)) { UOJLog::error("lock failed: {$path}"); return false; } - + $ret = $func(); - + flock($lock_fp, LOCK_UN | LOCK_NB); return $ret; @@ -46,7 +46,7 @@ class FS { public static function randomAvailableFileName($dir, $suffix = '') { do { $name = $dir . uojRandString(20) . $suffix; - } while (file_exists(UOJContext::storagePath().$name)); + } while (file_exists(UOJContext::storagePath() . $name)); return $name; } @@ -56,9 +56,18 @@ class FS { public static function randomAvailableSubmissionFileName() { $num = uojRand(1, 10000); - if (!file_exists(UOJContext::storagePath()."/submission/$num")) { - system("mkdir ".UOJContext::storagePath()."/submission/$num"); + if (!file_exists(UOJContext::storagePath() . "/submission/$num")) { + system("mkdir " . UOJContext::storagePath() . "/submission/$num"); } return static::randomAvailableFileName("/submission/$num/"); } + + public static function moveFilesInDir(string $src, string $dest) { + foreach (FS::scandir($src) as $name) { + if (!rename("{$src}/{$name}", "{$dest}/{$name}")) { + return false; + } + } + return true; + } } diff --git a/web/app/models/UOJProblem.php b/web/app/models/UOJProblem.php index bc592bc..cc97442 100644 --- a/web/app/models/UOJProblem.php +++ b/web/app/models/UOJProblem.php @@ -721,6 +721,33 @@ class UOJProblem { } return $conf->getNonTraditionalJudgeType(); } + + public function syncData($user = null) { + return (new UOJProblemDataSynchronizer($this, $user))->sync(); + } + + public function addHackPoint($uploaded_input_file, $uploaded_output_file, $reason = null, $user = null) { + if ($reason === null) { + if (UOJHack::cur()) { + $reason = [ + 'rejudge' => '自动重测本题所有获得100分的提交记录', + 'hack_url' => HTML::url(UOJHack::cur()->getUri()) + ]; + } else { + $reason = []; + } + } + + return (new UOJProblemDataSynchronizer($this, $user))->addHackPoint($uploaded_input_file, $uploaded_output_file, $reason); + } + + public function uploadDataViaZipFile($new_data_zip) { + return (new UOJProblemDataSynchronizer($this))->upload($new_data_zip); + } + + public function updateProblemConf($new_problem_conf) { + return (new UOJProblemDataSynchronizer($this))->updateProblemConf($new_problem_conf); + } } UOJProblem::$table_for_content = 'problems_contents'; diff --git a/web/app/models/UOJProblemConfigure.php b/web/app/models/UOJProblemConfigure.php index bfa9a87..6e528cd 100644 --- a/web/app/models/UOJProblemConfigure.php +++ b/web/app/models/UOJProblemConfigure.php @@ -357,7 +357,7 @@ class UOJProblemConfigure { } } - $err = dataUpdateProblemConf($this->problem->info, $conf); + $err = $this->problem->updateProblemConf($conf); if ($err) { UOJResponse::message('
' . $err . '
返回'); } diff --git a/web/app/models/UOJProblemDataSynchronizer.php b/web/app/models/UOJProblemDataSynchronizer.php new file mode 100644 index 0000000..4ca346e --- /dev/null +++ b/web/app/models/UOJProblemDataSynchronizer.php @@ -0,0 +1,503 @@ +problem = $problem; + $this->user = $user; + + if (!validateUInt($this->problem->info['id'])) { + UOJLog::error("UOJProblemDataSynchronizer: hacker detected"); + return; + } + $this->id = (int)$this->problem->info['id']; + + $this->data_dir = "/var/uoj_data/{$this->id}"; + $this->prepare_dir = "/var/uoj_data/prepare_{$this->id}"; + $this->upload_dir = "/var/uoj_data/upload/{$this->id}"; + } + + /** + * $type can be either LOCK_SH or LOCK_EX + */ + private function lock($type, $func) { + $ret = FS::lock_file("/var/uoj_data/{$this->id}_lock", $type, $func); + return $ret === false ? $this->retryMsg() : $ret; + } + + private function check_conf_on($name) { + return isset($this->problem_conf[$name]) && $this->problem_conf[$name] == 'on'; + } + + private function create_prepare_folder() { + return mkdir($this->prepare_dir, 0755); + } + private function remove_prepare_folder() { + return UOJLocalRun::exec(['rm', $this->prepare_dir, '-rf']); + } + + private function copy_to_prepare($file_name) { + if (!isset($this->allow_files[$file_name])) { + throw new UOJFileNotFoundException($file_name); + } + + $src = "{$this->upload_dir}/$file_name"; + $dest = "{$this->prepare_dir}/$file_name"; + + if (file_exists($dest)) { + return; + } + + if (isset($this->problem_extra_config['dont_use_formatter']) || !is_file("{$this->upload_dir}/$file_name")) { + $ret = UOJLocalRun::exec(['cp', $src, $dest, '-r']); + } else { + $ret = UOJLocalRun::formatter($src, $dest); + } + + if ($ret === false) { + throw new UOJFileNotFoundException($file_name); + } + } + + private function copy_file_to_prepare($file_name) { + if (!isset($this->allow_files[$file_name]) || !is_file("{$this->upload_dir}/$file_name")) { + throw new UOJFileNotFoundException($file_name); + } + + $this->copy_to_prepare($file_name); + } + + private function copy_source_code_to_prepare($code_name) { // file name without suffix + $src = UOJLang::findSourceCode($code_name, $this->upload_dir); + + if ($src === false) { + throw new UOJFileNotFoundException($code_name); + } + + $this->copy_to_prepare($src['path']); + } + + private function compile_at_prepare($name, $config = []) { + $include_path = UOJLocalRun::$judger_include_path; + + $src = UOJLang::findSourceCode($name, $this->prepare_dir); + + if (isset($config['path'])) { + if (rename("{$this->prepare_dir}/{$src['path']}", "{$this->prepare_dir}/{$config['path']}/{$src['path']}") === false) { + throw new Exception("$name : move failed"); + } + $work_path = "{$this->prepare_dir}/{$config['path']}"; + } else { + $work_path = $this->prepare_dir; + } + + $compile_options = [ + ['custom', UOJLocalRun::$judger_run_path] + ]; + $runp_options = [ + ['in', '/dev/null'], + ['out', 'stderr'], + ['err', "{$this->prepare_dir}/compiler_result.txt"], + ['tl', 60], + ['ml', 512], + ['ol', 64], + ['type', 'compiler'], + ['work-path', $work_path], + ]; + if (!empty($config['need_include_header'])) { + $compile_options[] = ['cinclude', $include_path]; + $runp_options[] = ['add-readable-raw', "{$include_path}/"]; + } + if (!empty($config['implementer'])) { + $compile_options[] = ['impl', $config['implementer']]; + } + $res = UOJLocalRun::compile($name, $compile_options, $runp_options); + $this->final_problem_conf["{$name}_run_type"] = UOJLang::getRunTypeFromLanguage($src['lang']); + $rstype = isset($res['rstype']) ? $res['rstype'] : 7; + + if ($rstype != 0 || $res['exit_code'] != 0) { + if ($rstype == 0) { + throw new Exception("$name : compile error
\n" . HTML::escape(uojFilePreview("{$this->prepare_dir}/compiler_result.txt", 10000)) . "\n
"); + } elseif ($rstype == 7) { + throw new Exception("$name : compile error. No comment"); + } else { + throw new Exception("$name : compile error. Compiler " . judgerCodeStr($rstype)); + } + } + + unlink("{$this->prepare_dir}/compiler_result.txt"); + + if (isset($config['path'])) { + rename("{$this->prepare_dir}/{$config['path']}/{$src['path']}", "{$this->prepare_dir}/{$src['path']}"); + rename("{$this->prepare_dir}/{$config['path']}/$name", "{$this->prepare_dir}/$name"); + } + } + + private function makefile_at_prepare() { + $include_path = UOJLocalRun::$judger_include_path; + + $res = UOJLocalRun::exec(['/usr/bin/make', "INCLUDE_PATH={$include_path}"], [ + ['in', '/dev/null'], + ['out', 'stderr'], + ['err', "{$this->prepare_dir}/makefile_result.txt"], + ['tl', 60], + ['ml', 512], + ['ol', 64], + ['type', 'compiler'], + ['work-path', $this->prepare_dir], + ['add-readable-raw', "{$include_path}/"] + ]); + $rstype = isset($res['rstype']) ? $res['rstype'] : 7; + + if ($rstype != 0 || $res['exit_code'] != 0) { + if ($rstype == 0) { + throw new Exception("Makefile : compile error
\n" . HTML::escape(uojFilePreview("{$this->prepare_dir}/makefile_result.txt", 10000)) . "\n
"); + } elseif ($rstype == 7) { + throw new Exception("Makefile : compile error. No comment"); + } else { + throw new Exception("Makefile : compile error. Compiler " . judgerCodeStr($rstype)); + } + } + + unlink("{$this->prepare_dir}/makefile_result.txt"); + } + + private function _upload($new_data_zip) { + try { + // Clear upload dir + foreach (FS::scandir_r($this->upload_dir) as $file_name) { + unlink("{$this->upload_dir}/{$file_name}"); + } + + $zip = new ZipArchive(); + if ($zip->open($new_data_zip) !== true) { + throw new UOJUploadFailedException('压缩文件打开失败'); + } + if (!$zip->extractTo($this->upload_dir)) { + throw new UOJUploadFailedException('压缩文件解压失败'); + } + if (!$zip->close()) { + throw new UOJUploadFailedException('压缩文件关闭失败'); + } + + $files = FS::scandir($this->upload_dir); + + if (count($files) == 1 && is_dir("{$this->upload_dir}/{$files[0]}")) { + if (!FS::moveFilesInDir("{$this->upload_dir}/{$files[0]}", $this->upload_dir)) { + throw new UOJUploadFailedException('操作解压后的文件时发生错误'); + } + } + + return ''; + } catch (Exception $e) { + return $e->getMessage(); + } finally { + $this->remove_prepare_folder(); + } + } + public function upload($new_data_zip) { + return $this->lock(LOCK_EX, fn () => $this->_upload($new_data_zip)); + } + + public function _updateProblemConf($new_problem_conf) { + try { + putUOJConf("{$this->upload_dir}/problem.conf", $new_problem_conf); + + $this->_sync(); + return ''; + } catch (Exception $e) { + return $e->getMessage(); + } + } + public function updateProblemConf($new_problem_conf) { + return $this->lock(LOCK_EX, fn () => $this->_updateProblemConf($new_problem_conf)); + } + + private function _addHackPoint($uploaded_input_file, $uploaded_output_file, $reason) { + try { + switch ($this->problem->getExtraConfig('add_hack_as')) { + case 'test': + $key_num = 'n_tests'; + $msg = 'add new test'; + $gen_in_name = 'getUOJProblemInputFileName'; + $gen_out_name = 'getUOJProblemOutputFileName'; + break; + case 'ex_test': + $key_num = 'n_ex_tests'; + $msg = 'add new extra test'; + $gen_in_name = 'getUOJProblemExtraInputFileName'; + $gen_out_name = 'getUOJProblemExtraOutputFileName'; + break; + default: + return 'add hack to data failed: add_hack_as should be either "ex_test" or "test"'; + } + + $new_problem_conf = $this->problem->getProblemConfArray(); + if ($new_problem_conf == -1 || $new_problem_conf == -2) { + return $new_problem_conf; + } + $new_problem_conf[$key_num] = getUOJConfVal($new_problem_conf, $key_num, 0) + 1; + + putUOJConf("{$this->upload_dir}/problem.conf", $new_problem_conf); + + $new_input_name = $gen_in_name($new_problem_conf, $new_problem_conf[$key_num]); + $new_output_name = $gen_out_name($new_problem_conf, $new_problem_conf[$key_num]); + + if (!copy($uploaded_input_file, "{$this->upload_dir}/$new_input_name")) { + return "input file not found"; + } + if (!copy($uploaded_output_file, "{$this->upload_dir}/$new_output_name")) { + return "output file not found"; + } + } catch (Exception $e) { + return $e->getMessage(); + } + + $ret = $this->_sync(); + if ($ret !== '') { + return "hack successfully but sync failed: $ret"; + } + + if (isset($reason['hack_url'])) { + UOJSystemUpdate::updateProblem($this->problem, [ + 'text' => 'Hack 成功,自动添加数据', + 'url' => $reason['hack_url'] + ]); + } + UOJSubmission::rejudgeProblemAC($this->problem, [ + 'reason_text' => $reason['rejudge'], + 'requestor' => '' + ]); + return ''; + } + + public function addHackPoint($uploaded_input_file, $uploaded_output_file, $reason = []) { + return $this->lock(LOCK_EX, fn () => $this->_addHackPoint($uploaded_input_file, $uploaded_output_file, $reason)); + } + + public function fast_hackable_check() { + if (!$this->problem->info['hackable']) { + return; + } + if (!$this->check_conf_on('use_builtin_judger')) { + return; + } + + if ($this->check_conf_on('submit_answer')) { + throw new UOJProblemConfException("提交答案题不可 Hack,请先停用本题的 Hack 功能。"); + } else { + if (UOJLang::findSourceCode('std', $this->upload_dir) === false) { + throw new UOJProblemConfException("找不到本题的 std。请上传 std 代码文件,或停用本题的 Hack 功能。"); + } + if (UOJLang::findSourceCode('val', $this->upload_dir) === false) { + throw new UOJProblemConfException("找不到本题的 val。请上传 val 代码文件,或停用本题的 Hack 功能。"); + } + } + } + + private function _sync() { + try { + if (!$this->create_prepare_folder()) { + throw new UOJSyncFailedException('创建临时文件夹失败'); + } + + $this->requirement = []; + $this->problem_extra_config = $this->problem->getExtraConfig();; + if (!is_file("{$this->upload_dir}/problem.conf")) { + throw new UOJFileNotFoundException("problem.conf"); + } + + $this->problem_conf = getUOJConf("{$this->upload_dir}/problem.conf"); + $this->final_problem_conf = $this->problem_conf; + if ($this->problem_conf === -1) { + throw new UOJFileNotFoundException("problem.conf"); + } elseif ($this->problem_conf === -2) { + throw new UOJProblemConfException("syntax error: duplicate keys"); + } + + $this->allow_files = array_flip(FS::scandir($this->upload_dir)); + + $zip_file = new ZipArchive(); + if ($zip_file->open("{$this->prepare_dir}/download.zip", ZipArchive::CREATE) !== true) { + throw new Exception("download.zip : failed to create the zip file"); + } + + if (isset($this->allow_files['require']) && is_dir("{$this->upload_dir}/require")) { + $this->copy_to_prepare('require'); + } + + if (isset($this->allow_files['testlib.h']) && is_file("{$this->upload_dir}/testlib.h")) { + $this->copy_file_to_prepare('testlib.h'); + } + + $this->fast_hackable_check(); + + if ($this->check_conf_on('use_builtin_judger')) { + $n_tests = getUOJConfVal($this->problem_conf, 'n_tests', 10); + if (!validateUInt($n_tests) || $n_tests <= 0) { + throw new UOJProblemConfException("n_tests must be a positive integer"); + } + for ($num = 1; $num <= $n_tests; $num++) { + $input_file_name = getUOJProblemInputFileName($this->problem_conf, $num); + $output_file_name = getUOJProblemOutputFileName($this->problem_conf, $num); + + $this->copy_file_to_prepare($input_file_name); + $this->copy_file_to_prepare($output_file_name); + } + + if (!$this->check_conf_on('interaction_mode')) { + if (isset($this->problem_conf['use_builtin_checker'])) { + if (!preg_match('/^[a-zA-Z0-9_]{1,20}$/', $this->problem_conf['use_builtin_checker'])) { + throw new Exception("" . HTML::escape($this->problem_conf['use_builtin_checker']) . " is not a valid checker"); + } + } else { + $this->copy_source_code_to_prepare('chk'); + $this->compile_at_prepare('chk', ['need_include_header' => true]); + } + } + + if ($this->check_conf_on('submit_answer')) { + if (!isset($this->problem_extra_config['dont_download_input'])) { + for ($num = 1; $num <= $n_tests; $num++) { + $input_file_name = getUOJProblemInputFileName($this->problem_conf, $num); + $zip_file->addFile("{$this->prepare_dir}/$input_file_name", "$input_file_name"); + } + } + + $n_output_files = 0; + for ($num = 1; $num <= $n_tests; $num++) { + $output_file_id = getUOJConfVal($this->problem_conf, ["output_file_id_{$num}", "output_file_id"], "$num"); + if (!validateUInt($output_file_id) || $output_file_id < 0 || $output_file_id > $n_tests) { + throw new UOJProblemConfException("output_file_id/output_file_id_{$num} must be in [1, n_tests]"); + } + $n_output_files = max($n_output_files, $output_file_id); + } + for ($num = 1; $num <= $n_output_files; $num++) { + $output_file_name = getUOJProblemOutputFileName($this->problem_conf, $num); + $this->requirement[] = ['name' => "output$num", 'type' => 'text', 'file_name' => $output_file_name]; + } + } else { + $n_ex_tests = getUOJConfVal($this->problem_conf, 'n_ex_tests', 0); + if (!validateUInt($n_ex_tests) || $n_ex_tests < 0) { + throw new UOJProblemConfException('n_ex_tests must be a non-negative integer. Current value: ' . HTML::escape($n_ex_tests)); + } + + for ($num = 1; $num <= $n_ex_tests; $num++) { + $input_file_name = getUOJProblemExtraInputFileName($this->problem_conf, $num); + $output_file_name = getUOJProblemExtraOutputFileName($this->problem_conf, $num); + + $this->copy_file_to_prepare($input_file_name); + $this->copy_file_to_prepare($output_file_name); + } + + if ($this->problem->info['hackable']) { + $this->copy_source_code_to_prepare('std'); + if (isset($this->problem_conf['with_implementer']) && $this->problem_conf['with_implementer'] == 'on') { + $this->compile_at_prepare('std', [ + 'implementer' => 'implementer', + 'path' => 'require' + ]); + } else { + $this->compile_at_prepare('std'); + } + $this->copy_source_code_to_prepare('val'); + $this->compile_at_prepare('val', ['need_include_header' => true]); + } + + if ($this->check_conf_on('interaction_mode')) { + $this->copy_source_code_to_prepare('interactor'); + $this->compile_at_prepare('interactor', ['need_include_header' => true]); + } + + $n_sample_tests = getUOJConfVal($this->problem_conf, 'n_sample_tests', $n_tests); + if (!validateUInt($n_sample_tests) || $n_sample_tests < 0) { + throw new UOJProblemConfException('n_sample_tests must be a non-negative integer. Current value: ' . HTML::escape($n_sample_tests)); + } + if ($n_sample_tests > $n_ex_tests) { + throw new UOJProblemConfException("n_sample_tests can't be greater than n_ex_tests"); + } + + if (!isset($this->problem_extra_config['dont_download_sample'])) { + for ($num = 1; $num <= $n_sample_tests; $num++) { + $input_file_name = getUOJProblemExtraInputFileName($this->problem_conf, $num); + $output_file_name = getUOJProblemExtraOutputFileName($this->problem_conf, $num); + $zip_file->addFile("{$this->prepare_dir}/{$input_file_name}", "$input_file_name"); + if (!isset($this->problem_extra_config['dont_download_sample_output'])) { + $zip_file->addFile("{$this->prepare_dir}/{$output_file_name}", "$output_file_name"); + } + } + } + + $this->requirement[] = ['name' => 'answer', 'type' => 'source code', 'file_name' => 'answer.code']; + } + } else { + if ($this->user !== 'root' && !isSuperUser($this->user)) { + throw new UOJProblemConfException("use_builtin_judger must be on."); + } else { + foreach ($this->allow_files as $file_name => $file_num) { + $this->copy_to_prepare($file_name); + } + $this->makefile_at_prepare(); + + $this->requirement[] = ['name' => 'answer', 'type' => 'source code', 'file_name' => 'answer.code']; + } + } + putUOJConf("{$this->prepare_dir}/problem.conf", $this->final_problem_conf); + + if (isset($this->allow_files['download']) && is_dir("{$this->upload_dir}/download")) { + $download_dir = "{$this->upload_dir}/download"; + foreach (FS::scandir_r($download_dir) as $file_name) { + if (is_file("{$download_dir}/{$file_name}")) { + $zip_file->addFile("{$download_dir}/{$file_name}", $file_name); + } + } + } + + $zip_file->close(); + + $orig_requirement = $this->problem->getSubmissionRequirement(); + if (!$orig_requirement) { + DB::update([ + "update problems", + "set", ["submission_requirement" => json_encode($this->requirement)], + "where", ["id" => $this->id] + ]); + } + + UOJSystemUpdate::updateProblemInternally($this->problem, [ + 'text' => 'sync', + 'requestor' => Auth::check() ? Auth::id() : null + ]); + } catch (Exception $e) { + $this->remove_prepare_folder(); + return $e->getMessage(); + } + + UOJLocalRun::exec(['rm', $this->data_dir, '-r']); + rename($this->prepare_dir, $this->data_dir); + + UOJLocalRun::execAnd([ + ['cd', '/var/uoj_data'], + ['zip', "{$this->id}.next.zip", $this->id, '-r', '-q'], + ['mv', "{$this->id}.next.zip", "{$this->id}.zip", '-f'], + ]); + + return ''; + } + + public function sync() { + return $this->lock(LOCK_EX, fn () => $this->_sync()); + } +}