Prevent capture failures from corrupting the BlueStacks instance
All checks were successful
build / build-test (push) Successful in 1m52s

This commit is contained in:
Jan-Peter von Hunnius 2026-10-02 09:18:48 +02:00
parent 75589d2383
commit 077b4c9067
2 changed files with 61 additions and 4 deletions

View file

@ -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<String, String>` to `HashMap<String, HashSet<(String, String)>>`
- `compute_car_counts()` now accepts `owned_configs` parameter

View file

@ -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();