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
19 changes: 18 additions & 1 deletion varManager_backend/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,23 @@ fn normalize_optional(value: Option<String>) -> Option<String> {
})
}

/// Normalize vam_exec: if user provides a full path, extract just the filename.
/// The system always combines vampath + vam_exec, so only the filename is needed.
fn normalize_vam_exec(value: Option<String>) -> Option<String> {
normalize_optional(value).map(|s| {
let path = std::path::Path::new(&s);
if path.is_absolute() || s.contains('\\') || s.contains('/') {
// Extract filename from path
path.file_name()
.and_then(|n| n.to_str())
.map(|n| n.to_string())
.unwrap_or(s)
} else {
s
}
})
}

fn normalize_proxy(mut proxy: crate::app::ProxyConfig) -> crate::app::ProxyConfig {
proxy.host = proxy.host.trim().to_string();
proxy.username = normalize_optional(proxy.username);
Expand Down Expand Up @@ -702,7 +719,7 @@ fn apply_config_update(current: &Config, req: UpdateConfigRequest) -> Result<Con
next.vampath = normalize_optional(req.vampath);
}
if req.vam_exec.is_some() {
next.vam_exec = normalize_optional(req.vam_exec);
next.vam_exec = normalize_vam_exec(req.vam_exec);
}
if req.downloader_save_path.is_some() {
next.downloader_save_path = normalize_optional(req.downloader_save_path);
Expand Down
69 changes: 55 additions & 14 deletions varManager_backend/src/infra/system_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ use crate::app::AppState;
use serde_json::json;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::{Command, Stdio};
use sysinfo::{ProcessesToUpdate, System};

#[cfg(windows)]
use std::os::windows::process::CommandExt;

#[cfg(windows)]
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000;

const DEFAULT_VAM_EXEC: &str = "VaM (Desktop Mode).bat";

pub fn start_vam(state: &AppState) -> Result<(), String> {
Expand All @@ -31,19 +39,7 @@ pub fn start_vam(state: &AppState) -> Result<(), String> {
return Err(format!("vam executable not found: {}", exec_path.display()));
}

if is_cmd_script(&exec_path) {
let exec = format!("\"{}\"", exec_path.to_string_lossy());
Command::new("cmd")
.args(["/C", exec.as_str()])
.current_dir(&vampath)
.spawn()
.map_err(|err| err.to_string())?;
} else {
Command::new(&exec_path)
.current_dir(&vampath)
.spawn()
.map_err(|err| err.to_string())?;
}
spawn_detached(&exec_path, &vampath)?;
Ok(())
}

Expand Down Expand Up @@ -108,3 +104,48 @@ fn is_cmd_script(path: &Path) -> bool {
.map(|ext| ext.eq_ignore_ascii_case("bat") || ext.eq_ignore_ascii_case("cmd"))
.unwrap_or(false)
}

/// Spawn a process fully detached from the parent.
/// This prevents the child from blocking on inherited stdin/stdout/stderr handles.
#[cfg(windows)]
fn spawn_detached(exec_path: &Path, working_dir: &Path) -> Result<(), String> {
if is_cmd_script(exec_path) {
// For bat/cmd files: use "start" to launch in a new console window.
// Use /B and CREATE_NO_WINDOW to keep it hidden while preserving START behavior.
let exec = exec_path.to_string_lossy();
let workdir = working_dir.to_string_lossy();
Command::new("cmd")
.args(["/C", "start", "", "/B", "/D", &workdir, &*exec])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP)
.spawn()
.map_err(|err| err.to_string())?;
} else {
// For exe files: launch directly with detached flags
Command::new(exec_path)
.current_dir(working_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP)
.spawn()
.map_err(|err| err.to_string())?;
}

Ok(())
}

#[cfg(not(windows))]
fn spawn_detached(exec_path: &Path, working_dir: &Path) -> Result<(), String> {
Command::new(exec_path)
.current_dir(working_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|err| err.to_string())?;

Ok(())
}
69 changes: 61 additions & 8 deletions varManager_backend/src/jobs/update_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ pub async fn run_update_db_job(state: AppState, reporter: JobReporter) -> Result
}

fn update_db_blocking(state: &AppState, reporter: &JobReporter) -> Result<(), String> {
let overall_start = std::time::Instant::now();
let (varspath, vampath) = config_paths(state)?;
reporter.log(format!("UpdateDB start: varspath={}", varspath.display()));
reporter.log(format!("UpdateDB started: varspath={}", varspath.display()));
reporter.progress(1);

let addon_vars = match vampath.as_ref() {
Expand All @@ -64,6 +65,7 @@ fn update_db_blocking(state: &AppState, reporter: &JobReporter) -> Result<(), St
vars_for_install = dedup_strings(vars_for_install);
save_vars_for_install(&vars_for_install)?;

reporter.log("Phase 1/5: Tidying VAR files...".to_string());
tidy_vars(
&varspath,
if vampath.is_some() { Some(&addon_vars) } else { None },
Expand Down Expand Up @@ -94,6 +96,8 @@ fn update_db_blocking(state: &AppState, reporter: &JobReporter) -> Result<(), St
return Ok(());
}

reporter.log(format!("Phase 2/5: Processing {} VAR files into database...", var_files.len()));

let dependency_regex = Regex::new(
r#"\x22(([^\r\n\x22\x3A\x2E]{1,60})\x2E([^\r\n\x22\x3A\x2E]{1,80})\x2E(\d+|latest))(\x22?\s*)\x3A"#,
)
Expand All @@ -108,6 +112,9 @@ fn update_db_blocking(state: &AppState, reporter: &JobReporter) -> Result<(), St
handle.block_on(async move {
let mut exist_vars: HashSet<String> = HashSet::new();
let mut tx = pool_for_tx.begin().await.map_err(|err| err.to_string())?;
let total_vars = var_files.len();
let start_time = std::time::Instant::now();

for (idx, var_file) in var_files.iter().enumerate() {
let basename = match var_file.file_stem() {
Some(stem) => stem.to_string_lossy().to_string(),
Expand Down Expand Up @@ -150,13 +157,27 @@ fn update_db_blocking(state: &AppState, reporter: &JobReporter) -> Result<(), St
Err(ProcessError::Io(err)) => return Err(err),
}

let progress = 10 + ((idx + 1) * 80 / var_files.len()) as u8;
if idx % 200 == 0 || idx + 1 == var_files.len() {
let progress = 10 + ((idx + 1) * 80 / total_vars) as u8;
// Report progress every 50 VARs or at completion, with detailed info
if idx % 50 == 0 || idx + 1 == total_vars {
let elapsed = start_time.elapsed().as_secs_f64();
let speed = if elapsed > 0.0 { (idx + 1) as f64 / elapsed } else { 0.0 };
let remaining = if speed > 0.0 { (total_vars - idx - 1) as f64 / speed } else { 0.0 };

reporter_async.progress(progress.min(90));
reporter_async.log(format!(
"Processing VARs: {}/{} ({:.1}%) | Speed: {:.1} VAR/s | ETA: {:.0}s | Current: {}",
idx + 1, total_vars,
(idx + 1) as f64 / total_vars as f64 * 100.0,
speed,
remaining,
basename
));
}
}

cleanup_missing_vars(&mut tx, &exist_vars, &varspath_async, &reporter_async).await?;
reporter_async.log("Phase 3/5: Committing database changes...".to_string());
tx.commit().await.map_err(|err| err.to_string())?;
Ok::<(), String>(())
})?;
Expand All @@ -165,12 +186,15 @@ fn update_db_blocking(state: &AppState, reporter: &JobReporter) -> Result<(), St

if !vars_for_install.is_empty() {
if let Some(vampath) = vampath.as_ref() {
reporter.log("Phase 4/5: Installing pending VARs...".to_string());
reporter.log(format!(
"Install pending vars (varsForInstall): {}",
vars_for_install.len()
));
let pending = handle.block_on(vars_dependencies(&pool, vars_for_install))?;
let total = pending.len();
let start_time = std::time::Instant::now();

for (idx, var_name) in pending.iter().enumerate() {
match handle.block_on(install_var(&pool, &varspath, vampath, var_name)) {
Ok(InstallOutcome::Installed) => {
Expand All @@ -181,9 +205,20 @@ fn update_db_blocking(state: &AppState, reporter: &JobReporter) -> Result<(), St
reporter.log(format!("install pending failed {} ({})", var_name, err));
}
}
if total > 0 && (idx % 50 == 0 || idx + 1 == total) {
if total > 0 && (idx % 20 == 0 || idx + 1 == total) {
let progress = 90 + ((idx + 1) * 5 / total) as u8;
let elapsed = start_time.elapsed().as_secs_f64();
let speed = if elapsed > 0.0 { (idx + 1) as f64 / elapsed } else { 0.0 };
let remaining = if speed > 0.0 { (total - idx - 1) as f64 / speed } else { 0.0 };

reporter.progress(progress.min(95));
reporter.log(format!(
"Installing VARs: {}/{} ({:.1}%) | Speed: {:.1} VAR/s | ETA: {:.0}s",
idx + 1, total,
(idx + 1) as f64 / total as f64 * 100.0,
speed,
remaining
));
}
}
let _ = clear_vars_for_install();
Expand All @@ -195,6 +230,7 @@ fn update_db_blocking(state: &AppState, reporter: &JobReporter) -> Result<(), St
reporter.progress(95);

if let Some(vampath) = vampath.as_ref() {
reporter.log("Phase 5/5: Refreshing installation status...".to_string());
handle.block_on(refresh_install_status(&pool, vampath, reporter))?;
reporter.progress(97);
match system_ops::rescan_packages(state) {
Expand All @@ -207,7 +243,13 @@ fn update_db_blocking(state: &AppState, reporter: &JobReporter) -> Result<(), St
}

reporter.progress(100);
reporter.log("UpdateDB completed".to_string());
let total_elapsed = overall_start.elapsed();
reporter.log(format!(
"UpdateDB completed in {:.1}s ({:.0}m {:.0}s)",
total_elapsed.as_secs_f64(),
total_elapsed.as_secs() / 60,
total_elapsed.as_secs() % 60
));
Ok(())
}

Expand Down Expand Up @@ -305,6 +347,8 @@ fn tidy_vars(
}

let total = vars.len();
let start_time = std::time::Instant::now();

for (idx, varfile) in vars.into_iter().enumerate() {
if !varfile.exists() {
continue;
Expand Down Expand Up @@ -345,11 +389,20 @@ fn tidy_vars(
move_file(&varfile, &dest)?;
}

if idx % 500 == 0 && total > 0 {
reporter.log(format!("TidyVars progress: {}/{}", idx + 1, total));
if idx % 200 == 0 && total > 0 {
let elapsed = start_time.elapsed().as_secs_f64();
let speed = if elapsed > 0.0 { (idx + 1) as f64 / elapsed } else { 0.0 };
let remaining = if speed > 0.0 { (total - idx - 1) as f64 / speed } else { 0.0 };
reporter.log(format!(
"TidyVars: {}/{} ({:.1}%) | Speed: {:.1} VAR/s | ETA: {:.0}s",
idx + 1, total,
(idx + 1) as f64 / total as f64 * 100.0,
speed,
remaining
));
}
}
reporter.log("TidyVars completed".to_string());
reporter.log(format!("TidyVars completed: {} files processed", total));
Ok(())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ class BootstrapController extends Notifier<BootstrapState> {
final proxy = config?.proxy ?? ProxyConfig.empty;
final proxyMode = config?.proxyMode ?? ProxyMode.system;

// Normalize vam_exec: extract filename if full path provided
if (vamExec.trim().isNotEmpty) {
vamExec = p.basename(vamExec.trim());
}
if (vamExec.trim().isEmpty) {
vamExec = _vamDesktopBat;
}
Expand Down
54 changes: 40 additions & 14 deletions varmanager_flutter/lib/features/settings/settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,44 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_downloaderSavePath.text = next;
}
}
final previous = _config;
if (previous == null) return;
final listenHost = _listenHost.text.trim();
final listenPort = int.tryParse(_listenPort.text.trim()) ?? 57123;
final logLevel = _logLevel.text.trim();
final jobConcurrency = int.tryParse(_jobConcurrency.text.trim()) ?? 10;
final varspath = _varspath.text.trim();
final vampath = _vampath.text.trim();
final vamExec = _vamExec.text.trim();
final downloaderSavePath = _downloaderSavePath.text.trim();
final proxyMode = _proxyMode;
final proxyHost = _proxyHost.text.trim();
final proxyPort = int.tryParse(_proxyPort.text.trim()) ?? 0;
final proxyUsername = _proxyUsername.text.trim();
final proxyPassword = _proxyPassword.text.trim();
final needsRestartHint = previous.listenHost != listenHost ||
previous.listenPort != listenPort ||
previous.proxyMode != proxyMode ||
previous.proxy.host != proxyHost ||
previous.proxy.port != proxyPort ||
(previous.proxy.username ?? '') != proxyUsername ||
(previous.proxy.password ?? '') != proxyPassword;
final client = ref.read(backendClientProvider);
final update = <String, dynamic>{
'listen_host': _listenHost.text.trim(),
'listen_port': int.tryParse(_listenPort.text.trim()) ?? 57123,
'log_level': _logLevel.text.trim(),
'job_concurrency': int.tryParse(_jobConcurrency.text.trim()) ?? 10,
'varspath': _varspath.text.trim(),
'vampath': _vampath.text.trim(),
'vam_exec': _vamExec.text.trim(),
'downloader_save_path': _downloaderSavePath.text.trim(),
'proxy_mode': _proxyMode.name,
'listen_host': listenHost,
'listen_port': listenPort,
'log_level': logLevel,
'job_concurrency': jobConcurrency,
'varspath': varspath,
'vampath': vampath,
'vam_exec': vamExec,
'downloader_save_path': downloaderSavePath,
'proxy_mode': proxyMode.name,
'proxy': {
'host': _proxyHost.text.trim(),
'port': int.tryParse(_proxyPort.text.trim()) ?? 0,
'username': _proxyUsername.text.trim(),
'password': _proxyPassword.text.trim(),
'host': proxyHost,
'port': proxyPort,
'username': proxyUsername,
'password': proxyPassword,
},
};
final cfg = await client.updateConfig(update);
Expand All @@ -126,7 +148,11 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_config = cfg;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.configSavedRestartHint)),
SnackBar(
content: Text(needsRestartHint
? context.l10n.configSavedRestartHint
: context.l10n.configSaved),
),
);
}

Expand Down
1 change: 1 addition & 0 deletions varmanager_flutter/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"chooseAddonPackagesHint": "Recommended: choose AddonPackages",
"appVersionLabel": "App version",
"backendVersionLabel": "Backend version",
"configSaved": "Config saved",
"configSavedRestartHint": "Config saved; listen_host/port and proxy apply after restart.",
"searchVarPackageLabel": "Search var/package",
"creatorLabel": "Creator",
Expand Down
6 changes: 6 additions & 0 deletions varmanager_flutter/lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,12 @@ abstract class AppLocalizations {
/// **'Backend version'**
String get backendVersionLabel;

/// No description provided for @configSaved.
///
/// In en, this message translates to:
/// **'Config saved'**
String get configSaved;

/// No description provided for @configSavedRestartHint.
///
/// In en, this message translates to:
Expand Down
3 changes: 3 additions & 0 deletions varmanager_flutter/lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get backendVersionLabel => 'Backend version';

@override
String get configSaved => 'Config saved';

@override
String get configSavedRestartHint =>
'Config saved; listen_host/port and proxy apply after restart.';
Expand Down
Loading
Loading