243 lines
8.7 KiB
Rust
243 lines
8.7 KiB
Rust
|
|
//! Модуль истории команд для 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<String, VecDeque<String>>,
|
|||
|
|
max_size: usize,
|
|||
|
|
history_file: String,
|
|||
|
|
save_queue: SegQueue<HistoryUpdate>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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<String> {
|
|||
|
|
self.commands.get(session_id)
|
|||
|
|
.map(|history| history.iter().cloned().collect())
|
|||
|
|
.unwrap_or_default()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Поиск команд по префиксу
|
|||
|
|
pub fn search(&self, session_id: &str, prefix: &str) -> Vec<String> {
|
|||
|
|
self.get_history(session_id)
|
|||
|
|
.into_iter()
|
|||
|
|
.filter(|cmd| cmd.starts_with(prefix))
|
|||
|
|
.collect()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Получение последней команды
|
|||
|
|
pub fn last(&self, session_id: &str) -> Option<String> {
|
|||
|
|
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::<Vec<_>>()
|
|||
|
|
.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<String> = 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<String> {
|
|||
|
|
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,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|