use std::{process::Stdio, time::Duration};
use chrono::Utc;
use clap::Parser;
use tokio::{
io::{AsyncBufReadExt, AsyncRead, BufReader},
process::Command,
sync::mpsc::{self, Receiver, Sender},
time,
};
use crate::{
protocol::{
self, AuthVersion, BreakpointConnectError, Command as CommandMsg, Connection, Finished,
OutputLine, OutputType, SecureMsg, XWingKeypair,
},
util,
};
#[derive(Parser, Debug)]
pub struct Cli {
#[arg(long, value_enum)]
auth: AuthVersion,
#[arg(required = true, long, short = 'c', value_name = "HOST:PORT")]
callback: String,
#[arg(required = true, long, short = 'i')]
controller: String,
#[arg(long, short = 'w')]
which: Option<String>,
}
struct Params {
callback: String,
controller_pubkey_digest: String,
which: Option<String>,
}
fn parse_args(args: &Cli) -> Result<Params, String> {
util::parse_address(&args.callback)
.map_err(|err| format!("Invalid callback address: {err}"))?;
Ok(Params {
callback: args.callback.to_string(),
controller_pubkey_digest: args.controller.clone(),
which: args.which.clone(),
})
}
fn log(msg: &str) {
let timestamp = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
println!("[{timestamp}] {msg}");
}
fn spool_output<T>(source: T, output_type: OutputType, sink: Sender<SecureMsg>, seq: u32)
where
T: AsyncRead + Send + Unpin + 'static,
{
tokio::spawn(async move {
let mut reader = BufReader::new(source);
loop {
let mut buf = Vec::new();
let num_read = reader.read_until(0x0A, &mut buf).await.unwrap();
if num_read == 0 {
return;
}
let out_msg = SecureMsg::OutputLine(OutputLine {
bytes: buf,
gender: output_type.clone(),
seq,
});
if sink.send(out_msg).await.is_err() {
return;
}
}
});
}
async fn execute_and_stream_results(
conn: &mut Connection,
command_msg: &CommandMsg,
) -> Result<(), String> {
let mut child = Command::new("bash")
.arg("-c")
.arg(&command_msg.command)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let child_out = child.stdout.take().unwrap();
let child_err = child.stderr.take().unwrap();
let (stdout_producer, mut msg_consumer): (Sender<SecureMsg>, Receiver<SecureMsg>) =
mpsc::channel(32);
let stderr_producer = stdout_producer.clone();
let seq = command_msg.seq;
spool_output(child_out, OutputType::Stdout, stdout_producer, seq);
spool_output(child_err, OutputType::Stderr, stderr_producer, seq);
let mut interval = time::interval(Duration::from_millis(10));
interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
loop {
tokio::select! {
biased;
controller_msg = conn.recv() => match controller_msg {
None => {
log("[DEBUG] Attempting to terminate the command due to lost connection...");
child.kill().await.map_err(|err| format!(
"Failed to kill child process after connection was lost: {err}"
))?;
Err("Lost connection while running command.")?;
},
Some(SecureMsg::TerminateCmd(term)) => {
if term.seq != seq {
Err(format!(
"Controller requested termination of command #{} but we're running command #{}.",
term.seq, seq
))?;
}
log("[DEBUG] Attempting to terminate the command at the controller's request...");
child.kill().await.map_err(|err| format!(
"Failed to kill child process at controller's request: {err}"
))?;
log("Done.");
break;
},
Some(wrong_variant) => {
Err(format!(
"Received unexpected message type while running command: {}",
Into::<&str>::into(wrong_variant)
))?;
},
},
return_msg_maybe = msg_consumer.recv() => match return_msg_maybe {
Some(msg) => conn.send(msg)
.await
.map_err(|err| format!("Could not send command output to controller: {err}"))?,
None => break,
},
}
interval.tick().await;
}
let exit_status = child.wait_with_output().await.unwrap();
let extra_stdout = exit_status.stdout.len();
let extra_stderr = exit_status.stderr.len();
if extra_stdout > 0 || extra_stderr > 0 {
eprintln!(
"Possible bug: There was still data in stdout ({extra_stdout} bytes) or stderr ({extra_stderr} bytes) by the time we called wait on the process."
);
}
conn.send(SecureMsg::Finished(Finished {
exit_code: exit_status.status.code(),
seq,
}))
.await
.map_err(|err| format!("Could not send command exit code to controller: {err}"))
}
async fn run_session(mut conn: Connection) -> Result<(), String> {
log("Waiting for first command...");
loop {
let command_msg = match conn
.recv()
.await
.ok_or("Controller no longer sending messages")?
{
SecureMsg::Command(msg) => msg,
SecureMsg::ExitBreak(_) => {
eprintln!("Controller asked for normal execution to resume; exiting breakpoint.");
return Ok(());
}
wrong_variant => Err(format!(
"Received unexpected message type: {}",
Into::<&str>::into(wrong_variant)
))?,
};
log(&format!("Running command: {}", command_msg.command));
execute_and_stream_results(&mut conn, &command_msg).await?;
}
}
pub async fn run(args: &Cli) -> Result<(), String> {
log(&format!(
"Breakpoint will attempt to connect to controller at '{}', expecting controller identity of '{}'.{}",
args.callback,
args.controller,
match &args.which {
None => String::new(),
Some(which) => format!(" Breakpoint identity: \"{which}\""),
},
));
let params = parse_args(args)?;
let backoff = time::Duration::from_secs(10);
loop {
let (controller_digest, breakpoint_keypair) = match crate::debug_utils::get_debug_keys() {
Some(keys) => (
protocol::controller_digest(&keys.controller_keypair.public_key),
keys.breakpoint_keypair,
),
None => (params.controller_pubkey_digest.clone(), XWingKeypair::new()),
};
let maybe_conn = {
match protocol::breakpoint_open_connection(
&breakpoint_keypair,
¶ms.callback.to_string(),
&controller_digest,
params.which.clone(),
)
.await
{
Ok(half_conn) => {
println!("This is the verification string for the breakpoint:");
println!();
println!(" {}", half_conn.breakpoint_digest);
println!();
println!("Copy that string into the breakmancer controller when prompted.");
half_conn.continue_after_verification_string_printed().await
}
Err(err) => Err(err),
}
};
let conn = match maybe_conn {
Ok(conn) => conn,
Err(BreakpointConnectError { message, permanent }) => {
log(&format!("Failed to connect to controller: {message}"));
if permanent {
return Err("Not attempting a new connection.".to_string());
}
log(&format!("Waiting {backoff:?} before trying again.",));
time::sleep(backoff).await;
continue;
}
};
match run_session(conn).await {
Ok(()) => {
return Ok(());
}
Err(err) => log(&format!("Encountered error, restarting connection: {err}")),
}
}
}