【實(shí)戰(zhàn)指南】從零構(gòu)建嵌入式遠(yuǎn)程Shell,提升跨地域協(xié)作效率(2)
[TOC]
引言
? 之前曾發(fā)布過(guò)一篇關(guān)于構(gòu)建嵌入式遠(yuǎn)程Shell的文章,詳細(xì)介紹了基礎(chǔ)版本的實(shí)現(xiàn)方法,詳見(jiàn)【實(shí)戰(zhàn)指南】從零構(gòu)建嵌入式遠(yuǎn)程Shell。然而,該版本在功能性和穩(wěn)定性方面還有待提升。本文將在此基礎(chǔ)上進(jìn)行改進(jìn)和優(yōu)化,進(jìn)一步完善遠(yuǎn)程Shell的功能。
概述
? 本次改進(jìn)主要集中在模塊化設(shè)計(jì)與功能增強(qiáng)上。模塊化方面,我們將系統(tǒng)拆分為LogManager和ShellEnv兩個(gè)獨(dú)立的類(lèi),以提高代碼的可維護(hù)性和復(fù)用性。在功能上,新增了對(duì)阻塞命令的支持以及提供用戶(hù)主動(dòng)結(jié)束Shell進(jìn)程的能力等。
優(yōu)化策略
? 在第一版代碼中,由于功能較為簡(jiǎn)單,未采用模塊化設(shè)計(jì);同時(shí),其實(shí)現(xiàn)也相對(duì)單一,僅支持非阻塞命令的執(zhí)行?;诖?,第二版實(shí)現(xiàn)中增加了以下功能:
- 將代碼按功能拆分為
LogManager和ShellEnv兩個(gè)模塊。前者專(zhuān)注于遠(yuǎn)程交互,后者負(fù)責(zé)shell命令的執(zhí)行。 - 支持非阻塞及阻塞命令的執(zhí)行,并能準(zhǔn)確回傳命令回執(zhí)打印。
- 增加交互日志,以便更好地追蹤和記錄操作過(guò)程。
詳細(xì)設(shè)計(jì)
? 將遠(yuǎn)程Shell拆解為LogManager和ShellEnv兩大模塊。其中,LogManager負(fù)責(zé)與遠(yuǎn)端的連接、shell進(jìn)程管理等;而ShellEnv則專(zhuān)注于Shell命令的執(zhí)行及其回執(zhí)輸出。
-
LogManager
LogManager主要負(fù)責(zé)與遠(yuǎn)程端口的連接以及 shell 進(jìn)程的管理功能。具體職責(zé)包括:① 建立TCP連接。目前僅支持作為T(mén)CP服務(wù)端運(yùn)行,允許遠(yuǎn)程客戶(hù)端以客戶(hù)端模式接入并輸入指令。
② 解析遠(yuǎn)程指令。接收來(lái)自遠(yuǎn)程客戶(hù)端的指令字節(jié)流,解析并判斷其是否屬于內(nèi)建指令。
③ 增加內(nèi)建指令集。增加一系列與RShellX使用相關(guān)的內(nèi)建指令,提升工具的易用性和功能性。
④ 觸發(fā)指令執(zhí)行。對(duì)于內(nèi)建指令直接進(jìn)行處理;而對(duì)于其他外部命令,則交由ShellEnv模塊執(zhí)行。
⑤ 回收Shell進(jìn)程。通過(guò)注冊(cè)SIGCHLD信號(hào)處理函數(shù),自動(dòng)回收已完成任務(wù)的Shell子進(jìn)程,確保系統(tǒng)資源的有效管理。
class LoginManager
{
public:
LoginManager();
~LoginManager();
static LoginManager* GetInstance();
int Init();
int BuildConnectAsTcpServer(short port);
int ConnectLoop();
private:
int Usage();
int ExitShell();
int ExitAll();
int WriteStdin(const std::string& buf);
int RegisterSignal();
int ListenPipeEvent(int pipeFd);
int ExecuteCmd(std::string& cmdBytes);
// int Login(const char* username, const char* password);
// int Logout();
private:
bool mIsLogin;
pid_t mCurPid;
static pid_t mShellPid;
int mStdin;
int mStdout;
int mStderr;
int mInPipe[2];
int mOutPipe[2];
std::shared_ptr<PPipe> mPipePtr;
std::shared_ptr<PSocket> mTcpSrvPtr;
std::list<std::shared_ptr<PSocket>> mTcpClients;
};
從 LogManager 的接口設(shè)計(jì),亦可以非常直觀地理解其承擔(dān)的主要職責(zé)。其中,一些細(xì)節(jié)的實(shí)現(xiàn)如下記錄:
-
BuildConnectAsTcpServer
當(dāng)接收到客戶(hù)端成功接入時(shí),需如下處理:
① 將服務(wù)端socket文件描述符fd,通過(guò)dup2重定向至標(biāo)準(zhǔn)輸入(stdin),確保從客戶(hù)端接收的數(shù)據(jù)可以直接作為輸入被處理。
② 將客戶(hù)端socket文件描述符fd,通過(guò)dup2重定向至標(biāo)準(zhǔn)輸出(stdout),確保終端回執(zhí)能夠直接傳輸至客戶(hù)端。
③ 向客戶(hù)端發(fā)送接入歡迎應(yīng)答。
int LoginManager::BuildConnectAsTcpServer(short port)
{
auto pEpoll = EpollEventHandler::GetInstance();
mTcpSrvPtr = make_shared<PSocket>(AF_INET, SOCK_STREAM, 0, [&](int cli, void *arg) {
PSocket* pSrvObj = (PSocket*)arg;
if (pSrvObj == nullptr) {
SPR_LOGE("PSocket is nullptr\n");
return;
}
auto tcpClient = make_shared<PSocket>(cli, [&](int sock, void *arg) {
PSocket* pCliObj = (PSocket*)arg;
if (pCliObj == nullptr) {
SPR_LOGE("PSocket is nullptr\n");
return;
}
std::string rBuf;
int rc = pCliObj->Read(sock, rBuf);
if (rc <= 0) {
mTcpClients.remove_if([sock, pEpoll, pCliObj](shared_ptr<PSocket>& v) {
pEpoll->DelPoll(pCliObj);
return (v->GetEpollFd() == sock);
});
return;
}
// SPR_LOGD("# RECV [%d]> %s shellpid = %d\n", sock, rBuf.c_str(), mShellPid);
ExecuteCmd(rBuf);
});
tcpClient->AsTcpClient();
pEpoll->AddPoll(tcpClient.get());
mTcpClients.push_back(tcpClient);
dup2(mTcpSrvPtr->GetEpollFd(), STDIN_FILENO);
dup2(tcpClient->GetEpollFd(), STDOUT_FILENO);
dup2(tcpClient->GetEpollFd(), STDERR_FILENO);
const string welcomes = "Welcome to RShellX! >_<\n";
if (write(STDOUT_FILENO, welcomes.c_str(), welcomes.size()) < 0) {
SPR_LOGE("# Write welcome failed! %s", strerror(errno));
}
});
mTcpSrvPtr->AsTcpServer(port, 5);
pEpoll->AddPoll(mTcpSrvPtr.get());
return 0;
}
-
ExecuteCmd
當(dāng)接收到客戶(hù)端發(fā)送來(lái)的字符串時(shí),需如下處理:
① 去除多余"\r\n"。確保指令格式正常。
② 判斷是否為內(nèi)建指令。是內(nèi)建指令直接處理并返回處理回執(zhí)。
③ 不為內(nèi)建指令。交由ShellEnv處理。
int LoginManager::ExecuteCmd(string& cmdBytes)
{
cmdBytes.erase(std::find_if(cmdBytes.rbegin(), cmdBytes.rend(), [](unsigned char ch) {
return ch != '\r' && ch != '\n';
}).base(), cmdBytes.end());
if (cmdBytes == "Quit" || cmdBytes == "Ctrl C" || cmdBytes == "Ctrl Q") {
return ExitShell();
} else if (cmdBytes == "Quit all") {
return ExitAll();
} else if (cmdBytes == "Help" || cmdBytes == "help" || cmdBytes == "?") {
return Usage();
}
// 上一個(gè)命令未執(zhí)行完,輸入作為參數(shù)傳入上一個(gè)命令
if (mShellPid > 0) {
SPR_LOGD("Last shell %d is running, send [%s] as parameter\n", mShellPid, cmdBytes.c_str());
// int rc = write(mInPipe[1], cmdBytes.c_str(), cmdBytes.size());
// if (rc > 0) {
// SPR_LOGD("# SEND [%d]> %s\n", mInPipe[1], cmdBytes.c_str());
// }
return 0;
}
ShellEnv shellEnv(mInPipe[0], mOutPipe[1], mOutPipe[1]);
mShellPid = shellEnv.Execute(cmdBytes);
return 0;
}
- RegisterSignal
通過(guò)監(jiān)聽(tīng)SIGCHLD, 及時(shí)回收已經(jīng)結(jié)束的子進(jìn)程資源。
int LoginManager::RegisterSignal()
{
signal(SIGCHLD, [](int) {
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
mShellPid = -1;
SPR_LOGD("Shell [%d] exit!\n", pid);
}
});
return 0;
}
-
ShellEnv
ShellEnv的功能比較簡(jiǎn)單,即創(chuàng)建子進(jìn)程,并執(zhí)行shell指令。
class ShellEnv
{
public:
explicit ShellEnv(int inFd, int outFd, int errFd);
~ShellEnv();
int Execute(const std::string& cmd);
};
ShellEnv 中的 Execute 方法用于執(zhí)行 shell 命令,其實(shí)現(xiàn)如下:
- Execute
① 解析命令字符串:首先查找命令字符串中的空格位置,分離出命令名稱(chēng)和參數(shù)。然后將整個(gè)命令字符串按空格分割成多個(gè)參數(shù),存儲(chǔ)在一個(gè)std::vector<std::string>中。
② 創(chuàng)建子進(jìn)程:通過(guò)fork()創(chuàng)建一個(gè)子進(jìn)程。在子進(jìn)程中,使用execvp函數(shù)執(zhí)行指定的shell命令。execvp會(huì)接管子進(jìn)程,用新的程序替換當(dāng)前進(jìn)程的鏡像。
③ 錯(cuò)誤處理:如果execvp執(zhí)行失敗,會(huì)記錄錯(cuò)誤信息并通過(guò)_exit退出子進(jìn)程,返回一個(gè)失敗狀態(tài)碼。
④ 父進(jìn)程記錄:父進(jìn)程記錄子進(jìn)程的PID和執(zhí)行的命令,便于后續(xù)管理和日志記錄。
int ShellEnv::Execute(const std::string& cmd)
{
size_t pos = cmd.find(' ');
std::string cmdName = cmd.substr(0, pos);
std::vector<std::string> tmpArgs = GeneralUtils::Split(cmd, ' ');
char* args[tmpArgs.size() + 1];
args[tmpArgs.size()] = nullptr;
for (size_t i = 0; i < tmpArgs.size(); i++) {
args[i] = const_cast<char*>(tmpArgs[i].c_str());
}
pid_t pid = fork();
if (pid == 0) {
int rc = execvp(cmdName.c_str(), args);
if (rc < 0) {
SPR_LOGE("execlp failed: %s\n", strerror(errno));
}
_exit(EXIT_FAILURE);
} else {
SPR_LOGD("ppid = %d, cpid = %d cmd: %s\n", getpid(), pid, cmd.c_str());
}
return pid;
}
驗(yàn)證
準(zhǔn)備三個(gè)終端輸入窗口,方便調(diào)試驗(yàn)證。
-
終端1
啟動(dòng)RShellX程序
$ ./rshellx 8080
-
終端2
Windows平臺(tái)使用網(wǎng)絡(luò)助手創(chuàng)建tcp客戶(hù)端連接RShellX,并輸入Shell命令驗(yàn)證反饋。
① 建立tcp客戶(hù)端,連接RShellX。
② 輸入touch test.log,創(chuàng)建文件test.log。
③ 輸入tail -f test.log,并執(zhí)行終端③行為(向test.log寫(xiě)入數(shù)據(jù)),觀察當(dāng)前終端輸出。
④ 輸入Quit,結(jié)束tail子進(jìn)程。輸入lsb_release -a,觀察輸出。

-
終端3
實(shí)時(shí)向test.log尾部實(shí)時(shí)增加內(nèi)容,觀察終端2打印情況。
$ echo "Hello" >> test.log
$ echo "I'm kangkang" >> test.log
$ echo "What's your name?" >> test.log
-
結(jié)論
通過(guò)上述驗(yàn)證,確認(rèn):
①TCP客戶(hù)端能夠正常使用阻塞命令tail。
② 輸入Quit結(jié)束tail阻塞子進(jìn)程后。輸入lsb_release -a,能夠正常執(zhí)行。
總結(jié)
- 本次改進(jìn)的主要特點(diǎn)是將之前的“單進(jìn)程 +
popen” 實(shí)現(xiàn)方式,優(yōu)化為“多進(jìn)程 +execvp”的方式。多進(jìn)程模式便于管理shell執(zhí)行的子進(jìn)程,而execvp則簡(jiǎn)化了命令輸出的重定向。 - 實(shí)現(xiàn)后,即使在非
Linux平臺(tái)一樣能夠使用Shell指令調(diào)試目標(biāo)設(shè)備,還是比較方便的。 - 當(dāng)前實(shí)現(xiàn)仍存在許多優(yōu)化空間:
例如不能與命令交互,像gdb這種運(yùn)行過(guò)程需要終端輸入的,目前還不能支持;
shell回顯的方式可以?xún)?yōu)化為與shell一致,增加當(dāng)前權(quán)限和路徑;
另外,向cd這類(lèi)非可執(zhí)行文件的命令,目前也無(wú)法支持。諸如此類(lèi)問(wèn)題,后續(xù)版本慢慢改進(jìn)。