use std::path::PathBuf;
use std::time::Duration;
use bevy::app::ScheduleRunnerPlugin;
use bevy::log::LogPlugin;
use bevy::prelude::*;
use bevy_net_backend::prelude::*;
use bevy_net_backend::{SshAuth, SshCommand, SshTarget};
#[allow(dead_code)]
#[path = "mock_ssh_server.rs"]
mod mock_ssh_server;
#[derive(Resource)]
struct Target(SshTarget);
#[derive(Resource, Default)]
struct Open(Vec<RequestId>);
fn main() -> AppExit {
let var = |name: &str| std::env::var(name).ok().filter(|v| !v.is_empty());
let (target, _mock) = match (var("SSH_HOST"), var("SSH_USER"), var("SSH_KEY"), var("SSH_KNOWN_HOSTS")) {
(Some(host), Some(user), Some(key), Some(known_hosts)) => {
(SshTarget::new(host, user).with_auth(SshAuth::key_file(key)).with_known_hosts_file(known_hosts), None)
}
_ => match start_mock() {
Ok(started) => started,
Err(e) => {
eprintln!("could not start the mock SSH server: {e}");
return AppExit::error();
}
},
};
App::new()
.add_plugins((
MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_millis(10))),
LogPlugin::default(),
BackendPlugin::default(),
))
.insert_resource(Target(target))
.init_resource::<Open>()
.add_systems(Startup, connect)
.add_systems(Update, (on_state, on_output, on_finished).chain())
.add_systems(Update, give_up)
.add_systems(Update, on_sftp.run_if(|| cfg!(feature = "sftp")))
.run()
}
fn start_mock() -> std::io::Result<(SshTarget, Option<mock_ssh_server::MockSshServer>)> {
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target").join("ssh-example");
let key = mock_ssh_server::random_key();
let key_path = dir.join("id_example");
mock_ssh_server::write_key(&key, &key_path, None)?;
let mock = mock_ssh_server::MockSshServer::start("tester", key.public_key().clone())?;
let known_hosts = dir.join("known_hosts");
std::fs::write(&known_hosts, format!("{}\n", mock.known_hosts_line("127.0.0.1")))?;
let target = SshTarget::new("127.0.0.1", "tester").with_port(mock.port()).with_auth(SshAuth::key_file(key_path)).with_known_hosts_file(known_hosts);
Ok((target, Some(mock)))
}
fn connect(ssh: Res<SshClient>, target: Res<Target>) {
ssh.connect("main", target.0.clone());
}
fn on_state(mut changes: MessageReader<SshStateChanged>, ssh: Res<SshClient>, mut open: ResMut<Open>, mut exit: MessageWriter<AppExit>) {
for change in changes.read() {
match (&change.state, &change.error) {
(SshState::Connected, _) => {
info!("`{}` connected", change.name);
open.0.push(ssh.run(&change.name, "uname -a"));
open.0.push(ssh.run(&change.name, "echo hello from Bevy"));
open.0.push(ssh.run(&change.name, SshCommand::new("fail").with_timeout(Duration::from_secs(5))));
#[cfg(feature = "sftp")]
open.0.extend(sftp_step(&ssh, 0));
}
(state, Some(error)) => {
error!("`{}` is {state:?}: {error}", change.name);
exit.write(AppExit::error());
}
(state, None) => info!("`{}` is {state:?}", change.name),
}
}
}
fn on_output(mut output: MessageReader<SshOutput>) {
for chunk in output.read() {
info!("{} {:?}: {}", chunk.id, chunk.stream, chunk.text().trim_end());
}
}
fn done(open: &mut Open, id: RequestId, ssh: &SshClient, exit: &mut MessageWriter<AppExit>) {
open.0.retain(|o| *o != id);
if open.0.is_empty() {
info!("all done");
ssh.disconnect("main");
exit.write(AppExit::Success);
}
}
fn on_finished(mut finished: MessageReader<SshFinished>, mut open: ResMut<Open>, ssh: Res<SshClient>, mut exit: MessageWriter<AppExit>) {
for answer in finished.read() {
match &answer.result {
Ok(end) => info!("{} ended with status {:?} (signal {:?})", answer.id, end.status, end.signal),
Err(error) => warn!("{} failed: {error} (started: {:?})", answer.id, answer.started),
}
done(&mut open, answer.id, &ssh, &mut exit);
}
}
#[cfg(feature = "sftp")]
fn sftp_step(ssh: &SshClient, step: usize) -> Option<RequestId> {
let file = "bnb-example.txt";
match step {
0 => Some(ssh.upload("main", file, b"written by the ssh_console example".to_vec())),
1 => Some(ssh.list_dir("main", ".")),
2 => Some(ssh.download("main", file)),
3 => Some(ssh.remove_file("main", file)),
_ => None,
}
}
#[cfg(feature = "sftp")]
fn on_sftp(mut finished: MessageReader<SftpFinished>, mut open: ResMut<Open>, mut step: Local<usize>, ssh: Res<SshClient>, mut exit: MessageWriter<AppExit>) {
for answer in finished.read() {
*step += 1;
open.0.extend(sftp_step(&ssh, *step));
match &answer.result {
Ok(SftpOutcome::Data(data)) => info!("{} downloaded: {}", answer.id, String::from_utf8_lossy(data)),
Ok(SftpOutcome::Listing(entries)) => info!("{} listing: {:?}", answer.id, entries.iter().map(|e| e.name.as_str()).collect::<Vec<_>>()),
Ok(outcome) => info!("{} done: {outcome:?}", answer.id),
Err(error) => warn!("{} failed: {error}", answer.id),
}
done(&mut open, answer.id, &ssh, &mut exit);
}
}
#[cfg(not(feature = "sftp"))]
fn on_sftp() {}
fn give_up(time: Res<Time<Real>>, mut exit: MessageWriter<AppExit>) {
if time.elapsed() > Duration::from_secs(10) {
error!("not done after 10 s");
exit.write(AppExit::error());
}
}