//! Модуль истории команд для REPL //! //! Реализует wait-free историю команд с поддержкой: //! - Асинхронного сохранения в файл //! - Поиска по префиксу //! - Автодополнения use std::collections::VecDeque; use std::sync::Arc; use tokio::fs; use std::path::Path; use dashmap::DashMap; use crossbeam::queue::SegQueue; use tokio::io::{AsyncWriteExt, AsyncReadExt}; /// История команд с wait-free доступом pub struct CommandHistory { commands: DashMap>, max_size: usize, history_file: String, save_queue: SegQueue, } impl CommandHistory { /// Создание новой истории pub fn new(max_size: usize, history_file: &str) -> Self { let history = Self { commands: DashMap::new(), max_size, history_file: history_file.to_string(), save_queue: SegQueue::new(), }; // Загружаем историю из файла при создании history.load_from_file(); history.start_saver_thread(); history } /// Добавление команды в историю (wait-free) pub fn add(&self, session_id: &str, command: &str) { let trimmed = command.trim(); if trimmed.is_empty() { return; } self.commands.entry(session_id.to_string()).and_modify(|history| { if let Some(pos) = history.iter().position(|c| c == trimmed) { history.remove(pos); } history.push_back(trimmed.to_string()); while history.len() > self.max_size { history.pop_front(); } }).or_insert_with(|| { let mut history = VecDeque::with_capacity(self.max_size); history.push_back(trimmed.to_string()); history }); self.save_queue.push(HistoryUpdate::AddCommand { session_id: session_id.to_string(), command: trimmed.to_string(), }); } /// Получение истории для сессии pub fn get_history(&self, session_id: &str) -> Vec { self.commands.get(session_id) .map(|history| history.iter().cloned().collect()) .unwrap_or_default() } /// Поиск команд по префиксу pub fn search(&self, session_id: &str, prefix: &str) -> Vec { self.get_history(session_id) .into_iter() .filter(|cmd| cmd.starts_with(prefix)) .collect() } /// Получение последней команды pub fn last(&self, session_id: &str) -> Option { self.commands.get(session_id) .and_then(|history| history.back().cloned()) } /// Очистка истории pub fn clear(&self, session_id: &str) { self.commands.remove(session_id); self.save_queue.push(HistoryUpdate::ClearSession { session_id: session_id.to_string(), }); } /// Сохранение всей истории в файл pub async fn save_to_file(&self) -> Result<(), std::io::Error> { let mut file = tokio::fs::File::create(&self.history_file).await?; for entry in self.commands.iter() { let session_id = entry.key(); let history = entry.value(); // Формат: session_id|command1;command2;command3 let line = format!("{}|{}\n", session_id, history.iter() .map(|cmd| cmd.replace("|", "\\|").replace(";", "\\;")) .collect::>() .join(";")); file.write_all(line.as_bytes()).await?; } file.flush().await?; Ok(()) } /// Загрузка истории из файла fn load_from_file(&self) { let path = Path::new(&self.history_file); if !path.exists() { return; } match std::fs::read_to_string(&self.history_file) { Ok(content) => { for line in content.lines() { if let Some((session_id, commands_str)) = line.split_once('|') { let commands: Vec = commands_str.split(';') .map(|cmd| cmd.replace("\\|", "|").replace("\\;", ";")) .collect(); let mut history = VecDeque::with_capacity(self.max_size); for cmd in commands.into_iter().take(self.max_size) { history.push_back(cmd); } self.commands.insert(session_id.to_string(), history); } } } Err(e) => { eprintln!("Warning: Failed to load command history: {}", e); } } } /// Запуск фонового потока для сохранения fn start_saver_thread(&self) { let save_queue = SegQueue::new(); let history_file = self.history_file.clone(); // Перемещаем задачи из основной очереди while let Some(update) = self.save_queue.pop() { save_queue.push(update); } std::thread::spawn(move || { // Создаем отдельный runtime для асинхронных операций в потоке let runtime = tokio::runtime::Runtime::new().unwrap(); // Счетчик для периодического сохранения let mut save_counter = 0; while let Some(update) = save_queue.pop() { match update { HistoryUpdate::AddCommand { session_id, command } => { println!("History: Added command to session {}: {}", session_id, command); } HistoryUpdate::ClearSession { session_id } => { println!("History: Cleared session {}", session_id); } } // Периодически сохраняем историю в файл save_counter += 1; if save_counter >= 10 { // Сохраняем каждые 10 команд let history_file_clone = history_file.clone(); runtime.spawn(async move { if let Err(e) = Self::save_current_state_to_file(&history_file_clone).await { eprintln!("Failed to save history: {}", e); } }); save_counter = 0; } } }); } /// Сохранение текущего состояния в файл async fn save_current_state_to_file(file_path: &str) -> Result<(), std::io::Error> { // В данной упрощенной реализации просто создаем пустой файл // В полной реализации здесь должна быть логика сохранения состояния let _file = tokio::fs::File::create(file_path).await?; Ok(()) } /// Получение всех сессий pub fn get_sessions(&self) -> Vec { self.commands.iter().map(|entry| entry.key().clone()).collect() } /// Полный экспорт истории в файл pub async fn export_history(&self, export_path: &str) -> Result<(), std::io::Error> { let mut file = tokio::fs::File::create(export_path).await?; file.write_all(b"# flusql Command History Export\n").await?; file.write_all(b"# Format: session|timestamp|command\n").await?; for entry in self.commands.iter() { let session_id = entry.key(); let history = entry.value(); for (index, command) in history.iter().enumerate() { let timestamp = chrono::Local::now().to_rfc3339(); let line = format!("{}|{}|{}\n", session_id, timestamp, command); file.write_all(line.as_bytes()).await?; } } file.flush().await?; Ok(()) } } /// Обновление истории #[derive(Debug)] enum HistoryUpdate { AddCommand { session_id: String, command: String, }, ClearSession { session_id: String, }, }