【實(shí)戰(zhàn)指南】從零構(gòu)建嵌入式遠(yuǎn)程Shell,提升跨地域協(xié)作效率(2)

【實(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)拆分為LogManagerShellEnv兩個(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)中增加了以下功能:

  • 將代碼按功能拆分為LogManagerShellEnv兩個(gè)模塊。前者專(zhuān)注于遠(yuǎn)程交互,后者負(fù)責(zé)shell命令的執(zhí)行。
  • 支持非阻塞及阻塞命令的執(zhí)行,并能準(zhǔn)確回傳命令回執(zhí)打印。
  • 增加交互日志,以便更好地追蹤和記錄操作過(guò)程。

詳細(xì)設(shè)計(jì)

? 將遠(yuǎn)程Shell拆解為LogManagerShellEnv兩大模塊。其中,LogManager負(fù)責(zé)與遠(yuǎn)端的連接、shell進(jìn)程管理等;而ShellEnv則專(zhuān)注于Shell命令的執(zhí)行及其回執(zhí)輸出。

  1. 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;
}
  1. 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,觀察輸出。
tcp客戶(hù)端
  • 終端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)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容