Skip to content

Commit 35ed534

Browse files
committed
fix: resolve PTY session termination hanging issue after exit command
- Add proper SSH channel cleanup when exit command is sent - Ensure terminal state is fully restored with disable_raw_mode() - Fix input task blocking by using shorter polling timeout (100ms) - Add explicit shutdown signal handling for EOF/Close messages - Force program exit after session ends to prevent hanging
1 parent f029217 commit 35ed534

4 files changed

Lines changed: 66 additions & 6 deletions

File tree

src/commands/interactive.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ impl InteractiveCommand {
205205
pty_manager.run_multiplex_sessions(session_ids).await?;
206206
}
207207

208+
// Ensure terminal is fully restored after PTY session ends
209+
let _ = crossterm::terminal::disable_raw_mode();
210+
let _ = std::io::Write::flush(&mut std::io::stdout());
211+
208212
Ok(InteractiveResult {
209213
duration: start_time.elapsed(),
210214
commands_executed: 0, // PTY mode doesn't count discrete commands
@@ -618,6 +622,12 @@ impl InteractiveCommand {
618622
match rl.readline(&prompt) {
619623
Ok(line) => {
620624
if line.trim() == "exit" {
625+
// Send exit command to remote server before breaking
626+
let mut session_guard = session_arc.lock().await;
627+
session_guard.send_command("exit").await?;
628+
drop(session_guard);
629+
// Give the SSH session a moment to process the exit
630+
tokio::time::sleep(Duration::from_millis(100)).await;
621631
break;
622632
}
623633

@@ -649,7 +659,18 @@ impl InteractiveCommand {
649659
}
650660

651661
// Clean up
662+
shutdown.store(true, Ordering::Relaxed);
652663
output_reader.abort();
664+
665+
// Properly close the SSH session
666+
let mut session_guard = session_arc.lock().await;
667+
if session_guard.is_connected {
668+
// Close the SSH channel properly
669+
let _ = session_guard.channel.close().await;
670+
session_guard.is_connected = false;
671+
}
672+
drop(session_guard);
673+
653674
let _ = rl.save_history(&history_path);
654675

655676
Ok(commands_executed)

src/main.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,12 +379,20 @@ async fn main() -> Result<()> {
379379
use_pty,
380380
};
381381
let result = interactive_cmd.execute().await?;
382+
383+
// Ensure terminal is fully restored before printing
384+
let _ = crossterm::terminal::disable_raw_mode();
385+
let _ = crossterm::cursor::Show;
386+
let _ = std::io::Write::flush(&mut std::io::stdout());
387+
382388
println!("\nSession ended.");
383389
if cli.verbose > 0 {
384390
println!("Duration: {}", format_duration(result.duration));
385391
println!("Commands executed: {}", result.commands_executed);
386392
}
387-
Ok(())
393+
394+
// Force exit to ensure proper termination
395+
std::process::exit(0);
388396
} else {
389397
// Determine timeout: CLI argument takes precedence over config
390398
let timeout = if cli.timeout > 0 {

src/pty/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,16 @@ impl PtyManager {
135135

136136
/// Run a single PTY session
137137
pub async fn run_single_session(&mut self, session_id: usize) -> Result<()> {
138-
if let Some(session) = self.active_sessions.get_mut(session_id) {
138+
let result = if let Some(session) = self.active_sessions.get_mut(session_id) {
139139
session.run().await
140140
} else {
141141
anyhow::bail!("PTY session {session_id} not found")
142-
}
142+
};
143+
144+
// Ensure terminal is properly restored after session ends
145+
let _ = crossterm::terminal::disable_raw_mode();
146+
147+
result
143148
}
144149

145150
/// Run multiple PTY sessions with session switching

src/pty/session.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,16 +161,19 @@ impl PtySession {
161161
.ok_or_else(|| anyhow::anyhow!("Message sender not available"))?
162162
.clone();
163163
let shutdown_for_input = Arc::clone(&self.shutdown);
164-
let timeout_duration = self.config.timeout;
165164

166165
let input_task = tokio::spawn(async move {
167166
loop {
168167
if shutdown_for_input.load(Ordering::Relaxed) {
169168
break;
170169
}
171170

171+
// Use a shorter polling timeout for better responsiveness
172+
// This allows the task to check the shutdown flag more frequently
173+
let poll_timeout = Duration::from_millis(100);
174+
172175
// Check for input events with timeout
173-
if crossterm::event::poll(timeout_duration).unwrap_or(false) {
176+
if crossterm::event::poll(poll_timeout).unwrap_or(false) {
174177
match crossterm::event::read() {
175178
Ok(event) => {
176179
if let Some(data) = Self::handle_input_event(event) {
@@ -219,6 +222,8 @@ impl PtySession {
219222
}
220223
ChannelMsg::Eof | ChannelMsg::Close => {
221224
tracing::debug!("SSH channel closed");
225+
// Set shutdown signal before terminating to ensure input task stops
226+
self.shutdown.store(true, Ordering::Relaxed);
222227
should_terminate = true;
223228
}
224229
_ => {}
@@ -271,15 +276,36 @@ impl PtySession {
271276
}
272277
}
273278

274-
// Cleanup tasks
279+
// Signal shutdown first
280+
self.shutdown.store(true, Ordering::Relaxed);
281+
282+
// Abort tasks immediately
275283
resize_task.abort();
276284
input_task.abort();
277285

286+
// Wait for tasks to complete their abort
287+
let _ = tokio::time::timeout(Duration::from_millis(100), async {
288+
while !resize_task.is_finished() || !input_task.is_finished() {
289+
tokio::time::sleep(Duration::from_millis(10)).await;
290+
}
291+
})
292+
.await;
293+
278294
// Disable mouse support if we enabled it
279295
if self.config.enable_mouse {
280296
let _ = TerminalOps::disable_mouse();
281297
}
282298

299+
// IMPORTANT: Explicitly restore terminal state by dropping the guard
300+
// This ensures raw mode is disabled before we return
301+
self.terminal_guard = None;
302+
303+
// Ensure terminal is fully restored
304+
let _ = crossterm::terminal::disable_raw_mode();
305+
306+
// Flush stdout to ensure all output is written
307+
let _ = io::stdout().flush();
308+
283309
self.state = PtyState::Closed;
284310
Ok(())
285311
}

0 commit comments

Comments
 (0)