关于 Ed Discussion 在线评测机制的讨论

Apr 28 · 12min

English version here: A Discussion About OJ of Ed Discussion

省流

在Ed Discussion平台提供的在线评测系统中,你应该在终端手动编译运行,而不是点击「运行」按钮来运行你的代码。


问题描述

Kuperman 教授布置的挑战作业要求我们改进 C 语言代码,以实现顺利创建两个文件夹。相信大多数人和我一样,在做这个挑战时遇到了以下情况:

  • 修改的代码在本地运行成功,顺利地创建了两个文件夹;
  • 修改的代码在 Ed 平台的虚拟机终端手动编译并运行成功,顺利地在 HOME 里创建了两个文件夹;此时点击「评测」按钮,评测成功
  • 修改的代码仅点击「运行」按钮自动编译运行,终端无输出;提示程序正常退出;HOME 文件夹下没有创建文件夹;此时点击「评测」按钮,评测失败
  • 如果当前目录中有 MyFirstDirectoryMySecondDirectory 文件夹,无论如何运行都会提示文件夹已存在;但点击「评测」按钮,评测成功

这篇文章我将记录我是如何排查这个异常现象,并得出一个可靠的结论的。


「评测」按钮

首先我们可以肯定的是,「评测」的机制非常简单,就是检测当前虚拟机中 HOME 文件夹中是否有两个文件夹:MyFirstDirectoryMySecondDirectory。可以非常简单地检验得到:

mkdir MyFirstDirectory && mkdir MySecondDirectory
# 此时点击「评测」,顺利通过

这个逻辑应该是教师端设计的,用于检测代码是否在虚拟机中成功创建了文件夹。

「运行」按钮

点击「运行」按钮后,终端内的文件没有发生任何变化;我由此做了以下尝试:

在以下尝试中,我都使得代码尽量短、包含尽量少的逻辑,以便排除其它因素的干扰。这符合 GitHub 开源项目调试的「最小重现」原则。

猜想 1:源代码被复制到本虚拟机另一个文件夹执行

有没有可能源代码被复制到另一个文件夹里编译并运行,并在那个文件夹里新创建了文件夹,从而导致我们在 HOME 文件夹里看不到新创建的文件夹呢?简单测试一下:

#include <stdio.h>
#include <unistd.h>

int main() {
    char buf[200];
    puts(getcwd(buf, sizeof(buf)));
    return 0;
}

这段代码在点击「运行」按钮后,终端输出了/home,说明这个猜想是错误的。

猜想 2:源代码在 HOME 文件夹中编译运行,但程序退出后立刻将多生成的文件(夹)全部删除

因为我观察到本目录下没有任何多生成的文件,甚至没有编译结果(二进制文件,如a.out),有没有可能程序在运行退出后,立刻将编译结果连同新生成的文件夹(即所有在点击「运行」按钮之后生成的文件)一起自动删除了呢?

昨天我有了这个猜想之后非常兴奋,我以为我已经找到正确答案了,但是做了以下尝试后发现它也是错误的:

#include <unistd.h>

int main() {
    sleep(5);
}

让这个程序什么也不做,只是等待5秒(这是一个足够长的时间)。点击「运行」按钮后,依然是什么也没有产生,没有编译结果(如a.out)。

我不甘心,又使用了一个命令行工具watchexec,它是用 Rust 语言编写的,可以实时监控文件变化并执行指定的命令。

在这个 Ed 的虚拟机装这个工具非常痛苦,因为其包管理工具 pacman 需要 sudo 权限。所以我将这个包含 watchexec 的二进制压缩包上传到平台再手动解压缩,就像这样:

tar -xf ./watchexec-2.3.0-x86_64-unknown-linux-gnu.tar.gz # 解压缩
./watchexec-2.3.0-x86_64-unknown-linux-gnu/watchexec "ls -a" # 每次 HOME 文件夹下有文件变化时,执行 ls -a 命令

点击「运行」按钮,没有文件变化,我死心了。

猜想 3:HOME 文件夹下的所有文件(夹)都被复制到另一个虚拟机的 HOME 中进行评测,但评测结果只返回终端输出

要检验这种猜想,我们需要运行一个 C 语言程序,证明它不是在当前虚拟机中运行的,而是在另一个虚拟机中。

方法有特别多,我用了几种方法来检验,下面只给出一个最简单的:

因为我们猜想只有 HOME 下的文件会被复制走,所以我们可以在本虚拟机中一个 HOME 之外的地方手动创建一个文件夹,并在程序中检测这个文件夹是否存在:

mkdir /tmp/123  # 这个文件夹对所有用户开放了写权限
ls /tmp

输出结果:

123    run

然后点击「运行」按钮运行:

#include <unistd.h>

int main() {
    execl("/bin/ls", "/bin/ls", "-d", "/tmp", NULL);
}

输出结果:

run

没有这个文件夹!而这时我们回到终端中ls /tmp,发现它依然存在!说明这个程序是在另一个虚拟机中运行的。

所以我们就可以解释以上这些异常现象:

  • 点击运行按钮后,本地没有创建文件夹是因为它在另一个评测用的虚拟机中创建了文件夹。
  • 点击运行按钮后,本地没有编译结果是因为它在另一个评测用的虚拟机中编译了程序。
  • 但是它同样是在 HOME 文件夹下编译执行的,同时也会把终端的输出结果转发回这个终端。这给了我们一个「在本地运行」的错觉,也是我走了很多弯路的原因。

这篇文章有啥用?

你会发现我折腾半天,最后得到了一个很简单的结论,看起来没什么意义😭。

不过这是一个非常典型的「调试环境」的流程。弄清楚这个对我来说还挺有趣的,希望你也能在这篇文章里有所收获!


>