rust实现远程shell

最近在尝试使用bcachefs作为nas的存储管理,但又不喜欢命令行的方式,受unraid的启发,写了个简单的 通过网页管理的服务器+客户端,这部分折腾到一个程度会放出来。目前使用的rust实现的服务器端,rust.js 实现的前端页面。使用rust语言实现完全就是一个实验尝试,想看下rust到底走到了那一步,开发是否方便。 总之,一言难尽,说多了都是泪。

先说今天的主题,前端页面做了基本的框架后,我想通过网页来远程管理系统,前端相对而言好办,使用xterm.js, 网上也可以找到各种使用教程,各种参考,但如何将远程的控制台放到网页中,相关方面熟悉的朋友可能觉得很 简单,也知道方向,但我无论哪方面都是半吊子水平,于是就求助了chatgpt,然后chatgpt给我推荐了pty, 并且,热心的给我用rust实现了代码,在接下来的两天里,我就不断的调试chatgpt的各种代码,问chatgpt 的各种问题,chatgpt也完美的给出了鸡生蛋,蛋孵鸡的完美大循环答案。最后查看C的openpty等相关接口, 以及结合相关demo,了解了pty的相关流程,然后配合chatpgpt的rust代码,实现了一个websocket版本的远程控制台。

下面是枯燥的源代码环节,已经尽可能详细的注释了。

use nix::pty::{posix_openpt, grantpt, unlockpt, ptsname};
use nix::unistd::{fork, ForkResult, setsid, close, dup2};
use std::ffi::CStr;
use std::io::Write;
use std::io::Read;
use std::fs::File;
use std::os::fd::FromRawFd;
use std::os::unix::io::AsRawFd;
use nix::fcntl::{OFlag};
use futures_util::{StreamExt, SinkExt};
use tokio_tungstenite::tungstenite::protocol::Message;
use tokio_tungstenite::tungstenite::Error;
use hyper_tungstenite::{HyperWebsocket};
use hyper::{Body, Request, Response};
use std::convert::Infallible;
use nix::sys::signal::{kill, Signal};
use nix::sys::wait::{waitpid, WaitStatus};

pub async fn ws_handler(mut req: Request<Body>) -> Result<Response<Body>, Infallible> {
    if hyper_tungstenite::is_upgrade_request(&req) {
        let Ok((response, websocket)) = hyper_tungstenite::upgrade(&mut req, None) else {todo!()};
        tokio::spawn(async move {
             let _ = handle_xterm_session(websocket) .await;
        });
        return Ok(response)
    }
    // Return a 400 Bad Request response for non-WebSocket requests.
    let mut response = Response::new(Body::empty());
    *response.status_mut() = hyper::StatusCode::BAD_REQUEST;
    Ok(response)
}

async fn handle_xterm_session(websocket: HyperWebsocket) -> Result<(), Error> {
    let Ok(websocket) = websocket.await else { todo!() };
    let mut master_fd = posix_openpt(nix::fcntl::OFlag::O_RDWR).expect("error posix_openpt");

    // Grant access to the slave PTY
    grantpt(&master_fd).expect("error grantpt");
    unlockpt(&master_fd).expect("error unlockpt");

    // Get the name of the slave PTY
    let slave_name = unsafe {
        let cstr = ptsname(&master_fd).expect("error ptsname");
        CStr::from_ptr(cstr.as_ptr() as *const i8).to_str().expect("error cstr::from_ptr")
    };
    // Get the name of the slave PTY
    // Fork a child process
     match unsafe{fork()} {
         Ok(ForkResult::Parent { child, .. }) => {
             // This is the parent process
             // Perform any parent-specific tasks here
             //println!("Parent process ID: {}", nix::unistd::getpid());
             //println!("Child process ID: {}", child);
             // Close the master PTY file descriptor in the parent process
             let (mut ws_writer, mut ws_reader) = websocket.split();
             let mut input = unsafe { File::from_raw_fd(master_fd.as_raw_fd()) };
             tokio::spawn(async move {
                 let mut buf = vec![0; 1024];
                 loop {
                     match input.read(&mut buf) {
                         Err(err) => {
                             println!("line: {} Error reading from stream: {}, read={:?}", line!(), err,input);
                             if let Err(_e) = ws_writer.send(Message::Close(None)).await {
                             }
                             break;
                         }
                         Ok(0) => {
                             println!("eof===");
                             break; // EOF from PTY
                         }
                         Ok(n) => {
                             if let Err(e) = ws_writer.send(Message::Text(String::from_utf8_lossy(&buf[..n]).to_string())).await {
                                 println!("send remote shell connection closed. error:{:?}", e);
                                 break;
                             }
                         }
                     }
                 }
                 println!("end pty to wsocket");
             });
             //let mut output = unsafe { File::from_raw_fd(master_fd.as_raw_fd()) };
             while let Some(message) = ws_reader.next().await {
                 match message {
                     Ok(Message::Close(_)) => {
                         println!("WebSocket connection closed.");
                         break;
                     }
                     Ok(Message::Text(message)) => {
                         let _ = master_fd.write_all(message.as_bytes());
                     }
                     Err(e) => {
                         println!("shell write error:{:?}", e);
                         break;
                     }
                     _ => todo!()
                 }
             }
             // 关闭WebSocket后发送SIGTERM信号给子进程
             if let Err(e) = kill(child, Signal::SIGTERM) {
                 eprintln!("Error sending SIGTERM to child process: {}", e);
             }

             // 等待子进程退出
             match waitpid(child, None).expect("waitpid failed") {
                 WaitStatus::Exited(_, _) => {
                     println!("Child process has exited");
                 }
                 _ => {
                     println!("Child process did not exit as expected");
                 }
             }
             println!("end websocket");
             close(master_fd.as_raw_fd()).expect("close master_fd error");
         }
        Ok(ForkResult::Child) => {
            // This is the child process
            // Perform any child-specific tasks here
            println!("Child process ID: {}", nix::unistd::getpid());

            // Create a new session and make the child process the session leader
            setsid().expect("setsid failed");

            // Open the slave PTY and associate it with standard input, output, and error
            let slave_fd = nix::fcntl::open(slave_name, OFlag::O_RDWR, nix::sys::stat::Mode::empty())
                .expect("open slave PTY failed");
            // Duplicate the slave PTY file descriptor to standard input, output, and error
            dup2(slave_fd, 0).expect("dup2 for stdin failed");
            dup2(slave_fd, 1).expect("dup2 for stdout failed");
            dup2(slave_fd, 2).expect("dup2 for stderr failed");

            // Close the original slave PTY file descriptor
            close(slave_fd).expect("close slave_fd in child process failed");

            // Now the child process is set up with the PTY
            // You can execute shell or other programs here
            let shell_cmd = std::process::Command::new("/bin/sh").spawn();
            shell_cmd?.wait().expect("error wait");
            // Exit the child process
            println!("exit child");
            std::process::exit(0);
        }
        Err(e) => {
            eprintln!("Fork failed: {}", e);
        }
    }

    Ok(())
}

使用方式:

let ws_route = Router::builder()
        .any_method("/shell", remoteshell::ws_handler)
        .build()
        .unwrap();
Router::builder()
        .scope("/api/ws", ws_route)

xterm.js通过websocket连接/api/ws/shell即可。如下图: rust_remote_shell.png

rust吐槽时间:

我不太清楚各个大牛是如何记住rust的各种语法糖和各种扩展类型的,我大部分时间都在找某个类型是否 具有某个扩展,以及这个扩展是否能转换为其他的扩展。动不动就要看各个库的源代码实现,然后就看到 各种花式的语法糖和更多的扩展,拜托,我只是想用一下相关的库而已,不想去研究如何挖矿锻造打铁造车 造芯片从而上升到光科技遥遥领先。想比而言,我更想念C的基本类型,即使近十多年没写过C了,那些基本 类型也不成为障碍。各种库的版本,还有特性,还有基于这些库的扩展,以及扩展的扩展,为啥整个系统弄 的比php,golang,node.js这些还要乱呢,在我看来,这些开发者就这么乐忠于重复造轮子,然后兼容性还 不怎么好,最后可能还不如glibc的生态区。

关于rust的borrow问题,也折腾的我比较头疼。我在使用的时候,动不动就出现某个变量要在多个地方使用 的话,然后被borrow了,然后还没有clone支持,每次碰到这种就要想着绕。

学golang的时候,把基础教程翻了翻,然后就开始写项目了,到rust这边,基础教程翻了又翻,看源代码的 时候,依然是云里雾里,不知道突然蹦出来的某个写法或者缩写是个啥意思,要表达个啥。我一次又一次的 想着要不要把这个rust项目使用golang重写一遍算了。

发布日期:
分类:技术

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据