add engine and rpc client, archive old code

This commit is contained in:
timurgordon
2025-06-03 21:47:36 +03:00
parent 061aee6f1d
commit b20140785e
42 changed files with 10310 additions and 0 deletions

1320
_archive/devtools/reloadd/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
[package]
name = "reloadd"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.21"
notify = "6"
futures-util = "0.3"

View File

@@ -0,0 +1,124 @@
# Reloadd
A powerful development tool for automatically restarting your application and reloading your browser when files change.
## Features
- 🔄 **File Watching**: Monitor multiple directories for changes
- 🚀 **Auto-Restart**: Automatically restart your application when files change
- 🌐 **Browser Reload**: Automatically reload connected browsers
- 🔌 **WebSocket Integration**: Uses WebSockets for instant browser reloading
- 📊 **Sequential Commands**: Run multiple commands in sequence
- 🔧 **Configurable Ports**: Customize web server and WebSocket ports
- 🛠️ **Robust Error Handling**: Clear error messages and graceful recovery
## Installation
### From Source
```bash
# Clone the repository
git clone https://github.com/yourusername/reloadd.git
cd reloadd
# Build and install
cargo install --path .
```
## Usage
### Basic Usage
```bash
reloadd --watch src --watch templates -- run --example server
```
### With Sequential Commands
```bash
reloadd --watch src --run "cargo build" --run "cargo test" --run "cargo run --example server"
```
Commands will run in the order specified. All commands except the last one will run to completion. The last command is treated as the long-running server process.
### With Custom Port
```bash
reloadd --watch src --port 3000 --run "cargo run --example server"
```
### In a Shell Script
Create a `develop.sh` script for your project:
```bash
#!/usr/bin/env bash
set -e
# Start dev server with file watching and browser reload
reloadd \
--watch src \
--watch templates \
--port 8080 \
--run "cargo run --example server"
```
Make it executable and run it:
```bash
chmod +x develop.sh
./develop.sh
```
## Command Line Options
```
Usage: reloadd [OPTIONS] --watch <PATH>... [-- <COMMAND>...]
Arguments:
[COMMAND]... Command to run on change (legacy format)
Options:
-w, --watch <PATH>... Paths to watch (like src/, templates/, scripts/)
-p, --port <PORT> Port for the web server [default: 8080]
-r, --run <COMMAND>... Multiple commands to run
-h, --help Print help
-V, --version Print version
```
## LiveReload Integration
When you start the tool, it will output a script tag that you can add to your HTML files:
```html
<script>
const ws = new WebSocket("ws://localhost:35729");
ws.onmessage = (msg) => {
if (msg.data === "reload") location.reload();
};
</script>
```
Add this script to your HTML templates to enable automatic browser reloading.
## How It Works
1. Reloadd watches specified directories for file changes
2. When a change is detected, it runs your commands in sequence
3. Build commands (all except the last) run to completion before proceeding
4. The last command (typically a server) runs and stays active
5. After a brief delay, it sends a reload signal to connected browsers
6. Browsers with the LiveReload script will automatically refresh
## Error Handling
Reloadd includes robust error handling:
- Validates watch paths before starting
- Checks for port availability
- Provides clear error messages
- Gracefully exits with error codes when critical errors occur
## License
MIT

View File

@@ -0,0 +1,97 @@
use clap::{Parser};
use notify::{RecursiveMode, Watcher, EventKind, recommended_watcher};
use std::sync::{Arc, Mutex};
use std::process::{Command, Child, Stdio};
use tokio::net::TcpListener;
use tokio::sync::broadcast;
use tokio_tungstenite::accept_async;
use futures_util::{SinkExt, StreamExt};
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
/// Paths to watch (like src/, templates/, scripts/)
#[arg(short, long, value_name = "PATH", num_args = 1.., required = true)]
watch: Vec<String>,
/// Command to run on change (like: -- run --example server)
#[arg(last = true)]
command: Vec<String>,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
let (tx, _) = broadcast::channel::<()>(10);
let tx_ws = tx.clone();
let server_process = Arc::new(Mutex::new(None::<Child>));
// Start WebSocket reload server
tokio::spawn(start_websocket_server(tx_ws.clone()));
// Start watching files and restarting command
let server_process_clone = Arc::clone(&server_process);
let watch_paths = args.watch.clone();
let cmd_args = args.command.clone();
std::thread::spawn(move || {
let mut watcher = recommended_watcher(move |res| {
if let Ok(event) = res {
if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)) {
println!("📦 Change detected, restarting...");
// Kill previous process
if let Some(mut child) = server_process_clone.lock().unwrap().take() {
let _ = child.kill();
}
// Run new process
let mut cmd = Command::new("cargo");
cmd.args(&cmd_args);
cmd.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let child = cmd.spawn().expect("Failed to spawn");
*server_process_clone.lock().unwrap() = Some(child);
// Notify browser
let _ = tx.send(());
}
}
}).expect("watcher failed");
// Add watches
for path in &watch_paths {
watcher.watch(path, RecursiveMode::Recursive).unwrap();
}
loop {
std::thread::sleep(std::time::Duration::from_secs(3600));
}
});
println!("🔁 Watching paths: {:?}", args.watch);
println!("🌐 Connect browser to ws://localhost:35729");
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
}
}
async fn start_websocket_server(tx: broadcast::Sender<()>) {
let listener = TcpListener::bind("127.0.0.1:35729").await.unwrap();
while let Ok((stream, _)) = listener.accept().await {
let tx = tx.clone();
let mut rx = tx.subscribe();
tokio::spawn(async move {
let ws_stream = accept_async(stream).await.unwrap();
let (mut write, _) = ws_stream.split();
while rx.recv().await.is_ok() {
let _ = write.send(tokio_tungstenite::tungstenite::Message::Text("reload".into())).await;
}
});
}
}

View File

@@ -0,0 +1,333 @@
use clap::{Parser};
use notify::{RecursiveMode, Watcher, EventKind, recommended_watcher};
use std::sync::{Arc, Mutex};
use std::process::{Command, Child, Stdio};
use tokio::net::TcpStream;
use tokio::sync::broadcast;
use tokio_tungstenite::accept_async;
use futures_util::{SinkExt, StreamExt};
use tokio::time::{sleep, Duration};
use tokio::io::AsyncWriteExt;
use std::net::TcpListener as StdTcpListener;
use std::net::SocketAddr;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
/// Paths to watch (like src/, templates/, scripts/)
#[arg(short, long, value_name = "PATH", num_args = 1.., required = true)]
watch: Vec<String>,
/// Port for the web server (default: 8080)
#[arg(short, long, default_value = "8080")]
port: u16,
/// Multiple commands to run (format: --run "cargo build" --run "cargo test")
#[arg(short, long, value_name = "COMMAND", num_args = 1..)]
run: Vec<String>,
/// Command to run on change (like: -- run --example server)
/// This is kept for backward compatibility
#[arg(last = true)]
command: Vec<String>,
}
// Run commands in sequence, with the last one potentially being a long-running server
async fn run_commands_in_sequence(commands: &[Vec<String>], server_process: Arc<Mutex<Option<Child>>>) -> bool {
// Run all commands in sequence
for (i, command) in commands.iter().enumerate() {
let is_last = i == commands.len() - 1;
let program = if command[0] == "cargo" || command[0].ends_with("/cargo") {
"cargo".to_string()
} else {
command[0].clone()
};
let args = if command[0] == "cargo" || command[0].ends_with("/cargo") {
command.iter().skip(1).cloned().collect::<Vec<String>>()
} else {
command.iter().skip(1).cloned().collect::<Vec<String>>()
};
let mut cmd = Command::new(&program);
cmd.args(&args);
cmd.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
if is_last {
// Last command might be a long-running server
match cmd.spawn() {
Ok(child) => {
*server_process.lock().unwrap() = Some(child);
println!("🚀 Started server process: {} {}", program, args.join(" "));
},
Err(e) => {
eprintln!("❌ Failed to start server process: {}", e);
return false;
}
}
} else {
// For non-last commands, wait for them to complete
println!("🔄 Running build step: {} {}", program, args.join(" "));
match cmd.output() {
Ok(output) => {
if !output.status.success() {
eprintln!("❌ Command failed: {} {}", program, args.join(" "));
return false;
}
println!("✅ Command completed successfully");
},
Err(e) => {
eprintln!("❌ Failed to execute command: {}", e);
return false;
}
}
}
}
true
}
// Check if a port is already in use
fn is_port_in_use(port: u16) -> bool {
StdTcpListener::bind(format!("127.0.0.1:{}", port)).is_err()
}
// Find an available port starting from the given port
fn find_available_port(start_port: u16) -> Option<u16> {
let mut port = start_port;
// Try up to 10 ports (start_port through start_port+9)
for _ in 0..10 {
if !is_port_in_use(port) {
return Some(port);
}
port += 1;
}
None
}
// Generate LiveReload script with the given WebSocket port
fn generate_livereload_script(ws_port: u16) -> String {
format!(
r#"<script>
const ws = new WebSocket("ws://localhost:{}");
ws.onmessage = (msg) => {{
if (msg.data === "reload") location.reload();
}};
</script>"#,
ws_port
)
}
#[tokio::main]
async fn main() {
let args = Args::parse();
// Handle both the new --run and legacy command format
let commands = if !args.run.is_empty() {
// New format: each --run is a separate command
args.run.iter().map(|cmd| {
// Split the command string into arguments
cmd.split_whitespace().map(String::from).collect::<Vec<String>>()
}).collect::<Vec<Vec<String>>>()
} else if !args.command.is_empty() {
// Legacy format: single command from the trailing arguments
println!("Command: {:?}", args.command);
vec![args.command.clone()]
} else {
// No commands provided
eprintln!("❌ Error: No commands provided. Use --run or trailing arguments.");
std::process::exit(1);
};
// Check if server port is already in use
let server_port = args.port;
if is_port_in_use(server_port) {
eprintln!("❌ Error: Port {} is already in use. Stop any running instances before starting a new one.", server_port);
std::process::exit(1);
}
// Find an available WebSocket port
let ws_port = match find_available_port(35729) {
Some(port) => port,
None => {
eprintln!("❌ Error: Could not find an available WebSocket port. Please free up some ports and try again.");
std::process::exit(1);
}
};
let (tx, _) = broadcast::channel::<()>(10);
let tx_ws = tx.clone();
let server_process = Arc::new(Mutex::new(None::<Child>));
// Validate paths before starting the watcher
let mut invalid_paths = Vec::new();
for path in &args.watch {
if !std::path::Path::new(path).exists() {
invalid_paths.push(path.clone());
}
}
if !invalid_paths.is_empty() {
eprintln!("❌ Error: The following watch paths do not exist:");
for path in invalid_paths {
eprintln!(" - {}", path);
}
eprintln!("Please provide valid paths to watch.");
std::process::exit(1);
}
// Start WebSocket reload server
match start_websocket_server(tx_ws.clone(), ws_port).await {
Ok(_) => {},
Err(e) => {
eprintln!("❌ Failed to start WebSocket server: {}", e);
std::process::exit(1);
}
}
// Output the LiveReload script for users to add to their projects
println!("📋 Add this script to your HTML for live reloading:");
println!("{}", generate_livereload_script(ws_port));
// 🚀 Run all commands in sequence
if !run_commands_in_sequence(&commands, Arc::clone(&server_process)).await {
eprintln!("❌ Command execution failed.");
std::process::exit(1);
}
println!("🔁 Watching paths: {:?}", args.watch);
println!("🌐 Connect browser to ws://localhost:{}", ws_port);
println!("🖥️ Web server running on http://localhost:{}", server_port);
// Create a runtime handle for the watcher to use
let rt_handle = tokio::runtime::Handle::current();
// Clone necessary values before moving into the watcher thread
let watch_paths = args.watch.clone();
let commands_clone = commands.clone();
let server_process_clone = Arc::clone(&server_process);
let tx_clone = tx.clone();
// Create a dedicated thread for the file watcher
let watcher_thread = std::thread::spawn(move || {
// Create a new watcher
let mut watcher = match recommended_watcher(move |res: notify::Result<notify::Event>| {
if let Ok(event) = res {
if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)) {
println!("📦 Change detected, restarting...");
// Kill previous process
if let Some(mut child) = server_process_clone.lock().unwrap().take() {
let _ = child.kill();
}
// Run new process (only the primary command gets restarted)
if let Some(first_command) = commands.first() {
let program = if first_command[0] == "cargo" || first_command[0].ends_with("/cargo") {
"cargo".to_string()
} else {
first_command[0].clone()
};
let args = if first_command[0] == "cargo" || first_command[0].ends_with("/cargo") {
first_command.iter().skip(1).cloned().collect::<Vec<String>>()
} else {
first_command.iter().skip(1).cloned().collect::<Vec<String>>()
};
let mut cmd = Command::new(&program);
cmd.args(&args);
cmd.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
match cmd.spawn() {
Ok(child) => {
*server_process_clone.lock().unwrap() = Some(child);
// Immediately send reload signal without waiting for server
let tx = tx_clone.clone();
rt_handle.spawn(async move {
// Small delay to give the server a moment to start
sleep(Duration::from_millis(500)).await;
// Notify browser to reload
let _ = tx.send(());
});
},
Err(e) => {
eprintln!("❌ Failed to spawn process: {}", e);
}
}
}
}
} else if let Err(e) = res {
eprintln!("❌ Watch error: {}", e);
}
}) {
Ok(w) => w,
Err(e) => {
eprintln!("❌ Failed to create watcher: {}", e);
std::process::exit(1);
}
};
// Add watches
for path in &watch_paths {
match watcher.watch(path.as_ref(), RecursiveMode::Recursive) {
Ok(_) => println!("👁️ Watching path: {}", path),
Err(e) => {
eprintln!("❌ Failed to watch path '{}': {}", path, e);
std::process::exit(1);
}
}
}
// Keep the thread alive
loop {
std::thread::sleep(std::time::Duration::from_secs(3600));
}
});
// Wait for the watcher thread to finish (it shouldn't unless there's an error)
match watcher_thread.join() {
Ok(_) => {},
Err(e) => {
eprintln!("❌ Watcher thread panicked: {:?}", e);
std::process::exit(1);
}
}
}
async fn start_websocket_server(tx: broadcast::Sender<()>, ws_port: u16) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = format!("127.0.0.1:{}", ws_port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
println!("WebSocket server started on ws://localhost:{}", ws_port);
// Spawn a task to handle WebSocket connections
tokio::spawn(async move {
while let Ok((stream, addr)) = listener.accept().await {
println!("New WebSocket connection from: {}", addr);
let tx = tx.clone();
let mut rx = tx.subscribe();
tokio::spawn(async move {
match accept_async(stream).await {
Ok(ws_stream) => {
let (mut write, _) = ws_stream.split();
while rx.recv().await.is_ok() {
if let Err(e) = write.send(tokio_tungstenite::tungstenite::Message::Text("reload".into())).await {
eprintln!("❌ Error sending reload message to client {}: {}", addr, e);
break;
}
}
},
Err(e) => {
eprintln!("❌ Failed to accept WebSocket connection from {}: {}", addr, e);
}
}
});
}
});
Ok(())
}