-
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?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, I've been wanting to do this for a long time.
This review focused on the functionality of the binary crates and the interface of the library crate. I plan to take a closer look at the implementation once it is working.
With this we can probably remove sshpass from the dev container too |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies for the slow feedback.
* upstream/main: chore: Update rust toolchain to 1.85.1 (AxisCommunications#175)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bed time; will take another look tomorrow.
crates/acap-ssh-utils/src/lib.rs
Outdated
let mut stat = sftp.stat(path)?; | ||
let sftp = session.sftp().context("Creating sftp session")?; | ||
sftp.create(path) | ||
.context(format!("Creating {:?}", path))? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using with_context
to avoid creating these strings eagerly.
let sftp = session.sftp()?; | ||
sftp.create(path)?.write_all(&std::fs::read(prog)?)?; | ||
let mut stat = sftp.stat(path)?; | ||
let sftp = session.sftp().context("Creating sftp session")?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I'm usually too lazy to add context
, but it really does help when troubleshooting.
crates/acap-ssh-utils/src/lib.rs
Outdated
} | ||
cmd.arg(executable); | ||
if let Some(args) = args { | ||
cmd.arg("--"); // They should be passed to the process, not to 'su' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tested this?
I don't have a camera at hand, but su --help
on Debian does not mention that --
can be used and experimenting seems to confirm this:
$ su --help
Usage:
su [options] [-] [<user> [<argument>...]]
...
$ env --help
env --help
Usage: env [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]
...
$ su root -c env -- --help
SHELL=/bin/bash
...
I understand the camera might differ, so this is just to double check.
I will make sure to get a camera set up in the next week so that I can check things like this myself and not have to bother you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. When I was running the tests it was complaining since we pass --test-threads=1
to the test. Adding the '--' suppressed this but that's not the correct way to do it. We need to pass the executable with its arguments as a whole, i.e. as a string:
su root -c "env --help"
I've fixed that now and added a test in inspect_env
to make sure this stays working.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR replaces the usage of std::process::Command for SSH operations with the ssh2 crate, aiming to simplify and standardize remote command execution and file transfers. Key changes include:
- Establishing SSH sessions using ssh2::Session and TcpStream in both test and run commands.
- Updating acap-ssh-utils to use ssh2 for executing remote commands and performing SFTP operations.
- Adding the ssh2 dependency across the workspace Cargo.toml files.
Reviewed Changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
File | Description |
---|---|
crates/cargo-acap-sdk/src/commands/test_command.rs | Migrated SSH command execution to ssh2. |
crates/cargo-acap-sdk/src/commands/run_command.rs | Converted process-based SSH interactions to ssh2 sessions. |
crates/cargo-acap-sdk/Cargo.toml | Added ssh2 dependency. |
crates/acap-ssh-utils/src/main.rs | Updated CLI execution to utilize ssh2 sessions. |
crates/acap-ssh-utils/src/lib.rs | Redesigned remote command execution and file transfer routines using ssh2. |
crates/acap-ssh-utils/Cargo.toml | Added ssh2 dependency. |
Cargo.toml | Included ssh2 dependency for the main project. |
Files not reviewed (1)
- .devhost/install-system-packages.sh: Language not supported
Comments suppressed due to low confidence (1)
crates/acap-ssh-utils/src/lib.rs:50
- Using debug formatting ("{cmd:?}") to construct the remote command string may introduce unintended quotes or formatting artifacts; consider using a standard string formatting method to build the command.
cmd: format("{cmd:?}"),
sess.userauth_password(&cli.netloc.user, &cli.netloc.pass) | ||
.unwrap(); | ||
|
||
match Cli::parse().exec(&sess, cli.netloc.user == "root") { |
Copilot
AI
Apr 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.
match Cli::parse().exec(&sess, cli.netloc.user == "root") { | |
match cli.exec(&sess, cli.netloc.user == "root") { |
Copilot uses AI. Check for mistakes.
if n == 0 { | ||
break; | ||
} | ||
print!("{}", std::str::from_utf8(&buf[..n])?); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is there a print here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the stdout from the (remote) process being redirected to the stdout of the local process (acap-ssh-utils/cargo-acap-sdk)
/// `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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I reading this right?
as_root=true
means the program is never executed as root.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doh. It should of course be the other way around. There's no converse to then
for false
so I'll just invert the parameter: as_package_user
.
.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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
inspect_env
has a couple of symlinks, so that could be useful for testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not. I'll fix that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry that I have been slow to review this PR repeatedly, I will try to be quicker in the future.
However, going forward I would like to ask that you change one thing at a time when feasible. That way it will be easier for me to respond in a timely manner and we will hopefully not need as many iterations. I think some of the suspected regressions that I have pointed out could have been avoided, or at least deferred, if this PR only replaced Command
with ssh2
and did not also rework the way we build the command that is run on the remote.
I appreciate your contributions across this project and I hope the above blunt feedback does not discourage you from finishing this contribution and contributing again in the future 😊
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I hope the above blunt feedback does not discourage you ...
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.
When I try this, it doesn't work. Are you able to reproduce the error I see?
No I'm not, however I have another issue: there's no output from the program. As you rightly pointed, patch_package
didn't work with symlinks.
* upstream/main: fix(axstorage)! Return `GString` from `get_path` (AxisCommunications#170) fix(inspect_app): Print environment variables individually (AxisCommunications#181) chore(deps): bump thiserror from 1.0.63 to 2.0.12 (AxisCommunications#178) chore(deps): bump openssl from 0.10.64 to 0.10.71 (AxisCommunications#177) chore(deps): bump ghcr.io/devcontainers/features/common-utils (AxisCommunications#179) chore(deps): bump glib and glib-sys to 0.20.9 (AxisCommunications#180) Provide a futures_lite::Stream implementation for event subscription (AxisCommunications#174) chore(deps): bump the default group across 1 directory with 22 updates (AxisCommunications#173)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just skimmed it this time because I'm not sure if it's ready for another round of reviews. So please let me know if/when it is.
PathBuf::from("/usr/local/packages").join(PACKAGE_NAME) | ||
} | ||
|
||
#[test] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of adding more automated tests.
However, the intention behind these tests is not only to ensure that apps are invoked as expected using the run
and test
commands, but also to help ensure that the way these commands invoke our program is the same as the way the ACAP system in the camera invokes our program. As such, these should all pass if the app is installed and started the "normal" way e.g. using the GUI.
My thinking is roughly:
- We create tests to verify some functionality
- The important thing is whether that functionality works as intended when the app is used as intended, i.e. installed and started via official non-dev APIs.
- Therefore our tests should all pass when under the same conditions.
So these should give the same results (all tests passing):
cargo-acap-sdk install -- -p inspect_env --tests && cargo-acap-sdk start -p inspect_env
cargo-acap-sdk test -- -p inspect_env
That being said, as I was writing the above I realized that this is not true for tests that cannot run in parallel, which is why I added --test-threads=1
in the first place. My best idea for how to solve this right now is to establish the convention that tests that require --test-threads=1
should be ignore
d and that the test
command also sets --ignored
. Not fantastic, but could work.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another idea is to rewrite mutually exclusive tests as integration tests. This probably works out of the box if the crate has an ACAP manifest in the Cargo manifest directory, as all the apps do.
Remove the use of std::process::Command("ssh") and use ssh2 create instead. It's a bit clunky when it comes to creating processes but it gets the job done.