add web terminal emulator (closes #6)
This commit is contained in:
parent
62c40358a2
commit
ac709e66f5
15 changed files with 586 additions and 87 deletions
160
Cargo.lock
generated
160
Cargo.lock
generated
|
@ -76,7 +76,7 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -86,7 +86,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -250,7 +250,7 @@ dependencies = [
|
|||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -334,12 +334,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
|||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.7"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8"
|
||||
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -483,7 +483,7 @@ version = "0.5.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -653,9 +653,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.150"
|
||||
version = "0.2.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||
checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
|
@ -663,6 +663,16 @@ version = "0.4.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
|
@ -704,7 +714,7 @@ checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -772,6 +782,29 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.0"
|
||||
|
@ -912,6 +945,7 @@ dependencies = [
|
|||
"prost",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rustix",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sysinfo",
|
||||
|
@ -1045,15 +1079,16 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
|||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.24"
|
||||
version = "0.38.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234"
|
||||
checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
"itoa",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1068,6 +1103,12 @@ version = "1.0.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.192"
|
||||
|
@ -1161,7 +1202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1232,7 +1273,7 @@ dependencies = [
|
|||
"fastrand",
|
||||
"redox_syscall",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1271,11 +1312,12 @@ dependencies = [
|
|||
"libc",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.5",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1654,7 +1696,7 @@ version = "0.51.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1663,7 +1705,16 @@ version = "0.48.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1672,13 +1723,28 @@ version = "0.48.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.0",
|
||||
"windows_aarch64_msvc 0.52.0",
|
||||
"windows_i686_gnu 0.52.0",
|
||||
"windows_i686_msvc 0.52.0",
|
||||
"windows_x86_64_gnu 0.52.0",
|
||||
"windows_x86_64_gnullvm 0.52.0",
|
||||
"windows_x86_64_msvc 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1687,42 +1753,84 @@ version = "0.48.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
|
@ -1730,5 +1838,5 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
|
|
@ -13,10 +13,11 @@ once_cell = "1.18.0"
|
|||
prost = "0.12.1"
|
||||
regex = "1.10.2"
|
||||
reqwest = { version = "0.11.18", features = ["blocking", "json"], default-features = false }
|
||||
rustix = { version = "0.38.28", features = ["fs", "process", "pty", "stdio", "termios"] }
|
||||
serde = { version = "1.0.173", features = ["derive"] }
|
||||
serde_json = "1.0.103"
|
||||
sysinfo = { version = "0.29.2", default-features = false }
|
||||
tokio = { version = "1.28.2", features = ["rt-multi-thread", "io-util", "process", "macros", "signal"] }
|
||||
tokio = { version = "1.28.2", features = ["full"] }
|
||||
tokio-stream = { version = "0.1.14", features = ["net", "sync"] }
|
||||
tokio-util = { version = "0.7.10", features = ["codec"] }
|
||||
tonic = { version = "0.10.2" }
|
||||
|
|
|
@ -2,6 +2,7 @@ pub mod config;
|
|||
pub mod debian;
|
||||
pub mod health;
|
||||
pub mod info;
|
||||
pub mod pty;
|
||||
pub mod self_update;
|
||||
pub mod server;
|
||||
pub mod task;
|
||||
|
|
166
agent/src/pty.rs
Normal file
166
agent/src/pty.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
use std::{io, task::ready};
|
||||
|
||||
use rustix::{
|
||||
fd::OwnedFd,
|
||||
fs::{fcntl_getfl, fcntl_setfl, OFlags},
|
||||
process::{ioctl_tiocsctty, setsid},
|
||||
pty::{grantpt, ioctl_tiocgptpeer, openpt, unlockpt, OpenptFlags},
|
||||
stdio::{dup2_stderr, dup2_stdin, dup2_stdout},
|
||||
termios::{tcsetwinsize, Winsize},
|
||||
};
|
||||
use tokio::{
|
||||
io::{unix::AsyncFd, AsyncRead, AsyncWrite},
|
||||
process::Child,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Pty {
|
||||
fd: AsyncFd<OwnedFd>,
|
||||
}
|
||||
|
||||
impl Pty {
|
||||
pub fn open() -> io::Result<Self> {
|
||||
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)?;
|
||||
grantpt(&master)?;
|
||||
unlockpt(&master)?;
|
||||
|
||||
// Set nonblocking
|
||||
let flags = fcntl_getfl(&master)?;
|
||||
fcntl_setfl(&master, flags | OFlags::NONBLOCK)?;
|
||||
|
||||
let fd = AsyncFd::new(master)?;
|
||||
|
||||
Ok(Self { fd })
|
||||
}
|
||||
|
||||
pub fn child(&self) -> io::Result<PtyChild> {
|
||||
// NOTE: Linux v4.13 and above
|
||||
let fd = ioctl_tiocgptpeer(&self.fd, OpenptFlags::RDWR | OpenptFlags::NOCTTY)?;
|
||||
let child = PtyChild { fd };
|
||||
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
pub fn resize_window(&self, rows: u16, cols: u16) -> io::Result<()> {
|
||||
let winsize = Winsize {
|
||||
ws_row: rows,
|
||||
ws_col: cols,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
|
||||
tcsetwinsize(&self.fd, winsize)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn try_clone(&self) -> io::Result<Pty> {
|
||||
let fd = self.fd.get_ref().try_clone()?;
|
||||
|
||||
Ok(Pty {
|
||||
fd: AsyncFd::new(fd)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for Pty {
|
||||
fn poll_read(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
loop {
|
||||
let mut guard = ready!(self.fd.poll_read_ready(cx)?);
|
||||
|
||||
match guard.try_io(|inner| {
|
||||
let fd = inner.get_ref();
|
||||
let n = rustix::io::read(fd, buf.initialize_unfilled())?;
|
||||
buf.advance(n);
|
||||
|
||||
Ok(())
|
||||
}) {
|
||||
Ok(result) => return std::task::Poll::Ready(result),
|
||||
Err(_would_block) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for Pty {
|
||||
fn poll_write(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<Result<usize, io::Error>> {
|
||||
loop {
|
||||
let mut guard = ready!(self.fd.poll_write_ready(cx))?;
|
||||
|
||||
match guard.try_io(|inner| Ok(rustix::io::write(inner.get_ref(), buf)?)) {
|
||||
Ok(result) => return std::task::Poll::Ready(result),
|
||||
Err(_would_block) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), io::Error>> {
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), io::Error>> {
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PtyChild {
|
||||
fd: OwnedFd,
|
||||
}
|
||||
|
||||
impl PtyChild {
|
||||
pub fn login_tty(&self) -> io::Result<()> {
|
||||
setsid()?;
|
||||
ioctl_tiocsctty(&self.fd)?;
|
||||
dup2_stdin(&self.fd)?;
|
||||
dup2_stdout(&self.fd)?;
|
||||
dup2_stderr(&self.fd)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_shell(pty_child: PtyChild) -> io::Result<Child> {
|
||||
let mut cmd = tokio::process::Command::new("/bin/bash");
|
||||
|
||||
unsafe {
|
||||
cmd.pre_exec(move || {
|
||||
pty_child.login_tty()?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
cmd.spawn()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use rustix::fd::AsRawFd;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_open_pty() {
|
||||
let pty = Pty::open().unwrap();
|
||||
let child = pty.child().unwrap();
|
||||
|
||||
let master_fd = pty.fd.get_ref().as_raw_fd();
|
||||
let child_fd = child.fd.as_raw_fd();
|
||||
|
||||
assert!(master_fd != child_fd);
|
||||
}
|
||||
}
|
|
@ -1,14 +1,20 @@
|
|||
use std::{pin::Pin, process::Stdio, sync::Mutex};
|
||||
|
||||
use tokio::process::Command;
|
||||
use tokio::{io::AsyncWriteExt, process::Command};
|
||||
use tokio_stream::{
|
||||
wrappers::{ReceiverStream, WatchStream},
|
||||
Stream, StreamExt,
|
||||
};
|
||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||
use tonic::{Request, Response, Status};
|
||||
use tonic::{Request, Response, Status, Streaming};
|
||||
|
||||
use crate::{debian, health::HealthMonitor, info::Info, task::TaskBuilder};
|
||||
use crate::{
|
||||
debian,
|
||||
health::HealthMonitor,
|
||||
info::Info,
|
||||
pty::{open_shell, Pty},
|
||||
task::TaskBuilder,
|
||||
};
|
||||
|
||||
use super::proto::*;
|
||||
|
||||
|
@ -168,4 +174,51 @@ impl agent_server::Agent for AgentService<'static> {
|
|||
|
||||
Ok(Response::new(Box::pin(stream)))
|
||||
}
|
||||
|
||||
type TerminalStream = Pin<Box<dyn Stream<Item = Result<TerminalResponse, Status>> + Send>>;
|
||||
|
||||
async fn terminal(
|
||||
&self,
|
||||
req: Request<Streaming<TerminalRequest>>,
|
||||
) -> AgentResult<Self::TerminalStream> {
|
||||
let mut in_stream = req.into_inner();
|
||||
let mut pty = Pty::open()?;
|
||||
let pty_clone = pty.try_clone()?;
|
||||
let pty_child = pty.child()?;
|
||||
let mut child = open_shell(pty_child)?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
// TODO: Handle errors inside here
|
||||
while let Some(result) = in_stream.next().await {
|
||||
match result {
|
||||
Ok(req) => {
|
||||
if let Some(resize) = req.resize {
|
||||
pty.resize_window(resize.rows as u16, resize.cols as u16)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pty.write_all(&req.input[..]).await.unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
// Log and ignore the error...
|
||||
tracing::warn!(%err, "received an incoming stream error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Maybe there's a more graceful way to stop the process?
|
||||
child.kill().await.unwrap();
|
||||
});
|
||||
|
||||
let out_stream = FramedRead::new(pty_clone, BytesCodec::new()).map(|inner| {
|
||||
inner
|
||||
.map(|b| TerminalResponse { output: b.to_vec() })
|
||||
.map_err(|err| {
|
||||
tracing::error!(%err, "read error on pseudoterminal");
|
||||
Status::internal("terminal read error")
|
||||
})
|
||||
});
|
||||
|
||||
Ok(Response::new(Box::pin(out_stream)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
@import "xterm";
|
||||
|
||||
/* This file is for your main application CSS */
|
||||
|
|
|
@ -2,27 +2,52 @@
|
|||
// to get started and then uncomment the line below.
|
||||
// import "./user_socket.js"
|
||||
|
||||
// You can include dependencies in two ways.
|
||||
//
|
||||
// The simplest option is to put them in assets/vendor and
|
||||
// import them using relative paths:
|
||||
//
|
||||
// import "../vendor/some-package.js"
|
||||
//
|
||||
// Alternatively, you can `npm install some-package --prefix assets` and import
|
||||
// them using a path starting with the package name:
|
||||
//
|
||||
// import "some-package"
|
||||
//
|
||||
|
||||
import "phoenix_html"
|
||||
import { Socket } from "phoenix"
|
||||
import { LiveSocket } from "phoenix_live_view"
|
||||
import topbar from "../vendor/topbar"
|
||||
import Alpine from "alpinejs"
|
||||
import { Terminal } from "xterm"
|
||||
import { FitAddon } from "@xterm/addon-fit"
|
||||
|
||||
Alpine.start()
|
||||
window.Alpine = Alpine
|
||||
let Hooks = {}
|
||||
|
||||
// TODO: Move this code and xterm into a separate generated file, and load that file only when needed
|
||||
Hooks.Terminal = {
|
||||
mounted() {
|
||||
const term = new Terminal({
|
||||
fontFamily: '"Courier New", "DejaVu Sans Mono", "Everson Mono", monospace',
|
||||
fontSize: 13,
|
||||
convertEol: true,
|
||||
theme: {
|
||||
background: "#24273a",
|
||||
foreground: "#cad3f5",
|
||||
cursor: "#f4dbd6",
|
||||
black: "#494D64",
|
||||
red: "#ed8796",
|
||||
green: "#a6da95",
|
||||
yellow: "#eed49f",
|
||||
blue: "#8aadf4",
|
||||
magenta: "#f5bde6",
|
||||
cyan: "#8bd5ca",
|
||||
white: "#b8c0e0",
|
||||
}
|
||||
})
|
||||
|
||||
const fitAddon = new FitAddon()
|
||||
const resizeObserver = new ResizeObserver(() => fitAddon.fit())
|
||||
|
||||
term.loadAddon(fitAddon)
|
||||
term.open(this.el)
|
||||
term.onData(data => this.pushEventTo(this.el, "data_event", data))
|
||||
term.onResize(resize => this.pushEventTo(this.el, "resize_event", resize))
|
||||
|
||||
this.handleEvent("data", payload => term.write(payload.data || ""))
|
||||
|
||||
fitAddon.fit()
|
||||
resizeObserver.observe(this.el)
|
||||
}
|
||||
}
|
||||
|
||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
let liveSocket = new LiveSocket("/live", Socket, {
|
||||
|
@ -33,11 +58,12 @@ let liveSocket = new LiveSocket("/live", Socket, {
|
|||
window.Alpine.clone(from, to)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
hooks: Hooks,
|
||||
})
|
||||
|
||||
// Show progress bar on live navigation and form submits
|
||||
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
|
||||
topbar.config({ barColors: { 0: "#29d" }, shaDowColor: "rgba(0, 0, 0, .3)" })
|
||||
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
|
||||
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
|
||||
|
||||
|
@ -50,3 +76,5 @@ liveSocket.connect()
|
|||
// >> liveSocket.disableLatencySim()
|
||||
window.liveSocket = liveSocket
|
||||
|
||||
Alpine.start()
|
||||
window.Alpine = Alpine
|
||||
|
|
17
app/assets/package-lock.json
generated
17
app/assets/package-lock.json
generated
|
@ -5,7 +5,9 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"alpinejs": "^3.13.3"
|
||||
"@xterm/addon-fit": "^0.9.0-beta.1",
|
||||
"alpinejs": "^3.13.3",
|
||||
"xterm": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
|
@ -21,6 +23,14 @@
|
|||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
|
||||
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
"version": "0.9.0-beta.1",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.9.0-beta.1.tgz",
|
||||
"integrity": "sha512-HmGRUMMamUpQYuQBF2VP1LJ0xzqF85LMFfpaNu84t1Tsrl1lPKJWtqX9FDZ22Rf5q6bnKdbj44TRVAUHgDRbLA==",
|
||||
"peerDependencies": {
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/alpinejs": {
|
||||
"version": "3.13.3",
|
||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.3.tgz",
|
||||
|
@ -28,6 +38,11 @@
|
|||
"dependencies": {
|
||||
"@vue/reactivity": "~3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/xterm": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
|
||||
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"alpinejs": "^3.13.3"
|
||||
"@xterm/addon-fit": "^0.9.0-beta.1",
|
||||
"alpinejs": "^3.13.3",
|
||||
"xterm": "^5.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,6 +131,16 @@ defmodule Prymn.Agents do
|
|||
def sys_update(%Agent{} = agent, request) when is_map(request),
|
||||
do: sys_update(agent, struct(SysUpdateRequest, request))
|
||||
|
||||
def terminal(%Agent{} = agent) do
|
||||
# TODO: Find a better solve for bi-directional GRPC stream
|
||||
with {:ok, channel} <- get_channel(agent),
|
||||
stream <- Stub.terminal(channel) do
|
||||
stream
|
||||
else
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_channel(%Agent{} = agent) do
|
||||
case start_connection(agent.host_address) do
|
||||
{:ok, pid} -> {:ok, Connection.get_channel(pid)}
|
||||
|
|
|
@ -15,6 +15,7 @@ defmodule Prymn.Application do
|
|||
{Finch, name: Prymn.Finch},
|
||||
{Oban, Application.fetch_env!(:prymn, Oban)},
|
||||
Prymn.Agents.Supervisor,
|
||||
{Task.Supervisor, name: Prymn.TaskSupervisor},
|
||||
PrymnWeb.Endpoint
|
||||
]
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule PrymnWeb.SystemInfo do
|
|||
|
||||
require Logger
|
||||
alias Phoenix.LiveView.AsyncResult
|
||||
alias PrymnProto.Prymn.SysInfoResponse
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
|
@ -59,7 +60,7 @@ defmodule PrymnWeb.SystemInfo do
|
|||
"""
|
||||
end
|
||||
|
||||
def handle_async(:get_sys_info, {:ok, {:ok, sys_info}}, socket) do
|
||||
def handle_async(:get_sys_info, {:ok, %SysInfoResponse{} = sys_info}, socket) do
|
||||
%{sys_info: sys_info_result, agent: agent} = socket.assigns
|
||||
|
||||
{:noreply,
|
||||
|
|
120
app/lib/prymn_web/components/terminal.ex
Normal file
120
app/lib/prymn_web/components/terminal.ex
Normal file
|
@ -0,0 +1,120 @@
|
|||
defmodule PrymnWeb.Terminal do
|
||||
use PrymnWeb, :live_component
|
||||
|
||||
alias PrymnProto.Prymn.TerminalRequest
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, assign(socket, :open, false)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket =
|
||||
if assigns[:data],
|
||||
do: push_event(socket, "data", %{"data" => assigns[:data]}),
|
||||
else: socket
|
||||
|
||||
{:ok, assign(socket, assigns)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<PrymnWeb.Button.primary
|
||||
:if={not @open}
|
||||
type="button"
|
||||
phx-target={@myself}
|
||||
phx-click="open_terminal"
|
||||
>
|
||||
Open Terminal
|
||||
</PrymnWeb.Button.primary>
|
||||
<PrymnWeb.Button.primary
|
||||
:if={@open}
|
||||
type="button"
|
||||
phx-target={@myself}
|
||||
phx-click="close_terminal"
|
||||
>
|
||||
Close Terminal
|
||||
</PrymnWeb.Button.primary>
|
||||
<div :if={@open} class="mt-2 bg-black p-2">
|
||||
<div phx-hook="Terminal" id="terminal" />
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("open_terminal", _params, socket) do
|
||||
agent = Prymn.Agents.from_server(socket.assigns.server)
|
||||
pid = self()
|
||||
|
||||
Task.Supervisor.start_child(Prymn.TaskSupervisor, fn ->
|
||||
# FIXME: Have to wrap this in a Task because gun sends unsolicited messages
|
||||
# to calling process
|
||||
stream = Prymn.Agents.terminal(agent)
|
||||
|
||||
{:ok, mux_pid} =
|
||||
Task.Supervisor.start_child(Prymn.TaskSupervisor, fn -> receive_loop(stream) end)
|
||||
|
||||
send_update(pid, PrymnWeb.Terminal, id: "terminal", mux_pid: mux_pid, open: true)
|
||||
|
||||
case GRPC.Stub.recv(stream, timeout: :infinity) do
|
||||
{:ok, stream} ->
|
||||
Enum.map(stream, fn
|
||||
{:ok, %{output: data}} ->
|
||||
send(mux_pid, :data)
|
||||
send_update(pid, PrymnWeb.Terminal, id: "terminal", data: data)
|
||||
|
||||
{:error, _err} ->
|
||||
send_update(pid, PrymnWeb.Terminal, id: "terminal", open: false)
|
||||
end)
|
||||
|
||||
{:error, error} ->
|
||||
dbg(error)
|
||||
end
|
||||
end)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("close_terminal", _params, socket) do
|
||||
send(socket.assigns.mux_pid, :close)
|
||||
{:noreply, assign(socket, :open, false)}
|
||||
end
|
||||
|
||||
def handle_event("data_event", data, socket) when is_binary(data) do
|
||||
send(socket.assigns.mux_pid, {:data_event, data})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("resize_event", %{"cols" => cols, "rows" => rows}, socket) do
|
||||
send(socket.assigns.mux_pid, {:resize_event, rows, cols})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp receive_loop(stream) do
|
||||
receive do
|
||||
{:data_event, data} ->
|
||||
GRPC.Stub.send_request(stream, %TerminalRequest{input: data})
|
||||
receive_loop(stream)
|
||||
|
||||
{:resize_event, rows, cols} ->
|
||||
GRPC.Stub.send_request(stream, %TerminalRequest{
|
||||
resize: %TerminalRequest.Resize{rows: rows, cols: cols}
|
||||
})
|
||||
|
||||
receive_loop(stream)
|
||||
|
||||
:data ->
|
||||
receive_loop(stream)
|
||||
|
||||
:close ->
|
||||
GRPC.Stub.send_request(stream, %TerminalRequest{input: ""}, end_stream: true)
|
||||
after
|
||||
120_000 ->
|
||||
GRPC.Stub.send_request(stream, %TerminalRequest{input: ""}, end_stream: true)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -71,20 +71,18 @@ defmodule PrymnWeb.ServerLive.Show do
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div :if={@server.status == :registered} class="my-10">
|
||||
<form phx-change="change_dry_run">
|
||||
<.input type="checkbox" name="dry_run" value={@dry_run} label="Enable dry-run operations" />
|
||||
</form>
|
||||
<div :if={@server.status == :registered} class="my-10 space-y-5 divide-y">
|
||||
<.live_component
|
||||
id={"system_info-#{@server.name}"}
|
||||
module={PrymnWeb.SystemInfo}
|
||||
agent={assigns[:agent]}
|
||||
/>
|
||||
<section class="mt-4">
|
||||
<form phx-change="change_dry_run">
|
||||
<.input type="checkbox" name="dry_run" value={@dry_run} label="Enable dry-run operations" />
|
||||
</form>
|
||||
</section>
|
||||
<section class="mt-4">
|
||||
<h2 class="border-b border-solid border-gray-500 pb-1 text-2xl font-medium">System</h2>
|
||||
<p class="mt-4">
|
||||
<section>
|
||||
<h2 class="my-5 text-xl">System</h2>
|
||||
<p>
|
||||
Updates: <%= 0 %> pending updates.
|
||||
<Button.primary type="button" class="ml-4" phx-click="system_update">
|
||||
Update now
|
||||
|
@ -94,32 +92,11 @@ defmodule PrymnWeb.ServerLive.Show do
|
|||
</p>
|
||||
</p>
|
||||
</section>
|
||||
<section class="mt-4">
|
||||
<h2 class="border-b border-solid border-gray-500 pb-1 text-2xl font-medium">
|
||||
Backups
|
||||
<section>
|
||||
<h2 class="my-5 text-xl">
|
||||
Terminal
|
||||
</h2>
|
||||
<.table id="backups" rows={[%{date: "2023-10-11"}, %{date: "2023-10-10"}]}>
|
||||
<:col :let={backup} label="Date"><%= backup.date %></:col>
|
||||
<:action>
|
||||
<Button.primary>Restore</Button.primary>
|
||||
</:action>
|
||||
</.table>
|
||||
</section>
|
||||
<section class="mt-4">
|
||||
<h2 class="border-b border-solid border-gray-500 pb-1 text-2xl font-medium">
|
||||
Manage Services
|
||||
</h2>
|
||||
<.table
|
||||
id="services"
|
||||
rows={[%{name: "mariadb", status: "Active"}, %{name: "php8.0", status: "Disabled"}]}
|
||||
>
|
||||
<:col :let={service} label="Service"><%= service.name %></:col>
|
||||
<:col :let={service} label="Status"><%= service.status %></:col>
|
||||
<:action>
|
||||
<Button.primary>Activate</Button.primary>
|
||||
<Button.secondary>Deactivate</Button.secondary>
|
||||
</:action>
|
||||
</.table>
|
||||
<.live_component id="terminal" module={PrymnWeb.Terminal} server={@server} />
|
||||
</section>
|
||||
</div>
|
||||
<.back navigate={~p"/servers"}>Back to servers</.back>
|
||||
|
|
|
@ -69,9 +69,24 @@ message SysUpdateResponse {
|
|||
int32 progress = 2;
|
||||
}
|
||||
|
||||
message TerminalRequest {
|
||||
message Resize {
|
||||
uint32 rows = 1;
|
||||
uint32 cols = 2;
|
||||
}
|
||||
|
||||
bytes input = 1;
|
||||
optional Resize resize = 2;
|
||||
}
|
||||
|
||||
message TerminalResponse {
|
||||
bytes output = 1;
|
||||
}
|
||||
|
||||
service Agent {
|
||||
rpc Health(google.protobuf.Empty) returns (stream HealthResponse);
|
||||
rpc Exec(ExecRequest) returns (stream ExecResponse);
|
||||
rpc Terminal(stream TerminalRequest) returns (stream TerminalResponse);
|
||||
rpc GetSysInfo(google.protobuf.Empty) returns (SysInfoResponse);
|
||||
rpc SysUpdate(SysUpdateRequest) returns (stream SysUpdateResponse);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue