Remove unused heuristics for now

This commit is contained in:
Armin Ronacher 2025-06-17 00:33:41 +02:00
parent 09d6b8c578
commit 4f29fd899e
4 changed files with 6 additions and 418 deletions

View file

@ -1,293 +0,0 @@
use std::time::{Duration, Instant};
/// A buffer that safely handles UTF-8 sequences, including partial ones
#[derive(Debug, Clone)]
struct Utf8Buffer {
data: Vec<u8>,
max_len: usize,
}
impl Utf8Buffer {
fn new(max_len: usize) -> Self {
Self {
data: Vec::new(),
max_len,
}
}
fn push_bytes(&mut self, bytes: &[u8]) {
self.data.extend_from_slice(bytes);
self.truncate_to_valid_utf8();
}
fn truncate_to_valid_utf8(&mut self) {
if self.data.len() <= self.max_len {
return;
}
// Find a safe truncation point that preserves UTF-8 boundaries
let target_len = self.max_len;
let mut truncate_at = target_len;
// Work backwards from target length to find a valid UTF-8 boundary
while truncate_at > 0 {
if std::str::from_utf8(&self.data[self.data.len() - truncate_at..]).is_ok() {
break;
}
truncate_at -= 1;
}
if truncate_at > 0 {
let start = self.data.len() - truncate_at;
self.data = self.data[start..].to_vec();
} else {
// If we can't find a valid boundary, clear the buffer
self.data.clear();
}
}
fn as_str(&self) -> &str {
// Return the valid UTF-8 portion, replacing invalid sequences
std::str::from_utf8(&self.data).unwrap_or("")
}
}
#[derive(Debug, Clone)]
pub struct InputDetectionHeuristics {
last_output_time: Option<Instant>,
last_input_time: Option<Instant>,
idle_threshold: Duration,
prompt_patterns: Vec<&'static str>,
recent_output: Utf8Buffer,
consecutive_idle_periods: u32,
}
impl Default for InputDetectionHeuristics {
fn default() -> Self {
Self {
last_output_time: None,
last_input_time: None,
idle_threshold: Duration::from_millis(500),
prompt_patterns: vec![
"$ ",
"# ",
"> ",
"? ",
": ",
">> ",
">>> ",
"Password:",
"password:",
"Enter ",
"Please enter",
"Continue?",
"(y/n)",
"[y/N]",
"[Y/n]",
"Press any key",
"Do you want to",
],
recent_output: Utf8Buffer::new(512),
consecutive_idle_periods: 0,
}
}
}
impl InputDetectionHeuristics {
pub fn new() -> Self {
Self::default()
}
pub fn record_output(&mut self, data: &[u8]) {
self.last_output_time = Some(Instant::now());
self.consecutive_idle_periods = 0;
// Always push the raw bytes, even if they contain invalid UTF-8
self.recent_output.push_bytes(data);
}
pub fn record_input(&mut self) {
self.last_input_time = Some(Instant::now());
}
pub fn check_waiting_for_input(&mut self) -> bool {
let now = Instant::now();
let is_idle = match self.last_output_time {
Some(last_output) => now.duration_since(last_output) >= self.idle_threshold,
None => false,
};
if is_idle {
self.consecutive_idle_periods += 1;
}
let has_prompt_pattern = self.detect_prompt_pattern();
let recent_activity = match (self.last_input_time, self.last_output_time) {
(Some(input_time), Some(output_time)) => {
let since_input = now.duration_since(input_time);
let since_output = now.duration_since(output_time);
since_input > Duration::from_millis(100)
&& since_output > Duration::from_millis(100)
&& since_output >= self.idle_threshold
}
(None, Some(output_time)) => now.duration_since(output_time) >= self.idle_threshold,
_ => false,
};
let confidence_score =
self.calculate_confidence_score(is_idle, has_prompt_pattern, recent_activity);
confidence_score >= 0.6
}
fn detect_prompt_pattern(&self) -> bool {
let recent_lines = self.get_recent_lines(3);
for line in &recent_lines {
// Check both trimmed and untrimmed versions
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
for pattern in &self.prompt_patterns {
if line.ends_with(pattern)
|| line.contains(pattern)
|| trimmed.ends_with(pattern.trim())
|| trimmed.contains(pattern)
{
return true;
}
}
if self.looks_like_prompt(trimmed) {
return true;
}
}
false
}
fn looks_like_prompt(&self, line: &str) -> bool {
let line = line.trim();
if line.is_empty() {
return false;
}
if line.ends_with(':') && line.len() < 50 {
return true;
}
if line.ends_with('?') && line.len() < 100 {
return true;
}
let words = line.split_whitespace().collect::<Vec<_>>();
if words.len() <= 5
&& (line.contains("enter")
|| line.contains("input")
|| line.contains("type")
|| line.contains("choose")
|| line.contains("select"))
{
return true;
}
false
}
fn get_recent_lines(&self, max_lines: usize) -> Vec<String> {
self.recent_output
.as_str()
.lines()
.rev()
.take(max_lines)
.map(|s| s.to_string())
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect()
}
fn calculate_confidence_score(
&self,
is_idle: bool,
has_prompt: bool,
recent_activity: bool,
) -> f32 {
let mut score: f32 = 0.0;
if is_idle {
score += 0.3;
}
if has_prompt {
score += 0.5;
}
if recent_activity {
score += 0.2;
}
if self.consecutive_idle_periods >= 2 {
score += 0.1;
}
if self.consecutive_idle_periods >= 5 {
score += 0.1;
}
score.min(1.0)
}
pub fn get_debug_info(&self) -> String {
format!(
"Heuristics Debug: last_output={:?}, consecutive_idle={}, recent_output_len={}, patterns_detected={}",
self.last_output_time.map(|t| t.elapsed()),
self.consecutive_idle_periods,
self.recent_output.as_str().len(),
self.detect_prompt_pattern()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prompt_detection() {
let mut heuristics = InputDetectionHeuristics::new();
heuristics.record_output(b"user@host:~$ ");
// Wait enough to trigger idle detection
std::thread::sleep(Duration::from_millis(600));
assert!(heuristics.check_waiting_for_input());
let mut heuristics = InputDetectionHeuristics::new();
heuristics.record_output(b"Password: ");
std::thread::sleep(Duration::from_millis(600));
assert!(heuristics.check_waiting_for_input());
let mut heuristics = InputDetectionHeuristics::new();
heuristics.record_output(b"Do you want to continue? (y/n) ");
std::thread::sleep(Duration::from_millis(600));
assert!(heuristics.check_waiting_for_input());
}
#[test]
fn test_idle_detection() {
let mut heuristics = InputDetectionHeuristics::new();
heuristics.idle_threshold = Duration::from_millis(100);
heuristics.record_output(b"$ ");
assert!(!heuristics.check_waiting_for_input());
std::thread::sleep(Duration::from_millis(150));
assert!(heuristics.check_waiting_for_input());
}
}

View file

@ -1,5 +1,4 @@
mod api_server;
mod heuristics;
mod http_server;
mod protocol;
mod sessions;

View file

@ -16,8 +16,6 @@ pub struct SessionInfo {
pub exit_code: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<Timestamp>,
#[serde(default)]
pub waiting: bool,
#[serde(default = "get_default_term")]
pub term: String,
}

View file

@ -9,7 +9,6 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tempfile::NamedTempFile;
use crate::heuristics::InputDetectionHeuristics;
use crate::protocol::{
AsciinemaEvent, AsciinemaEventType, NotificationEvent, NotificationWriter, SessionInfo,
StreamWriter,
@ -195,7 +194,6 @@ pub fn create_session_info(
status: "starting".to_string(),
exit_code: None,
started_at: Some(Timestamp::now()),
waiting: false,
term,
};
@ -239,23 +237,6 @@ fn update_session_status(
Ok(())
}
/// Updates the waiting status in the session JSON file
fn update_session_waiting(session_json_path: &Path, waiting: bool) -> Result<(), io::Error> {
if let Ok(content) = std::fs::read_to_string(session_json_path) {
if let Ok(mut session_info) = serde_json::from_str::<SessionInfo>(&content) {
session_info.waiting = waiting;
let updated_content = serde_json::to_string(&session_info)?;
// Write to temporary file first, then move to final location
let temp_file = NamedTempFile::new_in(
session_json_path.parent().unwrap_or_else(|| Path::new(".")),
)?;
std::fs::write(temp_file.path(), updated_content)?;
temp_file.persist(session_json_path)?;
}
}
Ok(())
}
/// Spawns a process in a PTY in a manor similar to `script`
/// but with separate stdout/stderr.
@ -548,16 +529,13 @@ fn communication_loop(
in_file: Option<&mut File>,
stderr: Option<OwnedFd>,
flush: bool,
mut notification_writer: Option<&mut NotificationWriter>,
session_json_path: Option<&Path>,
_notification_writer: Option<&mut NotificationWriter>,
_session_json_path: Option<&Path>,
) -> Result<i32, Errno> {
let mut buf = [0; 4096];
let mut read_stdin = is_tty;
let mut done = false;
let stdin = io::stdin();
let mut heuristics = InputDetectionHeuristics::new();
let mut input_notification_sent = false;
let mut current_waiting_state = false;
let got_winch = Arc::new(AtomicBool::new(false));
if is_tty {
@ -575,7 +553,7 @@ fn communication_loop(
}
let mut read_fds = FdSet::new();
let mut timeout = TimeVal::new(2, 0); // 2 second timeout
let mut timeout = TimeVal::new(0, 100_000); // 100ms timeout
read_fds.insert(master.as_fd());
if !read_stdin && is_tty {
read_stdin = true;
@ -591,36 +569,7 @@ fn communication_loop(
}
match select(None, Some(&mut read_fds), None, None, Some(&mut timeout)) {
Ok(0) => {
// Timeout occurred - check if we're waiting for input
let is_waiting = heuristics.check_waiting_for_input();
// Update session waiting state if it changed
if is_waiting != current_waiting_state {
current_waiting_state = is_waiting;
if let Some(session_json_path) = session_json_path {
let _ = update_session_waiting(session_json_path, is_waiting);
}
}
// Send notification only once per waiting period
if let Some(notification_writer) = &mut notification_writer {
if is_waiting && !input_notification_sent {
let event = NotificationEvent {
timestamp: jiff::Timestamp::now(),
event: "input_requested".to_string(),
data: serde_json::json!({
"title": "Input Requested",
"message": "The terminal appears to be waiting for input",
"debug_info": heuristics.get_debug_info()
}),
};
if notification_writer.write_notification(event).is_err() {
// Ignore notification write errors to not interrupt the main flow
}
input_notification_sent = true;
}
}
// Timeout occurred - just continue
continue;
}
Err(Errno::EINTR | Errno::EAGAIN) => continue,
@ -635,17 +584,6 @@ fn communication_loop(
read_stdin = false;
}
Ok(n) => {
heuristics.record_input();
input_notification_sent = false; // Reset notification state on user input
// Update waiting state to false when there's input
if current_waiting_state {
current_waiting_state = false;
if let Some(session_json_path) = session_json_path {
let _ = update_session_waiting(session_json_path, false);
}
}
write_all(master.as_fd(), &buf[..n])?;
}
Err(Errno::EINTR | Errno::EAGAIN) => {}
@ -665,16 +603,6 @@ fn communication_loop(
Ok(0) | Err(Errno::EAGAIN | Errno::EINTR) => {}
Err(err) => return Err(err),
Ok(n) => {
heuristics.record_input();
// Update waiting state to false when there's input from FIFO
if current_waiting_state {
current_waiting_state = false;
if let Some(session_json_path) = session_json_path {
let _ = update_session_waiting(session_json_path, false);
}
}
write_all(master.as_fd(), &buf[..n])?;
}
}
@ -697,7 +625,6 @@ fn communication_loop(
done = true;
}
Ok(n) => {
heuristics.record_output(&buf[..n]);
forward_and_log(io::stdout().as_fd(), &mut stream_writer, &buf[..n], flush)?
}
Err(Errno::EAGAIN | Errno::EINTR) => {}
@ -833,13 +760,10 @@ fn monitor_detached_session(
) -> Result<(), Errno> {
let mut buf = [0; 4096];
let mut done = false;
let mut heuristics = InputDetectionHeuristics::new();
let mut input_notification_sent = false;
let mut current_waiting_state = false;
while !done {
let mut read_fds = FdSet::new();
let mut timeout = TimeVal::new(2, 0); // 2 second timeout
let mut timeout = TimeVal::new(0, 100_000); // 100ms timeout
read_fds.insert(master.as_fd());
if let Some(ref f) = stdin_file {
@ -848,36 +772,7 @@ fn monitor_detached_session(
match select(None, Some(&mut read_fds), None, None, Some(&mut timeout)) {
Ok(0) => {
// Timeout occurred - check if we're waiting for input
let is_waiting = heuristics.check_waiting_for_input();
// Update session waiting state if it changed
if is_waiting != current_waiting_state {
current_waiting_state = is_waiting;
if let Some(session_json_path) = session_json_path {
let _ = update_session_waiting(session_json_path, is_waiting);
}
}
// Send notification only once per waiting period
if let Some(notification_writer) = &mut notification_writer {
if is_waiting && !input_notification_sent {
let event = NotificationEvent {
timestamp: jiff::Timestamp::now(),
event: "input_requested".to_string(),
data: serde_json::json!({
"title": "Input Requested",
"message": "The terminal appears to be waiting for input",
"debug_info": heuristics.get_debug_info()
}),
};
if notification_writer.write_notification(event).is_err() {
// Ignore notification write errors to not interrupt the main flow
}
input_notification_sent = true;
}
}
// Timeout occurred - just continue
continue;
}
Err(Errno::EINTR | Errno::EAGAIN) => continue,
@ -891,16 +786,6 @@ fn monitor_detached_session(
Ok(0) | Err(Errno::EAGAIN | Errno::EINTR) => {}
Err(err) => return Err(err),
Ok(n) => {
heuristics.record_input();
// Update waiting state to false when there's input from FIFO
if current_waiting_state {
current_waiting_state = false;
if let Some(session_json_path) = session_json_path {
let _ = update_session_waiting(session_json_path, false);
}
}
write_all(master.as_fd(), &buf[..n])?;
}
}
@ -914,7 +799,6 @@ fn monitor_detached_session(
done = true;
}
Ok(n) => {
heuristics.record_output(&buf[..n]);
// Only log to stream writer, don't write to stdout since we're detached
if let Some(writer) = &mut stream_writer {
let time = writer.elapsed_time();