From 077b4c9067ae5f72576897bd6bf9dfafcad5098c Mon Sep 17 00:00:00 2001 From: Jan-Peter von Hunnius Date: Fri, 2 Oct 2026 09:18:48 +0200 Subject: [PATCH] Prevent capture failures from corrupting the BlueStacks instance --- CHANGELOG.md | 2 ++ src/capture/windows.rs | 63 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df66bb7..e801cba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ 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 diff --git a/src/capture/windows.rs b/src/capture/windows.rs index f50281a..99519ed 100644 --- a/src/capture/windows.rs +++ b/src/capture/windows.rs @@ -651,6 +651,22 @@ 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)); @@ -695,6 +711,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()), @@ -704,11 +721,11 @@ async fn command_run(config: &CaptureConfig) -> Result<()> { 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"); - if config.device.shutdown_bluestacks { - close_bluestacks(); - } match outcome { Ok(()) => { @@ -833,7 +850,18 @@ async fn command_clean(config: &CaptureConfig) -> Result<()> { Ok(()) } -fn close_bluestacks() { +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() { let silent = |args: &[&str]| { let _ = Command::new("taskkill") .args(args) @@ -845,6 +873,24 @@ fn close_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"); } @@ -922,6 +968,15 @@ 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();