Implement proper resizing with terminado.

This commit is contained in:
Fabian Freyer 2019-01-03 18:15:07 +01:00
parent b965cc8763
commit abb0d810de
5 changed files with 659 additions and 379 deletions

711
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -26,10 +26,15 @@
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE. // POSSIBILITY OF SUCH DAMAGE.
#![feature(try_from)]
extern crate actix; extern crate actix;
extern crate actix_web; extern crate actix_web;
extern crate futures; extern crate futures;
extern crate libc;
extern crate serde;
extern crate serde_json;
extern crate tokio;
extern crate tokio_codec; extern crate tokio_codec;
extern crate tokio_io; extern crate tokio_io;
extern crate tokio_pty_process; extern crate tokio_pty_process;
@ -38,26 +43,29 @@ extern crate log;
extern crate pretty_env_logger; extern crate pretty_env_logger;
use actix::*; use actix::*;
use actix_web::{Binary, fs::NamedFile, fs::StaticFiles, server, ws, App, HttpRequest, Result}; use actix_web::{fs::NamedFile, fs::StaticFiles, server, ws, App, Binary, HttpRequest, Result};
use futures::future::Future; use futures::prelude::*;
use libc::c_ushort;
use std::process::Command;
use std::time::{Instant, Duration};
use std::io::Write; use std::io::Write;
use std::process::Command;
use std::time::{Duration, Instant};
use tokio_codec::{BytesCodec, Decoder, FramedRead}; use tokio_codec::{BytesCodec, Decoder, FramedRead};
use tokio_io::{AsyncRead, AsyncWrite}; use tokio_pty_process::{AsyncPtyMaster, AsyncPtyMasterWriteHalf, Child, CommandExt, PtyMaster};
use tokio_io::io::WriteHalf;
use tokio_pty_process::{AsyncPtyMaster, Child, CommandExt};
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
type BytesMut = <BytesCodec as Decoder>::Item; type BytesMut = <BytesCodec as Decoder>::Item;
#[derive(Debug)] mod terminado;
struct IO(BytesMut); use crate::terminado::TerminadoMessage;
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct IO(BytesMut);
impl Message for IO { impl Message for IO {
type Result = (); type Result = ();
@ -83,7 +91,12 @@ impl From<Binary> for IO {
impl From<String> for IO { impl From<String> for IO {
fn from(s: String) -> Self { fn from(s: String) -> Self {
Self(s.into())
}
}
impl From<&str> for IO {
fn from(s: &str) -> Self {
Self(s.into()) Self(s.into())
} }
} }
@ -126,9 +139,30 @@ impl Handler<IO> for Ws {
} }
} }
impl Handler<TerminadoMessage> for Ws {
type Result = ();
fn handle(&mut self, msg: TerminadoMessage, ctx: &mut <Self as Actor>::Context) {
trace!("Ws <- Cons : {:?}", msg);
match msg {
TerminadoMessage::Stdout(_) => {
let json = serde_json::to_string(&msg);
if let Ok(json) = json {
ctx.text(json);
}
}
_ => error!(r#"Invalid TerminadoMessage to Websocket: only "stdout" supported"#),
}
}
}
impl Ws { impl Ws {
pub fn new() -> Self { pub fn new() -> Self {
Self { hb: Instant::now(), cons: None } Self {
hb: Instant::now(),
cons: None,
}
} }
fn hb(&self, ctx: &mut <Self as Actor>::Context) { fn hb(&self, ctx: &mut <Self as Actor>::Context) {
@ -156,37 +190,49 @@ impl StreamHandler<ws::Message, ws::ProtocolError> for Ws {
error!("Console died, closing websocket."); error!("Console died, closing websocket.");
ctx.stop(); ctx.stop();
return; return;
}, }
}; };
match msg { match msg {
ws::Message::Ping(msg) => { ws::Message::Ping(msg) => {
self.hb = Instant::now(); self.hb = Instant::now();
ctx.pong(&msg); ctx.pong(&msg);
}, }
ws::Message::Pong(_) => self.hb = Instant::now(), ws::Message::Pong(_) => self.hb = Instant::now(),
ws::Message::Text(t) => cons.do_send(t.into()), ws::Message::Text(t) => {
ws::Message::Binary(b) => cons.do_send(b.into()), // Attempt to parse the message as JSON.
if let Ok(tmsg) = TerminadoMessage::from_json(&t) {
cons.do_send(tmsg);
} else {
// Otherwise, it's just byte data.
cons.do_send(IO::from(t));
}
}
ws::Message::Binary(b) => cons.do_send(IO::from(b)),
ws::Message::Close(_) => ctx.stop(), ws::Message::Close(_) => ctx.stop(),
}; };
} }
} }
struct Cons { struct Cons {
pty_write: Option<WriteHalf<AsyncPtyMaster>>, pty_write: Option<AsyncPtyMasterWriteHalf>,
child: Option<Child>, child: Option<Child>,
ws: Addr<Ws>, ws: Addr<Ws>,
} }
impl Cons { impl Cons {
pub fn new(ws: Addr<Ws>) -> Self { pub fn new(ws: Addr<Ws>) -> Self {
Self { pty_write: None, child: None, ws } Self {
pty_write: None,
child: None,
ws,
}
} }
} }
impl StreamHandler<<BytesCodec as Decoder>::Item, <BytesCodec as Decoder>::Error> for Cons { impl StreamHandler<<BytesCodec as Decoder>::Item, <BytesCodec as Decoder>::Error> for Cons {
fn handle(&mut self, msg: <BytesCodec as Decoder>::Item, _ctx: &mut Self::Context) { fn handle(&mut self, msg: <BytesCodec as Decoder>::Item, _ctx: &mut Self::Context) {
self.ws.do_send(IO(msg.into())); self.ws.do_send(TerminadoMessage::Stdout(IO(msg)));
} }
} }
@ -270,6 +316,52 @@ impl Handler<IO> for Cons {
} }
} }
struct Resize<T: PtyMaster> {
pty: T,
rows: c_ushort,
cols: c_ushort,
}
impl<T: PtyMaster> Future for Resize<T> {
type Item = ();
type Error = std::io::Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
self.pty.resize(self.rows, self.cols)
}
}
impl Handler<TerminadoMessage> for Cons {
type Result = ();
fn handle(&mut self, msg: TerminadoMessage, ctx: &mut <Self as Actor>::Context) {
let pty = match self.pty_write {
Some(ref mut p) => p,
None => {
error!("Write half of PTY died, stopping Cons.");
ctx.stop();
return;
}
};
trace!("Ws -> Cons : {:?}", msg);
match msg {
TerminadoMessage::Stdin(io) => {
pty.write(io.as_ref());
}
TerminadoMessage::Resize { cols, rows } => {
info!("Resize: cols = {}, rows = {}", cols, rows);
Resize { pty, cols, rows }.wait().map_err(|e| {
error!("Resize failed: {}", e);
});
}
TerminadoMessage::Stdout(_) => {
error!("Invalid Terminado Message: Stdin cannot go to PTY")
}
};
}
}
fn main() { fn main() {
pretty_env_logger::init(); pretty_env_logger::init();
@ -281,9 +373,7 @@ fn main() {
.unwrap() .unwrap()
.show_files_listing(), .show_files_listing(),
) )
.resource("/websocket", |r| { .resource("/websocket", |r| r.f(|req| ws::start(req, Ws::new())))
r.f(|req| ws::start(req, Ws::new()))
})
.resource("/", |r| r.f(index)) .resource("/", |r| r.f(index))
}) })
.bind("127.0.0.1:8080") .bind("127.0.0.1:8080")

191
src/terminado.rs Normal file
View File

@ -0,0 +1,191 @@
use actix::Message;
use libc::c_ushort;
use std::convert::TryFrom;
use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use serde_json;
use crate::IO;
impl Message for TerminadoMessage {
type Result = ();
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum TerminadoMessage {
Resize { rows: c_ushort, cols: c_ushort },
Stdin(IO),
Stdout(IO),
}
impl TerminadoMessage {
pub fn from_json(json: &str) -> Result<Self, ()> {
let value: serde_json::Value = serde_json::from_str(json).map_err(|_| {
error!("Invalid Terminado message: Invalid JSON");
()
})?;
let list: &Vec<serde_json::Value> = value.as_array().ok_or_else(|| {
error!("Invalid Terminado message: Needs to be an array!");
})?;
match list
.first()
.ok_or_else(|| {
error!("Invalid Terminado message: Empty array!");
})?
.as_str()
.ok_or_else(|| {
error!("Invalid Terminado message: Type field not a string!");
})? {
"stdin" => {
if list.len() != 2 {
error!(r#"Invalid Terminado message: "stdin" length != 2"#);
return Err(());
}
Ok(TerminadoMessage::Stdin(IO::from(
list[1].as_str().ok_or_else(|| {
error!(r#"Invalid Terminado message: "stdin" needs to be a String"#);
})?,
)))
}
"stdout" => {
if list.len() != 2 {
error!(r#"Invalid Terminado message: "stdout" length != 2"#);
return Err(());
}
Ok(TerminadoMessage::Stdout(IO::from(
list[1].as_str().ok_or_else(|| {
error!(r#"Invalid Terminado message: "stdout" needs to be a String"#);
})?,
)))
}
"set_size" => {
if list.len() != 3 {
error!(r#"Invalid Terminado message: "set_size" length != 2"#);
return Err(());
}
let rows: u16 = u16::try_from(list[1].as_u64().ok_or_else(|| {
error!(
r#"Invalid Terminado message: "set_size" element 1 needs to be an integer"#
);
})?)
.map_err(|_| {
error!(r#"Invalid Terminado message. "set_size" rows out of range."#);
})?;
let cols: u16 = u16::try_from(list[2].as_u64().ok_or_else(|| {
error!(
r#"Invalid Terminado message: "set_size" element 2 needs to be an integer"#
);
})?)
.map_err(|_| {
error!(r#"Invalid Terminado message. "set_size" cols out of range."#);
})?;
Ok(TerminadoMessage::Resize { rows, cols })
}
v => {
error!("Invalid Terminado message: Unknown type {:?}", v);
Err(())
}
}
}
}
impl Serialize for TerminadoMessage {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
TerminadoMessage::Resize { rows, cols } => {
let mut seq = serializer.serialize_seq(Some(3))?;
seq.serialize_element("set_size")?;
seq.serialize_element(rows)?;
seq.serialize_element(cols)?;
seq.end()
}
TerminadoMessage::Stdin(stdin) => {
let mut seq = serializer.serialize_seq(Some(2))?;
seq.serialize_element("stdin")?;
seq.serialize_element(&String::from_utf8_lossy(stdin.0.as_ref()))?;
seq.end()
}
TerminadoMessage::Stdout(stdin) => {
let mut seq = serializer.serialize_seq(Some(2))?;
seq.serialize_element("stdout")?;
seq.serialize_element(&String::from_utf8_lossy(stdin.0.as_ref()))?;
seq.end()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize_resize() {
let res = TerminadoMessage::Resize { rows: 25, cols: 80 };
assert_eq!(
serde_json::to_string(&res).unwrap(),
r#"["set_size",25,80]"#
);
}
#[test]
fn test_serialize_stdin() {
let res = TerminadoMessage::Stdin(IO::from("hello world"));
assert_eq!(
serde_json::to_string(&res).unwrap(),
r#"["stdin","hello world"]"#
);
}
#[test]
fn test_serialize_stdout() {
let res = TerminadoMessage::stdout(IO::from("hello world"));
assert_eq!(
serde_json::to_string(&res).unwrap(),
r#"["stdout","hello world"]"#
);
}
#[test]
fn test_deserialize_resize() {
let json = r#"["set_size", 25, 80]"#;
let value = TerminadoMessage::from_json(json).expect("Could not parse TerminadoMessage");
assert_eq!(value, TerminadoMessage::Resize { rows: 25, cols: 80 });
}
#[test]
fn test_deserialize_stdin() {
let json = r#"["stdin", "hello world"]"#;
let value = TerminadoMessage::from_json(json).expect("Could not parse TerminadoMessage");
assert_eq!(value, TerminadoMessage::Stdin("hello world".into()));
}
#[test]
fn test_deserialize_stdout() {
let json = r#"["stdout", "hello world"]"#;
let value = TerminadoMessage::from_json(json).expect("Could not parse TerminadoMessage");
assert_eq!(value, TerminadoMessage::stdout("hello world".into()));
}
}

View File

@ -25,7 +25,7 @@ SPDX-License-Identifier: BSD-3-Clause
<body> <body>
<div id="terminal"></div> <div id="terminal"></div>
<script> <script>
Terminal.applyAddon(attach); Terminal.applyAddon(terminado);
Terminal.applyAddon(fit); Terminal.applyAddon(fit);
Terminal.applyAddon(search); Terminal.applyAddon(search);
@ -35,7 +35,7 @@ SPDX-License-Identifier: BSD-3-Clause
var sock = new WebSocket(socketURL); var sock = new WebSocket(socketURL);
sock.addEventListener('open', function() { sock.addEventListener('open', function() {
term.attach(sock); term.terminadoAttach(sock);
term.fit(); term.fit();
}); });

2
vendor/stund vendored

@ -1 +1 @@
Subproject commit b56e613b835321acd34a2716765c4f311b9ffed4 Subproject commit 1867f633f3979ae4a0a7d2d6431d8a3426a49e29