mirror of
https://github.com/wez/wezterm.git
synced 2024-12-23 05:12:40 +03:00
wezterm-client: cut over to new wezterm-ssh bits
refs: https://github.com/wez/wezterm/issues/507
This commit is contained in:
parent
e96011bc26
commit
369888c94e
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -2165,7 +2165,6 @@ dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"smol",
|
||||
"ssh2",
|
||||
"sysinfo",
|
||||
"terminfo",
|
||||
"termwiz",
|
||||
@ -4399,12 +4398,12 @@ dependencies = [
|
||||
"rangeset",
|
||||
"ratelim",
|
||||
"smol",
|
||||
"ssh2",
|
||||
"termwiz",
|
||||
"textwrap 0.13.4",
|
||||
"thiserror",
|
||||
"uds_windows",
|
||||
"url",
|
||||
"wezterm-ssh",
|
||||
"wezterm-term",
|
||||
]
|
||||
|
||||
|
@ -26,7 +26,6 @@ ratelim= { path = "../ratelim" }
|
||||
regex = "1"
|
||||
serde = {version="1.0", features = ["rc", "derive"]}
|
||||
smol = "1.2"
|
||||
ssh2 = "0.9"
|
||||
terminfo = "0.7"
|
||||
termwiz = { path = "../termwiz" }
|
||||
textwrap = "0.13"
|
||||
|
221
mux/src/ssh.rs
221
mux/src/ssh.rs
@ -11,10 +11,8 @@ use filedescriptor::{socketpair, FileDescriptor};
|
||||
use portable_pty::cmdbuilder::CommandBuilder;
|
||||
use portable_pty::{ExitStatus, MasterPty, PtySize};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use std::collections::{BTreeMap, HashMap, VecDeque};
|
||||
use std::io::{BufWriter, Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
|
||||
use std::time::Duration;
|
||||
@ -53,187 +51,88 @@ impl LineEditorHost for PasswordPromptHost {
|
||||
}
|
||||
}
|
||||
|
||||
impl ssh2::KeyboardInteractivePrompt for ConnectionUI {
|
||||
fn prompt<'b>(
|
||||
&mut self,
|
||||
_username: &str,
|
||||
instructions: &str,
|
||||
prompts: &[ssh2::Prompt<'b>],
|
||||
) -> Vec<String> {
|
||||
prompts
|
||||
.iter()
|
||||
.map(|p| {
|
||||
self.output_str(&format!("{}\n", instructions));
|
||||
if p.echo {
|
||||
self.input(&p.text)
|
||||
} else {
|
||||
self.password(&p.text)
|
||||
}
|
||||
.unwrap_or_else(|_| String::new())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ssh_connect_with_ui(
|
||||
remote_address: &str,
|
||||
username: &str,
|
||||
ui: &mut ConnectionUI,
|
||||
) -> anyhow::Result<ssh2::Session> {
|
||||
) -> anyhow::Result<Session> {
|
||||
let cloned_ui = ui.clone();
|
||||
cloned_ui.run_and_log_error(move || {
|
||||
let mut sess = ssh2::Session::new()?;
|
||||
let mut ssh_config = wezterm_ssh::Config::new();
|
||||
ssh_config.add_default_config_files();
|
||||
|
||||
let (remote_address, remote_host_name, port) = {
|
||||
let (remote_host_name, port) = {
|
||||
let parts: Vec<&str> = remote_address.split(':').collect();
|
||||
|
||||
if parts.len() == 2 {
|
||||
(remote_address.to_string(), parts[0], parts[1].parse()?)
|
||||
(parts[0], Some(parts[1].parse::<u16>()?))
|
||||
} else {
|
||||
(format!("{}:22", remote_address), remote_address, 22)
|
||||
(remote_address, None)
|
||||
}
|
||||
};
|
||||
|
||||
let mut ssh_config = ssh_config.for_host(&remote_host_name);
|
||||
ssh_config.insert("user".to_string(), username.to_string());
|
||||
if let Some(port) = port {
|
||||
ssh_config.insert("port".to_string(), port.to_string());
|
||||
}
|
||||
|
||||
ui.output_str(&format!("Connecting to {} using SSH\n", remote_address));
|
||||
let (session, events) = Session::connect(ssh_config.clone())?;
|
||||
|
||||
let tcp = TcpStream::connect(&remote_address)
|
||||
.with_context(|| format!("ssh connecting to {}", remote_address))?;
|
||||
ui.output_str("SSH: Connected OK!\n");
|
||||
tcp.set_nodelay(true)?;
|
||||
sess.set_tcp_stream(tcp);
|
||||
sess.handshake()
|
||||
.with_context(|| format!("ssh handshake with {}", remote_address))?;
|
||||
|
||||
if let Ok(mut known_hosts) = sess.known_hosts() {
|
||||
let varname = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
|
||||
let var = std::env::var_os(varname)
|
||||
.ok_or_else(|| anyhow!("environment variable {} is missing", varname))?;
|
||||
let file = Path::new(&var).join(".ssh/known_hosts");
|
||||
if file.exists() {
|
||||
known_hosts
|
||||
.read_file(&file, ssh2::KnownHostFileKind::OpenSSH)
|
||||
.with_context(|| format!("reading known_hosts file {}", file.display()))?;
|
||||
}
|
||||
|
||||
let (key, key_type) = sess
|
||||
.host_key()
|
||||
.ok_or_else(|| anyhow!("failed to get ssh host key"))?;
|
||||
|
||||
let fingerprint = sess
|
||||
.host_key_hash(ssh2::HashType::Sha256)
|
||||
.map(|fingerprint| {
|
||||
format!(
|
||||
"SHA256:{}",
|
||||
base64::encode_config(
|
||||
fingerprint,
|
||||
base64::Config::new(base64::CharacterSet::Standard, false)
|
||||
)
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
// Querying for the Sha256 can fail if for example we were linked
|
||||
// against libssh < 1.9, so let's fall back to Sha1 in that case.
|
||||
sess.host_key_hash(ssh2::HashType::Sha1).map(|fingerprint| {
|
||||
let mut res = vec![];
|
||||
write!(&mut res, "SHA1").ok();
|
||||
for b in fingerprint {
|
||||
write!(&mut res, ":{:02x}", *b).ok();
|
||||
}
|
||||
String::from_utf8(res).unwrap()
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| anyhow!("failed to get host fingerprint"))?;
|
||||
|
||||
use ssh2::CheckResult;
|
||||
match known_hosts.check_port(&remote_host_name, port, key) {
|
||||
CheckResult::Match => {}
|
||||
CheckResult::NotFound => {
|
||||
ui.output_str(&format!(
|
||||
"SSH host {} is not yet trusted.\n\
|
||||
{:?} Fingerprint: {}.\n\
|
||||
Trust and continue connecting?\n",
|
||||
remote_address, key_type, fingerprint
|
||||
));
|
||||
|
||||
loop {
|
||||
let line = ui.input("Enter [Y/n]> ")?;
|
||||
|
||||
while let Ok(event) = smol::block_on(events.recv()) {
|
||||
match event {
|
||||
SessionEvent::Banner(banner) => {
|
||||
if let Some(banner) = banner {
|
||||
ui.output_str(&format!("{}\n", banner));
|
||||
}
|
||||
}
|
||||
SessionEvent::HostVerify(verify) => {
|
||||
ui.output_str(&format!("{}\n", verify.message));
|
||||
let ok = if let Ok(line) = ui.input("Enter [y/n]> ") {
|
||||
match line.as_ref() {
|
||||
"y" | "Y" | "yes" | "YES" => break,
|
||||
"n" | "N" | "no" | "NO" => bail!("user declined to trust host"),
|
||||
_ => continue,
|
||||
"y" | "Y" | "yes" | "YES" => true,
|
||||
"n" | "N" | "no" | "NO" | _ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
smol::block_on(verify.answer(ok)).context("send verify response")?;
|
||||
}
|
||||
SessionEvent::Authenticate(auth) => {
|
||||
if !auth.username.is_empty() {
|
||||
ui.output_str(&format!("Authentication for {}\n", auth.username));
|
||||
}
|
||||
if !auth.instructions.is_empty() {
|
||||
ui.output_str(&format!("{}\n", auth.instructions));
|
||||
}
|
||||
let mut answers = vec![];
|
||||
for prompt in &auth.prompts {
|
||||
let mut prompt_lines = prompt.prompt.split('\n').collect::<Vec<_>>();
|
||||
let editor_prompt = prompt_lines.pop().unwrap();
|
||||
for line in &prompt_lines {
|
||||
ui.output_str(&format!("{}\n", line));
|
||||
}
|
||||
let res = if prompt.echo {
|
||||
ui.input(editor_prompt)
|
||||
} else {
|
||||
ui.password(editor_prompt)
|
||||
};
|
||||
if let Ok(line) = res {
|
||||
answers.push(line);
|
||||
} else {
|
||||
anyhow::bail!("Authentication was cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
known_hosts
|
||||
.add(remote_host_name, key, &remote_address, key_type.into())
|
||||
.context("adding known_hosts entry in memory")?;
|
||||
|
||||
known_hosts
|
||||
.write_file(&file, ssh2::KnownHostFileKind::OpenSSH)
|
||||
.with_context(|| format!("writing known_hosts file {}", file.display()))?;
|
||||
smol::block_on(auth.answer(answers))?;
|
||||
}
|
||||
CheckResult::Mismatch => {
|
||||
ui.output_str(&format!(
|
||||
"🛑 host key mismatch for ssh server {}.\n\
|
||||
Got fingerprint {} instead of expected value from known_hosts\n\
|
||||
file {}.\n\
|
||||
Refusing to connect.\n",
|
||||
remote_address,
|
||||
fingerprint,
|
||||
file.display()
|
||||
));
|
||||
bail!("host mismatch, man in the middle attack?!");
|
||||
}
|
||||
CheckResult::Failure => {
|
||||
ui.output_str("🛑 Failed to load and check known ssh hosts\n");
|
||||
bail!("failed to check the known hosts");
|
||||
SessionEvent::Error(err) => {
|
||||
anyhow::bail!("Error: {}", err);
|
||||
}
|
||||
SessionEvent::Authenticated => return Ok(session),
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..3 {
|
||||
if sess.authenticated() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Re-query the auth methods on each loop as a successful method
|
||||
// may unlock a new method on a subsequent iteration (eg: password
|
||||
// auth may then unlock 2fac)
|
||||
let methods: HashSet<&str> = sess.auth_methods(&username)?.split(',').collect();
|
||||
log::trace!("ssh auth methods: {:?}", methods);
|
||||
|
||||
if !sess.authenticated() && methods.contains("publickey") {
|
||||
if let Err(err) = sess.userauth_agent(&username) {
|
||||
log::warn!("while attempting agent auth: {}", err);
|
||||
} else if sess.authenticated() {
|
||||
ui.output_str("publickey auth successful!\n");
|
||||
}
|
||||
}
|
||||
|
||||
if !sess.authenticated() && methods.contains("password") {
|
||||
ui.output_str(&format!(
|
||||
"Password authentication for {}@{}\n",
|
||||
username, remote_address
|
||||
));
|
||||
let pass = ui.password("🔐 Password: ")?;
|
||||
if let Err(err) = sess.userauth_password(username, &pass) {
|
||||
log::error!("while attempting password auth: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if !sess.authenticated() && methods.contains("keyboard-interactive") {
|
||||
if let Err(err) = sess.userauth_keyboard_interactive(&username, ui) {
|
||||
log::error!("while attempting keyboard-interactive auth: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !sess.authenticated() {
|
||||
bail!("unable to authenticate session");
|
||||
}
|
||||
|
||||
Ok(sess)
|
||||
bail!("unable to authenticate session");
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -19,16 +19,16 @@ lru = "0.6"
|
||||
metrics = { version="0.14", features=["std"]}
|
||||
mux = { path = "../mux" }
|
||||
openssl = "0.10"
|
||||
portable-pty = { path = "../pty", features = ["serde_support", "ssh"]}
|
||||
portable-pty = { path = "../pty", features = ["serde_support"]}
|
||||
promise = { path = "../promise" }
|
||||
rangeset = { path = "../rangeset" }
|
||||
ratelim= { path = "../ratelim" }
|
||||
smol = "1.2"
|
||||
ssh2 = "0.9"
|
||||
termwiz = { path = "../termwiz" }
|
||||
textwrap = "0.13"
|
||||
thiserror = "1.0"
|
||||
url = "2"
|
||||
wezterm-ssh = { path = "../wezterm-ssh" }
|
||||
wezterm-term = { path = "../term", features=["use_serde"] }
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
|
@ -6,6 +6,7 @@ use async_ossl::AsyncSslStream;
|
||||
use async_trait::async_trait;
|
||||
use codec::*;
|
||||
use config::{configuration, SshDomain, TlsDomainClient, UnixDomain};
|
||||
use filedescriptor::FileDescriptor;
|
||||
use futures::FutureExt;
|
||||
use mux::connui::ConnectionUI;
|
||||
use mux::domain::{alloc_domain_id, DomainId};
|
||||
@ -18,7 +19,6 @@ use smol::channel::{bounded, unbounded, Receiver, Sender};
|
||||
use smol::prelude::*;
|
||||
use smol::{block_on, Async};
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::io::{Read, Write};
|
||||
use std::marker::Unpin;
|
||||
use std::net::TcpStream;
|
||||
@ -312,8 +312,9 @@ struct Reconnectable {
|
||||
}
|
||||
|
||||
struct SshStream {
|
||||
chan: ssh2::Channel,
|
||||
sess: ssh2::Session,
|
||||
stdin: FileDescriptor,
|
||||
stdout: FileDescriptor,
|
||||
_child: wezterm_ssh::SshChildProcess,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SshStream {
|
||||
@ -325,60 +326,29 @@ impl std::fmt::Debug for SshStream {
|
||||
#[cfg(unix)]
|
||||
impl std::os::unix::io::AsRawFd for SshStream {
|
||||
fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
|
||||
self.sess.as_raw_fd()
|
||||
self.stdout.as_raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl std::os::windows::io::AsRawSocket for SshStream {
|
||||
fn as_raw_socket(&self) -> std::os::windows::io::RawSocket {
|
||||
self.sess.as_raw_socket()
|
||||
}
|
||||
}
|
||||
|
||||
impl SshStream {
|
||||
fn process_stderr(&mut self) {
|
||||
let blocking = self.sess.is_blocking();
|
||||
self.sess.set_blocking(false);
|
||||
|
||||
loop {
|
||||
let mut buf = [0u8; 1024];
|
||||
match self.chan.stderr().read(&mut buf) {
|
||||
Ok(size) => {
|
||||
if size == 0 {
|
||||
break;
|
||||
} else {
|
||||
let stderr = &buf[0..size];
|
||||
log::error!("ssh stderr: {}", String::from_utf8_lossy(stderr));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() != std::io::ErrorKind::WouldBlock {
|
||||
log::error!("ssh error reading stderr: {}", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.sess.set_blocking(blocking);
|
||||
self.stdout.as_raw_socket()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for SshStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
|
||||
// Take the opportunity to read and show data from stderr
|
||||
self.process_stderr();
|
||||
self.chan.read(buf)
|
||||
self.stdout.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for SshStream {
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> {
|
||||
self.chan.write(buf)
|
||||
self.stdin.write(buf)
|
||||
}
|
||||
fn flush(&mut self) -> Result<(), std::io::Error> {
|
||||
self.chan.flush()
|
||||
self.stdin.flush()
|
||||
}
|
||||
}
|
||||
|
||||
@ -456,10 +426,6 @@ impl Reconnectable {
|
||||
ui: &mut ConnectionUI,
|
||||
) -> anyhow::Result<()> {
|
||||
let sess = ssh_connect_with_ui(&ssh_dom.remote_address, &ssh_dom.username, ui)?;
|
||||
sess.set_timeout(ssh_dom.timeout.as_secs().try_into()?);
|
||||
|
||||
let mut chan = sess.channel_session()?;
|
||||
|
||||
let proxy_bin = Self::wezterm_bin_path(&ssh_dom.remote_wezterm_path);
|
||||
|
||||
let cmd = if initial {
|
||||
@ -469,9 +435,27 @@ impl Reconnectable {
|
||||
};
|
||||
ui.output_str(&format!("Running: {}\n", cmd));
|
||||
log::error!("going to run {}", cmd);
|
||||
chan.exec(&cmd)?;
|
||||
|
||||
let stream: Box<dyn AsyncReadAndWrite> = Box::new(Async::new(SshStream { sess, chan })?);
|
||||
let exec = smol::block_on(sess.exec(&cmd, None))?;
|
||||
|
||||
let mut stderr = exec.stderr;
|
||||
std::thread::spawn(move || {
|
||||
let mut buf = [0u8; 1024];
|
||||
while let Ok(len) = stderr.read(&mut buf) {
|
||||
if len == 0 {
|
||||
break;
|
||||
} else {
|
||||
let stderr = &buf[0..len];
|
||||
log::error!("ssh stderr: {}", String::from_utf8_lossy(stderr));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let stream: Box<dyn AsyncReadAndWrite> = Box::new(Async::new(SshStream {
|
||||
stdin: exec.stdin,
|
||||
stdout: exec.stdout,
|
||||
_child: exec.child,
|
||||
})?);
|
||||
self.stream.replace(stream);
|
||||
Ok(())
|
||||
}
|
||||
@ -593,8 +577,6 @@ impl Reconnectable {
|
||||
ssh_connect_with_ui(&ssh_params.host_and_port, &ssh_params.username, ui)?;
|
||||
|
||||
let creds = ui.run_and_log_error(|| {
|
||||
let mut chan = sess.channel_session()?;
|
||||
|
||||
// The `tlscreds` command will start the server if needed and then
|
||||
// obtain client credentials that we can use for tls.
|
||||
let cmd = format!(
|
||||
@ -603,20 +585,20 @@ impl Reconnectable {
|
||||
);
|
||||
|
||||
ui.output_str(&format!("Running: {}\n", cmd));
|
||||
chan.exec(&cmd)
|
||||
let mut exec = smol::block_on(sess.exec(&cmd, None))
|
||||
.with_context(|| format!("executing `{}` on remote host", cmd))?;
|
||||
|
||||
// stdout holds an encoded pdu
|
||||
let mut buf = Vec::new();
|
||||
chan.read_to_end(&mut buf)
|
||||
exec.stdout
|
||||
.read_to_end(&mut buf)
|
||||
.context("reading tlscreds response to buffer")?;
|
||||
|
||||
chan.send_eof()?;
|
||||
chan.wait_eof()?;
|
||||
drop(exec.stdin);
|
||||
|
||||
// stderr is ideally empty
|
||||
let mut err = String::new();
|
||||
chan.stderr()
|
||||
exec.stderr
|
||||
.read_to_string(&mut err)
|
||||
.context("reading tlscreds stderr")?;
|
||||
if !err.is_empty() {
|
||||
@ -624,11 +606,11 @@ impl Reconnectable {
|
||||
}
|
||||
|
||||
let creds = match Pdu::decode(buf.as_slice())
|
||||
.with_context(|| format!("reading tlscreds response. stderr={}", err))?
|
||||
.context("reading tlscreds response")?
|
||||
.pdu
|
||||
{
|
||||
Pdu::GetTlsCredsResponse(creds) => creds,
|
||||
_ => bail!("unexpected response to tlscreds, stderr={}", err),
|
||||
_ => bail!("unexpected response to tlscreds"),
|
||||
};
|
||||
|
||||
// Save the credentials to disk, as that is currently the easiest
|
||||
|
@ -53,6 +53,14 @@ impl SessionSender {
|
||||
pub(crate) enum SessionRequest {
|
||||
NewPty(NewPty),
|
||||
ResizePty(ResizePty),
|
||||
Exec(Exec),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Exec {
|
||||
pub command_line: String,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
pub reply: Sender<ExecResult>,
|
||||
}
|
||||
|
||||
pub(crate) struct DescriptorState {
|
||||
@ -328,12 +336,89 @@ impl SessionInner {
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
SessionRequest::Exec(exec) => {
|
||||
if let Err(err) = self.exec(&sess, &exec) {
|
||||
log::error!("{:?} -> error: {:#}", exec, err);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
};
|
||||
sess.set_blocking(false);
|
||||
res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exec(&mut self, sess: &ssh2::Session, exec: &Exec) -> anyhow::Result<()> {
|
||||
sess.set_blocking(true);
|
||||
|
||||
let mut channel = sess.channel_session()?;
|
||||
|
||||
if let Some(env) = &exec.env {
|
||||
for (key, val) in env {
|
||||
if let Err(err) = channel.setenv(key, val) {
|
||||
// Depending on the server configuration, a given
|
||||
// setenv request may not succeed, but that doesn't
|
||||
// prevent the connection from being set up.
|
||||
log::warn!("ssh: setenv {}={} failed: {}", key, val, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
channel.exec(&exec.command_line)?;
|
||||
|
||||
let channel_id = self.next_channel_id;
|
||||
self.next_channel_id += 1;
|
||||
|
||||
let (write_to_stdin, mut read_from_stdin) = socketpair()?;
|
||||
let (mut write_to_stdout, read_from_stdout) = socketpair()?;
|
||||
let (mut write_to_stderr, read_from_stderr) = socketpair()?;
|
||||
|
||||
read_from_stdin.set_non_blocking(true)?;
|
||||
write_to_stdout.set_non_blocking(true)?;
|
||||
write_to_stderr.set_non_blocking(true)?;
|
||||
|
||||
let (exit_tx, exit_rx) = bounded(1);
|
||||
|
||||
let child = SshChildProcess {
|
||||
channel: channel_id,
|
||||
tx: None,
|
||||
exit: exit_rx,
|
||||
exited: None,
|
||||
};
|
||||
|
||||
let result = ExecResult {
|
||||
stdin: write_to_stdin,
|
||||
stdout: read_from_stdout,
|
||||
stderr: read_from_stderr,
|
||||
child,
|
||||
};
|
||||
|
||||
let info = ChannelInfo {
|
||||
channel_id,
|
||||
channel,
|
||||
exit: Some(exit_tx),
|
||||
descriptors: [
|
||||
DescriptorState {
|
||||
fd: Some(read_from_stdin),
|
||||
buf: VecDeque::with_capacity(8192),
|
||||
},
|
||||
DescriptorState {
|
||||
fd: Some(write_to_stdout),
|
||||
buf: VecDeque::with_capacity(8192),
|
||||
},
|
||||
DescriptorState {
|
||||
fd: Some(write_to_stderr),
|
||||
buf: VecDeque::with_capacity(8192),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
exec.reply.try_send(result)?;
|
||||
self.channels.insert(channel_id, info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -394,6 +479,32 @@ impl Session {
|
||||
child.tx.replace(self.tx.clone());
|
||||
Ok((ssh_pty, child))
|
||||
}
|
||||
|
||||
pub async fn exec(
|
||||
&self,
|
||||
command_line: &str,
|
||||
env: Option<HashMap<String, String>>,
|
||||
) -> anyhow::Result<ExecResult> {
|
||||
let (reply, rx) = bounded(1);
|
||||
self.tx
|
||||
.send(SessionRequest::Exec(Exec {
|
||||
command_line: command_line.to_string(),
|
||||
env,
|
||||
reply,
|
||||
}))
|
||||
.await?;
|
||||
let mut exec = rx.recv().await?;
|
||||
exec.child.tx.replace(self.tx.clone());
|
||||
Ok(exec)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecResult {
|
||||
pub stdin: FileDescriptor,
|
||||
pub stdout: FileDescriptor,
|
||||
pub stderr: FileDescriptor,
|
||||
pub child: SshChildProcess,
|
||||
}
|
||||
|
||||
fn write_from_buf<W: Write>(w: &mut W, buf: &mut VecDeque<u8>) -> std::io::Result<()> {
|
||||
|
Loading…
Reference in New Issue
Block a user