HUSTOJ的Windows版评判内核(一)

HUSTOJ的Windows版评判内核(一)

作者:游蓝海

个人主页:http://blog.csdn.net/you_lan_hai

 


2013.4.9 注:最新版本项目地址:https://github.com/NsLib/FreeJudger。新版FreeJudger,跟我之前写的这个已经完全不一样了,之前的这个废除。虽然现在工作忙,但我们会继续开发FreeJudger,直到功能完善,详见:HUSTOJ的Windows版评判内核(二)

 


        在线评测系统(Online Judge System,OJ),即在线评判用户所提交的代码是否能够解决相应的题目,常作为高校训练学生编程实践能力的平台已及ACM/ICPC等程序设计竞赛平台。目前,985、211类的实力高校,基本都拥有自己的OJ,而对于实力较弱的高校来说,开发一个OJ是相当困难的一件事。虽然OJ系统可以共享,但是如果作为内部练习和训练,依赖别人的oj,并不是很方便。如今,OJ系统已经非常多了,但是开源的系统却并不多,而且开源的评判内核就更少了,如果评判程序出了问题,还得等别人出更新。这也许是因为开发过程比较辛苦的原因吧。

        HUSTOJ是一个出色的开源的系统,它遵循GPL,不仅功能齐全,而且还有开发团队维护,免去了高校的重复劳动力(详见:http://code.google.com/p/hustoj/)。虽然web可以搭建到任何一款操作系统上,但是HUSTOJ的评判内核却是linux版的,我相信用windows的菜鸟团队会更多,至少我们当年就是这样的,要搭建一个linux环境何其困难呀。

        以前在学校的时候,我是ACM团队的负责人之一,我们没有自己的OJ,训练的时候都会去其他学校的OJ做题。大部分学校OJ的题库偏难,挫败了无数的新手,我目睹几百人的团队,后来流失到30多人,目睹了很多想成为编程高手的计算机系学弟去打游戏了,作为一个团队负责人,我感觉我失职了。虽然有部分学校的OJ题会简单些,但是我们经常会遇到这样的尴尬,重要时刻网站无法登录!

        作为苦逼的无人问津院校的最差专业的想为自己开发团队打造自主oj的你,加入我们吧:群117975329,验证信息CSDN。

        

        好了,废话不多说,无码无真相,windows版HUSTOJ评判内核项目:http://code.google.com/p/online-judger/ ,目前已经完成大部分功能,基本可用,建议用svn工具(推荐TortoiseSVN)下载最新源码。


原理说明
        1.数据库管理模块(DBManager)从数据库查询出当前结果为待定和等待重判的提交信息,并组装成内部可识别任务数据(Task),然后将Task转交给评判单元(JudgeCell),等待评判。
        2.评判单元(JudgeCell)管理着一组评判内核(JudgeCore),每个评判内核(JudgeCore)都运行在独立的线程中。评判内核不断的从评判单元中获取任务,如果发现任务就进行一次评判,然后通过回调接口,将评判结果反馈给上层,最终,评判的结果会反馈给DBManager。
        3.DBManager收到结果后,将结果写回数据库。
        4.一个评判内核(JudgeCore)由三个执行部件(Excuter)组成,分别是编译器(Compiler)、执行器(Runner)、匹配器(Matcher)组成,分别负责编译用户代码,执行用户程序并生成输出数据,匹配用户程序的输出数据是否与测试数据匹配。JudgeCore目前只支持c和c++两种语言。


开发工具

        visual studio 2008 

        

解决方案构成
        解决方案共有3个项目,LZData,acm,Judger。
        1.LZData,是我其他项目中读/写配置文件的工具,目前支持LZD和XML两种格式。对XML格式的支持不是很完善,目前只支持赋值类语法,不支持注释、帮助等格式的语法。
        2.acm,简单的封装了一些常用的windows API,如线程、进程、网络通信、文件处理、MySql等。
        3.Judger,是HUSTOJ的评判内核程序。Judger/bin是内核程序的输出目录。

TODO
1.支持评判内核沙箱运行模式。
2.增加对Java代码评判的支持。
3.增加对sim的支持。
4.评判单元多进程化。
5.HUSTOJ IDE测试功能的支持。


简易测试环境搭建

1.安装wamp(windows+apache+mysql+php)集成环境。

2.下载HUSTOJ,将其web目录放置到wamp/www目录下。数据库的配置参考hustoj/install/readme。

3.下载本windows版评判程序,编译后,在judger/bin目录下生成judger.exe。judger.exe可以放置到任意目录,但是配置文件(config.xml)中的<testDataPath>项路径必须与hustoj 测试数据的配置项‘OJ_DATA’保持一致,此路径最好都用绝对路径。

        程序写的不是很好,欢迎各位windows编程砖家以及oj内核砖家留下您宝贵的一砖。其中在编写windows沙箱(job)功能时遇到了几个棘手的问题:

        1.job时间已经到了,程序还未终止,往往要多等待2-5s。

        2.评判含有静态声明的大数组(数组内存超出job的限制内存)代码时,在进程添加到job之后,调用ResumeThread的瞬间,子进程就弹出错误对话框了。如:

#include .....
char buffer[100*1024*1024];//超出job限制内存。
int main().....

        因此,程序执行程序没有使用到沙箱功能,而是简单的启动监视线程来监视子进程的执行情况,如果子进程超出限制,则强行结束。

        附上一段沙箱(job)实现代码,望高手指点:

bool ZProcessJob::create(const tstring &  cmd, bool start_/* = true*/)
{
    if (NULL != m_hProcess)
    {
        OutputMsg(_T("process has been created!"));
        return false;
    }

    int64 limitTime = m_limitTime * 10000; //100ns (1s = 10^9ns)
    int limitMemory = m_limitMemory * 1024; //bytes
    if (limitMemory < 0)//超出int范围了
    {
        limitMemory = 128*1024*1024; //默认128M
    }

    //////////////////////////////////////////////////////////////////////////
    //创建作业沙箱(job)
    //////////////////////////////////////////////////////////////////////////
    
    tstring jobName;
    generateGUID(jobName);
    if(!m_job.create(jobName))
    {
        OutputMsg(_T("create job faild!"));
        return false;
    }

    //////////////////////////////////////////////////////////////////////////
    //设置job信息
    //////////////////////////////////////////////////////////////////////////

    //设置基本限制信息
    JOBOBJECT_EXTENDED_LIMIT_INFORMATION subProcessLimitRes;
    ZeroMemory(&subProcessLimitRes, sizeof(subProcessLimitRes));

    JOBOBJECT_BASIC_LIMIT_INFORMATION & basicInfo = subProcessLimitRes.BasicLimitInformation;
    basicInfo.LimitFlags = JOB_OBJECT_LIMIT_PROCESS_TIME| \
        JOB_OBJECT_LIMIT_PRIORITY_CLASS| \
        JOB_OBJECT_LIMIT_PROCESS_MEMORY| \
        JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION;
    basicInfo.PriorityClass = NORMAL_PRIORITY_CLASS;      //优先级为默认
    basicInfo.PerProcessUserTimeLimit.QuadPart = limitTime; //子进程执行时间ns(1s=10^9ns)
    subProcessLimitRes.ProcessMemoryLimit = limitMemory;    //内存限制

    m_job.setInformation(
        JobObjectExtendedLimitInformation,
        &subProcessLimitRes, 
        sizeof(subProcessLimitRes));


    //让完成端口发出时间限制的消息
    JOBOBJECT_END_OF_JOB_TIME_INFORMATION timeReport;
    ZeroMemory(&timeReport, sizeof(timeReport));

    timeReport.EndOfJobTimeAction = JOB_OBJECT_POST_AT_END_OF_JOB;

    m_job.setInformation(
        JobObjectEndOfJobTimeInformation, 
        &timeReport,
        sizeof(JOBOBJECT_END_OF_JOB_TIME_INFORMATION));


    //UI限制
    JOBOBJECT_BASIC_UI_RESTRICTIONS subProcessLimitUi;
    ZeroMemory(&subProcessLimitUi, sizeof(subProcessLimitUi));

    subProcessLimitUi.UIRestrictionsClass = JOB_OBJECT_UILIMIT_NONE| \
        JOB_OBJECT_UILIMIT_DESKTOP| \
        JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS| \
        JOB_OBJECT_UILIMIT_DISPLAYSETTINGS| \
        JOB_OBJECT_UILIMIT_EXITWINDOWS| \
        JOB_OBJECT_UILIMIT_GLOBALATOMS| \
        JOB_OBJECT_UILIMIT_HANDLES| \
        JOB_OBJECT_UILIMIT_READCLIPBOARD;

    m_job.setInformation(
        JobObjectBasicUIRestrictions,
        &subProcessLimitUi,
        sizeof(subProcessLimitUi));


    //将作业关联到完成端口,以确定其运行情况,及退出的原因
    int id = generateID();
    m_ioCPHandle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, id, 0);

    JOBOBJECT_ASSOCIATE_COMPLETION_PORT jobCP;
    ZeroMemory(&jobCP, sizeof(jobCP));

    jobCP.CompletionKey = (PVOID)id;
    jobCP.CompletionPort = m_ioCPHandle;

    m_job.setInformation(
        JobObjectAssociateCompletionPortInformation,
        &jobCP,
        sizeof(jobCP));

    //////////////////////////////////////////////////////////////////////////
    //创建子进程
    //////////////////////////////////////////////////////////////////////////

    TCHAR cmd_[BUFSIZE];
    lstrcpy(cmd_, cmd.c_str());

    m_hInput = createInputFile();
    m_hOutput = createOutputFile();

	/*CreateProcess的第一个参数
	将标准输出和错误输出定向到我们建立的m_hOutput上
	将标准输入定向到我们建立的m_hInput上
	设置子进程接受StdIn以及StdOut的重定向
	*/
	STARTUPINFO StartupInfo;
    ZeroMemory(&StartupInfo, sizeof(StartupInfo));
	StartupInfo.cb = sizeof(STARTUPINFO);
	StartupInfo.hStdOutput = m_hOutput;
    StartupInfo.hStdError = m_hOutput;
	StartupInfo.hStdInput = m_hInput;
	StartupInfo.dwFlags = STARTF_USESTDHANDLES;

    PROCESS_INFORMATION ProcessInfo;
    ZeroMemory(&ProcessInfo, sizeof(ProcessInfo));
    
    if(!createProcess(cmd_, TRUE, CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB,
        StartupInfo, ProcessInfo))
    { 
        return false;
    }

    m_hProcess = ProcessInfo.hProcess;
    m_hThread = ProcessInfo.hThread;

    //////////////////////////////////////////////////////////////////////////
    //将子进程与job关联
    //////////////////////////////////////////////////////////////////////////

    if (start_)
    {
        start();
    }

    return true;
}

bool ZProcessJob::start()
{
    OutputMsgA("start run.");

    if(!m_job.assinProcess(m_hProcess))
    {
        OutputMsg(_T("应用进程到job失败!%d"), GetLastError());
        return false;
    }

    //启动子进程
    ResumeThread(m_hThread);
    
    //关闭标准输入文件和零时输出文件的句柄
    SAFE_CLOSE_HANDLE(m_hInput);
    SAFE_CLOSE_HANDLE(m_hOutput);

    //关闭主进程主线程句柄
    SAFE_CLOSE_HANDLE(m_hThread);

    //等待进程子进程处理完毕或耗尽资源退出

    DWORD ExecuteResult = -1;
    unsigned long completeKey;
    LPOVERLAPPED processInfo;
    bool done = false;
    while(!done)
    {
        GetQueuedCompletionStatus(
            m_ioCPHandle,
            &ExecuteResult, 
            &completeKey, 
            &processInfo, 
            INFINITE);

        switch (ExecuteResult) 
        {
        case JOB_OBJECT_MSG_NEW_PROCESS: 
            {
                OutputMsg(TEXT("New process (Id=%d) in Job"), processInfo);
            }
            break;

        case JOB_OBJECT_MSG_END_OF_JOB_TIME:
            {
                OutputMsg(TEXT("Job time limit reached"));
                m_exitCode = 1;
                done = true;
            }
            break;

        case JOB_OBJECT_MSG_END_OF_PROCESS_TIME: 
            {
                OutputMsg(TEXT("Job process (Id=%d) time limit reached"), processInfo);
                m_exitCode = 1;
                done = true;
            }
            break;

        case JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT: 
            {
                OutputMsg(TEXT("Process (Id=%d) exceeded memory limit"), processInfo);
                m_exitCode = 2;
                done = true;
            }
            break;

        case JOB_OBJECT_MSG_JOB_MEMORY_LIMIT: 
            {
                OutputMsg(TEXT("Process (Id=%d) exceeded job memory limit"), processInfo);
                m_exitCode = 2;
                done = true;
            }
            break;

        case JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT:
            {
                OutputMsg(TEXT("Too many active processes in job"));
            }
            break;

        case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO:
            {
                OutputMsg(TEXT("Job contains no active processes"));
                done = true;
            }
            break;

        case JOB_OBJECT_MSG_EXIT_PROCESS: 
            {
                OutputMsg(TEXT("Process (Id=%d) terminated"), processInfo);
                done = true;
            }
            break;

        case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS: 
            {
                OutputMsg(TEXT("Process (Id=%d) terminated abnormally"), processInfo);
                m_exitCode = 3;
                done = true;
            }
            break;

        default:
            OutputMsg(TEXT("Unknown notification: %d"), ExecuteResult);
            m_exitCode = 99;
            break;
        }
    }

    JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION jobai;
    ZeroMemory(&jobai, sizeof(jobai));
    QueryInformationJobObject(m_job.handle(), JobObjectBasicAndIoAccountingInformation, 
        &jobai, sizeof(jobai), NULL);

    JOBOBJECT_EXTENDED_LIMIT_INFORMATION joeli;
    ZeroMemory(&joeli, sizeof(joeli));
    QueryInformationJobObject(m_job.handle(), JobObjectExtendedLimitInformation, 
        &joeli, sizeof(joeli), NULL);

    m_runTime = jobai.BasicInfo.TotalUserTime.LowPart/10000;
    m_runMemory = joeli.PeakProcessMemoryUsed/1024;

    //关闭进程句柄
    SAFE_CLOSE_HANDLE(m_hProcess);

    //关闭完成端口
    SAFE_CLOSE_HANDLE(m_ioCPHandle);

    //为了安全,杀死作业内所有进程
    while(!m_job.terminate(0))
    {
        OutputMsg(_T("停止job失败!%d"), GetLastError());
        Sleep(1000);
    }
    //关闭作业句柄
    m_job.close();

    OutputMsgA("end run.");
    return true;
}



posted @ 2017-10-19 18:07  游蓝海2017  阅读(411)  评论(0编辑  收藏  举报