detached_shell/pty.rs
1use std::fs::File;
2use std::io::{self, Read, Write};
3use std::os::unix::io::{BorrowedFd, FromRawFd, RawFd};
4use std::os::unix::net::{UnixListener, UnixStream};
5use std::path::PathBuf;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::Arc;
8use std::thread;
9
10use crossterm::terminal;
11use nix::fcntl::{fcntl, FcntlArg, OFlag};
12use nix::sys::signal::{kill, Signal};
13use nix::sys::termios::{tcgetattr, tcsetattr, SetArg};
14use nix::unistd::{close, dup2, execvp, fork, setsid, ForkResult, Pid};
15
16use crate::error::{NdsError, Result};
17use crate::manager::SessionManager;
18use crate::pty_buffer::PtyBuffer;
19use crate::scrollback::ScrollbackViewer;
20use crate::session::Session;
21use crate::terminal_state::TerminalState;
22
23pub struct PtyProcess {
24 pub master_fd: RawFd,
25 pub pid: Pid,
26 pub socket_path: PathBuf,
27 listener: Option<UnixListener>,
28 output_buffer: Option<PtyBuffer>,
29}
30
31impl PtyProcess {
32 pub fn spawn_new_detached(session_id: &str) -> Result<Session> {
33 Self::spawn_new_detached_with_name(session_id, None)
34 }
35
36 pub fn spawn_new_detached_with_name(session_id: &str, name: Option<String>) -> Result<Session> {
37 // Capture terminal size BEFORE detaching
38 let (cols, rows) = terminal::size().unwrap_or((80, 24));
39
40 // First fork to create intermediate process
41 match unsafe { fork() }
42 .map_err(|e| NdsError::ForkError(format!("First fork failed: {}", e)))?
43 {
44 ForkResult::Parent { child: _ } => {
45 // Wait for the intermediate process to complete
46 std::thread::sleep(std::time::Duration::from_millis(200));
47
48 // Load the session that was created by the daemon
49 Session::load(session_id)
50 }
51 ForkResult::Child => {
52 // We're in the intermediate process
53 // Create a new session to detach from the terminal
54 setsid().map_err(|e| NdsError::ProcessError(format!("setsid failed: {}", e)))?;
55
56 // Second fork to ensure we can't acquire a controlling terminal
57 match unsafe { fork() }
58 .map_err(|e| NdsError::ForkError(format!("Second fork failed: {}", e)))?
59 {
60 ForkResult::Parent { child: _ } => {
61 // Intermediate process exits immediately
62 std::process::exit(0);
63 }
64 ForkResult::Child => {
65 // We're now in the daemon process
66 // Close standard file descriptors to fully detach
67 unsafe {
68 libc::close(0);
69 libc::close(1);
70 libc::close(2);
71
72 // Redirect to /dev/null
73 let dev_null = libc::open(
74 b"/dev/null\0".as_ptr() as *const libc::c_char,
75 libc::O_RDWR,
76 );
77 if dev_null >= 0 {
78 libc::dup2(dev_null, 0);
79 libc::dup2(dev_null, 1);
80 libc::dup2(dev_null, 2);
81 if dev_null > 2 {
82 libc::close(dev_null);
83 }
84 }
85 }
86
87 // Continue with PTY setup, passing the captured terminal size
88 let (pty_process, _session) =
89 Self::spawn_new_internal_with_size(session_id, name, cols, rows)?;
90
91 // Run the PTY handler
92 if let Err(_e) = pty_process.run_detached() {
93 // Can't print errors anymore since stdout is closed
94 }
95
96 // Clean up when done
97 Session::cleanup(session_id).ok();
98 std::process::exit(0);
99 }
100 }
101 }
102 }
103 }
104
105 fn spawn_new_internal_with_size(
106 session_id: &str,
107 name: Option<String>,
108 cols: u16,
109 rows: u16,
110 ) -> Result<(Self, Session)> {
111 // Open PTY using libc directly since nix 0.29 doesn't have pty module
112 let (master_fd, slave_fd) = Self::open_pty()?;
113
114 // Set terminal size on slave
115 unsafe {
116 let winsize = libc::winsize {
117 ws_row: rows,
118 ws_col: cols,
119 ws_xpixel: 0,
120 ws_ypixel: 0,
121 };
122 if libc::ioctl(slave_fd, libc::TIOCSWINSZ as u64, &winsize) < 0 {
123 return Err(NdsError::PtyError(
124 "Failed to set terminal size".to_string(),
125 ));
126 }
127 }
128
129 // Set non-blocking on master
130 let flags = fcntl(master_fd, FcntlArg::F_GETFL)
131 .map_err(|e| NdsError::PtyError(format!("Failed to get flags: {}", e)))?;
132 fcntl(
133 master_fd,
134 FcntlArg::F_SETFL(OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK),
135 )
136 .map_err(|e| NdsError::PtyError(format!("Failed to set non-blocking: {}", e)))?;
137
138 // Create socket for IPC
139 let socket_path = Session::socket_dir()?.join(format!("{}.sock", session_id));
140
141 // Remove socket if it exists
142 if socket_path.exists() {
143 std::fs::remove_file(&socket_path)?;
144 }
145
146 let listener = UnixListener::bind(&socket_path)
147 .map_err(|e| NdsError::SocketError(format!("Failed to bind socket: {}", e)))?;
148
149 // Fork process
150 match unsafe { fork() }.map_err(|e| NdsError::ForkError(e.to_string()))? {
151 ForkResult::Parent { child } => {
152 // Close slave in parent
153 let _ = close(slave_fd);
154
155 // Create session metadata
156 let session = Session::with_name(
157 session_id.to_string(),
158 name,
159 child.as_raw(),
160 socket_path.clone(),
161 );
162 session.save().map_err(|e| {
163 eprintln!("Failed to save session: {}", e);
164 e
165 })?;
166
167 let pty_process = PtyProcess {
168 master_fd,
169 pid: child,
170 socket_path,
171 listener: Some(listener),
172 output_buffer: Some(PtyBuffer::new(1024 * 1024)), // 1MB buffer
173 };
174
175 Ok((pty_process, session))
176 }
177 ForkResult::Child => {
178 // Close master in child
179 let _ = close(master_fd);
180
181 // Create new session
182 setsid().map_err(|e| NdsError::ProcessError(format!("setsid failed: {}", e)))?;
183
184 // Make slave the controlling terminal
185 unsafe {
186 if libc::ioctl(slave_fd, libc::TIOCSCTTY as u64, 0) < 0 {
187 eprintln!("Failed to set controlling terminal");
188 std::process::exit(1);
189 }
190 }
191
192 // Duplicate slave to stdin/stdout/stderr
193 dup2(slave_fd, 0)
194 .map_err(|e| NdsError::ProcessError(format!("dup2 stdin failed: {}", e)))?;
195 dup2(slave_fd, 1)
196 .map_err(|e| NdsError::ProcessError(format!("dup2 stdout failed: {}", e)))?;
197 dup2(slave_fd, 2)
198 .map_err(|e| NdsError::ProcessError(format!("dup2 stderr failed: {}", e)))?;
199
200 // Close original slave
201 if slave_fd > 2 {
202 let _ = close(slave_fd);
203 }
204
205 // Get shell
206 let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
207
208 // Execute shell
209 let shell_cstr = std::ffi::CString::new(shell.as_str()).unwrap();
210 let args = vec![shell_cstr.clone()];
211
212 execvp(&shell_cstr, &args)
213 .map_err(|e| NdsError::ProcessError(format!("execvp failed: {}", e)))?;
214
215 // Should never reach here
216 unreachable!()
217 }
218 }
219 }
220
221 fn open_pty() -> Result<(RawFd, RawFd)> {
222 unsafe {
223 // Open PTY master
224 let master_fd = libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY);
225 if master_fd < 0 {
226 return Err(NdsError::PtyError("Failed to open PTY master".to_string()));
227 }
228
229 // Grant access to slave
230 if libc::grantpt(master_fd) < 0 {
231 let _ = libc::close(master_fd);
232 return Err(NdsError::PtyError("Failed to grant PTY access".to_string()));
233 }
234
235 // Unlock slave
236 if libc::unlockpt(master_fd) < 0 {
237 let _ = libc::close(master_fd);
238 return Err(NdsError::PtyError("Failed to unlock PTY".to_string()));
239 }
240
241 // Get slave name
242 let slave_name = libc::ptsname(master_fd);
243 if slave_name.is_null() {
244 let _ = libc::close(master_fd);
245 return Err(NdsError::PtyError(
246 "Failed to get PTY slave name".to_string(),
247 ));
248 }
249
250 // Open slave
251 let slave_cstr = std::ffi::CStr::from_ptr(slave_name);
252 let slave_fd = libc::open(slave_cstr.as_ptr(), libc::O_RDWR);
253 if slave_fd < 0 {
254 let _ = libc::close(master_fd);
255 return Err(NdsError::PtyError("Failed to open PTY slave".to_string()));
256 }
257
258 Ok((master_fd, slave_fd))
259 }
260 }
261
262 pub fn attach_to_session(session: &Session) -> Result<Option<String>> {
263 // Save current terminal state
264 let stdin_fd = 0;
265 let stdin = unsafe { BorrowedFd::borrow_raw(stdin_fd) };
266 let original_termios = tcgetattr(&stdin).map_err(|e| {
267 NdsError::TerminalError(format!("Failed to get terminal attributes: {}", e))
268 })?;
269
270 // Capture current terminal state
271 let _terminal_state = TerminalState::capture(stdin_fd)?;
272
273 // Connect to session socket
274 let mut socket = session.connect_socket()?;
275
276 // Get current terminal size and send resize command
277 let (cols, rows) = terminal::size().map_err(|e| NdsError::TerminalError(e.to_string()))?;
278
279 // Send a special resize command to the daemon
280 // Format: \x1b]nds:resize:<cols>:<rows>\x07
281 let resize_cmd = format!("\x1b]nds:resize:{}:{}\x07", cols, rows);
282 let _ = socket.write_all(resize_cmd.as_bytes());
283 let _ = socket.flush();
284
285 // Small delay to let the resize happen
286 thread::sleep(std::time::Duration::from_millis(50));
287
288 // Send a sequence to help restore the terminal state
289 // First, send Ctrl+L to refresh the display
290 let _ = socket.write_all(b"\x0c");
291 let _ = socket.flush();
292
293 // Small delay to let the refresh happen
294 thread::sleep(std::time::Duration::from_millis(50));
295
296 // Set terminal to raw mode
297 let mut raw = original_termios.clone();
298 // Manually set raw mode flags
299 raw.input_flags = nix::sys::termios::InputFlags::empty();
300 raw.output_flags = nix::sys::termios::OutputFlags::empty();
301 raw.control_flags |= nix::sys::termios::ControlFlags::CS8;
302 raw.local_flags = nix::sys::termios::LocalFlags::empty();
303 raw.control_chars[nix::sys::termios::SpecialCharacterIndices::VMIN as usize] = 1;
304 raw.control_chars[nix::sys::termios::SpecialCharacterIndices::VTIME as usize] = 0;
305 tcsetattr(&stdin, SetArg::TCSANOW, &raw)
306 .map_err(|e| NdsError::TerminalError(format!("Failed to set raw mode: {}", e)))?;
307
308 // Create a flag for clean shutdown
309 let running = Arc::new(AtomicBool::new(true));
310 let r1 = running.clone();
311 let r2 = running.clone();
312
313 // Handle Ctrl+C
314 ctrlc::set_handler(move || {
315 r1.store(false, Ordering::SeqCst);
316 })
317 .map_err(|e| NdsError::SignalError(format!("Failed to set signal handler: {}", e)))?;
318
319 println!("\r\n[Attached to session {}]\r", session.id);
320 println!("[Press Enter then ~d to detach, ~s to switch, ~h for history]\r");
321
322 // Scrollback buffer (max 10MB)
323 let scrollback_buffer = Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
324
325 // Spawn thread to monitor terminal size changes
326 let socket_for_resize = socket
327 .try_clone()
328 .map_err(|e| NdsError::SocketError(format!("Failed to clone socket: {}", e)))?;
329 let resize_running = running.clone();
330 let _resize_monitor = thread::spawn(move || {
331 let mut last_size = (cols, rows);
332 let mut socket = socket_for_resize;
333
334 while resize_running.load(Ordering::SeqCst) {
335 if let Ok((new_cols, new_rows)) = terminal::size() {
336 if (new_cols, new_rows) != last_size {
337 // Terminal size changed, send resize command
338 let resize_cmd = format!("\x1b]nds:resize:{}:{}\x07", new_cols, new_rows);
339 let _ = socket.write_all(resize_cmd.as_bytes());
340 let _ = socket.flush();
341 last_size = (new_cols, new_rows);
342 }
343 }
344 thread::sleep(std::time::Duration::from_millis(250));
345 }
346 });
347
348 // Spawn thread to read from socket and write to stdout
349 let socket_clone = socket
350 .try_clone()
351 .map_err(|e| NdsError::SocketError(format!("Failed to clone socket: {}", e)))?;
352 let scrollback_clone = Arc::clone(&scrollback_buffer);
353 let socket_to_stdout = thread::spawn(move || {
354 let mut socket = socket_clone;
355 let mut stdout = io::stdout();
356 let mut buffer = [0u8; 4096];
357
358 while r2.load(Ordering::SeqCst) {
359 match socket.read(&mut buffer) {
360 Ok(0) => break, // Socket closed
361 Ok(n) => {
362 // Write to stdout
363 if let Err(_) = stdout.write_all(&buffer[..n]) {
364 break;
365 }
366 let _ = stdout.flush();
367
368 // Add to scrollback buffer
369 let mut scrollback = scrollback_clone.lock().unwrap();
370 scrollback.extend_from_slice(&buffer[..n]);
371
372 // Trim if too large
373 let scrollback_max = 10 * 1024 * 1024;
374 if scrollback.len() > scrollback_max {
375 let remove = scrollback.len() - scrollback_max;
376 scrollback.drain(..remove);
377 }
378 }
379 Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
380 thread::sleep(std::time::Duration::from_millis(10));
381 }
382 Err(ref e) if e.kind() == io::ErrorKind::BrokenPipe => {
383 // Expected when socket is closed, just exit cleanly
384 break;
385 }
386 Err(_) => break,
387 }
388 }
389 });
390
391 // Read from stdin and write to socket
392 let mut stdin = io::stdin();
393 let mut buffer = [0u8; 1024];
394
395 // SSH-style escape sequence: Enter ~d
396 // We'll track if we're at the beginning of a line
397 let mut at_line_start = true;
398 let mut escape_state = 0; // 0=normal, 1=saw tilde at line start
399 let mut escape_time = std::time::Instant::now();
400
401 loop {
402 if !running.load(Ordering::SeqCst) {
403 break;
404 }
405
406 match stdin.read(&mut buffer) {
407 Ok(0) => break,
408 Ok(n) => {
409 let mut should_detach = false;
410 let mut should_switch = false;
411 let mut should_scroll = false;
412 let mut data_to_forward = Vec::new();
413
414 // Check for escape timeout (reset after 1 second)
415 if escape_state == 1
416 && escape_time.elapsed() > std::time::Duration::from_secs(1)
417 {
418 // Timeout - forward the held tilde and reset
419 data_to_forward.push(b'~');
420 escape_state = 0;
421 }
422
423 // Process each byte for escape sequence
424 for i in 0..n {
425 let byte = buffer[i];
426
427 match escape_state {
428 0 => {
429 // Normal state
430 if at_line_start && byte == b'~' {
431 // Start of potential escape sequence
432 escape_state = 1;
433 escape_time = std::time::Instant::now();
434 // Don't forward the tilde yet
435 } else {
436 // Regular character
437 data_to_forward.push(byte);
438 // Update line start tracking
439 at_line_start =
440 byte == b'\r' || byte == b'\n' || byte == 10 || byte == 13;
441 }
442 }
443 1 => {
444 // We saw ~ at the beginning of a line
445 if byte == b'd' {
446 // Detach command ~d
447 should_detach = true;
448 break;
449 } else if byte == b's' {
450 // Switch sessions command ~s
451 should_switch = true;
452 break;
453 } else if byte == b'h' {
454 // History/scrollback command ~h
455 should_scroll = true;
456 break;
457 } else if byte == b'~' {
458 // ~~ means literal tilde
459 data_to_forward.push(b'~');
460 escape_state = 0;
461 at_line_start = false;
462 } else {
463 // Not an escape sequence, forward tilde and this char
464 data_to_forward.push(b'~');
465 data_to_forward.push(byte);
466 escape_state = 0;
467 at_line_start =
468 byte == b'\r' || byte == b'\n' || byte == 10 || byte == 13;
469 }
470 }
471 _ => {
472 escape_state = 0;
473 }
474 }
475 }
476
477 if should_detach {
478 println!("\r\n[Detaching from session {}]\r", session.id);
479 break;
480 }
481
482 if should_switch {
483 // Show session switcher
484 println!("\r\n[Session Switcher]\r");
485
486 // Get list of other sessions
487 match SessionManager::list_sessions() {
488 Ok(sessions) => {
489 let other_sessions: Vec<_> =
490 sessions.iter().filter(|s| s.id != session.id).collect();
491
492 // Show available sessions
493 println!("\r\nAvailable options:\r");
494
495 // Show existing sessions
496 if !other_sessions.is_empty() {
497 for (i, s) in other_sessions.iter().enumerate() {
498 println!(
499 "\r {}. {} (PID: {})\r",
500 i + 1,
501 s.display_name(),
502 s.pid
503 );
504 }
505 }
506
507 // Add new session option
508 let new_option_num = other_sessions.len() + 1;
509 println!("\r {}. [New Session]\r", new_option_num);
510 println!("\r 0. Cancel\r");
511 println!("\r\nSelect option (0-{}): ", new_option_num);
512 let _ = io::stdout().flush();
513
514 // Read user selection
515 let mut selection = String::new();
516 if let Ok(_) = io::stdin().read_line(&mut selection) {
517 if let Ok(num) = selection.trim().parse::<usize>() {
518 if num > 0 && num <= other_sessions.len() {
519 // Switch to selected session
520 let target_session = other_sessions[num - 1];
521 println!(
522 "\r\n[Switching to session {}]\r",
523 target_session.id
524 );
525
526 // Store the target session ID for return
527 return Ok(Some(target_session.id.clone()));
528 } else if num == new_option_num {
529 // Create new session
530 println!("\r\nEnter name for new session (or press Enter for no name): ");
531 let _ = io::stdout().flush();
532
533 let mut session_name = String::new();
534 if let Ok(_) = io::stdin().read_line(&mut session_name)
535 {
536 let session_name = session_name.trim();
537 let name = if session_name.is_empty() {
538 None
539 } else {
540 Some(session_name.to_string())
541 };
542
543 // Create new session
544 match SessionManager::create_session_with_name(
545 name.clone(),
546 ) {
547 Ok(new_session) => {
548 if let Some(ref n) = name {
549 println!("\r\n[Created and switching to new session '{}' ({})]", n, new_session.id);
550 } else {
551 println!("\r\n[Created and switching to new session {}]", new_session.id);
552 }
553
554 // Return the new session ID to switch to it
555 return Ok(Some(new_session.id.clone()));
556 }
557 Err(e) => {
558 eprintln!(
559 "\r\nError creating session: {}\r",
560 e
561 );
562 }
563 }
564 }
565 }
566 }
567 }
568
569 // Cancelled or invalid selection
570 println!("\r\n[Continuing current session]\r");
571 escape_state = 0;
572 at_line_start = true;
573 }
574 Err(e) => {
575 eprintln!("\r\nError listing sessions: {}\r", e);
576 escape_state = 0;
577 at_line_start = true;
578 }
579 }
580 }
581
582 if should_scroll {
583 // Show scrollback viewer
584 println!("\r\n[Opening scrollback viewer...]\r");
585
586 // Get scrollback content
587 let content = scrollback_buffer.lock().unwrap().clone();
588
589 // Temporarily restore terminal for viewer
590 let stdin_fd = 0;
591 let stdin = unsafe { BorrowedFd::borrow_raw(stdin_fd) };
592 tcsetattr(&stdin, SetArg::TCSANOW, &original_termios).map_err(|e| {
593 NdsError::TerminalError(format!("Failed to restore terminal: {}", e))
594 })?;
595
596 // Show scrollback viewer
597 let mut viewer = ScrollbackViewer::new(&content);
598 let _ = viewer.run(); // Ignore errors, just return to session
599
600 // Re-enter raw mode
601 tcsetattr(&stdin, SetArg::TCSANOW, &raw).map_err(|e| {
602 NdsError::TerminalError(format!("Failed to set raw mode: {}", e))
603 })?;
604
605 // Refresh display
606 let _ = socket.write_all(b"\x0c"); // Ctrl+L
607 let _ = socket.flush();
608
609 println!("\r\n[Returned to session]\r");
610
611 // Reset state
612 escape_state = 0;
613 at_line_start = true;
614 }
615
616 // Forward the processed data
617 if !data_to_forward.is_empty() {
618 if let Err(e) = socket.write_all(&data_to_forward) {
619 // Check if it's a broken pipe (expected on detach)
620 if e.kind() == io::ErrorKind::BrokenPipe {
621 // This is expected when detaching, just break
622 break;
623 } else {
624 eprintln!("\r\nError writing to socket: {}\r", e);
625 break;
626 }
627 }
628 }
629 }
630 Err(e) => {
631 eprintln!("\r\nError reading stdin: {}\r", e);
632 break;
633 }
634 }
635 }
636
637 // Stop the socket reader thread
638 running.store(false, Ordering::SeqCst);
639
640 // Close the socket to unblock the reader thread
641 drop(socket);
642
643 // Wait for the thread with a timeout
644 thread::sleep(std::time::Duration::from_millis(100));
645 let _ = socket_to_stdout.join();
646
647 // Restore terminal - do this BEFORE any output
648 let stdin_fd = 0;
649 let stdin = unsafe { BorrowedFd::borrow_raw(stdin_fd) };
650
651 // First restore the terminal settings
652 tcsetattr(&stdin, SetArg::TCSANOW, &original_termios)
653 .map_err(|e| NdsError::TerminalError(format!("Failed to restore terminal: {}", e)))?;
654
655 // Ensure we're back in cooked mode
656 terminal::disable_raw_mode().ok();
657
658 // Add a small delay to ensure terminal is fully restored
659 thread::sleep(std::time::Duration::from_millis(50));
660
661 // Now it's safe to print the detach message
662 println!("\n[Detached from session {}]", session.id);
663
664 // Flush stdout to ensure message is displayed
665 let _ = io::stdout().flush();
666
667 Ok(None)
668 }
669
670 pub fn run_detached(mut self) -> Result<()> {
671 let listener = self
672 .listener
673 .take()
674 .ok_or_else(|| NdsError::PtyError("No listener available".to_string()))?;
675
676 // Set listener to non-blocking
677 listener.set_nonblocking(true)?;
678
679 let running = Arc::new(AtomicBool::new(true));
680 let r = running.clone();
681
682 // Handle cleanup on exit
683 ctrlc::set_handler(move || {
684 r.store(false, Ordering::SeqCst);
685 })
686 .map_err(|e| NdsError::SignalError(format!("Failed to set signal handler: {}", e)))?;
687
688 let output_buffer = self
689 .output_buffer
690 .take()
691 .ok_or_else(|| NdsError::PtyError("No output buffer available".to_string()))?;
692
693 // Support multiple concurrent clients
694 let mut active_clients: Vec<UnixStream> = Vec::new();
695 let mut buffer = [0u8; 4096];
696
697 // Get session ID from socket path
698 let session_id = self
699 .socket_path
700 .file_stem()
701 .and_then(|s| s.to_str())
702 .unwrap_or("unknown")
703 .to_string();
704
705 while running.load(Ordering::SeqCst) {
706 // Check for new connections
707 match listener.accept() {
708 Ok((mut stream, _)) => {
709 stream.set_nonblocking(true)?;
710
711 // Notify existing clients about new connection
712 let notification = format!(
713 "\r\n[Another client connected to this session (total: {})]\r\n",
714 active_clients.len() + 1
715 );
716 for client in &mut active_clients {
717 let _ = client.write_all(notification.as_bytes());
718 let _ = client.flush();
719 }
720
721 // Send buffered output to new client
722 if !output_buffer.is_empty() {
723 let mut buffered_data = Vec::new();
724 output_buffer.drain_to(&mut buffered_data);
725
726 // Save cursor position, clear screen, and reset
727 let init_sequence = b"\x1b7\x1b[?47h\x1b[2J\x1b[H"; // Save cursor, alt screen, clear, home
728 let _ = stream.write_all(init_sequence);
729 let _ = stream.flush();
730
731 // Send buffered data in chunks to avoid overwhelming the client
732 for chunk in buffered_data.chunks(4096) {
733 let _ = stream.write_all(chunk);
734 let _ = stream.flush();
735 std::thread::sleep(std::time::Duration::from_millis(1));
736 }
737
738 // Exit alt screen and restore cursor
739 let restore_sequence = b"\x1b[?47l\x1b8"; // Exit alt screen, restore cursor
740 let _ = stream.write_all(restore_sequence);
741 let _ = stream.flush();
742
743 // Small delay for terminal to process
744 std::thread::sleep(std::time::Duration::from_millis(50));
745
746 // Send a full redraw command to the shell
747 let mut master_file = unsafe { File::from_raw_fd(self.master_fd) };
748 let _ = master_file.write_all(b"\x0c"); // Ctrl+L to refresh
749 std::mem::forget(master_file); // Don't close the fd
750
751 // Give time for the refresh to complete
752 std::thread::sleep(std::time::Duration::from_millis(100));
753 } else {
754 // No buffer, just request a refresh to sync state
755 let mut master_file = unsafe { File::from_raw_fd(self.master_fd) };
756 let _ = master_file.write_all(b"\x0c"); // Ctrl+L to refresh
757 std::mem::forget(master_file); // Don't close the fd
758 }
759
760 // Add new client to the list
761 active_clients.push(stream);
762
763 // Update client count in status file
764 let _ = Session::update_client_count(&session_id, active_clients.len());
765 }
766 Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
767 // No new connections
768 }
769 Err(_e) => {
770 // Error accepting connection, continue
771 }
772 }
773
774 // Read from PTY master
775 let master_file = unsafe { File::from_raw_fd(self.master_fd) };
776 let mut master_file_clone = master_file.try_clone()?;
777 std::mem::forget(master_file); // Don't close the fd
778
779 match master_file_clone.read(&mut buffer) {
780 Ok(0) => {
781 // Child process exited
782 break;
783 }
784 Ok(n) => {
785 let data = &buffer[..n];
786
787 // Broadcast to all connected clients
788 if !active_clients.is_empty() {
789 let mut disconnected_indices = Vec::new();
790
791 for (i, client) in active_clients.iter_mut().enumerate() {
792 if let Err(e) = client.write_all(data) {
793 if e.kind() == io::ErrorKind::BrokenPipe
794 || e.kind() == io::ErrorKind::ConnectionAborted
795 {
796 // Mark client for removal
797 disconnected_indices.push(i);
798 }
799 } else {
800 let _ = client.flush();
801 }
802 }
803
804 // Remove disconnected clients and notify others
805 if !disconnected_indices.is_empty() {
806 for i in disconnected_indices.iter().rev() {
807 active_clients.remove(*i);
808 }
809
810 // Update client count in status file
811 let _ = Session::update_client_count(&session_id, active_clients.len());
812
813 // Notify remaining clients
814 if !active_clients.is_empty() {
815 let notification = format!(
816 "\r\n[A client disconnected (remaining: {})]\r\n",
817 active_clients.len()
818 );
819 for client in &mut active_clients {
820 let _ = client.write_all(notification.as_bytes());
821 let _ = client.flush();
822 }
823 }
824 }
825
826 // If all clients disconnected, start buffering
827 if active_clients.is_empty() {
828 output_buffer.push(data);
829 }
830 } else {
831 // No clients connected, buffer the output
832 output_buffer.push(data);
833 }
834 }
835 Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
836 // No data available
837 }
838 Err(_) => {
839 // Other error, continue
840 }
841 }
842
843 // Read from clients and write to PTY
844 let mut disconnected_indices = Vec::new();
845
846 for (i, client) in active_clients.iter_mut().enumerate() {
847 let mut client_buffer = [0u8; 1024];
848 match client.read(&mut client_buffer) {
849 Ok(0) => {
850 // Client disconnected
851 disconnected_indices.push(i);
852 }
853 Ok(n) => {
854 let data = &client_buffer[..n];
855
856 // Check for special NDS commands
857 // Format: \x1b]nds:resize:<cols>:<rows>\x07
858 if n > 10 && data.starts_with(b"\x1b]nds:") {
859 if let Ok(cmd_str) = std::str::from_utf8(data) {
860 if let Some(end_idx) = cmd_str.find('\x07') {
861 let cmd = &cmd_str[2..end_idx]; // Skip \x1b]
862 if cmd.starts_with("nds:resize:") {
863 // Parse resize command
864 let parts: Vec<&str> =
865 cmd["nds:resize:".len()..].split(':').collect();
866 if parts.len() == 2 {
867 if let (Ok(cols), Ok(rows)) =
868 (parts[0].parse::<u16>(), parts[1].parse::<u16>())
869 {
870 // Resize the PTY
871 unsafe {
872 let winsize = libc::winsize {
873 ws_row: rows,
874 ws_col: cols,
875 ws_xpixel: 0,
876 ws_ypixel: 0,
877 };
878 libc::ioctl(
879 self.master_fd,
880 libc::TIOCSWINSZ as u64,
881 &winsize,
882 );
883 }
884
885 // Send SIGWINCH to the child process to notify of resize
886 let _ = kill(self.pid, Signal::SIGWINCH);
887
888 // Don't forward the resize command to the PTY
889 // But forward any remaining data after the command
890 if end_idx + 1 < n {
891 let remaining = &data[end_idx + 1..];
892 if !remaining.is_empty() {
893 let mut master_file = unsafe {
894 File::from_raw_fd(self.master_fd)
895 };
896 let _ = master_file.write_all(remaining);
897 std::mem::forget(master_file);
898 // Don't close the fd
899 }
900 }
901 continue; // Skip normal forwarding
902 }
903 }
904 }
905 }
906 }
907 }
908
909 // Normal data - forward to PTY
910 let mut master_file = unsafe { File::from_raw_fd(self.master_fd) };
911 let _ = master_file.write_all(data);
912 std::mem::forget(master_file); // Don't close the fd
913 }
914 Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
915 // No data available
916 }
917 Err(_) => {
918 // Client error, mark for removal
919 disconnected_indices.push(i);
920 }
921 }
922 }
923
924 // Remove disconnected clients and notify others
925 if !disconnected_indices.is_empty() {
926 for i in disconnected_indices.iter().rev() {
927 active_clients.remove(*i);
928 }
929
930 // Update client count in status file
931 let _ = Session::update_client_count(&session_id, active_clients.len());
932
933 // Notify remaining clients
934 if !active_clients.is_empty() {
935 let notification = format!(
936 "\r\n[A client disconnected (remaining: {})]\r\n",
937 active_clients.len()
938 );
939 for client in &mut active_clients {
940 let _ = client.write_all(notification.as_bytes());
941 let _ = client.flush();
942 }
943 }
944 }
945
946 // Small sleep to prevent busy loop
947 thread::sleep(std::time::Duration::from_millis(10));
948 }
949
950 Ok(())
951 }
952
953 pub fn kill_session(session_id: &str) -> Result<()> {
954 let session = Session::load(session_id)?;
955
956 // Send SIGTERM to the process
957 kill(Pid::from_raw(session.pid), Signal::SIGTERM)
958 .map_err(|e| NdsError::ProcessError(format!("Failed to kill process: {}", e)))?;
959
960 // Wait a moment for graceful shutdown
961 thread::sleep(std::time::Duration::from_millis(500));
962
963 // Force kill if still alive
964 if Session::is_process_alive(session.pid) {
965 kill(Pid::from_raw(session.pid), Signal::SIGKILL).map_err(|e| {
966 NdsError::ProcessError(format!("Failed to force kill process: {}", e))
967 })?;
968 }
969
970 // Clean up session files
971 Session::cleanup(session_id)?;
972
973 Ok(())
974 }
975}
976
977impl Drop for PtyProcess {
978 fn drop(&mut self) {
979 let _ = close(self.master_fd);
980 if let Some(listener) = self.listener.take() {
981 drop(listener);
982 }
983 }
984}