diff --git a/CHANGELOG.md b/CHANGELOG.md index e801cba..ecbef7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dedup check in `update_best_car` now considers `normalized_config` to prevent same car+config duplicates ### Changed -- `capture run` aborts with a clear error when the game package is missing on the device, before any proxy/CA state is set up — a missing package was the launch failure that previously triggered the proxy leak -- BlueStacks shutdown is now graceful: `adb reboot -p` first (clean ext4 unmount), with `taskkill /F` only as a 30 s fallback — force-killing the emulator corrupted `Data.vhdx` and BlueStacks' boot repair then discarded `packages.xml` and `accounts.db` as corrupt files, which took the installed game and the Google account with it (2026-10-02) - `CarCounts.by_rid` key changed from `String` to `(String, String)` for per-config tracking - `CarCounts.guid_to_rid` changed from `HashMap` to `HashMap>` - `compute_car_counts()` now accepts `owned_configs` parameter @@ -36,7 +34,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `plans/code-review-v2.md` (completed action items) ### Fixed -- `capture run` no longer leaves BlueStacks offline when a step fails after the device is set up: CA injection, proxy listener bind, proxy/reverse setup, and game launch errors — and Ctrl+C during setup — now run the same device revert as the happy path instead of skipping it (`revert_device_state` is now unconditional once the device is connected) - `trash.csv` Config column now lists only the copies to delete: for cars where just one tune is useless (e.g. owned 332 + best-everywhere 323) only the dead tune is shown instead of all owned configs - Cars the records site knows but has no times for (typically new prize cars, returned as `200` with an empty body) no longer abort `fetch` after 3 retries — they are skipped with an info log - Empty timing files are now treated like missing files instead of producing `EOF while parsing a value` parse errors diff --git a/src/capture/windows.rs b/src/capture/windows.rs index 99519ed..7094219 100644 --- a/src/capture/windows.rs +++ b/src/capture/windows.rs @@ -651,22 +651,6 @@ fn setup_adb_reverse(config: &CaptureConfig, port: u16) -> Result<()> { } } -fn package_listed(output: &str, package: &str) -> bool { - let entry = format!("package:{package}"); - output.lines().any(|line| line.trim() == entry) -} - -fn ensure_game_installed(config: &CaptureConfig) -> Result<()> { - let output = adb(config, &["shell", "pm", "list", "packages", GAME_PACKAGE])?; - if !package_listed(&output, GAME_PACKAGE) { - return Err(format!( - "package {GAME_PACKAGE} is not installed on the device; install it from the Play Store before capturing" - ) - .into()); - } - Ok(()) -} - fn launch_game(config: &CaptureConfig) -> Result<()> { adb_shell(config, &format!("am force-stop {GAME_PACKAGE}"))?; std::thread::sleep(Duration::from_secs(2)); @@ -711,38 +695,7 @@ async fn command_run(config: &CaptureConfig) -> Result<()> { if sdk_level > 28 { println!("warning: Android {sdk_level} detected, the tmpfs CA injection is only tested up to Android 9"); } - ensure_game_installed(config)?; - let outcome = tokio::select! { - result = capture_session(config, &project_root, &cert_pem, &key_pem) => result, - _ = tokio::signal::ctrl_c() => Err("aborted".into()), - }; - - revert_device_state(config); - - println!("stopping {GAME_PACKAGE}"); - let _ = adb_shell(config, &format!("am force-stop {GAME_PACKAGE}")); - if config.device.shutdown_bluestacks { - close_bluestacks(config); - } - let _ = run_adb(config, &["kill-server"]); - println!("adb stopped"); - - match outcome { - Ok(()) => { - println!("capture finished, run `cargo run --bin records -- best` to analyze"); - Ok(()) - } - Err(message) => Err(message), - } -} - -async fn capture_session( - config: &CaptureConfig, - project_root: &Path, - cert_pem: &str, - key_pem: &str, -) -> Result<()> { - inject_ca(config, cert_pem, project_root)?; + inject_ca(config, &cert_pem, &project_root)?; let listener = tokio::net::TcpListener::bind(std::net::SocketAddr::from(( [127, 0, 0, 1], @@ -759,11 +712,12 @@ async fn capture_session( let state = Arc::new(CaptureState { found: AtomicBool::new(false), done: tokio::sync::Notify::new(), - project_root: project_root.to_path_buf(), - output: output_path(config, project_root), + project_root: project_root.clone(), + output: output_path(config, &project_root), pending: std::sync::Mutex::new(None), }); - let proxy = build_proxy(listener, cert_pem, key_pem, CaptureHandler { state: state.clone() })?; + let proxy = + build_proxy(listener, &cert_pem, &key_pem, CaptureHandler { state: state.clone() })?; tokio::spawn(proxy); set_device_proxy(config, config.proxy.port)?; @@ -775,10 +729,28 @@ async fn capture_session( "waiting for the garage request (timeout {}s, Ctrl+C to abort)", config.capture.timeout_secs ); - tokio::select! { + let outcome: Result<()> = tokio::select! { _ = state.done.notified() => Ok(()), - _ = tokio::time::sleep(Duration::from_secs(config.capture.timeout_secs)) => - Err("timed out waiting for the garage request".into()), + _ = tokio::time::sleep(Duration::from_secs(config.capture.timeout_secs)) => Err("timed out waiting for the garage request".into()), + _ = tokio::signal::ctrl_c() => Err("aborted".into()), + }; + + revert_device_state(config); + + println!("stopping {GAME_PACKAGE}"); + let _ = adb_shell(config, &format!("am force-stop {GAME_PACKAGE}")); + let _ = run_adb(config, &["kill-server"]); + println!("adb stopped"); + if config.device.shutdown_bluestacks { + close_bluestacks(); + } + + match outcome { + Ok(()) => { + println!("capture finished, run `cargo run --bin records -- best` to analyze"); + Ok(()) + } + Err(message) => Err(message), } } @@ -850,18 +822,7 @@ async fn command_clean(config: &CaptureConfig) -> Result<()> { Ok(()) } -fn hd_player_running() -> bool { - let output = Command::new("tasklist") - .args(["/FI", "IMAGENAME eq HD-Player.exe", "/NH"]) - .stdin(Stdio::null()) - .output(); - match output { - Ok(output) => String::from_utf8_lossy(&output.stdout).contains("HD-Player.exe"), - Err(_) => true, - } -} - -fn force_kill_bluestacks() { +fn close_bluestacks() { let silent = |args: &[&str]| { let _ = Command::new("taskkill") .args(args) @@ -873,24 +834,6 @@ fn force_kill_bluestacks() { silent(&["/IM", "HD-Player.exe"]); std::thread::sleep(Duration::from_secs(5)); silent(&["/F", "/IM", "HD-Player.exe"]); -} - -fn close_bluestacks(config: &CaptureConfig) { - if let Err(error) = adb(config, &["reboot", "-p"]) { - eprintln!("graceful power-off attempt failed ({error})"); - } - - let deadline = Instant::now() + Duration::from_secs(30); - while Instant::now() < deadline { - if !hd_player_running() { - println!("BlueStacks shut down cleanly"); - return; - } - std::thread::sleep(Duration::from_secs(2)); - } - - println!("BlueStacks did not exit within 30s, force-killing HD-Player.exe"); - force_kill_bluestacks(); println!("BlueStacks closed"); } @@ -968,15 +911,6 @@ mod tests { assert!(!is_noise_host("topdrives-api.example.com")); } - #[test] - fn package_listing_matches_exact_entry_only() { - let output = "package:com.android.vending\npackage:com.hutchgames.cccg\n"; - assert!(package_listed(output, "com.hutchgames.cccg")); - assert!(!package_listed("package:com.hutchgames.cccg.other\n", "com.hutchgames.cccg")); - assert!(!package_listed("", "com.hutchgames.cccg")); - assert!(!package_listed("package:com.hutchgames.cccgx\n", "com.hutchgames.cccg")); - } - #[test] fn default_config_round_trips_through_toml() { let config = CaptureConfig::default();