mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix Ctrl+C signal handling and cross-server session compatibility
- Added proper PTY slave terminal configuration for signal interpretation - Enabled ISIG flag so Ctrl+C generates SIGINT instead of being treated as text - Configured ICANON and ECHO flags for proper terminal behavior - Applied configuration in both child process code paths (manual dup2 and login_tty) - Implemented hybrid proxy fallback system for cross-server session input - Rust server now proxies input to Node.js server when pipe write fails - Added reqwest HTTP client for seamless communication between servers - Reduced pipe timeout to 1 second for faster fallback detection - Added key translation for special keys when proxying to Node.js This fixes both: 1. Ctrl+C not interrupting processes in Rust-created sessions 2. "Device not configured" errors when accessing Node.js sessions from Rust server 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6391605267
commit
0b97181a04
4 changed files with 1260 additions and 9 deletions
1174
tty-fwd/Cargo.lock
generated
1174
tty-fwd/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -35,6 +35,7 @@ ctrlc = "3.4.7"
|
||||||
data-encoding = "2.9"
|
data-encoding = "2.9"
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
notify = "8.0"
|
notify = "8.0"
|
||||||
|
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows-sys = { version = "0.60", features = ["Win32_System_Console"] }
|
windows-sys = { version = "0.60", features = ["Win32_System_Console"] }
|
||||||
|
|
|
||||||
|
|
@ -164,10 +164,15 @@ pub fn send_key_to_session(
|
||||||
_ => return Err(anyhow!("Unknown key: {}", key)),
|
_ => return Err(anyhow!("Unknown key: {}", key)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use a timeout-protected write operation that also checks for readers
|
// Try to write to the pipe directly first
|
||||||
write_to_pipe_with_timeout(&stdin_path, key_bytes, Duration::from_secs(5))?;
|
match write_to_pipe_with_timeout(&stdin_path, key_bytes, Duration::from_secs(1)) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
Ok(())
|
Err(pipe_error) => {
|
||||||
|
// If pipe write fails, try to proxy to Node.js server
|
||||||
|
eprintln!("Direct pipe write failed: {}, trying Node.js proxy for key", pipe_error);
|
||||||
|
proxy_key_to_nodejs_server(session_id, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_text_to_session(
|
pub fn send_text_to_session(
|
||||||
|
|
@ -182,10 +187,56 @@ pub fn send_text_to_session(
|
||||||
return Err(anyhow!("Session {} not found or not running", session_id));
|
return Err(anyhow!("Session {} not found or not running", session_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a timeout-protected write operation that also checks for readers
|
// Try to write to the pipe directly first
|
||||||
write_to_pipe_with_timeout(&stdin_path, text.as_bytes(), Duration::from_secs(5))?;
|
match write_to_pipe_with_timeout(&stdin_path, text.as_bytes(), Duration::from_secs(1)) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(pipe_error) => {
|
||||||
|
// If pipe write fails, try to proxy to Node.js server
|
||||||
|
eprintln!("Direct pipe write failed: {}, trying Node.js proxy", pipe_error);
|
||||||
|
proxy_input_to_nodejs_server(session_id, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
fn proxy_input_to_nodejs_server(session_id: &str, text: &str) -> Result<(), anyhow::Error> {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
// Create HTTP client
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
|
||||||
|
// Create request body
|
||||||
|
let mut body = HashMap::new();
|
||||||
|
body.insert("text", text);
|
||||||
|
|
||||||
|
// Send request to Node.js server
|
||||||
|
let url = format!("http://localhost:3000/api/sessions/{}/input", session_id);
|
||||||
|
let response = client
|
||||||
|
.post(&url)
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.map_err(|e| anyhow!("Failed to proxy to Node.js server: {}", e))?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Node.js server returned error: {}", response.status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proxy_key_to_nodejs_server(session_id: &str, key: &str) -> Result<(), anyhow::Error> {
|
||||||
|
// Convert key to equivalent text sequence for Node.js server
|
||||||
|
let text = match key {
|
||||||
|
"arrow_up" => "\x1b[A",
|
||||||
|
"arrow_down" => "\x1b[B",
|
||||||
|
"arrow_right" => "\x1b[C",
|
||||||
|
"arrow_left" => "\x1b[D",
|
||||||
|
"escape" => "\x1b",
|
||||||
|
"enter" | "ctrl_enter" => "\r",
|
||||||
|
"shift_enter" => "\x1b\x0d",
|
||||||
|
_ => return Err(anyhow!("Unknown key for proxy: {}", key)),
|
||||||
|
};
|
||||||
|
|
||||||
|
proxy_input_to_nodejs_server(session_id, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_to_pipe_with_timeout(
|
fn write_to_pipe_with_timeout(
|
||||||
|
|
|
||||||
|
|
@ -711,6 +711,18 @@ fn spawn(mut opts: SpawnOptions) -> Result<i32, Error> {
|
||||||
dup2(&slave_owned_fd, &mut stdout_fd).expect("Failed to dup2 stdout");
|
dup2(&slave_owned_fd, &mut stdout_fd).expect("Failed to dup2 stdout");
|
||||||
dup2(&slave_owned_fd, &mut stderr_fd).expect("Failed to dup2 stderr");
|
dup2(&slave_owned_fd, &mut stderr_fd).expect("Failed to dup2 stderr");
|
||||||
|
|
||||||
|
// Configure the PTY slave for proper signal handling
|
||||||
|
if let Ok(mut attrs) = tcgetattr(&slave_owned_fd) {
|
||||||
|
// Enable signal interpretation (ISIG) so Ctrl+C generates SIGINT
|
||||||
|
attrs.local_flags.insert(LocalFlags::ISIG);
|
||||||
|
// Enable canonical mode for line editing but keep other flags
|
||||||
|
attrs.local_flags.insert(LocalFlags::ICANON);
|
||||||
|
// Keep echo enabled for interactive sessions
|
||||||
|
attrs.local_flags.insert(LocalFlags::ECHO);
|
||||||
|
// Apply the terminal attributes
|
||||||
|
tcsetattr(&slave_owned_fd, SetArg::TCSANOW, &attrs).ok();
|
||||||
|
}
|
||||||
|
|
||||||
// Forget the OwnedFd instances to prevent them from being closed
|
// Forget the OwnedFd instances to prevent them from being closed
|
||||||
std::mem::forget(stdin_fd);
|
std::mem::forget(stdin_fd);
|
||||||
std::mem::forget(stdout_fd);
|
std::mem::forget(stdout_fd);
|
||||||
|
|
@ -723,7 +735,24 @@ fn spawn(mut opts: SpawnOptions) -> Result<i32, Error> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let _slave_fd = pty.slave.as_raw_fd();
|
||||||
login_tty_compat(pty.slave.into_raw_fd())?;
|
login_tty_compat(pty.slave.into_raw_fd())?;
|
||||||
|
|
||||||
|
// Configure the PTY slave for proper signal handling after login_tty
|
||||||
|
use std::os::fd::{FromRawFd, OwnedFd};
|
||||||
|
let stdin_fd = OwnedFd::from_raw_fd(0); // stdin is now the slave
|
||||||
|
if let Ok(mut attrs) = tcgetattr(&stdin_fd) {
|
||||||
|
// Enable signal interpretation (ISIG) so Ctrl+C generates SIGINT
|
||||||
|
attrs.local_flags.insert(LocalFlags::ISIG);
|
||||||
|
// Enable canonical mode for line editing
|
||||||
|
attrs.local_flags.insert(LocalFlags::ICANON);
|
||||||
|
// Keep echo enabled for interactive sessions
|
||||||
|
attrs.local_flags.insert(LocalFlags::ECHO);
|
||||||
|
// Apply the terminal attributes
|
||||||
|
tcsetattr(&stdin_fd, SetArg::TCSANOW, &attrs).ok();
|
||||||
|
}
|
||||||
|
std::mem::forget(stdin_fd); // Don't close stdin
|
||||||
|
|
||||||
// No stderr redirection since script_mode is always false
|
// No stderr redirection since script_mode is always false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue