Compare commits
2 commits
119a59f145
...
077b4c9067
| Author | SHA1 | Date | |
|---|---|---|---|
| 077b4c9067 | |||
| 75589d2383 |
2 changed files with 96 additions and 27 deletions
|
|
@ -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
|
- Dedup check in `update_best_car` now considers `normalized_config` to prevent same car+config duplicates
|
||||||
|
|
||||||
### Changed
|
### 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.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)>>`
|
- `CarCounts.guid_to_rid` changed from `HashMap<String, String>` to `HashMap<String, HashSet<(String, String)>>`
|
||||||
- `compute_car_counts()` now accepts `owned_configs` parameter
|
- `compute_car_counts()` now accepts `owned_configs` parameter
|
||||||
|
|
@ -34,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- `plans/code-review-v2.md` (completed action items)
|
- `plans/code-review-v2.md` (completed action items)
|
||||||
|
|
||||||
### Fixed
|
### 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
|
- `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
|
||||||
|
|
|
||||||
|
|
@ -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<()> {
|
fn launch_game(config: &CaptureConfig) -> Result<()> {
|
||||||
adb_shell(config, &format!("am force-stop {GAME_PACKAGE}"))?;
|
adb_shell(config, &format!("am force-stop {GAME_PACKAGE}"))?;
|
||||||
std::thread::sleep(Duration::from_secs(2));
|
std::thread::sleep(Duration::from_secs(2));
|
||||||
|
|
@ -695,7 +711,38 @@ async fn command_run(config: &CaptureConfig) -> Result<()> {
|
||||||
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");
|
||||||
}
|
}
|
||||||
inject_ca(config, &cert_pem, &project_root)?;
|
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)?;
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(std::net::SocketAddr::from((
|
let listener = tokio::net::TcpListener::bind(std::net::SocketAddr::from((
|
||||||
[127, 0, 0, 1],
|
[127, 0, 0, 1],
|
||||||
|
|
@ -712,12 +759,11 @@ async fn command_run(config: &CaptureConfig) -> Result<()> {
|
||||||
let state = Arc::new(CaptureState {
|
let state = Arc::new(CaptureState {
|
||||||
found: AtomicBool::new(false),
|
found: AtomicBool::new(false),
|
||||||
done: tokio::sync::Notify::new(),
|
done: tokio::sync::Notify::new(),
|
||||||
project_root: project_root.clone(),
|
project_root: project_root.to_path_buf(),
|
||||||
output: output_path(config, &project_root),
|
output: output_path(config, project_root),
|
||||||
pending: std::sync::Mutex::new(None),
|
pending: std::sync::Mutex::new(None),
|
||||||
});
|
});
|
||||||
let proxy =
|
let proxy = build_proxy(listener, cert_pem, key_pem, CaptureHandler { state: state.clone() })?;
|
||||||
build_proxy(listener, &cert_pem, &key_pem, CaptureHandler { state: state.clone() })?;
|
|
||||||
tokio::spawn(proxy);
|
tokio::spawn(proxy);
|
||||||
|
|
||||||
set_device_proxy(config, config.proxy.port)?;
|
set_device_proxy(config, config.proxy.port)?;
|
||||||
|
|
@ -729,28 +775,10 @@ async fn command_run(config: &CaptureConfig) -> Result<()> {
|
||||||
"waiting for the garage request (timeout {}s, Ctrl+C to abort)",
|
"waiting for the garage request (timeout {}s, Ctrl+C to abort)",
|
||||||
config.capture.timeout_secs
|
config.capture.timeout_secs
|
||||||
);
|
);
|
||||||
let outcome: Result<()> = tokio::select! {
|
tokio::select! {
|
||||||
_ = state.done.notified() => Ok(()),
|
_ = 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)) =>
|
||||||
_ = tokio::signal::ctrl_c() => Err("aborted".into()),
|
Err("timed out waiting for the garage request".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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -822,7 +850,18 @@ async fn command_clean(config: &CaptureConfig) -> Result<()> {
|
||||||
Ok(())
|
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 silent = |args: &[&str]| {
|
||||||
let _ = Command::new("taskkill")
|
let _ = Command::new("taskkill")
|
||||||
.args(args)
|
.args(args)
|
||||||
|
|
@ -834,6 +873,24 @@ fn close_bluestacks() {
|
||||||
silent(&["/IM", "HD-Player.exe"]);
|
silent(&["/IM", "HD-Player.exe"]);
|
||||||
std::thread::sleep(Duration::from_secs(5));
|
std::thread::sleep(Duration::from_secs(5));
|
||||||
silent(&["/F", "/IM", "HD-Player.exe"]);
|
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");
|
println!("BlueStacks closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -911,6 +968,15 @@ mod tests {
|
||||||
assert!(!is_noise_host("topdrives-api.example.com"));
|
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]
|
#[test]
|
||||||
fn default_config_round_trips_through_toml() {
|
fn default_config_round_trips_through_toml() {
|
||||||
let config = CaptureConfig::default();
|
let config = CaptureConfig::default();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue