mirror of
https://github.com/wez/wezterm.git
synced 2024-11-27 02:25:28 +03:00
dc4c8f07dd
refs: https://github.com/wez/wezterm/issues/748 refs: https://github.com/wez/wezterm/issues/489
1010 lines
32 KiB
Rust
1010 lines
32 KiB
Rust
//! encode and decode the frames for the mux protocol.
|
|
//! The frames include the length of a PDU as well as an identifier
|
|
//! that informs us how to decode it. The length, ident and serial
|
|
//! number are encoded using a variable length integer encoding.
|
|
//! Rather than rely solely on serde to serialize and deserialize an
|
|
//! enum, we encode the enum variants with a version/identifier tag
|
|
//! for ourselves. This will make it a little easier to manage
|
|
//! client and server instances that are built from different versions
|
|
//! of this code; in this way the client and server can more gracefully
|
|
//! manage unknown enum variants.
|
|
#![allow(dead_code)]
|
|
#![cfg_attr(feature = "cargo-clippy", allow(clippy::range_plus_one))]
|
|
|
|
use anyhow::{bail, Context as _, Error};
|
|
use leb128;
|
|
use mux::domain::DomainId;
|
|
use mux::pane::PaneId;
|
|
use mux::renderable::{RenderableDimensions, StableCursorPosition};
|
|
use mux::tab::{PaneNode, SerdeUrl, SplitDirection, TabId};
|
|
use mux::window::WindowId;
|
|
use portable_pty::{CommandBuilder, PtySize};
|
|
use rangeset::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use smol::io::AsyncWriteExt;
|
|
use smol::prelude::*;
|
|
use std::convert::TryInto;
|
|
use std::io::Cursor;
|
|
use std::ops::Range;
|
|
use std::sync::Arc;
|
|
use termwiz::hyperlink::Hyperlink;
|
|
use termwiz::surface::Line;
|
|
use varbincode;
|
|
use wezterm_term::color::ColorPalette;
|
|
use wezterm_term::{Alert, ClipboardSelection, StableRowIndex};
|
|
|
|
/// Returns the encoded length of the leb128 representation of value
|
|
fn encoded_length(value: u64) -> usize {
|
|
struct NullWrite {}
|
|
impl std::io::Write for NullWrite {
|
|
fn write(&mut self, buf: &[u8]) -> std::result::Result<usize, std::io::Error> {
|
|
Ok(buf.len())
|
|
}
|
|
fn flush(&mut self) -> std::result::Result<(), std::io::Error> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
leb128::write::unsigned(&mut NullWrite {}, value).unwrap()
|
|
}
|
|
|
|
const COMPRESSED_MASK: u64 = 1 << 63;
|
|
|
|
fn encode_raw_as_vec(
|
|
ident: u64,
|
|
serial: u64,
|
|
data: &[u8],
|
|
is_compressed: bool,
|
|
) -> anyhow::Result<Vec<u8>> {
|
|
let len = data.len() + encoded_length(ident) + encoded_length(serial);
|
|
let masked_len = if is_compressed {
|
|
(len as u64) | COMPRESSED_MASK
|
|
} else {
|
|
len as u64
|
|
};
|
|
|
|
// Double-buffer the data; since we run with nodelay enabled, it is
|
|
// desirable for the write to be a single packet (or at least, for
|
|
// the header portion to go out in a single packet)
|
|
let mut buffer = Vec::with_capacity(len + encoded_length(masked_len));
|
|
|
|
leb128::write::unsigned(&mut buffer, masked_len).context("writing pdu len")?;
|
|
leb128::write::unsigned(&mut buffer, serial).context("writing pdu serial")?;
|
|
leb128::write::unsigned(&mut buffer, ident).context("writing pdu ident")?;
|
|
buffer.extend_from_slice(data);
|
|
|
|
if is_compressed {
|
|
metrics::histogram!("pdu.encode.compressed.size", buffer.len() as f64);
|
|
} else {
|
|
metrics::histogram!("pdu.encode.size", buffer.len() as f64);
|
|
}
|
|
|
|
Ok(buffer)
|
|
}
|
|
|
|
/// Encode a frame. If the data is compressed, the high bit of the length
|
|
/// is set to indicate that. The data written out has the format:
|
|
/// tagged_len: leb128 (u64 msb is set if data is compressed)
|
|
/// serial: leb128
|
|
/// ident: leb128
|
|
/// data bytes
|
|
fn encode_raw<W: std::io::Write>(
|
|
ident: u64,
|
|
serial: u64,
|
|
data: &[u8],
|
|
is_compressed: bool,
|
|
mut w: W,
|
|
) -> anyhow::Result<usize> {
|
|
let buffer = encode_raw_as_vec(ident, serial, data, is_compressed)?;
|
|
w.write_all(&buffer).context("writing pdu data buffer")?;
|
|
Ok(buffer.len())
|
|
}
|
|
|
|
async fn encode_raw_async<W: Unpin + AsyncWriteExt>(
|
|
ident: u64,
|
|
serial: u64,
|
|
data: &[u8],
|
|
is_compressed: bool,
|
|
w: &mut W,
|
|
) -> anyhow::Result<usize> {
|
|
let buffer = encode_raw_as_vec(ident, serial, data, is_compressed)?;
|
|
w.write_all(&buffer)
|
|
.await
|
|
.context("writing pdu data buffer")?;
|
|
Ok(buffer.len())
|
|
}
|
|
|
|
/// Read a single leb128 encoded value from the stream
|
|
async fn read_u64_async<R>(r: &mut R) -> anyhow::Result<u64>
|
|
where
|
|
R: Unpin + AsyncRead + std::fmt::Debug,
|
|
{
|
|
let mut buf = vec![];
|
|
loop {
|
|
let mut byte = [0u8];
|
|
let nread = r.read(&mut byte).await?;
|
|
if nread == 0 {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::UnexpectedEof,
|
|
"EOF while reading leb128 encoded value",
|
|
)
|
|
.into());
|
|
}
|
|
buf.push(byte[0]);
|
|
|
|
match leb128::read::unsigned(&mut buf.as_slice()) {
|
|
Ok(n) => {
|
|
return Ok(n);
|
|
}
|
|
Err(leb128::read::Error::IoError(_)) => continue,
|
|
Err(leb128::read::Error::Overflow) => anyhow::bail!("leb128 is too large"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Read a single leb128 encoded value from the stream
|
|
fn read_u64<R: std::io::Read>(mut r: R) -> anyhow::Result<u64> {
|
|
leb128::read::unsigned(&mut r)
|
|
.map_err(|err| match err {
|
|
leb128::read::Error::IoError(ioerr) => anyhow::Error::new(ioerr),
|
|
err => anyhow::Error::new(err),
|
|
})
|
|
.context("reading leb128")
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Decoded {
|
|
ident: u64,
|
|
serial: u64,
|
|
data: Vec<u8>,
|
|
is_compressed: bool,
|
|
}
|
|
|
|
/// Decode a frame.
|
|
/// See encode_raw() for the frame format.
|
|
async fn decode_raw_async<R: Unpin + AsyncRead + std::fmt::Debug>(
|
|
r: &mut R,
|
|
) -> anyhow::Result<Decoded> {
|
|
let len = read_u64_async(r).await.context("reading PDU length")?;
|
|
let (len, is_compressed) = if (len & COMPRESSED_MASK) != 0 {
|
|
(len & !COMPRESSED_MASK, true)
|
|
} else {
|
|
(len, false)
|
|
};
|
|
let serial = read_u64_async(r).await.context("reading PDU serial")?;
|
|
let ident = read_u64_async(r).await.context("reading PDU ident")?;
|
|
let data_len =
|
|
match (len as usize).overflowing_sub(encoded_length(ident) + encoded_length(serial)) {
|
|
(_, true) => {
|
|
anyhow::bail!(
|
|
"sizes don't make sense: len:{} serial:{} (enc={}) ident:{} (enc={})",
|
|
len,
|
|
serial,
|
|
encoded_length(serial),
|
|
ident,
|
|
encoded_length(ident)
|
|
);
|
|
}
|
|
(data_len, false) => data_len,
|
|
};
|
|
|
|
if is_compressed {
|
|
metrics::histogram!("pdu.decode.compressed.size", data_len as f64);
|
|
} else {
|
|
metrics::histogram!("pdu.decode.size", data_len as f64);
|
|
}
|
|
|
|
let mut data = vec![0u8; data_len];
|
|
r.read_exact(&mut data).await.with_context(|| {
|
|
format!(
|
|
"reading {} bytes of data for PDU of length {} with serial={} ident={}",
|
|
data_len, len, serial, ident
|
|
)
|
|
})?;
|
|
Ok(Decoded {
|
|
ident,
|
|
serial,
|
|
data,
|
|
is_compressed,
|
|
})
|
|
}
|
|
|
|
/// Decode a frame.
|
|
/// See encode_raw() for the frame format.
|
|
fn decode_raw<R: std::io::Read>(mut r: R) -> anyhow::Result<Decoded> {
|
|
let len = read_u64(r.by_ref()).context("reading PDU length")?;
|
|
let (len, is_compressed) = if (len & COMPRESSED_MASK) != 0 {
|
|
(len & !COMPRESSED_MASK, true)
|
|
} else {
|
|
(len, false)
|
|
};
|
|
let serial = read_u64(r.by_ref()).context("reading PDU serial")?;
|
|
let ident = read_u64(r.by_ref()).context("reading PDU ident")?;
|
|
let data_len =
|
|
match (len as usize).overflowing_sub(encoded_length(ident) + encoded_length(serial)) {
|
|
(_, true) => {
|
|
anyhow::bail!(
|
|
"sizes don't make sense: len:{} serial:{} (enc={}) ident:{} (enc={})",
|
|
len,
|
|
serial,
|
|
encoded_length(serial),
|
|
ident,
|
|
encoded_length(ident)
|
|
);
|
|
}
|
|
(data_len, false) => data_len,
|
|
};
|
|
|
|
if is_compressed {
|
|
metrics::histogram!("pdu.decode.compressed.size", data_len as f64);
|
|
} else {
|
|
metrics::histogram!("pdu.decode.size", data_len as f64);
|
|
}
|
|
|
|
let mut data = vec![0u8; data_len];
|
|
r.read_exact(&mut data).with_context(|| {
|
|
format!(
|
|
"reading {} bytes of data for PDU of length {} with serial={} ident={}",
|
|
data_len, len, serial, ident
|
|
)
|
|
})?;
|
|
Ok(Decoded {
|
|
ident,
|
|
serial,
|
|
data,
|
|
is_compressed,
|
|
})
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct DecodedPdu {
|
|
pub serial: u64,
|
|
pub pdu: Pdu,
|
|
}
|
|
|
|
/// If the serialized size is larger than this, then we'll consider compressing it
|
|
const COMPRESS_THRESH: usize = 32;
|
|
|
|
fn serialize<T: serde::Serialize>(t: &T) -> Result<(Vec<u8>, bool), Error> {
|
|
let mut uncompressed = Vec::new();
|
|
let mut encode = varbincode::Serializer::new(&mut uncompressed);
|
|
t.serialize(&mut encode)?;
|
|
|
|
if uncompressed.len() <= COMPRESS_THRESH {
|
|
return Ok((uncompressed, false));
|
|
}
|
|
// It's a little heavy; let's try compressing it
|
|
let mut compressed = Vec::new();
|
|
let mut compress = zstd::Encoder::new(&mut compressed, zstd::DEFAULT_COMPRESSION_LEVEL)?;
|
|
let mut encode = varbincode::Serializer::new(&mut compress);
|
|
t.serialize(&mut encode)?;
|
|
drop(encode);
|
|
compress.finish()?;
|
|
|
|
log::debug!(
|
|
"serialized+compress len {} vs {}",
|
|
compressed.len(),
|
|
uncompressed.len()
|
|
);
|
|
|
|
if compressed.len() < uncompressed.len() {
|
|
Ok((compressed, true))
|
|
} else {
|
|
Ok((uncompressed, false))
|
|
}
|
|
}
|
|
|
|
fn deserialize<T: serde::de::DeserializeOwned, R: std::io::Read>(
|
|
mut r: R,
|
|
is_compressed: bool,
|
|
) -> Result<T, Error> {
|
|
if is_compressed {
|
|
let mut decompress = zstd::Decoder::new(r)?;
|
|
let mut decode = varbincode::Deserializer::new(&mut decompress);
|
|
serde::Deserialize::deserialize(&mut decode).map_err(Into::into)
|
|
} else {
|
|
let mut decode = varbincode::Deserializer::new(&mut r);
|
|
serde::Deserialize::deserialize(&mut decode).map_err(Into::into)
|
|
}
|
|
}
|
|
|
|
macro_rules! pdu {
|
|
($( $name:ident:$vers:expr),* $(,)?) => {
|
|
#[derive(PartialEq, Debug)]
|
|
pub enum Pdu {
|
|
Invalid{ident: u64},
|
|
$(
|
|
$name($name)
|
|
,)*
|
|
}
|
|
|
|
impl Pdu {
|
|
pub fn encode<W: std::io::Write>(&self, w: W, serial: u64) -> Result<(), Error> {
|
|
match self {
|
|
Pdu::Invalid{..} => bail!("attempted to serialize Pdu::Invalid"),
|
|
$(
|
|
Pdu::$name(s) => {
|
|
let (data, is_compressed) = serialize(s)?;
|
|
let encoded_size = encode_raw($vers, serial, &data, is_compressed, w)?;
|
|
metrics::histogram!("pdu.size", encoded_size as f64, "pdu" => stringify!($name));
|
|
Ok(())
|
|
}
|
|
,)*
|
|
}
|
|
}
|
|
|
|
pub async fn encode_async<W: Unpin + AsyncWriteExt>(&self, w: &mut W, serial: u64) -> Result<(), Error> {
|
|
match self {
|
|
Pdu::Invalid{..} => bail!("attempted to serialize Pdu::Invalid"),
|
|
$(
|
|
Pdu::$name(s) => {
|
|
let (data, is_compressed) = serialize(s)?;
|
|
let encoded_size = encode_raw_async($vers, serial, &data, is_compressed, w).await?;
|
|
metrics::histogram!("pdu.size", encoded_size as f64, "pdu" => stringify!($name));
|
|
Ok(())
|
|
}
|
|
,)*
|
|
}
|
|
}
|
|
|
|
pub fn decode<R: std::io::Read>(r: R) -> Result<DecodedPdu, Error> {
|
|
let decoded = decode_raw(r).context("decoding a PDU")?;
|
|
match decoded.ident {
|
|
$(
|
|
$vers => {
|
|
metrics::histogram!("pdu.size", decoded.data.len() as f64, "pdu" => stringify!($name));
|
|
Ok(DecodedPdu {
|
|
serial: decoded.serial,
|
|
pdu: Pdu::$name(deserialize(decoded.data.as_slice(), decoded.is_compressed)?)
|
|
})
|
|
}
|
|
,)*
|
|
_ => {
|
|
metrics::histogram!("pdu.size", decoded.data.len() as f64, "pdu" => "??");
|
|
Ok(DecodedPdu {
|
|
serial: decoded.serial,
|
|
pdu: Pdu::Invalid{ident:decoded.ident}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn decode_async<R>(r: &mut R) -> Result<DecodedPdu, Error>
|
|
where R: std::marker::Unpin,
|
|
R: AsyncRead,
|
|
R: std::fmt::Debug
|
|
{
|
|
let decoded = decode_raw_async(r).await.context("decoding a PDU")?;
|
|
match decoded.ident {
|
|
$(
|
|
$vers => {
|
|
metrics::histogram!("pdu.size", decoded.data.len() as f64, "pdu" => stringify!($name));
|
|
Ok(DecodedPdu {
|
|
serial: decoded.serial,
|
|
pdu: Pdu::$name(deserialize(decoded.data.as_slice(), decoded.is_compressed)?)
|
|
})
|
|
}
|
|
,)*
|
|
_ => {
|
|
metrics::histogram!("pdu.size", decoded.data.len() as f64, "pdu" => "??");
|
|
Ok(DecodedPdu {
|
|
serial: decoded.serial,
|
|
pdu: Pdu::Invalid{ident:decoded.ident}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The overall version of the codec.
|
|
/// This must be bumped when backwards incompatible changes
|
|
/// are made to the types and protocol.
|
|
pub const CODEC_VERSION: usize = 8;
|
|
|
|
// Defines the Pdu enum.
|
|
// Each struct has an explicit identifying number.
|
|
// This allows removal of obsolete structs,
|
|
// and defining newer structs as the protocol evolves.
|
|
pdu! {
|
|
ErrorResponse: 0,
|
|
Ping: 1,
|
|
Pong: 2,
|
|
ListPanes: 3,
|
|
ListPanesResponse: 4,
|
|
Spawn: 7,
|
|
SpawnResponse: 8,
|
|
WriteToPane: 9,
|
|
UnitResponse: 10,
|
|
SendKeyDown: 11,
|
|
SendMouseEvent: 12,
|
|
SendPaste: 13,
|
|
Resize: 14,
|
|
SetClipboard: 20,
|
|
GetLines: 22,
|
|
GetLinesResponse: 23,
|
|
GetPaneRenderChanges: 24,
|
|
GetPaneRenderChangesResponse: 25,
|
|
GetCodecVersion: 26,
|
|
GetCodecVersionResponse: 27,
|
|
GetTlsCreds: 28,
|
|
GetTlsCredsResponse: 29,
|
|
LivenessResponse: 30,
|
|
SearchScrollbackRequest: 31,
|
|
SearchScrollbackResponse: 32,
|
|
SetPaneZoomed: 33,
|
|
SplitPane: 34,
|
|
KillPane: 35,
|
|
SpawnV2: 36,
|
|
PaneRemoved: 37,
|
|
SetPalette: 38,
|
|
NotifyAlert: 39,
|
|
}
|
|
|
|
impl Pdu {
|
|
pub fn stream_decode(buffer: &mut Vec<u8>) -> anyhow::Result<Option<DecodedPdu>> {
|
|
let mut cursor = Cursor::new(buffer.as_slice());
|
|
match Self::decode(&mut cursor) {
|
|
Ok(decoded) => {
|
|
let consumed = cursor.position() as usize;
|
|
let remain = buffer.len() - consumed;
|
|
// Remove `consumed` bytes from the start of the vec.
|
|
// This is safe because the vec is just bytes and we are
|
|
// constrained the offsets accordingly.
|
|
unsafe {
|
|
std::ptr::copy_nonoverlapping(
|
|
buffer.as_ptr().add(consumed),
|
|
buffer.as_mut_ptr(),
|
|
remain,
|
|
);
|
|
}
|
|
buffer.truncate(remain);
|
|
Ok(Some(decoded))
|
|
}
|
|
Err(err) => {
|
|
if let Some(ioerr) = err.root_cause().downcast_ref::<std::io::Error>() {
|
|
match ioerr.kind() {
|
|
std::io::ErrorKind::UnexpectedEof | std::io::ErrorKind::WouldBlock => {
|
|
return Ok(None);
|
|
}
|
|
_ => {}
|
|
}
|
|
} else {
|
|
log::error!("not an ioerror in stream_decode: {:?}", err);
|
|
}
|
|
Err(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn try_read_and_decode<R: std::io::Read>(
|
|
r: &mut R,
|
|
buffer: &mut Vec<u8>,
|
|
) -> anyhow::Result<Option<DecodedPdu>> {
|
|
loop {
|
|
if let Some(decoded) =
|
|
Self::stream_decode(buffer).context("stream_decode of buffer for PDU")?
|
|
{
|
|
return Ok(Some(decoded));
|
|
}
|
|
|
|
let mut buf = [0u8; 4096];
|
|
let size = match r.read(&mut buf) {
|
|
Ok(size) => size,
|
|
Err(err) => {
|
|
if err.kind() == std::io::ErrorKind::WouldBlock {
|
|
return Ok(None);
|
|
}
|
|
return Err(err.into());
|
|
}
|
|
};
|
|
if size == 0 {
|
|
return Err(
|
|
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "End Of File").into(),
|
|
);
|
|
}
|
|
|
|
buffer.extend_from_slice(&buf[0..size]);
|
|
}
|
|
}
|
|
|
|
pub fn pane_id(&self) -> Option<PaneId> {
|
|
match self {
|
|
Pdu::GetPaneRenderChangesResponse(GetPaneRenderChangesResponse { pane_id, .. })
|
|
| Pdu::SetPalette(SetPalette { pane_id, .. })
|
|
| Pdu::NotifyAlert(NotifyAlert { pane_id, .. })
|
|
| Pdu::SetClipboard(SetClipboard { pane_id, .. })
|
|
| Pdu::PaneRemoved(PaneRemoved { pane_id }) => Some(*pane_id),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct UnitResponse {}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct ErrorResponse {
|
|
pub reason: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct GetCodecVersion {}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct GetCodecVersionResponse {
|
|
pub codec_vers: usize,
|
|
pub version_string: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct Ping {}
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct Pong {}
|
|
|
|
/// Requests a client certificate to authenticate against
|
|
/// the TLS based server
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct GetTlsCreds {}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct GetTlsCredsResponse {
|
|
/// The signing certificate
|
|
pub ca_cert_pem: String,
|
|
/// A client authentication certificate and private
|
|
/// key, PEM encoded
|
|
pub client_cert_pem: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct ListPanes {}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct ListPanesResponse {
|
|
pub tabs: Vec<PaneNode>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct Spawn {
|
|
pub domain_id: DomainId,
|
|
/// If None, create a new window for this new tab
|
|
pub window_id: Option<WindowId>,
|
|
pub command: Option<CommandBuilder>,
|
|
pub command_dir: Option<String>,
|
|
pub size: PtySize,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct SplitPane {
|
|
pub pane_id: PaneId,
|
|
pub direction: SplitDirection,
|
|
pub command: Option<CommandBuilder>,
|
|
pub command_dir: Option<String>,
|
|
pub domain: config::keyassignment::SpawnTabDomain,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct SpawnV2 {
|
|
pub domain: config::keyassignment::SpawnTabDomain,
|
|
/// If None, create a new window for this new tab
|
|
pub window_id: Option<WindowId>,
|
|
pub command: Option<CommandBuilder>,
|
|
pub command_dir: Option<String>,
|
|
pub size: PtySize,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct PaneRemoved {
|
|
pub pane_id: PaneId,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct KillPane {
|
|
pub pane_id: PaneId,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct SpawnResponse {
|
|
pub tab_id: TabId,
|
|
pub pane_id: PaneId,
|
|
pub window_id: WindowId,
|
|
pub size: PtySize,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct WriteToPane {
|
|
pub pane_id: PaneId,
|
|
pub data: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct SendPaste {
|
|
pub pane_id: PaneId,
|
|
pub data: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct SendKeyDown {
|
|
pub pane_id: TabId,
|
|
pub event: termwiz::input::KeyEvent,
|
|
pub input_serial: InputSerial,
|
|
}
|
|
|
|
/// InputSerial is used to sequence input requests with output events.
|
|
/// It started life as a monotonic sequence number but evolved into
|
|
/// the number of milliseconds since the unix epoch.
|
|
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Copy, PartialOrd, Ord)]
|
|
pub struct InputSerial(u64);
|
|
|
|
impl InputSerial {
|
|
pub const fn empty() -> Self {
|
|
Self(0)
|
|
}
|
|
|
|
pub fn now() -> Self {
|
|
std::time::SystemTime::now().into()
|
|
}
|
|
|
|
pub fn elapsed_millis(&self) -> u64 {
|
|
let now = InputSerial::now();
|
|
now.0 - self.0
|
|
}
|
|
}
|
|
|
|
impl Into<InputSerial> for std::time::SystemTime {
|
|
fn into(self) -> InputSerial {
|
|
let duration = self
|
|
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
|
.expect("SystemTime before unix epoch?");
|
|
let millis: u64 = duration
|
|
.as_millis()
|
|
.try_into()
|
|
.expect("millisecond count to fit in u64");
|
|
InputSerial(millis)
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct SendMouseEvent {
|
|
pub pane_id: PaneId,
|
|
pub event: wezterm_term::input::MouseEvent,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct SetClipboard {
|
|
pub pane_id: PaneId,
|
|
pub clipboard: Option<String>,
|
|
pub selection: ClipboardSelection,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct SetPalette {
|
|
pub pane_id: PaneId,
|
|
pub palette: ColorPalette,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct NotifyAlert {
|
|
pub pane_id: PaneId,
|
|
pub alert: Alert,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct Resize {
|
|
pub containing_tab_id: TabId,
|
|
pub pane_id: PaneId,
|
|
pub size: PtySize,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct SetPaneZoomed {
|
|
pub containing_tab_id: TabId,
|
|
pub pane_id: PaneId,
|
|
pub zoomed: bool,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct GetPaneRenderChanges {
|
|
pub pane_id: PaneId,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct LivenessResponse {
|
|
pub pane_id: PaneId,
|
|
pub is_alive: bool,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct GetPaneRenderChangesResponse {
|
|
pub pane_id: PaneId,
|
|
pub mouse_grabbed: bool,
|
|
pub cursor_position: StableCursorPosition,
|
|
pub dimensions: RenderableDimensions,
|
|
pub dirty_lines: Vec<Range<StableRowIndex>>,
|
|
pub title: String,
|
|
pub working_dir: Option<SerdeUrl>,
|
|
/// Lines that the server thought we'd almost certainly
|
|
/// want to fetch as soon as we received this response
|
|
pub bonus_lines: SerializedLines,
|
|
|
|
pub input_serial: Option<InputSerial>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct GetLines {
|
|
pub pane_id: PaneId,
|
|
pub lines: Vec<Range<StableRowIndex>>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
struct CellCoordinates {
|
|
line_idx: usize,
|
|
cols: Range<usize>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
struct LineHyperlink {
|
|
link: Hyperlink,
|
|
coords: Vec<CellCoordinates>,
|
|
}
|
|
|
|
/// What's all this?
|
|
/// Cells hold references to Arc<Hyperlink> and it is important to us to
|
|
/// maintain identity of the hyperlinks in the individual cells, while also
|
|
/// only sending a single copy of the associated URL.
|
|
/// This section of code extracts the hyperlinks from the cells and builds
|
|
/// up a mapping that can be used to restore the identity when the `lines()`
|
|
/// method is called.
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct SerializedLines {
|
|
lines: Vec<(StableRowIndex, Line)>,
|
|
hyperlinks: Vec<LineHyperlink>,
|
|
// TODO: image references
|
|
}
|
|
|
|
impl SerializedLines {
|
|
pub fn lines(self) -> Vec<(StableRowIndex, Line)> {
|
|
self.into()
|
|
}
|
|
}
|
|
|
|
impl From<Vec<(StableRowIndex, Line)>> for SerializedLines {
|
|
fn from(mut lines: Vec<(StableRowIndex, Line)>) -> Self {
|
|
let mut hyperlinks = vec![];
|
|
|
|
for (line_idx, (_, line)) in lines.iter_mut().enumerate() {
|
|
let mut current_link: Option<Arc<Hyperlink>> = None;
|
|
let mut current_range = 0..0;
|
|
|
|
for (x, cell) in line
|
|
.cells_mut_for_attr_changes_only()
|
|
.iter_mut()
|
|
.enumerate()
|
|
{
|
|
// Unset the hyperlink on the cell, if any, and record that
|
|
// in the hyperlinks data for later restoration.
|
|
if let Some(link) = cell.attrs_mut().hyperlink().map(Arc::clone) {
|
|
cell.attrs_mut().set_hyperlink(None);
|
|
match current_link.as_ref() {
|
|
Some(current) if Arc::ptr_eq(¤t, &link) => {
|
|
// Continue the current streak
|
|
current_range = range_union(current_range, x..x + 1);
|
|
}
|
|
Some(prior) => {
|
|
// It's a different URL, push the current data and start a new one
|
|
hyperlinks.push(LineHyperlink {
|
|
link: (**prior).clone(),
|
|
coords: vec![CellCoordinates {
|
|
line_idx,
|
|
cols: current_range,
|
|
}],
|
|
});
|
|
current_range = x..x + 1;
|
|
current_link = Some(link);
|
|
}
|
|
None => {
|
|
// Starting a new streak
|
|
current_range = x..x + 1;
|
|
current_link = Some(link);
|
|
}
|
|
}
|
|
} else if let Some(link) = current_link.take() {
|
|
// Wrap up a prior streak
|
|
hyperlinks.push(LineHyperlink {
|
|
link: (*link).clone(),
|
|
coords: vec![CellCoordinates {
|
|
line_idx,
|
|
cols: current_range,
|
|
}],
|
|
});
|
|
current_range = 0..0;
|
|
}
|
|
|
|
// TODO: something smart for image cells
|
|
}
|
|
if let Some(link) = current_link.take() {
|
|
// Wrap up final streak
|
|
hyperlinks.push(LineHyperlink {
|
|
link: (*link).clone(),
|
|
coords: vec![CellCoordinates {
|
|
line_idx,
|
|
cols: current_range,
|
|
}],
|
|
});
|
|
}
|
|
}
|
|
|
|
Self { lines, hyperlinks }
|
|
}
|
|
}
|
|
|
|
/// Reconsitute hyperlinks or other attributes that were decomposed for
|
|
/// serialization, and return the line data.
|
|
impl Into<Vec<(StableRowIndex, Line)>> for SerializedLines {
|
|
fn into(self) -> Vec<(StableRowIndex, Line)> {
|
|
if self.hyperlinks.is_empty() {
|
|
self.lines
|
|
} else {
|
|
let mut lines = self.lines;
|
|
|
|
for link in self.hyperlinks {
|
|
let url = Arc::new(link.link);
|
|
|
|
for coord in link.coords {
|
|
if let Some((_, line)) = lines.get_mut(coord.line_idx) {
|
|
if let Some(cells) =
|
|
line.cells_mut_for_attr_changes_only().get_mut(coord.cols)
|
|
{
|
|
for cell in cells {
|
|
cell.attrs_mut().set_hyperlink(Some(Arc::clone(&url)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
lines
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct GetLinesResponse {
|
|
pub pane_id: PaneId,
|
|
pub lines: SerializedLines,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct SearchScrollbackRequest {
|
|
pub pane_id: PaneId,
|
|
pub pattern: mux::pane::Pattern,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
pub struct SearchScrollbackResponse {
|
|
pub results: Vec<mux::pane::SearchResult>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_frame() {
|
|
let mut encoded = Vec::new();
|
|
encode_raw(0x81, 0x42, b"hello", false, &mut encoded).unwrap();
|
|
assert_eq!(&encoded, b"\x08\x42\x81\x01hello");
|
|
let decoded = decode_raw(encoded.as_slice()).unwrap();
|
|
assert_eq!(decoded.ident, 0x81);
|
|
assert_eq!(decoded.serial, 0x42);
|
|
assert_eq!(decoded.data, b"hello");
|
|
}
|
|
|
|
#[test]
|
|
fn test_frame_lengths() {
|
|
let mut serial = 1;
|
|
for target_len in &[128, 247, 256, 65536, 16777216] {
|
|
let mut payload = Vec::with_capacity(*target_len);
|
|
payload.resize(*target_len, b'a');
|
|
let mut encoded = Vec::new();
|
|
encode_raw(0x42, serial, payload.as_slice(), false, &mut encoded).unwrap();
|
|
let decoded = decode_raw(encoded.as_slice()).unwrap();
|
|
assert_eq!(decoded.ident, 0x42);
|
|
assert_eq!(decoded.serial, serial);
|
|
assert_eq!(decoded.data, payload);
|
|
serial += 1;
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_pdu_ping() {
|
|
let mut encoded = Vec::new();
|
|
Pdu::Ping(Ping {}).encode(&mut encoded, 0x40).unwrap();
|
|
assert_eq!(&encoded, &[2, 0x40, 1]);
|
|
assert_eq!(
|
|
DecodedPdu {
|
|
serial: 0x40,
|
|
pdu: Pdu::Ping(Ping {})
|
|
},
|
|
Pdu::decode(encoded.as_slice()).unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stream_decode() {
|
|
let mut encoded = Vec::new();
|
|
Pdu::Ping(Ping {}).encode(&mut encoded, 0x1).unwrap();
|
|
Pdu::Pong(Pong {}).encode(&mut encoded, 0x2).unwrap();
|
|
assert_eq!(encoded.len(), 6);
|
|
|
|
let mut cursor = Cursor::new(encoded.as_slice());
|
|
let mut read_buffer = Vec::new();
|
|
|
|
assert_eq!(
|
|
Pdu::try_read_and_decode(&mut cursor, &mut read_buffer).unwrap(),
|
|
Some(DecodedPdu {
|
|
serial: 1,
|
|
pdu: Pdu::Ping(Ping {})
|
|
})
|
|
);
|
|
assert_eq!(
|
|
Pdu::try_read_and_decode(&mut cursor, &mut read_buffer).unwrap(),
|
|
Some(DecodedPdu {
|
|
serial: 2,
|
|
pdu: Pdu::Pong(Pong {})
|
|
})
|
|
);
|
|
let err = Pdu::try_read_and_decode(&mut cursor, &mut read_buffer).unwrap_err();
|
|
assert_eq!(
|
|
err.downcast_ref::<std::io::Error>().unwrap().kind(),
|
|
std::io::ErrorKind::UnexpectedEof
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pdu_ping_base91() {
|
|
let mut encoded = Vec::new();
|
|
{
|
|
let mut encoder = base91::Base91Encoder::new(&mut encoded);
|
|
Pdu::Ping(Ping {}).encode(&mut encoder, 0x41).unwrap();
|
|
}
|
|
assert_eq!(&encoded, &[60, 67, 75, 65]);
|
|
let decoded = base91::decode(&encoded);
|
|
assert_eq!(
|
|
DecodedPdu {
|
|
serial: 0x41,
|
|
pdu: Pdu::Ping(Ping {})
|
|
},
|
|
Pdu::decode(decoded.as_slice()).unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pdu_pong() {
|
|
let mut encoded = Vec::new();
|
|
Pdu::Pong(Pong {}).encode(&mut encoded, 0x42).unwrap();
|
|
assert_eq!(&encoded, &[2, 0x42, 2]);
|
|
assert_eq!(
|
|
DecodedPdu {
|
|
serial: 0x42,
|
|
pdu: Pdu::Pong(Pong {})
|
|
},
|
|
Pdu::decode(encoded.as_slice()).unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_bogus_pdu() {
|
|
let mut encoded = Vec::new();
|
|
encode_raw(0xdeadbeef, 0x42, b"hello", false, &mut encoded).unwrap();
|
|
assert_eq!(
|
|
DecodedPdu {
|
|
serial: 0x42,
|
|
pdu: Pdu::Invalid { ident: 0xdeadbeef }
|
|
},
|
|
Pdu::decode(encoded.as_slice()).unwrap()
|
|
);
|
|
}
|
|
}
|