//! CWD tracking via a SEPARATE SSH exec channel. //! //! This module opens an independent exec channel on the same SSH connection and //! periodically runs `pwd` to determine the remote shell's current working //! directory. The terminal data stream is NEVER touched — CWD is tracked //! entirely out of band. //! //! When the CWD changes, an `ssh:cwd:{session_id}` event is emitted to the //! frontend. use std::sync::Arc; use log::{debug, error, warn}; use russh::client::Handle; use russh::ChannelMsg; use tauri::{AppHandle, Emitter}; use tokio::sync::watch; use tokio::sync::Mutex as TokioMutex; use crate::ssh::session::SshClient; /// Tracks the current working directory of a remote shell by periodically /// running `pwd` over a separate exec channel. pub struct CwdTracker { _sender: watch::Sender, pub receiver: watch::Receiver, } impl CwdTracker { /// Create a new CWD tracker with an empty initial directory. pub fn new() -> Self { let (sender, receiver) = watch::channel(String::new()); Self { _sender: sender, receiver, } } /// Spawn a background tokio task that polls `pwd` every 2 seconds on a /// separate exec channel. /// /// The task runs until the SSH connection is closed or the channel cannot /// be opened. CWD changes are emitted as `ssh:cwd:{session_id}` events. pub fn start( &self, handle: Arc>>, app_handle: AppHandle, session_id: String, ) { let sender = self._sender.clone(); tokio::spawn(async move { // Brief initial delay to let the shell start up. tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; let mut previous_cwd = String::new(); loop { // Open a fresh exec channel for each `pwd` invocation. // Some SSH servers do not allow multiple exec requests on a // single channel, so we open a new one each time. let result = { let handle_guard = handle.lock().await; handle_guard.channel_open_session().await }; let mut exec_channel = match result { Ok(ch) => ch, Err(e) => { debug!( "CWD tracker for session {} could not open exec channel: {} — stopping", session_id, e ); break; } }; // Execute `pwd` on the exec channel. if let Err(e) = exec_channel.exec(true, "pwd").await { warn!( "CWD tracker for session {}: failed to exec pwd: {}", session_id, e ); break; } // Read the output. let mut output = String::new(); loop { match exec_channel.wait().await { Some(ChannelMsg::Data { ref data }) => { if let Ok(text) = std::str::from_utf8(data.as_ref()) { output.push_str(text); } } Some(ChannelMsg::Eof) | Some(ChannelMsg::Close) | None => break, Some(ChannelMsg::ExitStatus { .. }) => { // pwd exited — we may still get data, keep reading } _ => {} } } let cwd = output.trim().to_string(); if !cwd.is_empty() && cwd != previous_cwd { previous_cwd = cwd.clone(); // Update the watch channel. let _ = sender.send(cwd.clone()); // Emit event to frontend. let event_name = format!("ssh:cwd:{}", session_id); if let Err(e) = app_handle.emit(&event_name, &cwd) { error!( "CWD tracker for session {}: failed to emit event: {}", session_id, e ); } } // Wait 2 seconds before the next poll. tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; } debug!("CWD tracker for session {} stopped", session_id); }); } }