mod connect;
pub mod handler;
#[cfg(test)]
mod tests;
mod transfer;
use std::fmt;
use russh::ChannelMsg;
use russh::client;
pub use self::connect::{Client, ClientOptions};
use crate::error::{ClientError, Result};
use crate::session::{PtyOptions, PtySize, SessionState};
pub struct Session {
pub(crate) handle: client::Handle<handler::ClientHandler>,
pub(crate) channel: russh::Channel<client::Msg>,
pub(crate) connection: Option<iroh::endpoint::Connection>,
pub(crate) endpoint: Option<iroh::Endpoint>,
pub(crate) remote_metadata: Option<crate::transport::metadata::PeerMetadata>,
pub(crate) state: SessionState,
}
impl fmt::Debug for Session {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Session")
.field("has_connection", &self.connection.is_some())
.field("has_endpoint", &self.endpoint.is_some())
.field("has_remote_metadata", &self.remote_metadata.is_some())
.field("state", &self.state)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionEvent {
Stdout(Vec<u8>),
Stderr(Vec<u8>),
ExitStatus(u32),
Closed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransferProgress {
transferred: u64,
total: u64,
}
impl TransferProgress {
pub fn new(transferred: u64, total: u64) -> Self {
Self { transferred, total }
}
pub fn transferred(&self) -> u64 {
self.transferred
}
pub fn total(&self) -> u64 {
self.total
}
pub fn percent(&self) -> u8 {
if self.total == 0 {
100
} else {
((self.transferred.saturating_mul(100)) / self.total).min(100) as u8
}
}
}
impl Session {
pub fn state(&self) -> SessionState {
self.state
}
pub fn remote_metadata(&self) -> Option<&crate::transport::metadata::PeerMetadata> {
self.remote_metadata.as_ref()
}
pub async fn request_pty(&mut self, options: PtyOptions) -> Result<()> {
let size = options.size();
self.channel
.request_pty(
true,
options.term(),
size.cols as u32,
size.rows as u32,
size.pixel_width as u32,
size.pixel_height as u32,
options.modes_slice(),
)
.await
.map_err(|e| ClientError::PtyRequestFailed { source: e }.into())
}
pub async fn start_shell(&mut self) -> Result<()> {
self.channel
.request_shell(true)
.await
.map_err(|e| ClientError::ShellRequestFailed { source: e })?;
self.state = SessionState::ShellReady;
Ok(())
}
pub async fn exec(&mut self, command: &str) -> Result<()> {
self.channel
.exec(true, command)
.await
.map_err(|e| ClientError::ExecFailed { source: e })?;
self.state = SessionState::ShellReady;
Ok(())
}
pub async fn send(&mut self, data: &[u8]) -> Result<()> {
self.channel
.data(data)
.await
.map_err(|e| ClientError::DataSendFailed { source: e }.into())
}
pub async fn eof(&mut self) -> Result<()> {
self.channel
.eof()
.await
.map_err(|e| ClientError::EofSendFailed { source: e }.into())
}
pub async fn resize(&mut self, size: PtySize) -> Result<()> {
self.channel
.window_change(
size.cols as u32,
size.rows as u32,
size.pixel_width as u32,
size.pixel_height as u32,
)
.await
.map_err(|e| ClientError::WindowChangeFailed { source: e }.into())
}
pub async fn next_event(&mut self) -> Result<Option<SessionEvent>> {
loop {
let Some(message) = self.channel.wait().await else {
self.state = SessionState::Closed;
return Ok(None);
};
match message {
ChannelMsg::Data { data } => return Ok(Some(SessionEvent::Stdout(data.to_vec()))),
ChannelMsg::ExtendedData { data, .. } => {
return Ok(Some(SessionEvent::Stderr(data.to_vec())));
}
ChannelMsg::ExitStatus { exit_status } => {
return Ok(Some(SessionEvent::ExitStatus(exit_status)));
}
ChannelMsg::Eof | ChannelMsg::Close => {
self.state = SessionState::Closed;
return Ok(Some(SessionEvent::Closed));
}
_ => {}
}
}
}
pub async fn disconnect(&mut self) -> Result<()> {
self.handle
.disconnect(russh::Disconnect::ByApplication, "", "English")
.await
.map_err(|e| ClientError::DisconnectFailed { source: e })?;
if let Some(endpoint) = self.endpoint.as_ref() {
endpoint.close().await;
}
self.connection = None;
self.state = SessionState::Closed;
Ok(())
}
pub async fn close(mut self) -> Result<()> {
self.disconnect().await
}
}