Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 13 additions & 90 deletions crates/vmate-cli/src/commands/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,115 +3,32 @@
use crate::cli::AllArgs;
use crate::commands::connect::{persist_connect_defaults, resolve_connect};
use crate::settings::Settings;
use crate::ui::progress::{ProgressReporter, VerboseReporter};
use anyhow::Result;
use clap_verbosity_flag::Verbosity;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use vmate_core::connect::{Candidate, ConnectOptions, ConnectQueue, ConnectService};
use vmate_core::db::ConfigRepo;
use vmate_core::db::pool::init_pool;
use vmate_core::geo::IpInfoGeoLocator;
use vmate_core::ovpn::process::{RealOpenVpnRunner, RealVpnTester, VpnTester};
use vmate_core::scan::{ScanOptions, ScanProgress, ScanService};
use vmate_core::ovpn::process::RealOpenVpnRunner;
use vmate_core::settings::UserSettings;
use vmate_core::system::{
CleanupGuard, ProcessKiller, RealProcessKiller, require_root_for, shutdown_signal,
};
use vmate_core::system::{ProcessKiller, RealProcessKiller, require_root_for};

pub async fn run(settings: &Settings, args: &AllArgs, verbose: &Verbosity) -> Result<()> {
// --save-defaults is a pure settings operation: persist and exit without
// scanning or connecting (no root, no OpenVPN, no DB needed).
if settings.save_defaults {
crate::commands::scan::persist_scan_defaults(&args.scan)?;
let mut us = UserSettings::load();
persist_connect_defaults(&mut us, &args.connect)?;
persist_connect_defaults(&args.connect)?;
return Ok(());
}

require_root_for("run OpenVPN tests and connections", settings.no_elevate)?;

let us = UserSettings::load();
let (workers, limit, timeout) = crate::commands::scan::resolve_scan_defaults(&args.scan, &us);
let connect = resolve_connect(&us, &args.connect);

let dir = match &args.scan.dir {
Some(dir) => dir.clone(),
None => crate::commands::scan::materialize_builtins(&args.scan.provider, &args.scan.proto)?,
};

let pool = init_pool(&settings.db_path).await?;
let repo = Arc::new(ConfigRepo::new(pool));
let killer: Arc<dyn ProcessKiller> = Arc::new(RealProcessKiller {
killall_enabled: settings.killall_enabled,
});
let tester: Arc<dyn VpnTester> = Arc::new(RealVpnTester {
bin: settings.openvpn_bin.clone(),
killer: killer.clone(),
});
let geo = Arc::new(IpInfoGeoLocator::new(
repo.clone(),
settings.ipinfo_token.clone(),
));

let scan_service = ScanService {
tester,
geo,
repo: repo.clone(),
};

let scan_options = ScanOptions {
dir,
limit,
timeout,
workers,
modify: args.scan.modify,
backup: args.scan.backup,
no_save: args.scan.no_save,
filter: settings.filter.clone(),
};

let cancel = CancellationToken::new();
let cancel_task = cancel.clone();
let signal_task = tokio::spawn(async move {
let _ = shutdown_signal().await;
cancel_task.cancel();
});
let _guard = CleanupGuard::new(killer.clone(), settings.killall_enabled);

let progress: Arc<dyn ScanProgress> = if crate::app::is_verbose(verbose) {
Arc::new(VerboseReporter)
} else {
Arc::new(ProgressReporter::new(settings.filter.to_display()))
};

let report = scan_service.scan(&scan_options, progress, cancel).await?;
signal_task.abort();

println!();
println!("--- Scan Result ---");
println!("Scanned: {}", report.scanned);
println!("Tested: {}", report.tested);
println!("Success: {}", report.success);
println!("Matched: {}", report.matched);
println!("Filter: {}", report.filter);
for m in &report.matched_configs {
println!("{} -- {}", m.country, m.path.display());
}

// Export this scan's fresh matches (the scan already stored successes,
// so `vmate-cli recent` is updated as normal).
if let Some(export_dir) = &args.scan.export {
let dest = vmate_core::paths::expand_path(export_dir);
let result =
vmate_core::export::export_configs_from_matches(&report.matched_configs, &dest).await?;
println!(
"Exported {} of {} configs to {}",
result.exported,
result.total,
result.dest.display()
);
}
// The scan preamble (wiring, options, report, export) is shared with
// `scan`; `all` keeps only the connect half.
let (report, repo) =
crate::commands::scan::scan_pipeline(settings, &args.scan, verbose).await?;

if args.no_connect {
return Ok(());
Expand All @@ -138,8 +55,13 @@ pub async fn run(settings: &Settings, args: &AllArgs, verbose: &Verbosity) -> Re
}
let queue = ConnectQueue::new(candidates);

let registry = Arc::new(vmate_core::system::ProcessRegistry::new());
let runner = Arc::new(RealOpenVpnRunner {
bin: settings.openvpn_bin.clone(),
registry: registry.clone(),
});
let killer: Arc<dyn ProcessKiller> = Arc::new(RealProcessKiller {
killall_enabled: settings.killall_enabled,
});
let options = ConnectOptions {
connect_timeout: connect.connect_timeout,
Expand All @@ -157,6 +79,7 @@ pub async fn run(settings: &Settings, args: &AllArgs, verbose: &Verbosity) -> Re
let service = ConnectService {
runner,
killer,
registry,
repo,
options,
};
Expand Down
27 changes: 12 additions & 15 deletions crates/vmate-cli/src/commands/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,14 @@ pub(crate) fn resolve_connect(us: &UserSettings, args: &ConnectArgs) -> Resolved

/// Persist the explicitly-passed connect defaults, then confirm where they
/// were written.
pub(crate) fn persist_connect_defaults(us: &mut UserSettings, args: &ConnectArgs) -> Result<()> {
if let Some(v) = args.connect_timeout {
us.connect_timeout_secs = Some(v.as_secs());
}
if let Some(v) = args.cooldown {
us.cooldown_secs = Some(v.as_secs());
}
if let Some(v) = args.retry_count {
us.retry_count = Some(v);
}
if let Some(v) = args.stability_grace {
us.stability_grace_secs = Some(v.as_secs());
}
pub(crate) fn persist_connect_defaults(args: &ConnectArgs) -> Result<()> {
let mut us = UserSettings::load();
us.persist_connect(&vmate_core::settings::ConnectDefaults {
connect_timeout: args.connect_timeout,
cooldown: args.cooldown,
retry_count: args.retry_count,
stability_grace: args.stability_grace,
});
us.save()?;
println!(
"Saved connect defaults to {}",
Expand All @@ -61,8 +56,7 @@ pub async fn run(settings: &Settings, args: &ConnectArgs, verbose: &Verbosity) -
// --save-defaults is a pure settings operation: persist and exit without
// connecting (no root, no OpenVPN, no DB needed).
if settings.save_defaults {
let mut us = UserSettings::load();
persist_connect_defaults(&mut us, args)?;
persist_connect_defaults(args)?;
return Ok(());
}

Expand Down Expand Up @@ -95,8 +89,10 @@ pub async fn run(settings: &Settings, args: &ConnectArgs, verbose: &Verbosity) -
let killer: Arc<dyn ProcessKiller> = Arc::new(RealProcessKiller {
killall_enabled: settings.killall_enabled,
});
let registry = Arc::new(vmate_core::system::ProcessRegistry::new());
let runner = Arc::new(RealOpenVpnRunner {
bin: settings.openvpn_bin.clone(),
registry: registry.clone(),
});

let options = ConnectOptions {
Expand All @@ -115,6 +111,7 @@ pub async fn run(settings: &Settings, args: &ConnectArgs, verbose: &Verbosity) -
let service = ConnectService {
runner,
killer,
registry,
repo,
options,
};
Expand Down
4 changes: 2 additions & 2 deletions crates/vmate-cli/src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ pub async fn run(settings: &Settings) -> Result<()> {
format!("SQLite DB ({})", settings.db_path.display()),
"ok".to_string(),
]);
let repo = ConfigRepo::new(pool);
match repo.journal_mode().await {
match vmate_core::db::pool::journal_mode(&pool).await {
Ok(mode) if mode.eq_ignore_ascii_case("wal") => {
table.add_row(["WAL mode".to_string(), "ok".to_string()]);
}
Expand All @@ -38,6 +37,7 @@ pub async fn run(settings: &Settings) -> Result<()> {
}
}

let repo = ConfigRepo::new(pool);
let success = repo
.count_configs(ConfigStatus::Success)
.await
Expand Down
8 changes: 2 additions & 6 deletions crates/vmate-cli/src/commands/recent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,8 @@ fn print_plain(entries: &[StoredConfig]) -> Result<()> {
.last_success_at
.map(|t| t.format("%Y-%m-%d %H:%M").to_string())
.unwrap_or_else(|| "-".to_string());
let path = if vmate_core::builtin::is_builtin_path(Path::new(&entry.path)) {
Path::new(&entry.path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(&entry.path)
.to_string()
let path = if let Some(name) = vmate_core::builtin::display_name(Path::new(&entry.path)) {
name
} else if term::stdout_is_tty() {
hyperlink::osc8_file_hyperlink(&entry.path).unwrap_or_else(|| entry.path.clone())
} else {
Expand Down
56 changes: 34 additions & 22 deletions crates/vmate-cli/src/commands/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,34 @@ pub async fn run(settings: &Settings, args: &ScanArgs, verbose: &Verbosity) -> R

require_root_for("run OpenVPN tests", settings.no_elevate)?;

scan_pipeline(settings, args, verbose).await?;
Ok(())
}

/// The full scan orchestration: DB/repo/killer/tester/geo wiring, option
/// resolution, the scan itself, the report and the export. Returns the report
/// and repo so `all` can go on to connect. Shared by `scan` and `all` so the
/// preamble (and its report format) lives in one place.
pub(crate) async fn scan_pipeline(
settings: &Settings,
args: &ScanArgs,
verbose: &Verbosity,
) -> Result<(ScanReport, Arc<ConfigRepo>)> {
let us = UserSettings::load();
let (workers, limit, timeout) = resolve_scan_defaults(args, &us);

let dir = resolve_scan_dir(args)?;

let pool = init_pool(&settings.db_path).await?;
let repo = Arc::new(ConfigRepo::new(pool));
let registry = Arc::new(vmate_core::system::ProcessRegistry::new());
let killer: Arc<dyn ProcessKiller> = Arc::new(RealProcessKiller {
killall_enabled: settings.killall_enabled,
});
let tester: Arc<dyn VpnTester> = Arc::new(RealVpnTester {
bin: settings.openvpn_bin.clone(),
killer: killer.clone(),
registry: registry.clone(),
});
let geo = Arc::new(IpInfoGeoLocator::new(
repo.clone(),
Expand Down Expand Up @@ -72,7 +87,7 @@ pub async fn run(settings: &Settings, args: &ScanArgs, verbose: &Verbosity) -> R
let _ = shutdown_signal().await;
cancel_task.cancel();
});
let _guard = CleanupGuard::new(killer.clone(), settings.killall_enabled);
let _guard = CleanupGuard::new(killer.clone(), registry.clone(), settings.killall_enabled);

let progress: Arc<dyn ScanProgress> = if crate::app::is_verbose(verbose) {
Arc::new(VerboseReporter)
Expand All @@ -99,7 +114,7 @@ pub async fn run(settings: &Settings, args: &ScanArgs, verbose: &Verbosity) -> R
);
}

Ok(())
Ok((report, repo))
}

/// Resolve the scan `workers`/`limit`/`timeout` as
Expand All @@ -115,24 +130,15 @@ pub(crate) fn resolve_scan_defaults(
)
}

/// Apply the explicitly-passed scan default flags onto `us`.
fn apply_scan_defaults(us: &mut UserSettings, args: &ScanArgs) {
if let Some(v) = args.max {
us.max_workers = Some(v as u64);
}
if let Some(v) = args.limit {
us.limit = Some(v as u64);
}
if let Some(t) = args.timeout {
us.scan_timeout_secs = Some(t.as_secs());
}
}

/// Persist the explicitly-passed scan default flags to the user config and
/// print a confirmation. Only the flags actually passed are written.
pub(crate) fn persist_scan_defaults(args: &ScanArgs) -> Result<()> {
let mut us = UserSettings::load();
apply_scan_defaults(&mut us, args);
us.persist_scan(&vmate_core::settings::ScanDefaults {
max_workers: args.max.map(|v| v as u64),
limit: args.limit.map(|v| v as u64),
timeout: args.timeout,
});
us.save()?;
println!("Saved scan defaults to {}", UserSettings::path()?.display());
Ok(())
Expand Down Expand Up @@ -301,15 +307,17 @@ mod tests {
}

#[test]
fn apply_scan_defaults_only_writes_explicitly_passed_keys() {
fn persist_scan_only_writes_explicitly_passed_keys() {
use vmate_core::settings::ScanDefaults;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("settings.json");

let mut us = UserSettings::default();
apply_scan_defaults(
&mut us,
&scan_args(Some(500), None, Some(Duration::from_secs(20))),
);
us.persist_scan(&ScanDefaults {
max_workers: Some(500),
limit: None,
timeout: Some(Duration::from_secs(20)),
});
us.save_to(&path).unwrap();

let loaded = UserSettings::load_from(&path);
Expand All @@ -320,7 +328,11 @@ mod tests {
// A later save with a different explicit key must not resurrect keys
// that were never passed.
let mut us2 = loaded;
apply_scan_defaults(&mut us2, &scan_args(None, Some(25), None));
us2.persist_scan(&ScanDefaults {
max_workers: None,
limit: Some(25),
timeout: None,
});
us2.save_to(&path).unwrap();
let loaded2 = UserSettings::load_from(&path);
assert_eq!(loaded2.max_workers, Some(500));
Expand Down
Loading
Loading