-
Notifications
You must be signed in to change notification settings - Fork 6
Use ssh2 instead of std::process::Command #172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 15 commits
ed6d619
c5811d8
61d992c
a566f08
ee2828c
327dc82
7bc38a0
c236986
38a275d
537e846
1cbac37
1d8fd32
ede5781
193268f
6cb7f19
f832a11
5580324
6df6689
67ad2e5
ac11ef6
d1f8363
1f2dd63
5e8ff7a
feecdbb
ecd277e
29aa98b
3020e38
7afa257
1757a27
020cfb9
94ee183
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,11 +7,11 @@ use std::{ | |
path::{Path, PathBuf}, | ||
}; | ||
|
||
use anyhow::bail; | ||
use anyhow::{bail, Context}; | ||
use flate2::read::GzDecoder; | ||
use tar::Archive; | ||
|
||
use ssh2::{Channel, FileStat, Session}; | ||
use ssh2::{FileStat, Session}; | ||
|
||
use crate::acap::Manifest; | ||
|
||
|
@@ -21,14 +21,14 @@ struct RemoteCommand { | |
|
||
impl RemoteCommand { | ||
pub fn new( | ||
user: Option<&str>, | ||
user: Option<impl AsRef<str>>, | ||
env: Option<&[(impl AsRef<str>, impl AsRef<str>)]>, | ||
executable: &str, | ||
args: Option<&[&str]>, | ||
) -> Self { | ||
let mut cmd = if let Some(user) = user { | ||
let mut cmd = std::process::Command::new("su"); | ||
cmd.arg(user); | ||
cmd.arg(user.as_ref()); | ||
cmd | ||
} else { | ||
std::process::Command::new("sh") | ||
|
@@ -39,9 +39,11 @@ impl RemoteCommand { | |
if let Some(env) = env { | ||
cmd.envs(env.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))); | ||
apljungquist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
cmd.env("G_SLICE", "always-malloc"); | ||
|
||
cmd.arg(executable); | ||
if let Some(args) = args { | ||
cmd.arg("--"); // They should be passed to the process, not to 'su' | ||
|
||
cmd.args(args); | ||
} | ||
|
||
|
@@ -50,7 +52,10 @@ impl RemoteCommand { | |
} | ||
} | ||
|
||
pub fn exec(&self, channel: &mut Channel) -> Result<(), anyhow::Error> { | ||
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]; | ||
|
@@ -63,15 +68,35 @@ impl RemoteCommand { | |
stdout.flush()?; | ||
} | ||
|
||
channel.wait_eof()?; | ||
channel.wait_close()?; | ||
let code = channel.exit_status()?; | ||
|
||
if code != 0 { | ||
bail!("{} exited with status {}", self.cmd, code); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
pub fn exec_capture_stdout(&self, channel: &mut Channel) -> Result<String, anyhow::Error> { | ||
pub fn exec_capture_stdout(&self, session: &Session) -> Result<Vec<u8>, 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); | ||
apljungquist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let mut buf = [0; 4096]; | ||
let n = stdout.read(&mut buf)?; | ||
Ok(std::str::from_utf8(&buf[..n])?.to_string()) | ||
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) | ||
} | ||
} | ||
|
||
|
@@ -92,16 +117,32 @@ pub fn run_other<S: AsRef<str>>( | |
env: &[(S, S)], | ||
apljungquist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
args: &[&str], | ||
) -> anyhow::Result<()> { | ||
let sftp = session.sftp()?; | ||
let mut channel = session.channel_session()?; | ||
let tmp = RemoteCommand::new(None::<&str>, None::<&[(&str, &str)]>, "mktemp -u", None) | ||
.exec_capture_stdout(session)?; | ||
|
||
let tmp = RemoteCommand::new(None, None::<&[(&str, &str)]>, "mktemp", None) | ||
.exec_capture_stdout(&mut channel)?; | ||
// The output from `mktemp -u` contains a trailing '\n' | ||
let path = std::str::from_utf8(&tmp)?.strip_suffix('\n').unwrap(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is essentially parsing input from a source that we don't control, so I don't think we should panic if it doesn't conform to our expectations. |
||
|
||
sftp.create(Path::new(&tmp))? | ||
.write_all(&std::fs::read(prog)?)?; | ||
{ | ||
let path = Path::new(&path); | ||
|
||
RemoteCommand::new(None, Some(env), &tmp, Some(args)).exec(&mut channel) | ||
let sftp = session.sftp().context("Creating sftp session")?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! I'm usually too lazy to add |
||
sftp.create(path) | ||
.context(format!("Creating {:?}", path))? | ||
|
||
.write_all(&std::fs::read(prog)?) | ||
.context(format!("Writing {:?}", prog))?; | ||
let mut stat = sftp | ||
.stat(path) | ||
.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) | ||
.context(format!("Updating stat on {:?}", path))?; | ||
} | ||
|
||
RemoteCommand::new(None::<&str>, Some(env), path, Some(args)).exec(session) | ||
} | ||
|
||
// TODO: Consider abstracting away the difference between devices that support developer mode, and | ||
|
@@ -123,17 +164,16 @@ pub fn run_package<S: AsRef<str>>( | |
package: &str, | ||
env: &[(S, S)], | ||
args: &[&str], | ||
as_root: bool, | ||
kjughx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
) -> anyhow::Result<()> { | ||
let mut channel = session.channel_session()?; | ||
let cmd = RemoteCommand::new( | ||
Some(&format!("acap-{package}")), | ||
as_root.then(|| format!("acap-{package}")), | ||
Some(env), | ||
&format!("/usr/local/packages/{package}/{package}"), | ||
Some(args), | ||
); | ||
channel.handle_extended_data(ssh2::ExtendedData::Merge)?; | ||
|
||
cmd.exec(&mut channel) | ||
cmd.exec(session) | ||
} | ||
|
||
/// Update ACAP app on device without installing it | ||
|
@@ -151,10 +191,13 @@ pub fn patch_package(package: &Path, session: &Session) -> anyhow::Result<()> { | |
let mut full = Archive::new(GzDecoder::new(File::open(package)?)); | ||
let mut entries = full.entries()?; | ||
|
||
let app_name = if let Some(entry) = entries | ||
.by_ref() | ||
.find(|e| e.as_ref().unwrap().path().unwrap_or_default() == Path::new("manifest.json")) | ||
{ | ||
let app_name = if let Some(entry) = entries.by_ref().find(|e| { | ||
e.as_ref() | ||
.expect("The entry should be valid") | ||
apljungquist marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
.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)?; | ||
|
@@ -164,9 +207,9 @@ pub fn patch_package(package: &Path, session: &Session) -> anyhow::Result<()> { | |
}; | ||
|
||
let package_dir = PathBuf::from("/usr/local/packages").join(app_name); | ||
let sftp = session.sftp()?; | ||
let sftp = session.sftp().context("Creating sftp session")?; | ||
if sftp.stat(&package_dir).is_err() { | ||
return Err(anyhow::anyhow!("Package doesn't exist")); | ||
bail!("Package doesn't exist!"); | ||
} | ||
|
||
let mut full = Archive::new(GzDecoder::new(File::open(package)?)); | ||
|
@@ -186,11 +229,26 @@ pub fn patch_package(package: &Path, session: &Session) -> anyhow::Result<()> { | |
mtime: Some(header.mtime()?), | ||
size: None, | ||
}; | ||
_ = entry.read_to_end(&mut buf)?; | ||
println!("Writing file: {:?}", entry.path()?); | ||
let mut file = sftp.create(&package_dir.join(&entry.path()?))?; | ||
file.write_all(&buf)?; | ||
file.setstat(stat)?; | ||
|
||
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(entry.path()?).is_err() { | ||
sftp.mkdir(&header.path()?, header.mode()? as i32) | ||
.context(format!("Creating directory {:?}", entry.path()?))?; | ||
} | ||
|
||
continue; | ||
} | ||
|
||
entry.read_to_end(&mut buf)?; | ||
let mut file = sftp | ||
.create(&package_dir.join(&entry.path()?)) | ||
.context(format!("Creating {:?}", entry.path()?))?; | ||
file.write_all(&buf) | ||
.context(format!("Writing to {:?}", entry.path()?))?; | ||
file.setstat(stat) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this work if the app contains files that are symlinks? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does not. I'll fix that. |
||
.context(format!("Updating stat on {:?}", entry.path()?))?; | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -34,10 +34,10 @@ struct Cli { | |||||
} | ||||||
|
||||||
impl Cli { | ||||||
fn exec(self, session: &Session) -> anyhow::Result<()> { | ||||||
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), | ||||||
Command::RunApp(cmd) => cmd.exec(session, as_root), | ||||||
Command::RunOther(cmd) => cmd.exec(session), | ||||||
} | ||||||
} | ||||||
|
@@ -93,12 +93,13 @@ struct RunApp { | |||||
} | ||||||
|
||||||
impl RunApp { | ||||||
fn exec(self, session: &Session) -> anyhow::Result<()> { | ||||||
fn exec(self, session: &Session, as_root: bool) -> anyhow::Result<()> { | ||||||
run_package( | ||||||
session, | ||||||
&self.package, | ||||||
&self.environment, | ||||||
&self.args.iter().map(String::as_str).collect::<Vec<_>>(), | ||||||
as_root, | ||||||
) | ||||||
} | ||||||
} | ||||||
|
@@ -151,10 +152,7 @@ fn main() -> anyhow::Result<()> { | |||||
|
||||||
let cli = Cli::parse(); | ||||||
|
||||||
let mut host = cli.netloc.host.to_string(); | ||||||
if !host.contains(":") { | ||||||
host.push_str(":22"); | ||||||
} | ||||||
let host = format!("{}:22", cli.netloc.host); | ||||||
|
||||||
let tcp = TcpStream::connect(host).unwrap(); | ||||||
let mut sess = Session::new().unwrap(); | ||||||
apljungquist marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
@@ -163,9 +161,8 @@ fn main() -> anyhow::Result<()> { | |||||
|
||||||
sess.userauth_password(&cli.netloc.user, &cli.netloc.pass) | ||||||
.unwrap(); | ||||||
sess.set_blocking(false); | ||||||
|
||||||
match Cli::parse().exec(&sess) { | ||||||
match Cli::parse().exec(&sess, cli.netloc.user == "root") { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The CLI arguments are parsed twice; consider reusing the 'cli' variable that is already stored to avoid potential inconsistencies.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||
Ok(()) => Ok(()), | ||||||
Err(e) => { | ||||||
if let Some(log_file) = log_file { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I try this, it doesn't work. Are you able to reproduce the error I see? $ git checkout 5e8ff7a967a604fc16938de5daedb95e25498cc8
Previous HEAD position was efc9365 Provide a futures_lite::Stream implementation for event subscription (#174)
HEAD is now at 5e8ff7a Use is_ok_and instead of expect
$ cargo build --bin cargo-acap-sdk
# ...
$ ./target/debug/cargo-acap-sdk run -- -p inspect_env
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.50s
Error: G_SLICE="always-malloc" RUST_LOG="debug" RUST_LOG_STYLE="always" "su" "acap-inspect_env" "-c" "/usr/local/packages/inspect_env/inspect_env" "--" exited with status 1 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Of course not, I agree this review got a bit out of hand. If you want I can try and split it up into easier to digest PRs.
No I'm not, however I have another issue: there's no output from the program. As you rightly pointed, |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,10 +28,8 @@ impl RunCommand { | |
pass: password, | ||
} = deploy_options; | ||
|
||
let mut host = address.to_string(); | ||
if !host.contains(":") { | ||
host.push_str(":22"); | ||
} | ||
let host = format!("{}:22", address); | ||
|
||
let tcp = TcpStream::connect(host).unwrap(); | ||
|
||
let mut session = Session::new().unwrap(); | ||
session.set_tcp_stream(tcp); | ||
|
@@ -43,18 +41,14 @@ impl RunCommand { | |
.args(args) | ||
.execute()?; | ||
for artifact in artifacts { | ||
let envs = [ | ||
("RUST_LOG", "debug"), | ||
("RUST_LOG_STYLE", "always"), | ||
("G_SLICE", "always-malloc"), | ||
]; | ||
let envs = [("RUST_LOG", "debug"), ("RUST_LOG_STYLE", "always")]; | ||
match artifact { | ||
Artifact::Eap { path, name } => { | ||
// TODO: Install instead of patch when needed | ||
println!("Patching app {name}"); | ||
debug!("Patching app {name}"); | ||
acap_ssh_utils::patch_package(&path, &session)?; | ||
println!("Running app {name}"); | ||
acap_ssh_utils::run_package(&session, &name, &envs, &[])? | ||
debug!("Running app {name}"); | ||
acap_ssh_utils::run_package(&session, &name, &envs, &[], username == "root")? | ||
} | ||
Artifact::Exe { path } => { | ||
debug!( | ||
|
Uh oh!
There was an error while loading. Please reload this page.