use anyhow::{Context, Result, bail};
use std::io::Write;
use std::path::Path;
use tokio::net::UnixStream;
use super::protocol::{self, ClientMessage, Direction, LogStream, PROTOCOL_VERSION, ServerMessage};
use crate::home;
struct SessionEnv {
session_id: String,
token: String,
remote_sock: String,
}
fn session_from_env() -> Result<SessionEnv> {
let session_id = std::env::var("SHINE_SSH_SESSION").ok();
let token = std::env::var("SHINE_SSH_TOKEN").ok();
let remote_sock = std::env::var("SHINE_SSH_REMOTE_SOCK").ok();
let (Some(session_id), Some(token), Some(remote_sock)) = (session_id, token, remote_sock)
else {
bail!(
"this shell is not inside a `shine ssh` session (SHINE_SSH_SESSION/SHINE_SSH_TOKEN/SHINE_SSH_REMOTE_SOCK are not set); run `shine ssh <host>` first"
);
};
Ok(SessionEnv {
session_id,
token,
remote_sock,
})
}
async fn connect_and_handshake(remote_sock: &str) -> Result<UnixStream> {
let mut stream = UnixStream::connect(remote_sock).await.with_context(|| {
format!(
"could not reach the local shine transfer agent through the forwarded SSH connection at {remote_sock}; is the `shine ssh` session this shell was started under still alive?"
)
})?;
protocol::write_message(
&mut stream,
&ClientMessage::Hello {
protocol_version: PROTOCOL_VERSION,
},
)
.await?;
let ack: ServerMessage = protocol::read_message(&mut stream).await?;
match ack {
ServerMessage::HelloAck { protocol_version } if protocol_version == PROTOCOL_VERSION => {}
ServerMessage::HelloAck { protocol_version } => {
bail!(
"protocol version mismatch: this shine speaks v{PROTOCOL_VERSION}, the local agent speaks v{protocol_version}; upgrade whichever side is older"
);
}
ServerMessage::Error { message } => bail!("{message}"),
other => bail!("unexpected handshake response: {other:?}"),
}
Ok(stream)
}
fn absolutize_remote_spec(raw: &str) -> Result<String> {
let expanded = home::full_expand(raw).with_context(|| format!("expanding path {raw:?}"))?;
if Path::new(&expanded).is_absolute() {
return Ok(expanded);
}
let cwd = std::env::current_dir().context("reading current directory")?;
Ok(format!("{}/{}", cwd.display(), expanded))
}
pub async fn handle_download(
remote_source: &str,
local_destination: Option<&str>,
force: bool,
dry_run: bool,
use_scp: bool,
) -> Result<()> {
let session = session_from_env()?;
let remote_spec = absolutize_remote_spec(remote_source)?;
let mut stream = connect_and_handshake(&session.remote_sock).await?;
protocol::write_message(
&mut stream,
&ClientMessage::Transfer {
token: session.token,
direction: Direction::Download,
remote_spec,
local_spec: local_destination.map(str::to_string),
force,
dry_run,
use_scp,
},
)
.await?;
relay_until_done(&mut stream).await
}
pub async fn handle_upload(
local_source: &str,
remote_destination: Option<&str>,
force: bool,
dry_run: bool,
use_scp: bool,
) -> Result<()> {
let session = session_from_env()?;
let remote_spec = match remote_destination {
Some(dest) => absolutize_remote_spec(dest)?,
None => std::env::current_dir()
.context("reading current directory")?
.display()
.to_string(),
};
let mut stream = connect_and_handshake(&session.remote_sock).await?;
protocol::write_message(
&mut stream,
&ClientMessage::Transfer {
token: session.token,
direction: Direction::Upload,
remote_spec,
local_spec: Some(local_source.to_string()),
force,
dry_run,
use_scp,
},
)
.await?;
relay_until_done(&mut stream).await
}
async fn relay_until_done(stream: &mut UnixStream) -> Result<()> {
loop {
match protocol::read_message(stream).await? {
ServerMessage::Starting {
fell_back, note, ..
} => {
if fell_back && let Some(note) = note {
eprintln!("shine: {note}");
}
}
ServerMessage::Log {
stream: which,
chunk,
} => match which {
LogStream::Stdout => {
print!("{chunk}");
let _ = std::io::stdout().flush();
}
LogStream::Stderr => {
eprint!("{chunk}");
let _ = std::io::stderr().flush();
}
},
ServerMessage::Done { code } => {
if code != 0 {
std::process::exit(code);
}
return Ok(());
}
ServerMessage::Error { message } => bail!("{message}"),
other => bail!("unexpected response: {other:?}"),
}
}
}
pub async fn handle_status() -> Result<()> {
let session = session_from_env()?;
match connect_and_handshake(&session.remote_sock).await {
Ok(mut stream) => {
protocol::write_message(
&mut stream,
&ClientMessage::Status {
token: session.token,
},
)
.await?;
match protocol::read_message(&mut stream).await? {
ServerMessage::StatusResponse {
session_local_dir,
host,
} => {
println!("session: {}", session.session_id);
println!("connection: connected");
println!("protocol: v{PROTOCOL_VERSION}");
println!("host: {host}");
println!("local dir: {session_local_dir}");
Ok(())
}
ServerMessage::Error { message } => bail!("{message}"),
other => bail!("unexpected response: {other:?}"),
}
}
Err(error) => {
println!("session: {}", session.session_id);
println!("connection: unreachable ({error:#})");
Ok(())
}
}
}