初级代码游戏的专栏介绍与文章目录-CSDN博客
我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。
这些代码大部分以Linux为目标但部分代码是纯C++的,可以在任何平台上使用。
系列入口:编程实战:自己编写HTTP服务器(系列1:概述和应答)-CSDN博客
本文介绍执行后台命令的shell.asp的实现。
目录
一、概述
二、主体代码
三、详解
3.1 参数
3.2 设置进程组和打开管道执行命令
3.3 读取数据和返回码处理
一、概述
这个功能就相当于一个终端,不过只能执行一个命令。有什么好处看自己,可以加入自己喜欢的特性。
入口:
别的不说了,主体代码是doPageShell()。
二、主体代码
主体代码如下:
bool doPageShell(){
#ifdef _MS_VCreturn true;
#elseFILE * fp;string changedir=m_request.GetParam("changedir");string curdir=m_request.GetParam("curdir");string cmd=m_request.GetParam("command");bool noform=(m_request.GetParam("noform")=="true");bool term=(m_request.GetParam("term")=="true");long bufsize=1024*1024;char * buf=new char[bufsize];if(NULL==buf){m_respond.AppendBody("<P><FONT color=RED>内存不足</FONT><P>");return true;}//切换路径if(0!=curdir.size()){if(0!=chdir(curdir.c_str())){m_respond.AppendBody("<P><FONT color=RED>设置初始路径出错</FONT><P>"+curdir+"<P>");return true;}}if(0!=changedir.size()){if(0!=chdir(changedir.c_str())){m_respond.AppendBody("<P><FONT color=RED>切换工作路径出错</FONT><P>"+changedir+"<P>");return true;}}//执行命令if(0==cmd.size()){m_respond.AppendBody("<P>空命令<P>");}else{if(0!=setpgid(getpid(),getpid())){m_respond.AppendBody("设置进程组ID出错<P>");}if(NULL==(fp=popen((cmd+" 2>&1").c_str(),"r"))){m_respond.AppendBody("<P><FONT color=RED>无法执行,原因:popen error</FONT><P>");if(!m_respond.Flush(m_s))return true;}else{int fd=fileno(fp);int flags;fd_set fdset;struct timeval tv;tv.tv_sec=300;tv.tv_usec=0;flags = fcntl(fd, F_GETFL, 0);flags |= O_NONBLOCK;fcntl(fd, F_SETFL, flags);char * tmpp;m_respond.AppendBody("开始执行 ");m_respond.AppendBody(CHtmlDoc::HTMLEncode(cmd));m_respond.Flush(m_s);m_respond.AppendBody("<HR></HR><CODE>");while(true){FD_ZERO(&fdset);FD_SET(fd,&fdset);
#ifdef _HPOSint selectret=select(fd+1,(int *)&fdset,NULL,NULL,&tv);
#elseint selectret=select(fd+1,&fdset,NULL,NULL,&tv);
#endifif(selectret<0){LOG<<"select error"<<ENDE;}else if(0==selectret){//超时没有数据m_respond.AppendBody("注意,长时间没有收到输出.");if(!m_respond.Flush(m_s)){LOG<<"发送失败,客户端已断开,直接退出"<<ENDI;if(term){if(0!=kill(0,SIGTERM)){LOG<<"发送停止信号出错,shell会持续执行到命令结束"<<ENDE;}}return true;}continue;}else{}bool fileend=false;while(true){tmpp=fgets(buf,int(bufsize-1),fp);if(NULL==tmpp){//正常结束if(0!=feof(fp)){fileend=true;break;}if(EWOULDBLOCK==errno || EAGAIN==errno){//无数据break;}else{//出错结束m_respond.AppendBody("<P><FONT color=RED>执行出错,原因:read error</FONT><P>");m_respond.AppendBody(strerror(errno));m_respond.Flush(m_s);fileend=true;break;}}else{m_respond.AppendBody(LogToHtml(buf,false,false));m_respond.AppendBodyHtmlScroll();if(!m_respond.Flush(m_s)){LOG<<"发送失败,客户端已断开,直接退出"<<ENDI;if(term){if(0!=kill(0,SIGTERM)){LOG<<"发送停止信号出错,shell会持续执行到命令结束"<<ENDE;}}return true;}}}if(fileend)break;}m_respond.AppendBody("</CODE><HR></HR>");int ret=pclose(fp);if(0!=ret){if(WIFEXITED(ret)){sprintf(buf,"<FONT color=RED>执行完毕,返回代码 %d 。</FONT><BR>",WEXITSTATUS(ret));//(0xFF00&ret)/256);}else if(WIFSIGNALED(ret)){sprintf(buf,"<FONT color=RED>被信号终止,信号 %d 。</FONT><BR>",WTERMSIG(ret));}else if(WCOREDUMP(ret)){sprintf(buf,"<FONT color=RED>执行失败,COREDUMP。</FONT><BR>");}else{sprintf(buf,"<FONT color=RED>未知的返回值:%d。</FONT><BR>",ret);}}else sprintf(buf,"执行完毕,返回代码 %d 。<BR>",ret);m_respond.AppendBody(buf);}}if(!noform){char cwd[1024];if(NULL!=getcwd(cwd,1024)){sprintf(buf,"<FORM ACTION=\"/shell.asp\" METHOD=\"GET\" >\n""当前路径:%s<BR>""<INPUT TYPE=\"hidden\" NAME=\"curdir\" VALUE=\"%s\" >\n""切换路径到:<INPUT TYPE=\"text\" SIZE=\"30\" NAME=\"changedir\" ><BR>\n""Shell命令: <INPUT TYPE=\"text\" SIZE=\"30\" NAME=\"command\" VALUE=\"%s\">\n""<INPUT TYPE=SUBMIT VALUE=\"执行\" >\n""</FORM>\n",cwd,cwd,cmd.c_str());}else{sprintf(buf,"获取当前工作路径出错");}m_respond.AppendBody(buf);}delete[] buf;return true;
#endif}
三、详解
3.1 参数
最关键命令参数:command,包含要执行的命令,可以是一串命令的组合,也就是你能输到控制台运行的东西都行。
运行命令一定需要工作目录,显然不能在服务进程的工作目录下执行,谁知道会发生什么呢。目录用curdir和changedir参数来控制,如果curdir是当前工作目录,具体就是这个页面执行命令的工作目录,changedir用于切换目录,可以是相对目录。执行时首先将目录切换到curdir(因为服务进程有自己的工作目录,和期待的执行命令的工作目录不同),然后再切换到changedir,其实就是执行两次chdir()。
代码如下:
//切换路径if(0!=curdir.size()){if(0!=chdir(curdir.c_str())){m_respond.AppendBody("<P><FONT color=RED>设置初始路径出错</FONT><P>"+curdir+"<P>");return true;}}if(0!=changedir.size()){if(0!=chdir(changedir.c_str())){m_respond.AppendBody("<P><FONT color=RED>切换工作路径出错</FONT><P>"+changedir+"<P>");return true;}}
3.2 设置进程组和打开管道执行命令
启动新进程一般都要设置点东西,解除新进程和服务进程的关系,主要是进程间使用进程组广播信号的问题,新进程压根不应该知道服务进程存在。
popen()很实用的运行命令并获取输出的方法,其内部会打开单向管道,运行命令,返回输出(文件句柄)。
为了能看到命令的所有输出,我们需要在命令后面追加“ 2>&1”将标准出错重定向到标准输出。
相关代码如下:
if(0!=setpgid(getpid(),getpid())){m_respond.AppendBody("设置进程组ID出错<P>");}if(NULL==(fp=popen((cmd+" 2>&1").c_str(),"r"))){m_respond.AppendBody("<P><FONT color=RED>无法执行,原因:popen error</FONT><P>");if(!m_respond.Flush(m_s))return true;}else{
后面就是常规的从文件描述符读取数据直到结束。当然里面混合了格式化、发送,以及命令返回码的处理。
3.3 读取数据和返回码处理
select可以检查是否有数据,这个函数一般用于socket,但是其实是针对文件描述符的。
用fgets()逐行读取数据,如果出错errno会有各种结果,需要分别判断。
pclose获得返回码,具体分析和进程返回码是一样的。
(这里是结束,但不是整个系列的结束)