mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-24 14:47:39 +00:00
Remove unused heuristics for now
This commit is contained in:
parent
09d6b8c578
commit
4f29fd899e
4 changed files with 6 additions and 418 deletions
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
mod api_server;
|
||||
mod heuristics;
|
||||
mod http_server;
|
||||
mod protocol;
|
||||
mod sessions;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue