feat: add auto pipeline as default top-level command
This commit is contained in:
+54
-2
@@ -4,11 +4,21 @@ use std::path::PathBuf;
|
||||
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
|
||||
/// With no subcommand, the top-level args run the full auto pipeline
|
||||
/// (ping → latency → speed → export).
|
||||
#[derive(Parser)]
|
||||
#[command(name = "fast-xray", about = "Fast Cloudflare IP optimizer for CDN-fronted Xray/VLESS nodes.")]
|
||||
#[command(
|
||||
name = "fast-xray",
|
||||
about = "Fast Cloudflare IP optimizer for CDN-fronted Xray/VLESS nodes.",
|
||||
arg_required_else_help = true,
|
||||
args_conflicts_with_subcommands = true
|
||||
)]
|
||||
pub(crate) struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Command,
|
||||
pub(crate) command: Option<Command>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub(crate) auto: AutoArgs,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -23,6 +33,48 @@ pub(crate) enum Command {
|
||||
Export(ExportArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub(crate) struct AutoArgs {
|
||||
/// Input vless:// node URL (or use --node-file).
|
||||
pub(crate) node: Option<String>,
|
||||
|
||||
/// Read the vless:// node URL from a file (avoids shell escaping of `&`).
|
||||
#[arg(long)]
|
||||
pub(crate) node_file: Option<PathBuf>,
|
||||
|
||||
/// Stage 1: how many valid IPs the ping stage collects.
|
||||
#[arg(short = 'n', long, default_value_t = 50)]
|
||||
pub(crate) count: usize,
|
||||
|
||||
/// Stage 2: keep the fastest N after latency (feeds the speed stage).
|
||||
#[arg(long, default_value_t = 30)]
|
||||
pub(crate) lat_top: usize,
|
||||
|
||||
/// Stage 3: final node count written to result.txt.
|
||||
#[arg(long, default_value_t = 10)]
|
||||
pub(crate) speed_top: usize,
|
||||
|
||||
/// Also test IPv6 ranges (off by default).
|
||||
#[arg(short = '6', long = "ipv6")]
|
||||
pub(crate) ipv6: bool,
|
||||
|
||||
/// Quality gate: drop IPs whose real latency exceeds this (ms). 0 = off.
|
||||
#[arg(long, default_value_t = 0.0)]
|
||||
pub(crate) max_latency: f64,
|
||||
|
||||
/// Quality gate: drop IPs slower than this (Mbps). 0 = off.
|
||||
#[arg(long, default_value_t = 0.0)]
|
||||
pub(crate) min_speed: f64,
|
||||
|
||||
/// Output directory for all intermediate files and result.txt.
|
||||
#[arg(short = 'o', long, default_value = "result")]
|
||||
pub(crate) output: PathBuf,
|
||||
|
||||
/// Print each stage's result table.
|
||||
#[arg(short = 'v', long)]
|
||||
pub(crate) verbose: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub(crate) struct PingArgs {
|
||||
/// Target number of valid (reachable) IPs to collect.
|
||||
|
||||
+205
-73
@@ -1,34 +1,37 @@
|
||||
//! Subcommand orchestration: wire CLI args to the stage modules and report.
|
||||
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use console::style;
|
||||
use indicatif::ProgressBar;
|
||||
|
||||
use crate::cli::{ExportArgs, LatencyArgs, PingArgs, SpeedArgs};
|
||||
use crate::cli::{AutoArgs, ExportArgs, LatencyArgs, PingArgs, SpeedArgs};
|
||||
use crate::cloudflare::{self, Family};
|
||||
use crate::ping::{self, Progress};
|
||||
use crate::report::{
|
||||
LiveProgress, print_speed_table, print_table, read_export_rows, read_ip_list, read_latency_csv,
|
||||
resolve_node, write_csv, write_file, write_ips, write_speed_csv,
|
||||
};
|
||||
use crate::vless::VlessNode;
|
||||
use crate::{latency, speed};
|
||||
|
||||
// Stage defaults the auto pipeline uses for the knobs it doesn't expose.
|
||||
const PING_TIMEOUT: f64 = 3.0;
|
||||
const PING_CONCURRENCY: usize = 100;
|
||||
const PING_PROBE_SAMPLE: usize = 5;
|
||||
const PING_MIN_LATENCY_MS: f64 = 10.0;
|
||||
const PING_MAX_LATENCY_MS: f64 = 300.0;
|
||||
const LAT_CONCURRENCY: usize = 50;
|
||||
const LAT_TIMEOUT: f64 = 5.0;
|
||||
const SPEED_CONCURRENCY: usize = 1;
|
||||
const SPEED_TIMEOUT: f64 = 10.0;
|
||||
const SPEED_BYTES: u64 = 10_000_000;
|
||||
|
||||
// ---- individual subcommands ------------------------------------------------
|
||||
|
||||
pub(crate) async fn run_ping(args: PingArgs) -> Result<()> {
|
||||
let family = if args.ipv6 { Family::Both } else { Family::V4 };
|
||||
|
||||
let spinner = ProgressBar::new_spinner();
|
||||
spinner.enable_steady_tick(Duration::from_millis(90));
|
||||
spinner.set_message("Fetching Cloudflare ranges…");
|
||||
let ranges = cloudflare::fetch_ranges(family).await?;
|
||||
spinner.finish_with_message(format!(
|
||||
"{} Cloudflare ranges: {} v4, {} v6",
|
||||
style("✓").green().bold(),
|
||||
ranges.v4.len(),
|
||||
ranges.v6.len()
|
||||
));
|
||||
|
||||
let cfg = ping::PingConfig {
|
||||
count: args.count,
|
||||
timeout: Duration::from_secs_f64(args.timeout),
|
||||
@@ -43,31 +46,11 @@ pub(crate) async fn run_ping(args: PingArgs) -> Result<()> {
|
||||
max_loss: args.max_loss,
|
||||
};
|
||||
|
||||
let target = cfg.count;
|
||||
let mut progress = LiveProgress::new(cfg.count as u64, "valid");
|
||||
let results = ping::run(&ranges, &cfg, |event| match event {
|
||||
Progress::Probing { ip, ok, valid, probed } => {
|
||||
let mark = if ok { style("✓").green() } else { style("x").red() };
|
||||
progress.push(valid.min(target) as u64, probed, format!("{mark} {ip}"));
|
||||
}
|
||||
Progress::PreScan { live, segments, valid } => {
|
||||
progress.println(format!(
|
||||
" pre-scan: {} live / {} segments, {} already valid",
|
||||
style(live).cyan(),
|
||||
segments,
|
||||
style(valid).green(),
|
||||
));
|
||||
progress.set_position(valid.min(target) as u64);
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
progress.finish();
|
||||
|
||||
let results = stage_ping(&cfg).await?;
|
||||
if results.is_empty() {
|
||||
eprintln!("{} no reachable IP found", style("✗").red().bold());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if args.verbose {
|
||||
print_table(&results);
|
||||
}
|
||||
@@ -77,7 +60,6 @@ pub(crate) async fn run_ping(args: PingArgs) -> Result<()> {
|
||||
|
||||
pub(crate) async fn run_latency(args: LatencyArgs) -> Result<()> {
|
||||
let node = resolve_node(&args.node_file, &args.node)?;
|
||||
|
||||
let ips = read_ip_list(&args.input)?;
|
||||
if ips.is_empty() {
|
||||
return Err(anyhow!("no valid IPs in {}", args.input.display()));
|
||||
@@ -89,34 +71,19 @@ pub(crate) async fn run_latency(args: LatencyArgs) -> Result<()> {
|
||||
ips.len()
|
||||
);
|
||||
|
||||
let mut progress = LiveProgress::new(ips.len() as u64, "tested");
|
||||
let mut kept = 0usize;
|
||||
let results = latency::run(
|
||||
let results = stage_latency(
|
||||
&node,
|
||||
&ips,
|
||||
args.concurrency.max(1),
|
||||
Duration::from_secs_f64(args.timeout),
|
||||
Duration::from_secs_f64(args.max_latency / 1000.0),
|
||||
args.top,
|
||||
|p| {
|
||||
let line = match p.latency {
|
||||
Some(d) => {
|
||||
kept += 1;
|
||||
format!("{} {} {:.0}ms", style("✓").green(), p.ip, d.as_secs_f64() * 1000.0)
|
||||
}
|
||||
None => format!("{} {}", style("x").red(), p.ip),
|
||||
};
|
||||
progress.push(p.done as u64, kept, line);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
progress.finish();
|
||||
|
||||
if results.is_empty() {
|
||||
eprintln!("{} no IP passed the real latency test", style("✗").red().bold());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if args.verbose {
|
||||
print_table(&results);
|
||||
}
|
||||
@@ -127,44 +94,26 @@ pub(crate) async fn run_latency(args: LatencyArgs) -> Result<()> {
|
||||
|
||||
pub(crate) async fn run_speed(args: SpeedArgs) -> Result<()> {
|
||||
let node = resolve_node(&args.node_file, &args.node)?;
|
||||
|
||||
let inputs = read_latency_csv(&args.input)?;
|
||||
if inputs.is_empty() {
|
||||
return Err(anyhow!("no IP+latency rows in {}", args.input.display()));
|
||||
}
|
||||
eprintln!("Node host: {} | speed-testing {} IPs", style(&node.host).cyan(), inputs.len());
|
||||
|
||||
let mut progress = LiveProgress::new(inputs.len() as u64, "tested");
|
||||
let mut kept = 0usize;
|
||||
let mut results = speed::run(
|
||||
let results = stage_speed(
|
||||
&node,
|
||||
&inputs,
|
||||
args.concurrency.max(1),
|
||||
Duration::from_secs_f64(args.timeout),
|
||||
args.bytes,
|
||||
args.min_speed,
|
||||
|p| {
|
||||
let line = match p.speed_mbps {
|
||||
Some(mbps) => {
|
||||
kept += 1;
|
||||
format!("{} {} {:.2}Mbps", style("✓").green(), p.ip, mbps)
|
||||
}
|
||||
None => format!("{} {}", style("x").red(), p.ip),
|
||||
};
|
||||
progress.push(p.done as u64, kept, line);
|
||||
},
|
||||
args.top,
|
||||
)
|
||||
.await;
|
||||
progress.finish();
|
||||
|
||||
if results.is_empty() {
|
||||
eprintln!("{} no IP passed the speed test", style("✗").red().bold());
|
||||
return Ok(());
|
||||
}
|
||||
if args.top > 0 {
|
||||
results.truncate(args.top);
|
||||
}
|
||||
|
||||
if args.verbose {
|
||||
print_speed_table(&results);
|
||||
}
|
||||
@@ -181,12 +130,12 @@ pub(crate) fn run_export(args: ExportArgs) -> Result<()> {
|
||||
if rows.is_empty() {
|
||||
return Err(anyhow!("no valid IPs in {}", args.input.display()));
|
||||
}
|
||||
let path = args.output.join("result.txt");
|
||||
let mut body = String::new();
|
||||
for row in &rows {
|
||||
body.push_str(&node.to_url(row.ip, &row.alias()));
|
||||
body.push('\n');
|
||||
}
|
||||
let path = args.output.join("result.txt");
|
||||
write_file(&path, &body)?;
|
||||
eprintln!(
|
||||
"{} built {} nodes -> {}",
|
||||
@@ -196,3 +145,186 @@ pub(crate) fn run_export(args: ExportArgs) -> Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---- auto pipeline ---------------------------------------------------------
|
||||
|
||||
pub(crate) async fn run_auto(args: AutoArgs) -> Result<()> {
|
||||
let node = resolve_node(&args.node_file, &args.node)?;
|
||||
let out = &args.output;
|
||||
|
||||
eprintln!("{}", style("[1/4] ping").bold().cyan());
|
||||
let cfg = ping::PingConfig {
|
||||
count: args.count,
|
||||
timeout: Duration::from_secs_f64(PING_TIMEOUT),
|
||||
concurrency: PING_CONCURRENCY,
|
||||
ipv6: args.ipv6,
|
||||
port: 443,
|
||||
probe_sample: PING_PROBE_SAMPLE,
|
||||
max_probe: args.count.saturating_mul(100),
|
||||
times: 1,
|
||||
min_latency: Duration::from_secs_f64(PING_MIN_LATENCY_MS / 1000.0),
|
||||
max_latency: Duration::from_secs_f64(PING_MAX_LATENCY_MS / 1000.0),
|
||||
max_loss: 0.0,
|
||||
};
|
||||
let ping_results = stage_ping(&cfg).await?;
|
||||
if ping_results.is_empty() {
|
||||
eprintln!("{} no reachable IP found", style("✗").red().bold());
|
||||
return Ok(());
|
||||
}
|
||||
write_ips(&ping_results, &out.join("ip.txt"))?;
|
||||
let ips: Vec<IpAddr> = ping_results.iter().map(|r| r.ip).collect();
|
||||
|
||||
eprintln!("{}", style("[2/4] latency").bold().cyan());
|
||||
eprintln!("Node host: {} path: {}", style(&node.host).cyan(), style(&node.path).cyan());
|
||||
let lat_results = stage_latency(
|
||||
&node,
|
||||
&ips,
|
||||
LAT_CONCURRENCY,
|
||||
Duration::from_secs_f64(LAT_TIMEOUT),
|
||||
Duration::from_secs_f64(args.max_latency / 1000.0),
|
||||
args.lat_top,
|
||||
)
|
||||
.await;
|
||||
if lat_results.is_empty() {
|
||||
eprintln!("{} no IP passed the real latency test", style("✗").red().bold());
|
||||
return Ok(());
|
||||
}
|
||||
write_csv(&lat_results, &out.join("latency.csv"))?;
|
||||
write_ips(&lat_results, &out.join("latency.txt"))?;
|
||||
let inputs: Vec<(IpAddr, Duration)> = lat_results.iter().map(|r| (r.ip, r.latency)).collect();
|
||||
|
||||
eprintln!("{}", style("[3/4] speed").bold().cyan());
|
||||
let speed_results = stage_speed(
|
||||
&node,
|
||||
&inputs,
|
||||
SPEED_CONCURRENCY,
|
||||
Duration::from_secs_f64(SPEED_TIMEOUT),
|
||||
SPEED_BYTES,
|
||||
args.min_speed,
|
||||
args.speed_top,
|
||||
)
|
||||
.await;
|
||||
if speed_results.is_empty() {
|
||||
eprintln!("{} no IP passed the speed test", style("✗").red().bold());
|
||||
return Ok(());
|
||||
}
|
||||
write_speed_csv(&speed_results, &out.join("speed.csv"))?;
|
||||
write_ips(&speed_results, &out.join("speed.txt"))?;
|
||||
|
||||
eprintln!("{}", style("[4/4] export").bold().cyan());
|
||||
let mut body = String::new();
|
||||
for r in &speed_results {
|
||||
let alias =
|
||||
format!("{:.2}M-{:.0}ms-{}", r.speed_mbps, r.latency.as_secs_f64() * 1000.0, r.ip);
|
||||
body.push_str(&node.to_url(r.ip, &alias));
|
||||
body.push('\n');
|
||||
}
|
||||
let path = out.join("result.txt");
|
||||
write_file(&path, &body)?;
|
||||
eprintln!(
|
||||
"{} built {} nodes -> {}",
|
||||
style("✓").green().bold(),
|
||||
speed_results.len(),
|
||||
style(path.display()).cyan()
|
||||
);
|
||||
|
||||
if args.verbose {
|
||||
print_speed_table(&speed_results);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---- shared stage runners (used by both subcommands and auto) --------------
|
||||
|
||||
/// Fetch ranges and run the ping stage with the live progress UI.
|
||||
async fn stage_ping(cfg: &ping::PingConfig) -> Result<Vec<ping::PingResult>> {
|
||||
let family = if cfg.ipv6 { Family::Both } else { Family::V4 };
|
||||
let spinner = ProgressBar::new_spinner();
|
||||
spinner.enable_steady_tick(Duration::from_millis(90));
|
||||
spinner.set_message("Fetching Cloudflare ranges…");
|
||||
let ranges = cloudflare::fetch_ranges(family).await?;
|
||||
spinner.finish_with_message(format!(
|
||||
"{} Cloudflare ranges: {} v4, {} v6",
|
||||
style("✓").green().bold(),
|
||||
ranges.v4.len(),
|
||||
ranges.v6.len()
|
||||
));
|
||||
|
||||
let target = cfg.count;
|
||||
let mut progress = LiveProgress::new(cfg.count as u64, "valid");
|
||||
let results = ping::run(&ranges, cfg, |event| match event {
|
||||
Progress::Probing { ip, ok, valid, probed } => {
|
||||
let mark = if ok { style("✓").green() } else { style("x").red() };
|
||||
progress.push(valid.min(target) as u64, probed, format!("{mark} {ip}"));
|
||||
}
|
||||
Progress::PreScan { live, segments, valid } => {
|
||||
progress.println(format!(
|
||||
" pre-scan: {} live / {} segments, {} already valid",
|
||||
style(live).cyan(),
|
||||
segments,
|
||||
style(valid).green(),
|
||||
));
|
||||
progress.set_position(valid.min(target) as u64);
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
progress.finish();
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Run the latency stage with the live progress UI.
|
||||
async fn stage_latency(
|
||||
node: &VlessNode,
|
||||
ips: &[IpAddr],
|
||||
concurrency: usize,
|
||||
timeout: Duration,
|
||||
max_latency: Duration,
|
||||
top: usize,
|
||||
) -> Vec<latency::LatencyResult> {
|
||||
let mut progress = LiveProgress::new(ips.len() as u64, "tested");
|
||||
let mut kept = 0usize;
|
||||
let results = latency::run(node, ips, concurrency, timeout, max_latency, top, |p| {
|
||||
let line = match p.latency {
|
||||
Some(d) => {
|
||||
kept += 1;
|
||||
format!("{} {} {:.0}ms", style("✓").green(), p.ip, d.as_secs_f64() * 1000.0)
|
||||
}
|
||||
None => format!("{} {}", style("x").red(), p.ip),
|
||||
};
|
||||
progress.push(p.done as u64, kept, line);
|
||||
})
|
||||
.await;
|
||||
progress.finish();
|
||||
results
|
||||
}
|
||||
|
||||
/// Run the speed stage with the live progress UI, keeping the best `top`.
|
||||
async fn stage_speed(
|
||||
node: &VlessNode,
|
||||
inputs: &[(IpAddr, Duration)],
|
||||
concurrency: usize,
|
||||
timeout: Duration,
|
||||
bytes: u64,
|
||||
min_speed: f64,
|
||||
top: usize,
|
||||
) -> Vec<speed::SpeedResult> {
|
||||
let mut progress = LiveProgress::new(inputs.len() as u64, "tested");
|
||||
let mut kept = 0usize;
|
||||
let mut results =
|
||||
speed::run(node, inputs, concurrency, timeout, bytes, min_speed, |p| {
|
||||
let line = match p.speed_mbps {
|
||||
Some(mbps) => {
|
||||
kept += 1;
|
||||
format!("{} {} {:.2}Mbps", style("✓").green(), p.ip, mbps)
|
||||
}
|
||||
None => format!("{} {}", style("x").red(), p.ip),
|
||||
};
|
||||
progress.push(p.done as u64, kept, line);
|
||||
})
|
||||
.await;
|
||||
progress.finish();
|
||||
if top > 0 {
|
||||
results.truncate(top);
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
+5
-4
@@ -27,9 +27,10 @@ async fn main() -> Result<()> {
|
||||
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Command::Ping(args) => commands::run_ping(args).await,
|
||||
Command::Latency(args) => commands::run_latency(args).await,
|
||||
Command::Speed(args) => commands::run_speed(args).await,
|
||||
Command::Export(args) => commands::run_export(args),
|
||||
Some(Command::Ping(args)) => commands::run_ping(args).await,
|
||||
Some(Command::Latency(args)) => commands::run_latency(args).await,
|
||||
Some(Command::Speed(args)) => commands::run_speed(args).await,
|
||||
Some(Command::Export(args)) => commands::run_export(args),
|
||||
None => commands::run_auto(cli.auto).await,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user