rename client and move incomplete projects to research

This commit is contained in:
Timur Gordon
2025-07-09 23:38:47 +02:00
parent e75c050745
commit 3b1bc9a838
28 changed files with 229 additions and 126 deletions

275
research/repl/src/main.rs Normal file
View File

@@ -0,0 +1,275 @@
use anyhow::Context;
use rhai_dispatcher::{RhaiDispatcher, RhaiDispatcherBuilder, RhaiDispatcherError};
use rustyline::error::ReadlineError;
use rustyline::{Config, DefaultEditor, EditMode};
use std::env;
use std::fs;
use std::process::Command;
use std::time::Duration;
use tempfile::Builder as TempFileBuilder;
use tracing_subscriber::EnvFilter;
// Default timeout for script execution
const DEFAULT_SCRIPT_TIMEOUT_SECONDS: u64 = 30;
async fn execute_script(client: &RhaiDispatcher, circle_name: &str, script_content: String) {
if script_content.trim().is_empty() {
println!("Script is empty, not sending.");
return;
}
println!(
"Sending script to worker '{}':\n---\n{}\n---",
circle_name, script_content
);
let timeout = Duration::from_secs(DEFAULT_SCRIPT_TIMEOUT_SECONDS);
match client
.new_play_request()
.worker_id(circle_name)
.script(&script_content)
.timeout(timeout)
.await_response()
.await
{
Ok(task_details) => {
if let Some(output) = &task_details.output {
println!("worker: {}", output);
}
if let Some(error_msg) = &task_details.error {
eprintln!("Worker error: {}", error_msg);
}
if task_details.output.is_none() && task_details.error.is_none() {
println!(
"Worker finished with no explicit output or error. Status: {}",
task_details.status
);
}
}
Err(e) => match e {
RhaiDispatcherError::Timeout(task_id) => {
eprintln!(
"Error: Script execution timed out for task_id: {}.",
task_id
);
}
RhaiDispatcherError::RedisError(redis_err) => {
eprintln!(
"Error: Redis communication failed: {}. Check Redis connection and server status.",
redis_err
);
}
RhaiDispatcherError::SerializationError(serde_err) => {
eprintln!(
"Error: Failed to serialize/deserialize task data: {}.",
serde_err
);
}
RhaiDispatcherError::TaskNotFound(task_id) => {
eprintln!(
"Error: Task {} not found after submission (this should be rare).",
task_id
);
}
},
}
}
async fn run_repl(redis_url: String, circle_name: String) -> anyhow::Result<()> {
println!(
"Initializing Rhai REPL for worker '{}' via Redis at {}...",
circle_name, redis_url
);
let client = RhaiDispatcherBuilder::new()
.redis_url(&redis_url)
.caller_id("ui_repl") // Set a caller_id
.build()
.with_context(|| format!("Failed to create RhaiDispatcher for Redis URL: {}", redis_url))?;
// No explicit connect() needed for rhai_dispatcher, connection is handled per-operation or pooled.
println!(
"RhaiDispatcher initialized. Ready to send scripts to worker '{}'.",
circle_name
);
println!(
"Type Rhai scripts, '.edit' to use $EDITOR, '.run <path>' to execute a file, or 'exit'/'quit'."
);
println!("Vi mode enabled for input line.");
let config = Config::builder()
.edit_mode(EditMode::Vi)
.auto_add_history(true) // Automatically add to history
.build();
let mut rl = DefaultEditor::with_config(config)?;
let history_file = ".rhai_repl_history.txt"; // Simple history file in current dir
if rl.load_history(history_file).is_err() {
// No history found or error loading, not critical
}
let prompt = format!("rhai ({}) @ {}> ", circle_name, redis_url);
loop {
let readline = rl.readline(&prompt);
match readline {
Ok(line) => {
let input = line.trim();
if input.eq_ignore_ascii_case("exit") || input.eq_ignore_ascii_case("quit") {
println!("Exiting REPL.");
break;
} else if input.eq_ignore_ascii_case(".edit") {
// Correct way to create a temp file with a suffix
let temp_file = TempFileBuilder::new()
.prefix("rhai_script_") // Optional: add a prefix
.suffix(".rhai")
.tempfile_in(".") // Create in current directory for simplicity
.with_context(|| "Failed to create temp file")?;
// You can pre-populate the temp file if needed:
// use std::io::Write; // Add this import if using write_all
// if let Err(e) = temp_file.as_file().write_all(b"// Start your Rhai script here\n") {
// eprintln!("Failed to write initial content to temp file: {}", e);
// }
let temp_path = temp_file.path().to_path_buf();
let editor_cmd_str = env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
let mut editor_parts = editor_cmd_str.split_whitespace();
let editor_executable = editor_parts.next().unwrap_or("vi"); // Default to vi if $EDITOR is empty string
let editor_args: Vec<&str> = editor_parts.collect();
println!(
"Launching editor: '{}' with args: {:?} for script editing. Save and exit editor to execute.",
editor_executable, editor_args
);
let mut command = Command::new(editor_executable);
command.args(editor_args); // Add any arguments from $EDITOR (like -w)
command.arg(&temp_path); // Add the temp file path as the last argument
let status = command.status();
match status {
Ok(exit_status) if exit_status.success() => {
match fs::read_to_string(&temp_path) {
Ok(script_content) => {
execute_script(&client, &circle_name, script_content).await;
}
Err(e) => {
eprintln!("Error reading temp file {:?}: {}", temp_path, e)
}
}
}
Ok(exit_status) => eprintln!(
"Editor exited with status: {}. Script not executed.",
exit_status
),
Err(e) => eprintln!(
"Failed to launch editor '{}': {}. Ensure it's in your PATH.",
editor_executable, e
), // Changed 'editor' to 'editor_executable'
}
// temp_file is automatically deleted when it goes out of scope
} else if input.starts_with(".run ") || input.starts_with("run ") {
let parts: Vec<&str> = input.splitn(2, ' ').collect();
if parts.len() == 2 {
let file_path = parts[1];
println!("Attempting to run script from file: {}", file_path);
match fs::read_to_string(file_path) {
Ok(script_content) => {
execute_script(&client, &circle_name, script_content).await;
}
Err(e) => eprintln!("Error reading file {}: {}", file_path, e),
}
} else {
eprintln!("Usage: .run <filepath>");
}
} else if !input.is_empty() {
execute_script(&client, &circle_name, input.to_string()).await;
}
// rl.add_history_entry(line.as_str()) is handled by auto_add_history(true)
}
Err(ReadlineError::Interrupted) => {
// Ctrl-C
println!("Input interrupted. Type 'exit' or 'quit' to close.");
continue;
}
Err(ReadlineError::Eof) => {
// Ctrl-D
println!("Exiting REPL (EOF).");
break;
}
Err(err) => {
eprintln!("Error reading input: {:?}", err);
break;
}
}
}
if rl.save_history(history_file).is_err() {
// Failed to save history, not critical
}
// No explicit disconnect for RhaiDispatcher as it manages connections internally.
println!("Exited REPL.");
Ok(())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::from_default_env()
.add_directive("ui_repl=info".parse()?)
.add_directive("rhai_dispatcher=info".parse()?),
)
.init();
let args: Vec<String> = env::args().collect();
let redis_url_str = if args.len() > 1 {
args[1].clone()
} else {
let default_url = "redis://127.0.0.1/".to_string();
println!("No Redis URL provided. Defaulting to: {}", default_url);
default_url
};
let circle_name_str = if args.len() > 2 {
args[2].clone()
} else {
let default_circle = "default_worker".to_string();
println!(
"No worker/circle name provided. Defaulting to: {}",
default_circle
);
default_circle
};
println!(
"Usage: {} [redis_url] [worker_name]",
args.get(0).map_or("ui_repl", |s| s.as_str())
);
println!(
"Example: {} redis://127.0.0.1/ my_rhai_worker",
args.get(0).map_or("ui_repl", |s| s.as_str())
);
// Basic validation for Redis URL (scheme)
// A more robust validation might involve trying to parse it with redis::ConnectionInfo
if !redis_url_str.starts_with("redis://") {
eprintln!(
"Warning: Redis URL '{}' does not start with 'redis://'. Attempting to use it anyway.",
redis_url_str
);
}
if let Err(e) = run_repl(redis_url_str, circle_name_str).await {
eprintln!("REPL error: {:#}", e);
std::process::exit(1);
}
Ok(())
}