wip: initial pty work

This commit is contained in:
Ellie Huxtable 2024-06-06 18:18:36 +01:00
parent eb4a5ab4cd
commit 488bb2613c
6 changed files with 319 additions and 7 deletions

184
Cargo.lock generated
View File

@ -143,6 +143,12 @@ dependencies = [
"password-hash",
]
[[package]]
name = "arrayvec"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "async-stream"
version = "0.3.5"
@ -207,9 +213,11 @@ dependencies = [
"atuin-daemon",
"atuin-dotfiles",
"atuin-history",
"atuin-run",
"atuin-server",
"atuin-server-postgres",
"base64 0.22.1",
"bytes",
"clap",
"clap_complete",
"clap_complete_nushell",
@ -383,6 +391,17 @@ dependencies = [
"whoami",
]
[[package]]
name = "atuin-run"
version = "0.1.0"
dependencies = [
"bytes",
"eyre",
"portable-pty",
"tokio",
"vt100",
]
[[package]]
name = "atuin-server"
version = "18.3.0-prerelease.1"
@ -1900,6 +1919,15 @@ dependencies = [
"time",
]
[[package]]
name = "ioctl-rs"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d"
dependencies = [
"libc",
]
[[package]]
name = "ipnet"
version = "2.9.0"
@ -2232,6 +2260,20 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nix"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
dependencies = [
"autocfg",
"bitflags 1.3.2",
"cfg-if",
"libc",
"memoffset",
"pin-utils",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -2588,6 +2630,27 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
[[package]]
name = "portable-pty"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806ee80c2a03dbe1a9fb9534f8d19e4c0546b790cde8fd1fea9d6390644cb0be"
dependencies = [
"anyhow",
"bitflags 1.3.2",
"downcast-rs",
"filedescriptor",
"lazy_static",
"libc",
"log",
"nix 0.25.1",
"serial",
"shared_library",
"shell-words",
"winapi",
"winreg 0.10.1",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -2897,7 +2960,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
"winreg 0.50.0",
]
[[package]]
@ -3280,6 +3343,48 @@ dependencies = [
"syn 2.0.66",
]
[[package]]
name = "serial"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86"
dependencies = [
"serial-core",
"serial-unix",
"serial-windows",
]
[[package]]
name = "serial-core"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581"
dependencies = [
"libc",
]
[[package]]
name = "serial-unix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7"
dependencies = [
"ioctl-rs",
"libc",
"serial-core",
"termios",
]
[[package]]
name = "serial-windows"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162"
dependencies = [
"libc",
"serial-core",
]
[[package]]
name = "sha1"
version = "0.10.6"
@ -3311,6 +3416,22 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shared_library"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
dependencies = [
"lazy_static",
"libc",
]
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "shellexpand"
version = "3.1.0"
@ -3792,6 +3913,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "termios"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a"
dependencies = [
"libc",
]
[[package]]
name = "thiserror"
version = "1.0.61"
@ -4315,6 +4445,39 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vt100"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de"
dependencies = [
"itoa",
"log",
"unicode-width",
"vte",
]
[[package]]
name = "vte"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
dependencies = [
"arrayvec",
"utf8parse",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "want"
version = "0.3.1"
@ -4411,7 +4574,7 @@ dependencies = [
"bitflags 1.3.2",
"downcast-rs",
"libc",
"nix",
"nix 0.24.3",
"wayland-commons",
"wayland-scanner",
"wayland-sys",
@ -4423,7 +4586,7 @@ version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902"
dependencies = [
"nix",
"nix 0.24.3",
"once_cell",
"smallvec",
"wayland-sys",
@ -4677,6 +4840,15 @@ version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "winreg"
version = "0.50.0"
@ -4696,7 +4868,7 @@ dependencies = [
"derive-new",
"libc",
"log",
"nix",
"nix 0.24.3",
"os_pipe",
"tempfile",
"thiserror",
@ -4721,7 +4893,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507"
dependencies = [
"gethostname",
"nix",
"nix 0.24.3",
"winapi",
"winapi-wsapoll",
"x11rb-protocol",
@ -4733,7 +4905,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67"
dependencies = [
"nix",
"nix 0.24.3",
]
[[package]]

View File

@ -0,0 +1,19 @@
[package]
name = "atuin-run"
edition = "2021"
version = "0.1.0"
authors.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
readme.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
eyre.workspace = true
tokio.workspace = true
portable-pty = "0.8.1"
vt100 = "0.15.2"
bytes = "1.6.0"

View File

@ -0,0 +1 @@
pub mod run;

View File

@ -0,0 +1,93 @@
/// Create and manage pseudoterminals
use std::io::{Read, Write};
use eyre::{eyre, Result};
use bytes::Bytes;
use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize};
use tokio::sync::mpsc::{channel, Receiver, Sender};
pub struct Pty {
tx: Sender<Bytes>,
master: Box<dyn MasterPty>,
}
impl Pty {
pub async fn open_shell<'a>(rows: u16, cols: u16, shell: &str, dir: &str) -> Result<Self> {
let sys = native_pty_system();
let pair = sys
.openpty(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})
.map_err(|e| eyre!("Failed to open pty: {}", e))?;
let cmd = CommandBuilder::new(shell);
tokio::task::spawn_blocking(move || {
let mut child = pair.slave.spawn_command(cmd).unwrap();
// Wait for the child to exit
let _ = child.wait().unwrap();
// Ensure slave is dropped
// This closes file handles, we can deadlock if this is not done correctly.
drop(pair.slave);
});
// Handle input -> write to master writer
let (master_tx, mut master_rx) = channel::<Bytes>(32);
let mut writer = pair.master.take_writer().unwrap();
tokio::spawn(async move {
while let Some(bytes) = master_rx.recv().await {
writer.write_all(&bytes).unwrap();
writer.flush().unwrap();
}
// When the channel has been closed, we won't be getting any more input. Close the
// writer and the master.
// This will also close the writer, which sends EOF to the underlying shell. Ensuring
// that is also closed.
drop(writer);
});
Ok(Pty {
tx: master_tx,
master: pair.master,
})
}
pub async fn send_bytes(&self, bytes: Bytes) -> Result<()> {
self.tx
.send(bytes)
.await
.map_err(|e| eyre!("Failed to write to master tx: {}", e))
}
pub async fn send_string(&self, cmd: &str) -> Result<()> {
let bytes: Vec<u8> = cmd.bytes().collect();
let bytes = Bytes::from(bytes);
self.send_bytes(bytes).await
}
pub async fn send_single_string(&self, cmd: &str) -> Result<()> {
let mut bytes: Vec<u8> = cmd.bytes().collect();
bytes.push(0x04);
let bytes = Bytes::from(bytes);
self.send_bytes(bytes).await
}
pub fn reader(&self) -> Result<Box<dyn Read + Send>> {
self.master
.try_clone_reader()
.map_err(|e| eyre!("Failed to clone master reader: {}", e))
}
}

View File

@ -49,6 +49,7 @@ atuin-common = { path = "../atuin-common", version = "18.3.0-prerelease.1" }
atuin-dotfiles = { path = "../atuin-dotfiles", version = "0.2.0" }
atuin-history = { path = "../atuin-history", version = "0.1.0" }
atuin-daemon = { path = "../atuin-daemon", version = "0.1.0" }
atuin-run = { path = "../atuin-run", version = "0.1.0" }
log = { workspace = true }
env_logger = "0.11.2"
@ -85,6 +86,7 @@ uuid = { workspace = true }
unicode-segmentation = "1.11.0"
sysinfo = "0.30.7"
regex="1.10.4"
bytes = "1.6.0"
[target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies]
cli-clipboard = { version = "0.4.0", optional = true }

View File

@ -1,4 +1,5 @@
use std::path::PathBuf;
use bytes::Bytes;
use std::{io::BufRead, path::PathBuf};
use clap::Subcommand;
use eyre::{Result, WrapErr};
@ -83,6 +84,8 @@ pub enum Cmd {
/// Print example configuration
#[command()]
DefaultConfig,
Boop,
}
impl Cmd {
@ -158,6 +161,28 @@ impl Cmd {
#[cfg(feature = "daemon")]
Self::Daemon => daemon::run(settings, sqlite_store, db).await,
Self::Boop => {
let pty =
atuin_run::run::Pty::open_shell(24, 18, "/bin/zsh", "/Users/ellie").await?;
pty.send_single_string("ls\necho 'foo'\nsleep 5\npwd\n")
.await;
let mut reader = pty.reader().unwrap();
tokio::task::spawn_blocking(|| {
// Consume the output from the child
// Can't read the full buffer, since that would wait for EOF
let lines = std::io::BufReader::new(reader).lines();
for line in lines.flatten() {
println!("{line}");
}
})
.await;
Ok(())
}
_ => unimplemented!(),
}
}