顾得泉:个人主页
个人专栏:《Linux操作系统》 《C++从入门到精通》 《LeedCode刷题》
键盘敲烂,年薪百万!
前言
上一篇预热文章简单说过我们的编写思路,首先要实现的是编译功能,那么在编译功能版块中,我们首先要考虑一些问题:
1.编译出来的临时文件我们应该放在哪里?
2.每次编译出来的临时文件名称重复如何解决?
3.编译正确与否的相关信息在哪显示?
注:博客中项目相关代码展示不完整!!!
一、comm模块
为了解决上述问题,我们首先列出一个公共模块,里面存放两个主要功能的文件:
1.<util.hpp>: 生成存放临时文件的路径,生成各个文件唯一的文件名
2.<log.hpp>: 生成开放式日志接口,生成编译相关信息
1.util.hpp
1.拼接成固定临时文件路径
我们希望用户代码及其编译的临时文件都存放在temp文件夹下,所以我们可以设计一下拼接成文件完整路径的方法,定义在
PathUtil类
中,它所提供的方法都设置为public的静态方法,供外部直接调用。
class PathUtil
{
public:
static std::string AddSuffix(const std::string &file_name, const std::string &suffix)
{
std::string path_name = temp_path;
path_name += file_name;
path_name += suffix;
return path_name;
}
//编译时需要的临时文件
//构建源文件路径+后缀的完整文件名
//1234 -> ./temp/1234.cpp
static std::string Src(const std::string &file_name)
{
return AddSuffix(file_name, ".cpp");
}
//构建可执行程序的完整路径+后缀名
static std::string Exe(const std::string &file_name)
{
return AddSuffix(file_name, ".exe");
}
static std::string CompilerError(const std::string &file_name)
{
return AddSuffix(file_name, ".compiler_error");
}
//运行时需要的临时文件
static std::string Stdin(const std::string &file_name)
{
return AddSuffix(file_name, ".stdin");
}
static std::string Stdout(const std::string &file_name)
{
return AddSuffix(file_name, ".stdout");
}
//构建该程序对应的标准错误完整的路径+后缀名
static std::string Stderr(const std::string &file_name)
{
return AddSuffix(file_name, ".stderr");
}
};
2.获取秒级时间戳
为了下面的日志功能做铺垫,因为我们希望打印出来的文件名各不相同。
class TimeUtil
{
public:
static std::string GetTimeStamp()
{
struct timeval _time;
gettimeofday(&_time, nullptr);
return std::to_string(_time.tv_sec);
}
//获得毫秒时间
static std::string GetTimeMs()
{
struct timeval _time;
gettimeofday(&_time, nullptr);
return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);
}
};
static std::string UniqFileName()
{
std::atomic_uint id(0);
id++;
//毫秒级时间戳+原子性递增唯一值:保证唯一性
std::string ms = TimeUtil::GetTimeMs();
std::string uniq_id = std::to_string(id);
return ms + "." + uniq_id;
}
2.log.hpp
常见日志等级包括:
DEBUG(调试):用于调试目的的详细日志信息,通常不在生产环境中启用。
INFO(信息):用于提供一般信息的日志,例如应用程序的启动、关闭、关键操作等。
WARNING(警告):用于表示可能的问题或潜在的错误,但不会导致应用程序的终止或异常。
ERROR(错误):用于表示错误或异常情况,可能会导致应用程序的中止或异常。
FATAL(致命错误):用于表示严重的错误或不可恢复的情况,可能会导致应用程序的崩溃或无法继续运行。
实现简单的日志接口:
// 日志等级
enum
{
INFO, //就是整数
DEBUG,
WARNING,
ERROR,
FATAL
};
inline std::ostream &Log(const std::string &level, const std::string &file_name, intline)
{
// 添加日志等级
std::string message = "[";
message += level;
message += "]";
// 添加报错文件名称
message += "[";
message += file_name;
message += "]";
// 添加报错行
message += "[";
message += std::to_string(line);
message += "]";
// 日志时间戳
message += "[";
message += TimeUtil::GetTimeStamp();
message += "]";
// cout 本质 内部是包含缓冲区的
std::cout << message; //不要endl进行刷新
return std::cout;
}
// LOG(INFo) << "message" << "\n";
// 开放式日志
#define LOG(level) Log(#level, __FILE__, __LINE__)
二、编译服务整体架构
这个模块只负责进行代码编译,意味着我们目前默认是能够接收远端提交的代码文件,并且这个文件对于我们编译模块来说是一个临时文件,编译成功后也会形成一个临时的可执行文件,这些文件的存放位置已经解决,那么现在就开启编译之路。
三、编译服务主体
现在我们要做的就是把用户提交的代码进行编译形成可执行程序,对结果并进行展示。程序运行无非就是三种结果:
1.代码跑完,结果正确
2.代码跑完,结果不正确
3.代码没跑完,发生异常了
static bool Compile(const std::string &file_name)
{
pid_t pid = fork();
if(pid < 0)
{
LOG(ERROR) << "内部错误,创建子进程失败" << "\n";
return false;
}
else if(pid == 0)
{
umask(0);
int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
if(_stderr < 0)
{
LOG(WARNING) << "没有成功形成stderr文件" << "\n";
exit(1);
}
//重定向标准错误到_stderr
dup2(_stderr, 2);
//程序替换,并不影响进程的文件描述符表
//子进程:调用编译器,完成对代码的编译工作
//g++ -o target src -std=c++11
execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),
PathUtil::Src(file_name).c_str(), "-std=c++11", nullptr/*不要忘记*/);
LOG(ERROR) << "启动编译器g++失败,可能是参数错误" << "\n";
exit(2);
}
else
{
waitpid(pid, nullptr, 0);
//编译是否成功,就看有没有形成对应的可执行程序
if(FileUtil::IsFileExists(PathUtil::Exe(file_name)))
{
LOG(INFO) << PathUtil::Src(file_name) << "编译成功" << "\n";
return true;
}
}
LOG(ERROR) << "编译失败,没有形成可执行程序" << "\n";
return false;
}
调用dup2系统调用的时机在调用execlp之前,也就是说我们在程序替换前完成了重定向,而重定向不会影响进程已经打开的文件,也就是不影响子进程现存的文件描述符表,那么g++编译的报错信息就合乎预期地打印到了我们重定向的文件中了。
四、功能测试加其他
1.mkaefile助手
compile_server:compile_server.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp -lpthread
.PHONY:clean
clean:
rm -f compile_server
2.测试
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
Server svr;
svr.Get("/hello",[](const Request &req, Response &resp)
{
//用来进行基本测试
resp.set_content("hello httplib,你好", "text/plain; charset=utf-8");
});
svr.Post("/compile_and_run", [](const Request &req, Response &resp)
{
//用户请求的服务正文是我们想要的json string
std::string in_json = req.body;
std::string out_json;
if(!in_json.empty())
{
CompileAndRun::Start(in_json, &out_json);
resp.set_content(out_json, "application/json;charset=utf-8");
}
});
svr.listen("0.0.0.0", atoi(argv[1]));
return 0;
}
测试过程:
测试结果:
结语:关于项目本次的这里就结束了,如果大家有什么问题,欢迎大家在评论区留言~~~