use futures_util::StreamExt;
use std::sync::Arc;
use tracing::{debug, info};
use crate::builder::SessionBuilder;
use crate::errors::Result;
use crate::session::{CloseReason, Session, SessionManager};
use crate::terminal::{self, RawModeGuard, TerminalEvent, TerminalReader, TerminalSize};
#[derive(Debug, Clone)]
pub struct InteractiveConfig {
pub show_banner: bool,
pub send_initial_size: bool,
pub region: Option<String>,
pub reason: Option<String>,
}
impl Default for InteractiveConfig {
fn default() -> Self {
Self {
show_banner: true,
send_initial_size: true,
region: None,
reason: None,
}
}
}
#[derive(Debug, Default)]
pub struct InteractiveShell {
config: InteractiveConfig,
}
impl InteractiveShell {
pub fn new(config: InteractiveConfig) -> Self {
Self { config }
}
pub async fn run(&self, target: &str) -> Result<Option<i32>> {
terminal::require_terminal()?;
let mut builder = SessionBuilder::new(target).maybe_region(self.config.region.clone());
if let Some(reason) = &self.config.reason {
builder = builder.reason(reason.clone());
}
let session = Arc::new(builder.start().await?);
self.drive(session).await
}
pub async fn run_with(&self, target: &str, manager: &SessionManager) -> Result<Option<i32>> {
terminal::require_terminal()?;
let session = Arc::new(SessionBuilder::new(target).start_with(manager).await?);
self.drive(session).await
}
pub async fn attach(&self, session: Arc<Session>) -> Result<Option<i32>> {
terminal::require_terminal()?;
self.drive(session).await
}
async fn drive(&self, session: Arc<Session>) -> Result<Option<i32>> {
if self.config.show_banner {
println!("Starting session with SessionId: {}", session.id());
}
session.wait_ready().await?;
if let Some(banner) = session.banner() {
println!("{banner}");
}
let _raw = RawModeGuard::enter()?;
if self.config.send_initial_size {
let size = TerminalSize::current();
session.send_terminal_size(size.cols, size.rows).await?;
}
let result = self.pump(&session).await;
let _ = terminal::write_output(b"\r\n");
drop(_raw);
let reason = session.close_reason();
let exit_code = session.exit_code();
session.terminate().await?;
if self.config.show_banner {
match &reason {
Some(CloseReason::Terminated) | None => println!("Session ended."),
Some(reason) => println!("Session ended: {reason}"),
}
}
result.map(|()| exit_code)
}
async fn pump(&self, session: &Session) -> Result<()> {
let mut terminal_input = TerminalReader::start();
let mut output = session.output();
loop {
tokio::select! {
biased;
chunk = output.next() => match chunk {
Some(chunk) => terminal::write_output(&chunk)?,
None => {
debug!("session output ended");
return Ok(());
}
},
event = terminal_input.next() => match event {
Some(TerminalEvent::Input(bytes)) => session.send(bytes).await?,
Some(TerminalEvent::Resize(size)) => {
session.send_terminal_size(size.cols, size.rows).await?;
}
Some(TerminalEvent::Eof) | None => {
info!("terminal input ended");
return Ok(());
}
},
() = session.closed() => {
debug!(reason = ?session.close_reason(), "session ended");
return Ok(());
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_show_a_banner_and_send_the_size() {
let config = InteractiveConfig::default();
assert!(config.show_banner);
assert!(config.send_initial_size);
assert!(config.region.is_none());
}
#[tokio::test]
async fn running_without_a_terminal_is_rejected_before_any_aws_call() {
if terminal::is_terminal() {
return; }
let err = InteractiveShell::default()
.run("i-0123456789abcdef0")
.await
.unwrap_err();
assert!(err.to_string().contains("terminal"), "{err}");
}
}