Revert device to pre-capture state after capture run
All checks were successful
build / build-test (push) Successful in 1m46s

This commit is contained in:
Jan-Peter von Hunnius 2026-09-15 19:47:20 +02:00
parent 7eb9dc90f9
commit 94127bec46
3 changed files with 40 additions and 14 deletions

View file

@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- CSV output files now include `FinalBuild` column showing the normalized config - CSV output files now include `FinalBuild` column showing the normalized config
- `update_best_car()` tie detection now checks both `car_rid` and `normalized_config` - `update_best_car()` tie detection now checks both `car_rid` and `normalized_config`
- Applied `rustfmt` formatting across the entire codebase - Applied `rustfmt` formatting across the entire codebase
- `capture run` now reverts the device to its pre-capture state before closing BlueStacks — clears the global proxy, removes the adb reverse rule, unmounts the tmpfs CA overlay, and deletes the CA staging files in `/data/local/tmp` (same revert as `capture clean`)
### Removed ### Removed
- `plans/code-review-v2.md` (completed action items) - `plans/code-review-v2.md` (completed action items)
@ -36,6 +37,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `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 - `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 - 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 - Empty timing files are now treated like missing files instead of producing `EOF while parsing a value` parse errors
- A crashed or killed capture no longer breaks BlueStacks networking: `capture run` clears a stale global `http_proxy` left behind by a previous run at startup (the setting persists VM reboots and black-holes all proxy-honoring traffic, e.g. Play Store and the browser)
- Missing `u8` type annotation on `parse()` in output tests that broke compilation of `cargo test` and `cargo clippy --all-targets` on current rustc
## [0.3.0] - 2026-05-07 ## [0.3.0] - 2026-05-07

View file

@ -599,6 +599,35 @@ fn clear_device_proxy(config: &CaptureConfig) {
let _ = adb_shell(config, "settings delete global http_proxy"); let _ = adb_shell(config, "settings delete global http_proxy");
} }
fn remove_adb_reverse(config: &CaptureConfig) {
let remove_socket = format!("--remove tcp:{}", config.proxy.port);
let _ = adb(config, &["reverse", remove_socket.as_str()]);
}
fn clear_stale_proxy(config: &CaptureConfig) {
if let Ok(value) = adb_shell(config, "settings get global http_proxy") {
let value = value.trim();
if !value.is_empty() && value != "null" {
clear_device_proxy(config);
println!("cleared stale proxy setting left by a previous run: {value}");
}
}
remove_adb_reverse(config);
}
fn revert_device_state(config: &CaptureConfig) {
clear_device_proxy(config);
remove_adb_reverse(config);
let filename = android_ca_filename(CA_COMMON_NAME);
let _ = adb_shell_root(config, &format!("rm -f /system/etc/security/cacerts/{filename}"));
let _ = adb_shell_root(config, "umount /system/etc/security/cacerts");
let _ = adb_shell_root(config, "rm -rf /data/local/tmp/cacerts-backup");
let _ = adb_shell(config, "rm -f /data/local/tmp/topdrives-ca.pem");
println!(
"device reverted: proxy cleared, CA removed from the trust store, staging files deleted"
);
}
fn setup_adb_reverse(config: &CaptureConfig, port: u16) -> Result<()> { fn setup_adb_reverse(config: &CaptureConfig, port: u16) -> Result<()> {
let socket = format!("tcp:{port}"); let socket = format!("tcp:{port}");
match adb(config, &["reverse", socket.as_str(), socket.as_str()]) { match adb(config, &["reverse", socket.as_str(), socket.as_str()]) {
@ -661,6 +690,7 @@ async fn command_run(config: &CaptureConfig) -> Result<()> {
let (cert_pem, key_pem) = load_or_create_ca(&project_root)?; let (cert_pem, key_pem) = load_or_create_ca(&project_root)?;
ensure_device_connected(config)?; ensure_device_connected(config)?;
clear_stale_proxy(config);
let sdk_level = android_sdk_level(config)?; let sdk_level = android_sdk_level(config)?;
if sdk_level > 28 { if sdk_level > 28 {
println!("warning: Android {sdk_level} detected, the tmpfs CA injection is only tested up to Android 9"); println!("warning: Android {sdk_level} detected, the tmpfs CA injection is only tested up to Android 9");
@ -705,10 +735,7 @@ async fn command_run(config: &CaptureConfig) -> Result<()> {
_ = tokio::signal::ctrl_c() => Err("aborted".into()), _ = tokio::signal::ctrl_c() => Err("aborted".into()),
}; };
clear_device_proxy(config); revert_device_state(config);
let remove_socket = format!("--remove tcp:{}", config.proxy.port);
let _ = adb(config, &["reverse", remove_socket.as_str()]);
println!("device proxy settings restored");
println!("stopping {GAME_PACKAGE}"); println!("stopping {GAME_PACKAGE}");
let _ = adb_shell(config, &format!("am force-stop {GAME_PACKAGE}")); let _ = adb_shell(config, &format!("am force-stop {GAME_PACKAGE}"));
@ -791,13 +818,7 @@ async fn command_replay(config: &CaptureConfig) -> Result<()> {
async fn command_clean(config: &CaptureConfig) -> Result<()> { async fn command_clean(config: &CaptureConfig) -> Result<()> {
ensure_device_connected(config)?; ensure_device_connected(config)?;
clear_device_proxy(config); revert_device_state(config);
let remove_socket = format!("--remove tcp:{}", config.proxy.port);
let _ = adb(config, &["reverse", remove_socket.as_str()]);
let filename = android_ca_filename(CA_COMMON_NAME);
let _ = adb_shell_root(config, &format!("rm -f /system/etc/security/cacerts/{filename}"));
let _ = adb_shell_root(config, "umount /system/etc/security/cacerts");
println!("proxy settings cleared and CA removed from the device");
Ok(()) Ok(())
} }
@ -824,7 +845,7 @@ commands:
setup generate the CA and inject it into BlueStacks setup generate the CA and inject it into BlueStacks
run full flow: start BlueStacks if needed, proxy, game launch, garage capture run full flow: start BlueStacks if needed, proxy, game launch, garage capture
replay re-issue the stored garage request without the emulator replay re-issue the stored garage request without the emulator
clean remove proxy settings and the injected CA clean revert the device: clear the proxy, remove the injected CA and staging files
configuration is read from {CONFIG_FILE} in the working directory" configuration is read from {CONFIG_FILE} in the working directory"
); );

View file

@ -2228,7 +2228,8 @@ mod tests {
assert_eq!(written, 2, "low-value car should be filtered out"); assert_eq!(written, 2, "low-value car should be filtered out");
let content = fs::read_to_string(dir.path().join("protect.csv")).unwrap(); let content = fs::read_to_string(dir.path().join("protect.csv")).unwrap();
let rows: Vec<&str> = content.lines().skip(1).collect(); let rows: Vec<&str> = content.lines().skip(1).collect();
let rqs: Vec<u8> = rows.iter().filter_map(|r| r.split(',').next()?.parse().ok()).collect(); let rqs: Vec<u8> =
rows.iter().filter_map(|r| r.split(',').next()?.parse::<u8>().ok()).collect();
assert_eq!(rqs, vec![25, 85], "only cars above the band thresholds, got: {content}"); assert_eq!(rqs, vec![25, 85], "only cars above the band thresholds, got: {content}");
} }
@ -2457,7 +2458,8 @@ mod tests {
let content = fs::read_to_string(dir.path().join("trash.csv")).unwrap(); let content = fs::read_to_string(dir.path().join("trash.csv")).unwrap();
let rows: Vec<&str> = content.lines().skip(1).collect(); let rows: Vec<&str> = content.lines().skip(1).collect();
let rqs: Vec<u8> = rows.iter().filter_map(|r| r.split(',').next()?.parse().ok()).collect(); let rqs: Vec<u8> =
rows.iter().filter_map(|r| r.split(',').next()?.parse::<u8>().ok()).collect();
assert_eq!( assert_eq!(
rqs, rqs,
vec![15, 25, 35], vec![15, 25, 35],