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:
Mario Zechner 2025-06-19 23:06:39 +02:00
parent 6391605267
commit 0b97181a04
4 changed files with 1260 additions and 9 deletions

1174
tty-fwd/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,7 @@ ctrlc = "3.4.7"
data-encoding = "2.9"
glob = "0.3"
notify = "8.0"
reqwest = { version = "0.12", features = ["json", "blocking"] }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.60", features = ["Win32_System_Console"] }

View file

@ -164,10 +164,15 @@ pub fn send_key_to_session(
_ => return Err(anyhow!("Unknown key: {}", key)),
};
// Use a timeout-protected write operation that also checks for readers
write_to_pipe_with_timeout(&stdin_path, key_bytes, Duration::from_secs(5))?;
Ok(())
// Try to write to the pipe directly first
match write_to_pipe_with_timeout(&stdin_path, key_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 for key", pipe_error);
proxy_key_to_nodejs_server(session_id, key)
}
}
}
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));
}
// Use a timeout-protected write operation that also checks for readers
write_to_pipe_with_timeout(&stdin_path, text.as_bytes(), Duration::from_secs(5))?;
// Try to write to the pipe directly first
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(

View file

@ -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 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
std::mem::forget(stdin_fd);
std::mem::forget(stdout_fd);
@ -723,7 +735,24 @@ fn spawn(mut opts: SpawnOptions) -> Result<i32, Error> {
}
} else {
unsafe {
let _slave_fd = pty.slave.as_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
}
}