diff --git a/README.md b/README.md index e0a35eb5..eb1cc93b 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,9 @@ A tiny judging environment for OI contest based on Lemon + LemonPlus | Arch | 2021-07-11 | amd64 | KDE-Xorg | | Arch | 2020-10-31 | amd64 | KDE-Wayland | | Ubuntu | 20.04 | amd64 | GNOME 3 | -| Ubuntu | 18.04.4 | amd64 | GNOME 3 | | NOI Linux 2(Ubuntu) | 20.04 | amd64 | GNOME 3 | -| Linux Mint | 19.3 | amd64 | Cinnamon | -| Deepin | 15.11 | amd64 | DDE | | Deepin | 20 (1000) | amd64 | DDE | -| Debian | 10.3.0 | amd64 | LXQt; KDE-Xorg | | Fedora | 31-1.9 | amd64 | XFCE | -| openSUSE | Leap 15.1 | amd64 | iceWM | | openSUSE | Tumbleweed | amd64 | KDE-Xorg | | macOS | 15.2 Beta | arm64 | Aqua | diff --git a/src/core/processrunner_unix.cpp b/src/core/processrunner_unix.cpp index c34cf546..c7e57fe6 100644 --- a/src/core/processrunner_unix.cpp +++ b/src/core/processrunner_unix.cpp @@ -100,6 +100,8 @@ ProcessRunnerResult UnixProcessRunner::run() { argumentsList << config.outputFileName; } + argumentsList << QString("%1").arg(extraTime); + qDebug() << argumentsList; QString bwrapPath = QStandardPaths::findExecutable("bwrap"); @@ -162,6 +164,8 @@ ProcessRunnerResult UnixProcessRunner::run() { argumentsList << config.outputFileName; } + argumentsList << QString("%1").arg(extraTime); + qDebug() << argumentsList; runner->setProcessEnvironment(config.environment); @@ -182,9 +186,9 @@ ProcessRunnerResult UnixProcessRunner::run() { QElapsedTimer timer; timer.start(); - // Using rlimit to limit CPU time can only be accurate to seconds, - // so here it is rounded up to an integer second. - long long killTimeLimit = (config.timeLimit + 999) / 1000 * 1000 + extraTime; + // The watcher itself enforces the wall clock (timeLimit + extraTime) and exits shortly + // after the timer fires; this loop only guards against a hung watcher, with 1s of slack. + long long killTimeLimit = 1LL * config.timeLimit + extraTime + 1000; while (timer.elapsed() <= killTimeLimit) { if (runner->waitForFinished(10)) { isProgramFinishedInExtraTimeLimit = true; diff --git a/unix/test/CMakeLists.txt b/unix/test/CMakeLists.txt index 844f8713..78991477 100644 --- a/unix/test/CMakeLists.txt +++ b/unix/test/CMakeLists.txt @@ -18,6 +18,7 @@ file(COPY hello.sh DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) add_executable(tle tle.c) add_executable(add add.c) add_executable(re re.c) +add_executable(sleep_tle sleep_tle.c) file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/scripts DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) @@ -27,6 +28,7 @@ add_test(NAME watcher_run_sh_test COMMAND python3 scripts/run_sh.py) add_test(NAME watcher_MLE_static_test COMMAND python3 scripts/mle_static.py) add_test(NAME watcher_unlimit_memory_test COMMAND python3 scripts/unlimit.py) add_test(NAME watcher_TLE_test COMMAND python3 scripts/tle.py) +add_test(NAME watcher_wall_clock_TLE_test COMMAND python3 scripts/sleep_tle.py) add_test(NAME watcher_filename_with_space_test COMMAND python3 scripts/space.py) add_test(NAME watcher_symlink_abs_test COMMAND python3 scripts/symlink_abs.py) add_test(NAME watcher_symlink_rel_test COMMAND python3 scripts/symlink_rel.py) diff --git a/unix/test/scripts/mle_static.py b/unix/test/scripts/mle_static.py index f53fedf4..de2f1f58 100644 --- a/unix/test/scripts/mle_static.py +++ b/unix/test/scripts/mle_static.py @@ -4,7 +4,7 @@ pid = os.getpid() tmperr = f"_tmperr_{pid}" -p = subprocess.Popen(["./watcher_unix", "./mle_static", "", "", "", tmperr, "1000", "380", "1000", "380", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "./mle_static", "", "", "", tmperr, "1000", "380", "1000", "380", "", "", "0"], shell=False, stdout=subprocess.PIPE) stdout, _ = p.communicate() diff --git a/unix/test/scripts/redirect.py b/unix/test/scripts/redirect.py index 4a2af928..63979c53 100644 --- a/unix/test/scripts/redirect.py +++ b/unix/test/scripts/redirect.py @@ -10,7 +10,7 @@ with open(tmpin, 'w') as f: f.writelines(['1 1']) -p = subprocess.Popen(["./watcher_unix", "./add", "", tmpin, tmpout, tmperr, "1000", "100", "1000", "100", "", ""], shell=False) +p = subprocess.Popen(["./watcher_unix", "./add", "", tmpin, tmpout, tmperr, "1000", "100", "1000", "100", "", "", "0"], shell=False) time.sleep(2) p.kill() diff --git a/unix/test/scripts/run.py b/unix/test/scripts/run.py index 2ae512b8..d4fcaea6 100644 --- a/unix/test/scripts/run.py +++ b/unix/test/scripts/run.py @@ -5,7 +5,7 @@ tmpout = f"_tmpout_{pid}" tmperr = f"_tmperr_{pid}" -p = subprocess.Popen(["./watcher_unix", "./hello", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "./hello", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", "", "0"], shell=False, stdout=subprocess.PIPE) assert(p.wait() == 0) assert(os.path.exists(tmpout)) diff --git a/unix/test/scripts/run_sh.py b/unix/test/scripts/run_sh.py index 0ae2612c..d4c830c2 100644 --- a/unix/test/scripts/run_sh.py +++ b/unix/test/scripts/run_sh.py @@ -5,7 +5,7 @@ tmpout = f"_tmpout_{pid}" tmperr = f"_tmperr_{pid}" -p = subprocess.Popen(["./watcher_unix", "/bin/sh", "hello.sh", "", tmpout, tmperr, "1000", "100", "1000", "100", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "/bin/sh", "hello.sh", "", tmpout, tmperr, "1000", "100", "1000", "100", "", "", "0"], shell=False, stdout=subprocess.PIPE) assert(p.wait() == 0) assert(os.path.exists(tmpout)) diff --git a/unix/test/scripts/runtimeerr.py b/unix/test/scripts/runtimeerr.py index d69fbd9d..d5c62401 100644 --- a/unix/test/scripts/runtimeerr.py +++ b/unix/test/scripts/runtimeerr.py @@ -4,6 +4,6 @@ pid = os.getpid() tmperr = f"_tmperr_{pid}" -p = subprocess.Popen(["./watcher_unix", "./re", "", "", "", tmperr, "1000", "100", "1000", "100", "", ""], shell=False) +p = subprocess.Popen(["./watcher_unix", "./re", "", "", "", tmperr, "1000", "100", "1000", "100", "", "", "0"], shell=False) assert(p.wait() == 2) diff --git a/unix/test/scripts/sleep_tle.py b/unix/test/scripts/sleep_tle.py new file mode 100644 index 00000000..a8bf947e --- /dev/null +++ b/unix/test/scripts/sleep_tle.py @@ -0,0 +1,17 @@ +import subprocess +import time +import os + +pid = os.getpid() +tmperr = f"_tmperr_{pid}" + +# `sleep` does not consume CPU time, so it can bypass RLIMIT_CPU; +# the watcher must time it out by wall clock. +p = subprocess.Popen(["./watcher_unix", "./sleep_tle", "", "", "", tmperr, + "1000", "100", "1000", "100", "", "", "0"], + shell=False) + +time.sleep(3) +p.kill() + +assert(p.returncode == 3) diff --git a/unix/test/scripts/space.py b/unix/test/scripts/space.py index 15a5a70a..a8554ed2 100644 --- a/unix/test/scripts/space.py +++ b/unix/test/scripts/space.py @@ -8,7 +8,7 @@ shutil.copy("./hello", "./he llo") -p = subprocess.Popen(["./watcher_unix", "./he llo", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "./he llo", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", "", "0"], shell=False, stdout=subprocess.PIPE) assert(p.wait() == 0) assert(os.path.exists(tmpout)) diff --git a/unix/test/scripts/symlink_abs.py b/unix/test/scripts/symlink_abs.py index 66e9c10e..b10cdf82 100644 --- a/unix/test/scripts/symlink_abs.py +++ b/unix/test/scripts/symlink_abs.py @@ -10,7 +10,7 @@ os.symlink(os.path.join(os.getcwd(), "hello"), "hello_s_abs") -p = subprocess.Popen(["./watcher_unix", "./hello_s_abs", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "./hello_s_abs", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", "", "0"], shell=False, stdout=subprocess.PIPE) assert(p.wait() == 0) assert(os.path.exists(tmpout)) diff --git a/unix/test/scripts/symlink_rel.py b/unix/test/scripts/symlink_rel.py index b13ab1b2..6ecadfb0 100644 --- a/unix/test/scripts/symlink_rel.py +++ b/unix/test/scripts/symlink_rel.py @@ -10,7 +10,7 @@ os.symlink("hello", "hello_s_rel") -p = subprocess.Popen(["./watcher_unix", "./hello_s_rel", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "./hello_s_rel", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", "", "0"], shell=False, stdout=subprocess.PIPE) assert(p.wait() == 0) assert(os.path.exists(tmpout)) diff --git a/unix/test/scripts/tle.py b/unix/test/scripts/tle.py index a2ea1fed..5fab5bbf 100644 --- a/unix/test/scripts/tle.py +++ b/unix/test/scripts/tle.py @@ -6,7 +6,7 @@ tmpout = f"_tmpout_{pid}" tmperr = f"_tmperr_{pid}" -p = subprocess.Popen(["./watcher_unix", "./tle", "", "", "", tmperr, "1000", "100", "1000", "100", "", ""], shell=False) +p = subprocess.Popen(["./watcher_unix", "./tle", "", "", "", tmperr, "1000", "100", "1000", "100", "", "", "0"], shell=False) time.sleep(5) p.kill() diff --git a/unix/test/scripts/unlimit.py b/unix/test/scripts/unlimit.py index 68268c4c..23676ce8 100644 --- a/unix/test/scripts/unlimit.py +++ b/unix/test/scripts/unlimit.py @@ -5,7 +5,7 @@ tmpout = f"_tmpout_{pid}" tmperr = f"_tmperr_{pid}" -p = subprocess.Popen(["./watcher_unix", "./mle_static", "", "", tmpout, tmperr, "1000", "-1", "1000", "-1", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "./mle_static", "", "", tmpout, tmperr, "1000", "-1", "1000", "-1", "", "", "0"], shell=False, stdout=subprocess.PIPE) assert(p.wait() == 0) assert(os.path.exists(tmpout)) diff --git a/unix/test/sleep_tle.c b/unix/test/sleep_tle.c new file mode 100644 index 00000000..e13b08b0 --- /dev/null +++ b/unix/test/sleep_tle.c @@ -0,0 +1,6 @@ +#include + +int main() { + sleep(10); + return 0; +} diff --git a/unix/watcher_unix.cpp b/unix/watcher_unix.cpp index 096042e6..32d738d3 100644 --- a/unix/watcher_unix.cpp +++ b/unix/watcher_unix.cpp @@ -7,6 +7,7 @@ */ #include +#include #include #include #include @@ -18,6 +19,15 @@ #include #include #include +#if defined(__linux__) +#include +#include +#include +#include +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || \ + defined(__DragonFly__) +#include +#endif #include int pid; @@ -52,11 +62,12 @@ enum : int { * argv[9]: 原始(未经语言设置缩放的)空间限制(MiB) * argv[10]: 选手程序只读的文件 * argv[11]: 选手程序只写的文件 + * argv[12]: wall clock 额外超时时间(毫秒) */ auto main(int argc, char *argv[]) -> int { - if (argc != 12) { + if (argc != 13) { printf("-1\n-1\n"); - fprintf(stderr, "Expected 11 arguments, found %d\n", argc); + fprintf(stderr, "Expected 12 arguments, found %d\n", argc - 1); return RS_FAIL; } std::string fileName = argv[1]; @@ -70,6 +81,7 @@ auto main(int argc, char *argv[]) -> int { [[maybe_unused]] long long rawMemoryLimitMib = std::stoll(argv[9]); [[maybe_unused]] std::string readableFile = argv[10]; [[maybe_unused]] std::string writableFile = argv[11]; + long long extraTimeMs = std::stoll(argv[12]); initWatcher(); @@ -97,7 +109,22 @@ auto main(int argc, char *argv[]) -> int { ssize_t actualMemoryRLimit = getMemoryRLimit(memoryLimitMib); +#if defined(__linux__) + int childPfd = -1; + struct clone_args args{}; + args.flags = CLONE_PIDFD; + args.pidfd = (unsigned long long)&childPfd; + args.exit_signal = SIGCHLD; + pid = syscall(SYS_clone3, &args, sizeof(args)); +#else pid = fork(); +#endif + + if (pid < 0) { + perror("fork"); + printf("-1\n-1\n"); + return RS_FAIL; + } if (pid > 0) { // Parent process @@ -107,11 +134,111 @@ auto main(int argc, char *argv[]) -> int { struct rusage usage{}; int status = 0; + // Time limit is enforced by a wall-clock timer instead of `setrlimit(RLIMIT_CPU)`: + // RLIMIT_CPU counts CPU time while the judge measures user time, and a program that + // merely sleeps never hits it. + long long wallClockMs = timeLimitMs + extraTimeMs; +#if defined(__linux__) + int timerFd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); + if (timerFd < 0) { + perror("timerfd_create"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + struct itimerspec ts{}; + ts.it_value.tv_sec = wallClockMs / 1000; + ts.it_value.tv_nsec = (wallClockMs % 1000) * 1000000; + if (timerfd_settime(timerFd, 0, &ts, nullptr) < 0) { + perror("timerfd_settime"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + struct pollfd pfds[2]{}; + pfds[0].fd = childPfd; + pfds[0].events = POLLIN; + pfds[1].fd = timerFd; + pfds[1].events = POLLIN; + + int pollResult; + do { + pollResult = poll(pfds, 2, -1); + } while (pollResult < 0 && errno == EINTR); + + if (pollResult < 0) { + perror("poll"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + bool childExited = pfds[0].revents & POLLIN; + bool timedOut = pfds[1].revents & POLLIN; + + if (! childExited) + kill(pid, SIGKILL); + + wait4(pid, &status, 0, &usage); + + close(childPfd); + close(timerFd); + + if (timedOut) { + printf("-1\n-1\n"); + return RS_TLE; + } +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || \ + defined(__DragonFly__) + int kq = kqueue(); + if (kq < 0) { + perror("kqueue"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + struct kevent changes[2]; + EV_SET(&changes[0], pid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, NULL); + EV_SET(&changes[1], 0, EVFILT_TIMER, EV_ADD | EV_ONESHOT, 0, wallClockMs, NULL); + + struct kevent events[2]; + int n; + do { + n = kevent(kq, changes, 2, events, 2, NULL); + } while (n < 0 && errno == EINTR); + + if (n < 0) { + perror("kevent"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + bool childExited = false; + bool timedOut = false; + for (int i = 0; i < n; i++) { + if (events[i].filter == EVFILT_PROC) + childExited = true; + else if (events[i].filter == EVFILT_TIMER) + timedOut = true; + } + + if (timedOut && ! childExited) + kill(pid, SIGKILL); + + wait4(pid, &status, 0, &usage); + + close(kq); + + if (timedOut) { + printf("-1\n-1\n"); + return RS_TLE; + } +#else if (wait4(pid, &status, 0, &usage) == -1) { printf("-1\n-1\n"); perror("wait4"); return RS_FAIL; } +#endif if (WIFEXITED(status)) { long long timeUsedMs = @@ -144,6 +271,7 @@ auto main(int argc, char *argv[]) -> int { return RS_RE; } } else { + // Child process std::string finalStdinRedirect = stdinRedirect.empty() ? "/dev/null" : stdinRedirect; if (freopen(finalStdinRedirect.c_str(), "r", stdin) == NULL) { perror("freopen stdin"); @@ -160,7 +288,7 @@ auto main(int argc, char *argv[]) -> int { exit(RS_FAIL); } - rlimit memlim{}, stalim{}, timlim{}; + rlimit memlim{}, stalim{}; if (memoryLimitMib > 0) { memlim = (rlimit){(rlim_t)actualMemoryRLimit, (rlim_t)actualMemoryRLimit}; @@ -171,13 +299,8 @@ auto main(int argc, char *argv[]) -> int { stalim = (rlimit){(rlim_t)2147483647LL, (rlim_t)2147483647LL}; } - // Calculate time limit in seconds, rounding up - rlim_t soft_time_limit_sec = (timeLimitMs + 999) / 1000; - timlim = (rlimit){soft_time_limit_sec, soft_time_limit_sec + 1}; // Soft limit + 1 for hard limit - setrlimit(RLIMIT_AS, &memlim); setrlimit(RLIMIT_STACK, &stalim); - setrlimit(RLIMIT_CPU, &timlim); if (execlp("bash", "bash", "-c", runCmd.c_str(), NULL) == -1) { perror("execlp");