reorganize module
This commit is contained in:
1320
devtools/reloadd/Cargo.lock
generated
Normal file
1320
devtools/reloadd/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
124
devtools/reloadd/README.md
Normal file
124
devtools/reloadd/README.md
Normal 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
|
@@ -9,6 +9,7 @@ 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)]
|
||||
@@ -17,53 +18,142 @@ struct Args {
|
||||
#[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>,
|
||||
}
|
||||
|
||||
async fn wait_for_server(port: u16) -> bool {
|
||||
let address = format!("localhost:{}", port);
|
||||
let mut retries = 0;
|
||||
|
||||
while retries < 10 {
|
||||
if let Ok(mut stream) = TcpStream::connect(&address).await {
|
||||
let _ = stream.write_all(b"GET / HTTP/1.1\r\n\r\n").await; // A dummy GET request
|
||||
println!("✅ Server is ready on {}!", address);
|
||||
return true;
|
||||
// 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 {
|
||||
retries += 1;
|
||||
println!("⏳ Waiting for server to be ready (Attempt {}/10)...", retries);
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("❌ Server not ready after 10 attempts.");
|
||||
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();
|
||||
println!("Command: {:?}", args.command);
|
||||
|
||||
// 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 = 8080; // Adjust as needed
|
||||
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);
|
||||
}
|
||||
|
||||
// Check if WebSocket port is already in use
|
||||
let ws_port = 35729;
|
||||
if is_port_in_use(ws_port) {
|
||||
eprintln!("❌ Error: WebSocket port {} is already in use. Stop any running instances before starting a new one.", ws_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();
|
||||
@@ -87,46 +177,34 @@ async fn main() {
|
||||
}
|
||||
|
||||
// Start WebSocket reload server
|
||||
match start_websocket_server(tx_ws.clone()).await {
|
||||
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 the server immediately
|
||||
{
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.args(&args.command);
|
||||
cmd.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit());
|
||||
match cmd.spawn() {
|
||||
Ok(child) => {
|
||||
*server_process.lock().unwrap() = Some(child);
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to start initial process: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the server to be ready before triggering reloads
|
||||
if !wait_for_server(server_port).await {
|
||||
eprintln!("❌ Server failed to start properly.");
|
||||
// 🚀 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 cmd_args = args.command.clone();
|
||||
let commands_clone = commands.clone();
|
||||
let server_process_clone = Arc::clone(&server_process);
|
||||
let tx_clone = tx.clone();
|
||||
|
||||
@@ -143,27 +221,41 @@ async fn main() {
|
||||
let _ = child.kill();
|
||||
}
|
||||
|
||||
// Run new process
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.args(&cmd_args);
|
||||
cmd.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit());
|
||||
|
||||
match cmd.spawn() {
|
||||
Ok(child) => {
|
||||
*server_process_clone.lock().unwrap() = Some(child);
|
||||
|
||||
// Use the runtime handle to spawn a task
|
||||
let tx = tx_clone.clone();
|
||||
rt_handle.spawn(async move {
|
||||
if wait_for_server(server_port).await {
|
||||
// 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);
|
||||
});
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to spawn process: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,9 +297,10 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_websocket_server(tx: broadcast::Sender<()>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:35729").await?;
|
||||
println!("WebSocket server started on ws://localhost:35729");
|
||||
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 {
|
||||
|
Reference in New Issue
Block a user