一.前言
前面两天完成了编译和运行两个子模块,今天的任务是完成CompileRun模块,它的任务如下:
- 解析来自客户端的Json字符串,反序列化提取编译运行需要的数据,即代码,时间限制和空间限制
- 把代码写入临时文件,形成编译的源文件
- 调用编译和运行模块,把各字段序列化构建Json字符串,返回给外部构建response的body部分
- 最后删除编译运行期间形成的临时文件
另外,在CompileServer.cpp文件将整套编译运行服务打包成网络服务,具体来说:
使用httplib库,使用post方法注册回调函数。当接收到客户端编译服务的请求时,会调用回调函数,request中的Json字符串交给CompileRun模块,得到Json字符串,用它构建response的body部分,然后返回给客户端
整个编译运行服务的逻辑如下:
二.设计思路
CompileRun:
首先对传入的inJson反序列化,提取出代码code,输入input,时间限制cpuLimit,空间限制memLimit。
然后在工具模块实现一个形成独一无二,不会产生冲突的文件名方法,可采用,毫秒级时间戳和原子级计数器组合来形成文件名。
接着将代码写入到该文件中,随后调用编译模块和运行模块。
如果编译报错,则填写状态码字段status和原因reason,直接把outJson返回;;如果由于其它原因导致失败,如打开文件,程序替换等,则是我们服务端内部出现问题,与用户无关,则填写status和reason,直接返回outJson;如果编译运行成功,则除了填写以上两个字段,还有标准输出stdout和标准错误stderr,然后返回Json字符串
最后还要使用unlink接口,将编译运行形成的.cpp,.compile_error, .exe,.stdin, .stdout, .stderr临时文件全部清空
三.接口设计
参数:
1. const std::string &inJson, 输入型参数,来自客户端的Json字符串,包含以下字段:
* 1.code:用户提交的代码
* 2.input:用户提交的输入,不做处理
* 3.cpuLimit:CPU限制时间(s)
* 4.memLimit:虚拟内存限制大小(KB)
2. std::string *outJson, 输出型参数,将来发送给客户端的Json字符串,包含以下字段:
* 必填:
* 1.status:状态码
* 0:运行成功 -1:代码为空 -2:编译错误 -3:未知错误 >0:收到信号异常终止
* 2.reason:请求结果
* 选填:
* 3.stdout:程序输出运行结果
* 4.stderr:错误结果
四.代码实现
CompileRun.hpp:
#pragma once
#include <jsoncpp/json/json.h>
#include <string>
#include "Compiler.hpp"
#include "Runner.hpp"
#include "../Common/Log.hpp"
#include "../Common/Util.hpp"
namespace ns_compile_run
{
using namespace ns_runner;
using namespace ns_complier;
using namespace ns_util;
using namespace httplib;
class CompileRun
{
public:
/**********************
* 参数:
* 1.inJson:通过http来自client的json字符串
* 2.outJson:输出型参数,将来要发送给client
*
* inJson字段:
* 1.code:用户提交的代码
* 2.input:用户给自己提交代码对应的输入,不做处理
* 3.cpuLimit:CPU限制时间(s)
* 4.memLimit:虚拟内存限制大小(KB)
*
* outJson字段:
* 必填:
* 1.status:状态码
* 0:运行成功 -1:代码为空 -2:编译错误 -3:未知错误 >0:收到信号异常终止
* 2.reason:请求结果
* 选填:
* 3.stdout:程序输出运行结果
* 4.stderr:错误结果
* *******************/
static void start(const std::string &inJson, std::string *outJson)
{
// 反序列化
Json::Value inValue;
Json::Reader reader;
reader.parse(inJson, inValue);
std::string code = inValue["code"].asString();
std::string input = inValue["input"].asString();
int cpuLimit = inValue["cpuLimit"].asInt();
int memLimit = inValue["memLimit"].asInt();
int status = 0;
int runRet = 0;
std::string fileName;
if (code.size() == 0)
{
status = -1; // 代码为空
goto END;
}
fileName = FileUtil::uniqFileName(); // 形成唯一文件名
if (!FileUtil::writeFile(PathUtil::src(fileName), code)) // 形成临时源文件
{
status = -3; // 未知错误
goto END;
}
if (!Compiler::compile(fileName))
{
status = -2; // 编译错误
goto END;
}
runRet = Runner::run(fileName, cpuLimit, memLimit);
if (runRet < 0)
{
status = -3; // 未知错误
}
else if (runRet > 0)
{
status = runRet; // 异常终止
}
else
{
status = 0; // 运行成功
}
END:
Json::Value outValue;
outValue["status"] = status;
outValue["reason"] = statusToDesc(status, fileName);
if (status == 0) // 编译运行成功
{
std::string _stderr;
std::string _stdout;
FileUtil::readFile(PathUtil::stderr(fileName), &_stderr, true);
FileUtil::readFile(PathUtil::stdout(fileName), &_stdout, true);
outValue["stderr"] = _stderr;
outValue["stdout"] = _stdout;
}
Json::StyledWriter writer;
*outJson = writer.write(outValue);
//removeTmpFiles(fileName);
}
/***************************
* 功能:根据状态码返回相应的reason
* ************************/
static std::string statusToDesc(int status, const std::string &fileName)
{
std::string desc;
switch (status)
{
case 0:
desc = "编译运行运行成功";
break;
case -1:
desc = "代码为空";
break;
case -2:
FileUtil::readFile(PathUtil::complieError(fileName), &desc, true);
break;
case -3:
desc = "未知错误";
break;
case SIGFPE: // 8
desc = "浮点错误";
break;
case SIGXCPU: // 24
desc = "运行超时";
break;
case SIGABRT: // 6
desc = "内存超出限制";
break;
default:
desc = "未知";
break;
}
return desc;
}
/************************
* 功能:删除编译运行形成的临时文件
* 最多有:.cpp, .compile_error, .exe, .stdin, .stdout, .stderr
* ******************/
static void removeTmpFiles(const std::string &fileName)
{
std::string _src = PathUtil::src(fileName);
std::string _compileError = PathUtil::complieError(fileName);
std::string _exe = PathUtil::exe(fileName);
std::string _stdin = PathUtil::stdin(fileName);
std::string _stdout = PathUtil::stdout(fileName);
std::string _stderr = PathUtil::stderr(fileName);
if (FileUtil::isFileExists(_src))
{
unlink(_src.c_str());
}
if (FileUtil::isFileExists(_compileError))
{
unlink(_compileError.c_str());
}
if (FileUtil::isFileExists(_exe))
{
unlink(_exe.c_str());
}
if (FileUtil::isFileExists(_stdin))
{
unlink(_stdin.c_str());
}
if (FileUtil::isFileExists(_stdout))
{
unlink(_stdout.c_str());
}
if (FileUtil::isFileExists(_stderr))
{
unlink(_stderr.c_str());
}
}
};
}
工具模块用到的一些方法:
#pragma once
#include <string>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/time.h>
#include <atomic>
#include <fstream>
namespace ns_util
{
class TimeUtil
{
public:
/**********
* 功能:当前毫秒级时间
* ********/
static std::string getTimeMs()
{
struct timeval time;
gettimeofday(&time, nullptr);
return std::to_string(time.tv_sec + time.tv_usec / 1000);
}
};
class FileUtil
{
public:
/***************
* 功能:判定文件是否存在
* 参数:pathName是完整文件名
* 如1234-> ./tmp/1234.stderr
***************/
static bool isFileExists(const std::string &pathName)
{
struct stat st;
if (stat(pathName.c_str(), &st) == 0)
{
// 获取属性成功,说明文件存在
return true;
}
return false;
}
/************************
* 参数:
* 1.target:带路径和后缀的完整文件名
* 2.content:要写入的内容
* *********************/
static bool writeFile(const std::string& target, const std::string& content)
{
std::ofstream out(target.c_str());
if (!out.is_open())
{
return false;
}
out.write(content.c_str(), content.size());
return true;
}
/************************
* 参数:
* 1.target:带路径和后缀的完整文件名
* 2.content:输入型参数,把文件内容写到它里面
* 3.keep:是否保留'\n'(getline不会读取换行符)
* *********************/
static bool readFile(const std::string& target, std::string* content, bool keep = false)
{
std::ifstream in(target.c_str());
if (!in.is_open())
{
return false;
}
std::string line;
while (std::getline(in, line))
{
line += keep ? "\n" : "";
(*content) += line;
}
return true;
}
/*****************
* 功能:用时间和原子计数器生成一个独一无二,不产生冲突的文件名
* ******************/
static std::string uniqFileName()
{
static std::atomic<int> id(0);
std::string fileName = TimeUtil::getTimeMs();
fileName += "_";
fileName += to_string(id);
id++;
return fileName;
}
};
};
CompileServer.cpp:
#include <iostream>
#include <string>
#include <cstdlib>
#include "../Common/httplib.h"
#include "CompileRun.hpp"
#include "../Common/Util.hpp"
#include "../Common/Log.hpp"
using namespace ns_compile_run;
using namespace httplib;
using namespace ns_log;
void Usage(const char* proc)
{
std::cout << proc << "serverPort" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
Server svr;
svr.Post("/compile_and_run", [](const Request& req, Response& resp)
{
std::string inJson = req.body;
if (!inJson.empty())
{
std::string outJson;
CompileRun::start(inJson, &outJson);
resp.set_content(outJson, "application/json;charset=utf-8");
}
}); //注册回调方法
svr.listen("0.0.0.0", atoi(argv[1])); //启动网络服务
return 0;
}
五.备注
- httplib是一个只需要包含头文件,而无需安装动态库的“only header”库,它的方法定义都在httplib.h中了,我们只需将它拷贝到项目目录下即可包含使用
- 要使用httplib,必须使用高版本gcc编译器(7,8,9),否则编译或者运行时会出现问题
- 想要对编译运行模块测试,可以使用postman工具向服务端发送request