最近在尝试使用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吐槽时间:
我不太清楚各个大牛是如何记住rust的各种语法糖和各种扩展类型的,我大部分时间都在找某个类型是否 具有某个扩展,以及这个扩展是否能转换为其他的扩展。动不动就要看各个库的源代码实现,然后就看到 各种花式的语法糖和更多的扩展,拜托,我只是想用一下相关的库而已,不想去研究如何挖矿锻造打铁造车 造芯片从而上升到光科技遥遥领先。想比而言,我更想念C的基本类型,即使近十多年没写过C了,那些基本 类型也不成为障碍。各种库的版本,还有特性,还有基于这些库的扩展,以及扩展的扩展,为啥整个系统弄 的比php,golang,node.js这些还要乱呢,在我看来,这些开发者就这么乐忠于重复造轮子,然后兼容性还 不怎么好,最后可能还不如glibc的生态区。
关于rust的borrow问题,也折腾的我比较头疼。我在使用的时候,动不动就出现某个变量要在多个地方使用 的话,然后被borrow了,然后还没有clone支持,每次碰到这种就要想着绕。
学golang的时候,把基础教程翻了翻,然后就开始写项目了,到rust这边,基础教程翻了又翻,看源代码的 时候,依然是云里雾里,不知道突然蹦出来的某个写法或者缩写是个啥意思,要表达个啥。我一次又一次的 想着要不要把这个rust项目使用golang重写一遍算了。