Skip to content

Commit aa18bb5

Browse files
JohnYao93Aemulationoxkitsune
authored
sindri: Replace hand-rolled scp with rsync (#421)
Co-authored-by: Mark Honkoop <markhonkoop@hotmail.com> Co-authored-by: Gijs de Jong <14833076+oxkitsune@users.noreply.github.com>
1 parent c1ee273 commit aa18bb5

7 files changed

Lines changed: 55 additions & 178 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/sindri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
description = "tool that simplifies robot interaction by automating the process of building and deploying to the robots"
33
name = "sindri"
4-
version = "0.13.0"
4+
version = "0.14.0"
55

66
authors.workspace = true
77
categories.workspace = true

tools/sindri/src/cli/re_control.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,25 @@ pub async fn has_rerun() -> bool {
3838
get_rerun_version().await.is_ok_and(|success| success)
3939
}
4040

41+
/// Check if the `rsync` binary is installed.
42+
///
43+
/// We check if the `rsync` binary is installed by running `rsync --version` and checking if the
44+
/// command was successful.
45+
pub async fn has_rsync() -> bool {
46+
async fn get_rsync_version() -> Result<bool> {
47+
Ok(Command::new("rsync")
48+
.arg("--version")
49+
.stdout(Stdio::null())
50+
.stderr(Stdio::null())
51+
.status()
52+
.await
53+
.into_diagnostic()?
54+
.success())
55+
}
56+
57+
get_rsync_version().await.is_ok_and(|success| success)
58+
}
59+
4160
/// Compiles the `re_control` binary
4261
async fn build_re_control() -> Result<()> {
4362
let features = vec![];

tools/sindri/src/cli/robot_ops.rs

Lines changed: 22 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,11 @@ use clap::{builder::ArgPredicate, Parser};
22
use colored::Colorize;
33
use indicatif::{HumanDuration, ProgressBar, ProgressDrawTarget, ProgressStyle};
44
use miette::{miette, Context, IntoDiagnostic};
5-
use ssh2::{ErrorCode, OpenFlags, OpenType, Session, Sftp};
65
use std::{
7-
borrow::Cow,
8-
collections::HashMap,
9-
fmt, fs,
10-
io::BufWriter,
11-
net::Ipv4Addr,
12-
path::{Component, Path, PathBuf},
13-
str::FromStr,
6+
borrow::Cow, collections::HashMap, fmt, fs, net::Ipv4Addr, process::Stdio, str::FromStr,
147
time::Duration,
158
};
16-
use tokio::{self, net::TcpStream};
17-
use walkdir::{DirEntry, WalkDir};
9+
use tokio::{self, process::Command};
1810
use yggdrasil::core::config::showtime::ShowtimeConfig;
1911
use yggdrasil::prelude::*;
2012

@@ -33,14 +25,8 @@ const ROBOT_TARGET: &str = "x86_64-unknown-linux-gnu";
3325
const RELEASE_PATH_REMOTE: &str = "./target/x86_64-unknown-linux-gnu/release/yggdrasil";
3426
const RELEASE_PATH_LOCAL: &str = "./target/release/yggdrasil";
3527
const DEPLOY_PATH: &str = "./deploy/yggdrasil";
36-
const CONNECTION_TIMEOUT: u64 = 5;
37-
const LOCAL_ROBOT_ID_STR: &str = "0";
3828

39-
/// The size of the `BufWriter`'s buffer.
40-
///
41-
/// This is currently set to 1 MiB, as the [`Write`] implementation for [`ssh2::sftp::File`]
42-
/// is rather slow due to the locking mechanism.
43-
const UPLOAD_BUFFER_SIZE: usize = 1024 * 1024;
29+
const LOCAL_ROBOT_ID_STR: &str = "local";
4430

4531
// enum for either the name or the number of a robot thats given
4632
#[derive(Clone, Debug)]
@@ -601,157 +587,28 @@ pub(crate) async fn stop_single_yggdrasil_service(robot: &Robot, output: Output)
601587
}
602588

603589
/// Copy the contents of the 'deploy' folder to the robot.
604-
pub(crate) async fn upload_to_robot(addr: &Ipv4Addr, output: Output) -> Result<()> {
605-
output.connecting_phase(addr);
606-
let sftp = create_sftp_connection(addr).await?;
607-
match output.clone() {
608-
Output::Silent => {}
609-
Output::Multi(pb) => {
610-
pb.set_message(format!("{}", "Connected".bright_blue().bold()));
611-
}
612-
Output::Single(pb) => {
613-
pb.set_message(format!("{}", " Connected".bright_blue().bold()));
614-
}
615-
}
616-
617-
let entries: Vec<DirEntry> = WalkDir::new("./deploy")
618-
.into_iter()
619-
.filter_map(std::result::Result::ok)
620-
.filter(|e| !e.file_name().to_string_lossy().starts_with('.'))
621-
.collect();
622-
let num_files = entries
623-
.iter()
624-
.filter(|e| e.metadata().unwrap().is_file())
625-
.count();
626-
627-
output.upload_phase(num_files as u64);
628-
629-
for entry in &entries {
630-
let remote_path = get_remote_path(entry.path());
631-
632-
if entry.path().is_dir() {
633-
// Ensure all directories exist on remote
634-
ensure_directory_exists(&sftp, remote_path)?;
635-
continue;
636-
}
637-
638-
let file_remote = sftp
639-
.open_mode(
640-
&remote_path,
641-
OpenFlags::WRITE | OpenFlags::TRUNCATE,
642-
0o777,
643-
OpenType::File,
644-
)
645-
.map_err(|e| Error::Sftp {
646-
source: e,
647-
msg: format!("Failed to open remote file {:?}!", entry.path()),
648-
})?;
649-
650-
let mut file_local = std::fs::File::open(entry.path())?;
651-
652-
match output.clone() {
653-
Output::Silent => {}
654-
Output::Multi(pb) => {
655-
pb.set_message(format!("{}", entry.path().to_string_lossy().dimmed()));
656-
}
657-
Output::Single(pb) => {
658-
pb.set_length(file_local.metadata()?.len());
659-
pb.set_message(format!("{}", entry.path().to_string_lossy()));
660-
}
661-
}
662-
663-
// Since `file_remote` impl's Write, we can just copy directly using a BufWriter!
664-
// The Write impl is rather slow, so we set a large buffer size of 1 mb.
665-
let mut buf_writer = BufWriter::with_capacity(UPLOAD_BUFFER_SIZE, file_remote);
666-
667-
match output.clone() {
668-
Output::Silent => {
669-
std::io::copy(&mut file_local, &mut buf_writer)?;
670-
}
671-
Output::Multi(pb) => {
672-
std::io::copy(&mut file_local, &mut buf_writer)?;
673-
pb.inc(1);
674-
}
675-
Output::Single(pb) => {
676-
std::io::copy(&mut file_local, &mut pb.wrap_write(buf_writer))
677-
.map_err(Error::Io)?;
590+
pub(crate) async fn upload_to_robot(addr: &Ipv4Addr) -> Result<()> {
591+
let output = Command::new("rsync")
592+
.args([
593+
"-az",
594+
"deploy/",
595+
&format!("nao@{addr}:/home/nao"),
596+
"--out-format=\"%f\"",
597+
])
598+
.stdout(Stdio::piped())
599+
.stderr(Stdio::piped())
600+
.output()
601+
.await?;
678602

679-
pb.println(format!(
680-
"{} {}",
681-
" Uploaded".bright_blue().bold(),
682-
entry.path().to_string_lossy().dimmed()
683-
));
684-
}
603+
if !output.status.success() {
604+
if let Some(code) = output.status.code() {
605+
return Err(Error::Rsync {
606+
exit_code: code,
607+
reason: String::from_utf8_lossy(&output.stderr).into_owned(),
608+
});
685609
}
686610
}
687611

688-
output.spinner();
689-
690-
if let Output::Multi(pb) = &output {
691-
pb.set_message(format!(
692-
" {} {}",
693-
"Uploaded".green().bold(),
694-
addr.to_string().red()
695-
));
696-
}
697-
612+
println!("{}", String::from_utf8_lossy(&output.stdout));
698613
Ok(())
699614
}
700-
701-
async fn create_sftp_connection(ip: &Ipv4Addr) -> Result<Sftp> {
702-
let tcp = tokio::time::timeout(
703-
Duration::from_secs(CONNECTION_TIMEOUT),
704-
TcpStream::connect(format!("{ip}:22")),
705-
)
706-
.await
707-
.map_err(Error::Elapsed)??;
708-
let mut session = Session::new().map_err(|e| Error::Sftp {
709-
source: e,
710-
msg: "Failed to create ssh session!".to_owned(),
711-
})?;
712-
713-
session.set_tcp_stream(tcp);
714-
session.handshake().map_err(|e| Error::Sftp {
715-
source: e,
716-
msg: "Failed to perform ssh handshake!".to_owned(),
717-
})?;
718-
session
719-
.userauth_password("nao", "")
720-
.map_err(|e| Error::Sftp {
721-
source: e,
722-
msg: "Failed to authenticate using ssh!".to_owned(),
723-
})?;
724-
725-
session.sftp().map_err(|e| Error::Sftp {
726-
source: e,
727-
msg: "Failed to create sftp session!".to_owned(),
728-
})
729-
}
730-
731-
fn ensure_directory_exists(sftp: &Sftp, remote_path: impl AsRef<Path>) -> Result<()> {
732-
match sftp.mkdir(remote_path.as_ref(), 0o777) {
733-
Ok(()) => Ok(()),
734-
// Error code 4, means the directory already exists, so we can ignore it
735-
Err(error) if error.code() == ErrorCode::SFTP(4) => Ok(()),
736-
Err(error) => Err(Error::Sftp {
737-
source: error,
738-
msg: "Failed to ensure directory exists".to_owned(),
739-
}),
740-
}
741-
}
742-
743-
fn get_remote_path(local_path: &Path) -> PathBuf {
744-
let mut remote_path = PathBuf::from("/home/nao");
745-
746-
for component in local_path.components() {
747-
// Would be nice to replace this with an if let chain once https://github.com/rust-lang/rust/issues/53667#issuecomment-1374336460 is stable.
748-
match component {
749-
// Prevent "deploy" from being added to the remote path, as we'll deploy directly to home directory.
750-
Component::Normal(c) if c != "deploy" => remote_path.push(c),
751-
// Any other component kind should ignored, such as ".".
752-
_ => continue,
753-
}
754-
}
755-
756-
remote_path
757-
}

tools/sindri/src/cli/run.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::{
1212
config::SindriConfig,
1313
};
1414

15-
use super::re_control::{has_rerun, run_re_control};
15+
use super::re_control::{has_rerun, has_rsync, run_re_control};
1616

1717
const DEFAULT_TRACY_PORT: u16 = 8086;
1818

@@ -40,8 +40,13 @@ impl Run {
4040

4141
let local = self.robot_ops.local;
4242
let rerun = self.robot_ops.rerun_args.rerun.is_some();
43-
let has_rerun = has_rerun().await;
4443

44+
let has_rsync = has_rsync().await;
45+
if !has_rsync {
46+
bail!("rsync is not installed, install it using your package manager!")
47+
}
48+
49+
let has_rerun = has_rerun().await;
4550
if rerun && !has_rerun {
4651
println!(
4752
"{}: {}",
@@ -70,7 +75,7 @@ impl Run {
7075
if !self.robot_ops.local {
7176
output.spinner();
7277
robot_ops::stop_single_yggdrasil_service(&robot, output.clone()).await?;
73-
robot_ops::upload_to_robot(&robot.ip(), output.clone()).await?;
78+
robot_ops::upload_to_robot(&robot.ip()).await?;
7479

7580
if let Some(network) = self.robot_ops.network {
7681
output.spinner();

tools/sindri/src/cli/showtime.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ impl Showtime {
4343

4444
output.spinner();
4545
robot_ops::stop_single_yggdrasil_service(&robot, output.clone()).await?;
46-
robot_ops::upload_to_robot(&robot.ip(), output.clone()).await?;
46+
robot_ops::upload_to_robot(&robot.ip()).await?;
4747
output.spinner();
4848
robot_ops::start_single_yggdrasil_service(&robot, output.clone()).await?;
4949

@@ -101,7 +101,7 @@ impl Showtime {
101101
.block_on(async move {
102102
output.spinner();
103103
robot_ops::stop_single_yggdrasil_service(&robot, output.clone()).await?;
104-
robot_ops::upload_to_robot(&robot.ip(), output.clone()).await?;
104+
robot_ops::upload_to_robot(&robot.ip()).await?;
105105
output.spinner();
106106
robot_ops::start_single_yggdrasil_service(&robot, output.clone()).await?;
107107

tools/sindri/src/error.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,8 @@ pub enum Error {
1414
#[error(transparent)]
1515
Cargo(crate::cargo::CargoError),
1616

17-
#[error("Sftp error: {msg}")]
18-
Sftp {
19-
#[source]
20-
source: ssh2::Error,
21-
msg: String,
22-
},
17+
#[error("Rsync error: {reason}, look up rsync error code: {exit_code:?}")]
18+
Rsync { exit_code: i32, reason: String },
2319
#[error("Ssh error: {command}")]
2420
Ssh {
2521
#[source]

0 commit comments

Comments
 (0)