diff --git a/.devhost/install-system-packages.sh b/.devhost/install-system-packages.sh index edb3fb86..af7c9a91 100755 --- a/.devhost/install-system-packages.sh +++ b/.devhost/install-system-packages.sh @@ -16,5 +16,4 @@ apt-get install \ libglib2.0-dev \ libssl-dev \ pkg-config \ - python3-venv \ - sshpass + python3-venv diff --git a/Cargo.lock b/Cargo.lock index f55abf75..2d428659 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,6 +42,7 @@ dependencies = [ "log", "serde", "serde_json", + "ssh2", "tar", "url", ] @@ -484,6 +485,7 @@ dependencies = [ "env_logger", "log", "reqwest", + "ssh2", "tokio", "url", ] @@ -1593,6 +1595,32 @@ dependencies = [ "libc", ] +[[package]] +name = "libssh2-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "licensekey" version = "0.0.0" @@ -2513,6 +2541,18 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "ssh2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f84d13b3b8a0d4e91a2629911e951db1bb8671512f5c09d7d4ba34500ba68c8" +dependencies = [ + "bitflags 2.9.0", + "libc", + "libssh2-sys", + "parking_lot", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index a1e041fd..24a86f31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ reqwest-websocket = "0.4.1" semver = "1.0.23" serde = "1.0.204" serde_json = "1.0.120" +ssh2 = "0.9.4" syslog = "6.1.1" tar = "0.4.40" tempdir = "0.3.7" diff --git a/apps/inspect_env/src/main.rs b/apps/inspect_env/src/main.rs index e414ba4d..01abcd22 100644 --- a/apps/inspect_env/src/main.rs +++ b/apps/inspect_env/src/main.rs @@ -32,6 +32,17 @@ mod tests { PathBuf::from("/usr/local/packages").join(PACKAGE_NAME) } + #[test] + fn args_passed_as_expected() { + assert_eq!( + env::args().collect::>(), + vec![ + "/usr/local/packages/inspect_env/inspect_env", + "--test-threads=1" + ] + ) + } + #[test] fn selected_vars_are_set_as_expected() { assert_eq!(env::var("G_SLICE").unwrap(), "always-malloc"); diff --git a/crates/acap-ssh-utils/Cargo.toml b/crates/acap-ssh-utils/Cargo.toml index 8df596bc..b77916af 100644 --- a/crates/acap-ssh-utils/Cargo.toml +++ b/crates/acap-ssh-utils/Cargo.toml @@ -17,3 +17,4 @@ url = { workspace = true } tar = { workspace = true } cli-version = { workspace = true } +ssh2 = { workspace = true } diff --git a/crates/acap-ssh-utils/src/lib.rs b/crates/acap-ssh-utils/src/lib.rs index 519a0df4..4ba95669 100644 --- a/crates/acap-ssh-utils/src/lib.rs +++ b/crates/acap-ssh-utils/src/lib.rs @@ -2,148 +2,115 @@ mod acap; use std::{ - collections::HashMap, fs::File, - io::{BufRead, BufReader, Read, Write}, + io::{Read, Write}, path::{Path, PathBuf}, - process::Stdio, }; use anyhow::{bail, Context}; use flate2::read::GzDecoder; -use log::{debug, warn}; +use log::debug; use tar::Archive; -use url::Host; + +use ssh2::{FileStat, Session}; use crate::acap::Manifest; -// TODO: Investigate if a Rust library can be used to replace `sshpass`, `ssh`, `scp`. -// This would make password handling easier and reduce the number of system dependencies that users -// have to install. -fn sshpass(pass: &str, program: &str) -> std::process::Command { - let mut cmd = std::process::Command::new("sshpass"); - // TODO: Consider not passing the password as an argument - cmd.arg(format!("-p{pass}")) - .arg(program) - // The ssh client will try keys until it finds one that works. - // If it tries to many keys that fail it will be disconnected by the server. - .args(["-o", "PubkeyAuthentication=no"]); - cmd +struct RemoteCommand { + cmd: String, } -fn scp(src: &Path, user: &str, pass: &str, host: &Host, tgt: &str) -> std::process::Command { - let mut cmd = sshpass(pass, "scp"); - cmd.arg("-p"); // Ensure temporary files become executable. - cmd.arg(src); - cmd.arg(format!("{user}@{host}:{tgt}")); - cmd -} +impl RemoteCommand { + pub fn new( + user: Option>, + env: Option<&[(impl AsRef, impl AsRef)]>, + executable: &str, + cwd: Option<&str>, + args: Option<&[&str]>, + ) -> Self { + let mut cmd = if let Some(user) = user { + let mut cmd = std::process::Command::new("su"); + cmd.arg(user.as_ref()); + cmd + } else { + std::process::Command::new("sh") + }; -fn ssh(user: &str, pass: &str, host: &Host) -> std::process::Command { - let mut cmd = sshpass(pass, "ssh"); - cmd.arg("-x"); // Makes no difference when I have tested but seems to be the right thing to do. - cmd.arg(format!("{user}@{host}")); - cmd -} -fn spawn(mut cmd: std::process::Command) -> anyhow::Result { - match cmd.spawn() { - Ok(t) => Ok(t), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - let program = cmd.get_program().to_string_lossy().to_string(); - Err(e).context(format!( - "{program} not found, perhaps it must be installed." - )) + cmd.arg("-c"); + + if let Some(env) = env { + cmd.envs(env.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))); } - Err(e) => Err(e.into()), - } -} + cmd.env("G_SLICE", "always-malloc"); -trait RunWith { - fn run_with_captured_stdout(self) -> anyhow::Result; - fn run_with_logged_stdout(self) -> anyhow::Result<()>; - fn run_with_inherited_stdout(self) -> anyhow::Result<()>; -} + if let Some(cwd) = cwd { + cmd.current_dir(cwd); + } + + if let Some(args) = args { + let mut exec = std::process::Command::new(executable); + exec.args(args); + cmd.arg(format!("{exec:?}")); + } else { + cmd.arg(executable); + } -impl RunWith for std::process::Command { - fn run_with_captured_stdout(mut self) -> anyhow::Result { - self.stdout(Stdio::piped()); - debug!("Spawning child {self:#?}..."); - let mut child = spawn(self)?; - let mut stdout = child.stdout.take().unwrap(); - debug!("Waiting for child..."); - let status = child.wait()?; - if !status.success() { - bail!("Child failed: {status}"); + Self { + cmd: format!("{cmd:?}"), } - let mut decoded = String::new(); - stdout.read_to_string(&mut decoded)?; - Ok(decoded) } - fn run_with_logged_stdout(mut self: std::process::Command) -> anyhow::Result<()> { - self.stdout(Stdio::piped()); - debug!("Spawning child {self:#?}..."); - let mut child = spawn(self)?; - let stdout = child.stdout.take().unwrap(); - - let lines = BufReader::new(stdout).lines(); - for line in lines { - let line = line?; - if !line.is_empty() { - debug!("Child said {:?}.", line); + pub fn exec(&self, session: &Session) -> Result<(), anyhow::Error> { + let mut channel = session.channel_session()?; + channel.handle_extended_data(ssh2::ExtendedData::Merge)?; + + channel.exec(&self.cmd)?; + let mut stdout = channel.stream(0); + let mut buf = [0; 4096]; + loop { + let n = stdout.read(&mut buf)?; + if n == 0 { + break; } + print!("{}", std::str::from_utf8(&buf[..n])?); + stdout.flush()?; } - debug!("Waiting for child..."); - let status = child.wait()?; - if !status.success() { - bail!("Child failed: {status}"); - } - Ok(()) - } + channel.wait_eof()?; + channel.wait_close()?; + let code = channel.exit_status()?; - fn run_with_inherited_stdout(mut self: std::process::Command) -> anyhow::Result<()> { - debug!("Running command {self:#?}..."); - let status = self.status()?; - if !status.success() { - bail!("Child failed: {status}"); + if code != 0 { + bail!("{} exited with status {}", self.cmd, code); } + Ok(()) } -} -struct RemoteTemporaryFile { - path: String, - ssh_rm: Option, -} -impl RemoteTemporaryFile { - fn try_new(user: &str, pass: &str, host: &Host) -> anyhow::Result { - let mut ssh_mktemp = ssh(user, pass, host); - ssh_mktemp.arg("mktemp"); - let path = ssh_mktemp.run_with_captured_stdout()?.trim().to_string(); - let mut ssh_rm = ssh(user, pass, host); - ssh_rm.arg("rm"); - ssh_rm.arg(&path); - Ok(Self { - path, - ssh_rm: Some(ssh_rm), - }) - } -} -impl Drop for RemoteTemporaryFile { - fn drop(&mut self) { - let ssh_rm = self.ssh_rm.take().unwrap(); - let path = &self.path; - match ssh_rm.run_with_logged_stdout() { - Ok(()) => debug!("Successfully cleaned up temporary file {path}."), - Err(e) => warn!("Failed to clean up temporary file {path} because {e}."), + pub fn exec_capture_stdout(&self, session: &Session) -> Result, anyhow::Error> { + let mut channel = session.channel_session()?; + channel.handle_extended_data(ssh2::ExtendedData::Merge)?; + + channel.exec(&self.cmd)?; + let mut stdout = channel.stream(0); + let mut output = Vec::new(); + stdout.read_to_end(&mut output)?; + + channel.wait_eof()?; + channel.wait_close()?; + let code = channel.exit_status()?; + + if code != 0 { + bail!("{} exited with status {}", self.cmd, code); } + + Ok(output) } } + /// Run executable on device /// -/// `user` and `pass` are the credentials to use for the ssh connection. -/// `host` is the device to connect to. +/// `session` is a ssh2::Session connected to the remote host. /// `prog` is the path to the executable to run. /// `env` is a map of environment variables to override on the remote host. /// @@ -151,85 +118,83 @@ impl Drop for RemoteTemporaryFile { /// - enabled SSH on the device, /// - configured the SSH user with a password and the necessary permissions, and /// - stopped the app. -pub fn run_other( +pub fn run_other>( prog: &Path, - user: &str, - pass: &str, - host: &Host, - env: HashMap<&str, &str>, + session: &Session, + env: &[(S, S)], args: &[&str], ) -> anyhow::Result<()> { - let temp_file = RemoteTemporaryFile::try_new(user, pass, host)?; + let tmp = RemoteCommand::new( + None::<&str>, + None::<&[(&str, &str)]>, + "mktemp -u", + None, + None, + ) + .exec_capture_stdout(session)?; - scp(prog, user, pass, host, &temp_file.path).run_with_logged_stdout()?; + // The output from `mktemp -u` contains a trailing '\n' + let path = std::str::from_utf8(&tmp)?.strip_suffix('\n').unwrap(); - let mut exec = std::process::Command::new(&temp_file.path); - exec.envs(env); - exec.args(args); + { + let path = Path::new(&path); - let mut ssh_exec = ssh(user, pass, host); - ssh_exec.arg(format!("{exec:?}")); - ssh_exec.run_with_inherited_stdout()?; + let sftp = session.sftp().context("Creating sftp session")?; + sftp.create(path) + .with_context(|| format!("Creating {:?}", path))? + .write_all(&std::fs::read(prog)?) + .with_context(|| format!("Writing {:?}", prog))?; + let mut stat = sftp + .stat(path) + .with_context(|| format!("Running `stat` on {:?}", path))?; + // `sftp.create` creates a new file with write-only permissions, + // but since we expect to run this program we need to mark it executable + // for the user + stat.perm = Some(0o100744); + sftp.setstat(path, stat) + .with_context(|| format!("Updating stat on {:?}", path))?; + } - Ok(()) + RemoteCommand::new(None::<&str>, Some(env), path, None, Some(args)).exec(session) } // TODO: Consider abstracting away the difference between devices that support developer mode, and // those that don't. /// Run ACAP app on device in a realistic manner. /// -/// `user` and `pass` are the credentials to use for the ssh connection. -/// `host` is the device to connect to. +/// `session` is a ssh2::Session connected to the remote host. /// `package` is the name of the ACAP app to emulate. /// `env` is a map of environment variables to override on the remote host. +/// `as_root` is a boolean that indicates if the process should be run as root +/// or as the package-user. /// /// The function assumes that the user has already /// - enabled SSH on the device, /// - configured the SSH user with a password and the necessary permissions, /// - installed the app, and /// - stopped the app, if it was running. -pub fn run_package( - user: &str, - pass: &str, - host: &Host, +pub fn run_package>( + session: &Session, package: &str, - env: HashMap<&str, &str>, + env: &[(S, S)], args: &[&str], + as_package_user: bool, ) -> anyhow::Result<()> { - let mut cd = std::process::Command::new("cd"); - cd.arg(format!("/usr/local/packages/{package}")); - - let mut exec = std::process::Command::new(format!("./{package}")); - // TODO: Consider setting more environment variables - exec.env("G_SLICE", "always-malloc"); - exec.envs(env); - exec.args(args); - - let package_user = format!("acap-{package}"); - let exec_as_package = if user == package_user { - let mut sh = std::process::Command::new("sh"); - sh.args(["-c", &format!("{exec:?}")]); - sh - } else { - let mut su = std::process::Command::new("su"); - su.args(["--shell", "/bin/sh"]); - su.args(["--command", &format!("{exec:?}")]); - su.args([package_user]); - su - }; + let cmd = RemoteCommand::new( + as_package_user.then(|| format!("acap-{package}")), + Some(env), + &format!("/usr/local/packages/{package}/{package}"), + Some(&format!("/usr/local/packages/{package}")), + Some(args), + ); - // TODO: Consider giving user control over what happens with stdout when running concurrently. - // The escaping of quotation marks is ridiculous, but it's automatic and empirically verifiable. - let mut ssh_exec_as_package = ssh(user, pass, host); - ssh_exec_as_package.arg(format!("{cd:?} && {exec_as_package:?}")); - ssh_exec_as_package.run_with_inherited_stdout()?; - Ok(()) + cmd.exec(session) } /// Update ACAP app on device without installing it /// /// - `package` the location of the `.eap` to upload. -/// - `user` and `pass` are the credentials to use for the ssh connection. +/// - `session` is a ssh2::Session connected to the remote host. /// - `host` is the device to connect to. /// /// The function assumes that the user has already @@ -237,63 +202,87 @@ pub fn run_package( /// - configured the SSH user with a password and the necessary permissions, /// - installed the app, and /// - stopped the app, if it was running. -pub fn patch_package(package: &Path, user: &str, pass: &str, host: &Host) -> anyhow::Result<()> { - // Not all files can be replaced, so we upload only the ones that can. - // This archive will hold the files that will be uploded. - let mut partial = tar::Builder::new(Vec::new()); - +pub fn patch_package(package: &Path, session: &Session) -> anyhow::Result<()> { let mut full = Archive::new(GzDecoder::new(File::open(package)?)); - let mut app_name: Option = None; - for entry in full.entries()? { - let mut entry = entry?; - let path = entry.path()?; - if path == Path::new("manifest.json") { - let mut manifest = String::new(); - entry.read_to_string(&mut manifest)?; - let manifest: Manifest = serde_json::from_str(&manifest)?; - app_name = Some(manifest.acap_package_conf.setup.app_name) - } else if path != Path::new("package.conf") { - debug!("Adding {path:?} to new archive."); - let mut buf = Vec::new(); - _ = entry.read_to_end(&mut buf)?; - partial.append(entry.header(), &*buf)?; - } - } - let Some(app_name) = app_name else { + let mut entries = full.entries()?; + + let app_name = if let Some(entry) = entries.by_ref().find(|e| { + e.as_ref() + .is_ok_and(|entry| entry.path().unwrap_or_default() == Path::new("manifest.json")) + }) { + let mut manifest = String::new(); + entry?.read_to_string(&mut manifest)?; + let manifest: Manifest = serde_json::from_str(&manifest)?; + manifest.acap_package_conf.setup.app_name + } else { bail!("Could not find a manifest with the app name"); }; + let package_dir = PathBuf::from("/usr/local/packages").join(app_name); - // TODO: Copy only files that have been updated, e.g. as as decided by comparing the `mtime`. - // TODO: Remove files that are no longer relevant. - // Currently the error when an app is not installed is not very helpful: - // tar: can't change directory to '/usr/local/packages/': No such file or directory - // TODO: Better error when application is not installed - let mut ssh_tar = ssh(user, pass, host); - ssh_tar.args(["tar", "-xvC", package_dir.to_str().unwrap()]); - - ssh_tar.stdin(Stdio::piped()); - ssh_tar.stdout(Stdio::piped()); - debug!("Spawning {ssh_tar:#?}"); - let mut child = spawn(ssh_tar)?; - - child - .stdin - .take() - .unwrap() - .write_all(&partial.into_inner()?)?; - - let stdout = child.stdout.take().unwrap(); - for line in BufReader::new(stdout).lines() { - let line = line?; - if !line.is_empty() { - debug!("Child said {line:?}"); - } + let sftp = session.sftp().context("Creating sftp session")?; + if sftp.stat(&package_dir).is_err() { + bail!("Package doesn't exist!"); } - debug!("Waiting for child"); - let status = child.wait()?; - if !status.success() { - bail!("Child failed {status}"); + let mut full = Archive::new(GzDecoder::new(File::open(package)?)); + + // TODO: Only upload changed files + for entry in full.entries()? { + let mut entry = entry?; + let mut buf = Vec::new(); + let header = entry.header(); + let path = entry.path()?.to_path_buf(); + if path != Path::new("manifest.json") && path != Path::new("package.conf") { + let stat = FileStat { + gid: Some(header.gid()?.try_into()?), + uid: Some(header.uid()?.try_into()?), + perm: Some(header.mode()?), + atime: None, + mtime: Some(header.mtime()?), + size: None, + }; + + if let Ok(Some(link)) = entry.link_name() { + let target = package_dir.join(&path); + + // `file_name` fails if the path ends in '..' or is '/', neither of which should + // be the case for a symlink + if sftp + .readlink(&target) + .is_ok_and(|l| l.file_name().unwrap() == link) + { + debug!("Symlink {target:?} -> {link:?} exists, skipping"); + continue; + } + + debug!("Adding symlink {target:?} -> {link:?}"); + sftp.symlink(&package_dir.join(&link), &target) + .with_context(|| format!("Adding symlink {target:?} -> {link:?}"))?; + + continue; + } + + if header.entry_type().is_dir() { + // If the directory can't be opened, then it doesn't exist so we need to create it. + // TODO: What if permissions has changed? + if sftp.opendir(&path).is_err() { + sftp.mkdir(&header.path()?, header.mode()? as i32) + .with_context(|| format!("Creating directory {:?}", path))?; + } + + continue; + } + + entry.read_to_end(&mut buf)?; + let mut file = sftp + .create(&package_dir.join(&path)) + .with_context(|| format!("Creating {:?}", path))?; + file.write_all(&buf) + .with_context(|| format!("Writing to {:?}", path))?; + file.setstat(stat) + .with_context(|| format!("Updating stat on {:?}", path))?; + } } + Ok(()) } diff --git a/crates/acap-ssh-utils/src/main.rs b/crates/acap-ssh-utils/src/main.rs index df10d879..d9c54546 100644 --- a/crates/acap-ssh-utils/src/main.rs +++ b/crates/acap-ssh-utils/src/main.rs @@ -6,6 +6,8 @@ use anyhow::Context; use clap::{Parser, Subcommand}; use cli_version::version_with_commit_id; use log::debug; +use ssh2::Session; +use std::net::TcpStream; use url::Host; /// Utilities for interacting with Axis devices over SSH. @@ -32,12 +34,11 @@ struct Cli { } impl Cli { - fn exec(self) -> anyhow::Result<()> { - let Self { netloc, command } = self; - match command { - Command::Patch(cmd) => cmd.exec(netloc), - Command::RunApp(cmd) => cmd.exec(netloc), - Command::RunOther(cmd) => cmd.exec(netloc), + fn exec(self, session: &Session, as_root: bool) -> anyhow::Result<()> { + match self.command { + Command::Patch(cmd) => cmd.exec(session), + Command::RunApp(cmd) => cmd.exec(session, as_root), + Command::RunOther(cmd) => cmd.exec(session), } } } @@ -72,8 +73,8 @@ struct Patch { } impl Patch { - fn exec(self, netloc: Netloc) -> anyhow::Result<()> { - patch_package(&self.package, &netloc.user, &netloc.pass, &netloc.host) + fn exec(self, session: &Session) -> anyhow::Result<()> { + patch_package(&self.package, session) } } @@ -92,17 +93,13 @@ struct RunApp { } impl RunApp { - fn exec(self, netloc: Netloc) -> anyhow::Result<()> { + fn exec(self, session: &Session, as_root: bool) -> anyhow::Result<()> { run_package( - &netloc.user, - &netloc.pass, - &netloc.host, + session, &self.package, - self.environment - .iter() - .map(|(k, v)| (k.as_str(), v.as_str())) - .collect(), + &self.environment, &self.args.iter().map(String::as_str).collect::>(), + as_root, ) } } @@ -122,16 +119,11 @@ struct RunOther { } impl RunOther { - fn exec(self, netloc: Netloc) -> anyhow::Result<()> { + fn exec(self, session: &Session) -> anyhow::Result<()> { run_other( &self.package, - &netloc.user, - &netloc.pass, - &netloc.host, - self.environment - .iter() - .map(|(k, v)| (k.as_str(), v.as_str())) - .collect(), + session, + &self.environment, &self.args.iter().map(String::as_str).collect::>(), ) } @@ -158,7 +150,19 @@ fn main() -> anyhow::Result<()> { }; debug!("Logging initialized"); - match Cli::parse().exec() { + let cli = Cli::parse(); + + let host = format!("{}:22", cli.netloc.host); + + let tcp = TcpStream::connect(host).unwrap(); + let mut sess = Session::new().unwrap(); + sess.set_tcp_stream(tcp); + sess.handshake().unwrap(); + + sess.userauth_password(&cli.netloc.user, &cli.netloc.pass) + .unwrap(); + + match Cli::parse().exec(&sess, cli.netloc.user == "root") { Ok(()) => Ok(()), Err(e) => { if let Some(log_file) = log_file { diff --git a/crates/cargo-acap-sdk/Cargo.toml b/crates/cargo-acap-sdk/Cargo.toml index d362d71a..41ce8f97 100644 --- a/crates/cargo-acap-sdk/Cargo.toml +++ b/crates/cargo-acap-sdk/Cargo.toml @@ -18,3 +18,4 @@ acap-ssh-utils = { workspace = true } acap-vapix = { workspace = true } cargo-acap-build = { workspace = true } cli-version = { workspace = true } +ssh2 = { workspace = true } diff --git a/crates/cargo-acap-sdk/src/commands/run_command.rs b/crates/cargo-acap-sdk/src/commands/run_command.rs index 9926e430..5c13f22f 100644 --- a/crates/cargo-acap-sdk/src/commands/run_command.rs +++ b/crates/cargo-acap-sdk/src/commands/run_command.rs @@ -1,5 +1,7 @@ use cargo_acap_build::{AppBuilder, Architecture, Artifact}; use log::debug; +use ssh2::Session; +use std::net::TcpStream; use crate::{BuildOptions, DeployOptions, ResolvedBuildOptions}; @@ -26,27 +28,34 @@ impl RunCommand { pass: password, } = deploy_options; + let host = format!("{}:22", address); + + let tcp = TcpStream::connect(host)?; + let mut session = Session::new()?; + session.set_tcp_stream(tcp); + session.handshake().unwrap(); + + session.userauth_password(&username, &password)?; + let artifacts = AppBuilder::from_targets([Architecture::from(target)]) .args(args) .execute()?; for artifact in artifacts { - let envs = vec![("RUST_LOG", "debug"), ("RUST_LOG_STYLE", "always")] - .into_iter() - .collect(); + let envs = [("RUST_LOG", "debug"), ("RUST_LOG_STYLE", "always")]; match artifact { Artifact::Eap { path, name } => { // TODO: Install instead of patch when needed debug!("Patching app {name}"); - acap_ssh_utils::patch_package(&path, &username, &password, &address)?; + acap_ssh_utils::patch_package(&path, &session)?; debug!("Running app {name}"); - acap_ssh_utils::run_package(&username, &password, &address, &name, envs, &[])? + acap_ssh_utils::run_package(&session, &name, &envs, &[], username != "root")? } Artifact::Exe { path } => { debug!( "Running exe {}", path.file_name().unwrap().to_string_lossy() ); - acap_ssh_utils::run_other(&path, &username, &password, &address, envs, &[])?; + acap_ssh_utils::run_other(&path, &session, &envs, &[])?; } } } diff --git a/crates/cargo-acap-sdk/src/commands/test_command.rs b/crates/cargo-acap-sdk/src/commands/test_command.rs index a091bc30..913f59e8 100644 --- a/crates/cargo-acap-sdk/src/commands/test_command.rs +++ b/crates/cargo-acap-sdk/src/commands/test_command.rs @@ -1,5 +1,7 @@ use cargo_acap_build::{AppBuilder, Architecture, Artifact}; use log::debug; +use ssh2::Session; +use std::net::TcpStream; use crate::{BuildOptions, DeployOptions, ResolvedBuildOptions}; @@ -31,24 +33,33 @@ impl TestCommand { build_args.push("--tests".to_string()); + let tcp = TcpStream::connect(format!("{}:22", address)).unwrap(); + let mut session = Session::new().unwrap(); + session.set_tcp_stream(tcp); + session.handshake().unwrap(); + + session.userauth_password(&username, &password).unwrap(); + let artifacts = AppBuilder::from_targets([Architecture::from(target)]) .args(build_args) .execute()?; for artifact in artifacts { debug!("Running {:?}", artifact); - let envs = vec![("RUST_LOG", "debug"), ("RUST_LOG_STYLE", "always")] - .into_iter() - .collect(); + let envs = [("RUST_LOG", "debug"), ("RUST_LOG_STYLE", "always")]; let test_args = ["--test-threads=1"]; match artifact { Artifact::Eap { path, name } => { // TODO: Install instead of patch when needed debug!("Patching app {name}"); - acap_ssh_utils::patch_package(&path, &username, &password, &address)?; + acap_ssh_utils::patch_package(&path, &session)?; debug!("Running app {name}"); acap_ssh_utils::run_package( - &username, &password, &address, &name, envs, &test_args, + &session, + &name, + &envs, + &test_args, + username != "root", )? } Artifact::Exe { path } => { @@ -56,9 +67,7 @@ impl TestCommand { "Running exe {}", path.file_name().unwrap().to_string_lossy() ); - acap_ssh_utils::run_other( - &path, &username, &password, &address, envs, &test_args, - )?; + acap_ssh_utils::run_other(&path, &session, &envs, &test_args)?; } } }