blob: 553ef838f908f964d34bab394d57b6461c2b2a7a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::{
Result, ShellError,
control::{ControlError, WriteReceipt},
output::Output,
};
use maka_agent_facts::terminal::TerminalSize;
use maka_agent_facts::terminal::{PREVIEW_BYTES, TerminalScreen, display::Display};
use maka_process::pty::PtyIo;
use maka_process::terminal::Screen;
use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
pub(super) struct Terminal {
screen: Option<Screen>,
pub preview: Arc<TerminalScreen>,
frozen: Option<Arc<Display>>,
incomplete: bool,
decoder: Decoder,
pub writes: VecDeque<PendingWrite>,
output: Output,
}
impl Terminal {
pub fn new(size: TerminalSize, output: Output) -> Self {
Self {
screen: Some(Screen::new(size)),
preview: Arc::new(TerminalScreen::new(size)),
frozen: None,
incomplete: false,
decoder: Decoder::default(),
writes: VecDeque::new(),
output,
}
}
pub fn output(&mut self, bytes: &[u8], eof: bool) -> Result<()> {
// Native bytes remain observable even when decoding replaces invalid
// text or the parser has failed and final native output is draining.
self.output.publish(bytes)?;
let text = self.decoder.decode(bytes, eof);
let Some(screen) = &mut self.screen else {
return Ok(());
};
match screen.write(&text) {
Ok(replies) => {
self.preview = Arc::new(screen.preview(PREVIEW_BYTES)?);
if !replies.is_empty() {
let pending: usize = self
.writes
.iter()
// Accepted user controls own independent queued custody;
// their extent is not an automatic parser reply budget.
.filter(|write| write.reply.is_none())
.map(|write| write.remaining().len())
.sum();
if pending.saturating_add(replies.len()) > 1024 * 1024 {
self.freeze();
return Err(ShellError::Rejected("terminal reply queue exceeds 1 MiB"));
}
self.writes
.push_back(PendingWrite::new(replies, None, false, false));
}
Ok(())
}
Err(error) => {
// Freeze the applied prefix once, mark it incomplete, then release the parser.
self.freeze();
Err(error.into())
}
}
}
pub fn bracketed_paste(&self) -> bool {
self.screen.as_ref().is_some_and(Screen::bracketed_paste)
}
pub fn resize(&mut self, size: TerminalSize) -> Result<()> {
let screen = self
.screen
.as_mut()
.ok_or(ShellError::Rejected("terminal parser unavailable"))?;
screen.resize(size)?;
self.preview = Arc::new(screen.preview(PREVIEW_BYTES)?);
Ok(())
}
pub fn fail_writes(&mut self, message: &str) {
for mut pending in self.writes.drain(..) {
if let Some(reply) = pending.reply.take() {
let _ = reply.send(Err(ControlError::new(message, pending.accepted)
.after_resize(pending.resized, pending.resize_changed)));
}
}
}
pub fn display(&self) -> Arc<Display> {
if let Some(screen) = &self.screen {
let mut display = screen.display();
display.metadata.truncated |= self.incomplete;
Arc::new(display)
} else {
self.frozen
.as_ref()
.expect("failed parser froze its display")
.clone()
}
}
fn freeze(&mut self) {
if let Some(screen) = self.screen.take() {
let mut display = screen.display();
display.metadata.truncated = true;
self.frozen = Some(Arc::new(display));
let mut preview = screen.text_preview(PREVIEW_BYTES);
preview.truncated = true;
self.preview = Arc::new(preview);
}
}
pub fn close(&mut self) {
self.screen = None;
}
}
pub(super) struct PendingWrite {
bytes: Vec<u8>,
pub accepted: usize,
reply: Option<oneshot::Sender<std::result::Result<WriteReceipt, ControlError>>>,
resized: bool,
resize_changed: bool,
}
impl PendingWrite {
pub fn new(
text: String,
reply: Option<oneshot::Sender<std::result::Result<WriteReceipt, ControlError>>>,
resized: bool,
resize_changed: bool,
) -> Self {
Self {
bytes: text.into_bytes(),
accepted: 0,
reply,
resized,
resize_changed,
}
}
pub fn remaining(&self) -> &[u8] {
&self.bytes[self.accepted..]
}
pub fn advance(&mut self, count: usize) -> bool {
self.accepted += count;
self.accepted == self.bytes.len()
}
pub fn finish(mut self, record: std::sync::Arc<maka_agent_facts::shell_run::ShellObservation>) {
if let Some(reply) = self.reply.take() {
let _ = reply.send(Ok(WriteReceipt {
accepted_bytes: self.accepted,
resized: self.resized,
resize_changed: self.resize_changed,
record,
}));
}
}
}
/// Retains only the incomplete UTF-8 suffix (at most three bytes). Malformed
/// bytes and a genuinely incomplete EOF use replacement characters, like xterm.
#[derive(Default)]
pub(super) struct Decoder {
pending: Vec<u8>,
}
impl Decoder {
pub(super) fn decode(&mut self, bytes: &[u8], eof: bool) -> String {
self.pending.extend_from_slice(bytes);
let mut text = String::new();
let mut consumed = 0;
while consumed < self.pending.len() {
match std::str::from_utf8(&self.pending[consumed..]) {
Ok(valid) => {
text.push_str(valid);
consumed = self.pending.len();
}
Err(error) => {
let end = consumed + error.valid_up_to();
text.push_str(std::str::from_utf8(&self.pending[consumed..end]).unwrap());
consumed = end;
match error.error_len() {
Some(length) => {
text.push('\u{fffd}');
consumed += length;
}
None if eof => {
text.push('\u{fffd}');
consumed = self.pending.len();
}
None => break,
}
}
}
}
self.pending.drain(..consumed);
text
}
}
pub(super) async fn drain(
terminal: &mut Terminal,
io: &PtyIo,
exited: &CancellationToken,
) -> Result<()> {
let deadline = async {
exited.cancelled().await;
tokio::time::sleep(Duration::from_secs(2)).await;
};
tokio::pin!(deadline);
let mut buffer = [0; 16 * 1024];
let mut failure = None;
loop {
let count = tokio::select! {
_ = &mut deadline => {
io.discard_output()?;
terminal.incomplete = true;
Arc::make_mut(&mut terminal.preview).truncated = true;
if let Some(display) = &mut terminal.frozen { Arc::make_mut(display).metadata.truncated = true; }
return Err(ShellError::Rejected("PTY output did not drain after root exit"));
}
read = io.read(&mut buffer) => match read {
Ok(count) => count,
Err(error) => { io.discard_output()?; return Err(error.into()); }
}
};
if let Err(error) = terminal.output(&buffer[..count], count == 0) {
failure.get_or_insert(error);
}
// After root exit replies have no consumer and are not input effects.
terminal.fail_writes("PTY root exited");
if count == 0 {
return failure.map_or(Ok(()), Err);
}
}
}
#[cfg(test)]
mod tests {
use super::Decoder;
#[tokio::test]
async fn raw_output_preserves_native_bytes_before_screen_decoding() {
use super::{Output, Terminal};
use maka_agent_facts::terminal::TerminalSize;
let size = TerminalSize::new(80, 24).unwrap();
let output = Output::new(size, tokio::sync::watch::channel(()).0).unwrap();
let mut terminal = Terminal::new(size, output.clone());
// Exercise the actual Host decoder/parser/replay path. These supplied
// native-byte cuts are not a claim of OS PTY transport verification.
terminal.output(b"\xff\0\x1b[31m\xe4", false).unwrap();
terminal.output(b"\xb8\xad\x1b[0m", false).unwrap();
terminal.output(b"\xe2\x82", true).unwrap();
let mut stream = output.stream(0).unwrap();
let mut raw = Vec::new();
while let super::super::output::PtyStreamEvent::Data { bytes, .. } =
stream.read(16 * 1024).await.unwrap()
{
raw.extend_from_slice(&bytes);
}
assert_eq!(
raw.as_slice(),
b"\xff\0\x1b[31m\xe4\xb8\xad\x1b[0m\xe2\x82",
"the raw replay must preserve invalid bytes, NUL, ANSI and incomplete EOF independently of the screen decoder"
);
}
#[tokio::test]
async fn parser_failure_and_later_drain_preserve_original_raw_bytes() {
use super::super::output::PtyStreamEvent;
use super::{Output, Terminal};
use maka_agent_facts::terminal::TerminalSize;
let size = TerminalSize::new(80, 24).unwrap();
let output = Output::new(size, tokio::sync::watch::channel(()).0).unwrap();
let mut terminal = Terminal::new(size, output.clone());
terminal.output(b"\x1b]0;", false).unwrap();
let chunk = vec![b'x'; 16 * 1024];
for _ in 0..3 {
terminal.output(&chunk, false).unwrap();
}
assert!(terminal.output(&chunk, false).is_err());
assert!(terminal.screen.is_none());
terminal.output(b"final-drain\xff", true).unwrap();
let mut expected = b"\x1b]0;".to_vec();
expected.extend_from_slice(&chunk.repeat(4));
expected.extend_from_slice(b"final-drain\xff");
let mut stream = output.stream(0).unwrap();
let mut raw = Vec::new();
while let PtyStreamEvent::Data { bytes, .. } = stream.read(16 * 1024).await.unwrap() {
raw.extend_from_slice(&bytes);
}
assert_eq!(raw, expected);
}
#[test]
fn large_user_control_does_not_consume_automatic_reply_capacity() {
use super::{Output, PendingWrite, Terminal};
use maka_agent_facts::terminal::TerminalSize;
let size = TerminalSize::new(80, 24).unwrap();
let output = Output::new(size, tokio::sync::watch::channel(()).0).unwrap();
let mut terminal = Terminal::new(size, output);
let (reply, mut receipt) = tokio::sync::oneshot::channel();
let input = "x".repeat(1024 * 1024 + 1);
terminal
.writes
.push_back(PendingWrite::new(input.clone(), Some(reply), false, false));
terminal.output(b"\x1b[6n", false).unwrap();
assert!(terminal.screen.is_some());
assert_eq!(terminal.writes.len(), 2);
assert_eq!(terminal.writes[0].remaining(), input.as_bytes());
assert!(terminal.writes[0].reply.is_some());
assert!(terminal.writes[1].reply.is_none());
assert_eq!(terminal.writes[1].remaining(), b"\x1b[1;1R");
assert!(matches!(
receipt.try_recv(),
Err(tokio::sync::oneshot::error::TryRecvError::Empty)
));
}
#[test]
fn every_utf8_cut_matches_lossy_eof_without_losing_terminal_controls() {
let bytes = b"\x1b[31m\xe4\xb8\xad\xf0\x9f\x98\x80\xff\xe2\x82";
for first in 0..=bytes.len() {
for second in first..=bytes.len() {
let mut decoder = Decoder::default();
let actual = decoder.decode(&bytes[..first], false)
+ &decoder.decode(&bytes[first..second], false)
+ &decoder.decode(&bytes[second..], true);
assert_eq!(actual, String::from_utf8_lossy(bytes));
assert!(decoder.pending.is_empty());
}
}
}
}