android.rs (20843B)
1 use crate::capabilities::AndroidOptions; 2 use mozdevice::{AndroidStorage, Device, Host, RemoteMetadata, UnixPathBuf}; 3 use mozprofile::profile::Profile; 4 use std::fs::File; 5 use std::io; 6 use std::path::PathBuf; 7 use std::time; 8 use thiserror::Error; 9 use webdriver::error::{ErrorStatus, WebDriverError}; 10 use yaml_rust::yaml::{Hash, Yaml}; 11 12 // TODO: avoid port clashes across GeckoView-vehicles. 13 // For now, we always use target port 2829, leading to issues like bug 1533704. 14 const MARIONETTE_TARGET_PORT: u16 = 2829; 15 16 const CONFIG_FILE_HEADING: &str = r#"## GeckoView configuration YAML 17 ## 18 ## Auto-generated by geckodriver. 19 ## See https://mozilla.github.io/geckoview/consumer/docs/automation. 20 "#; 21 22 pub type Result<T> = std::result::Result<T, AndroidError>; 23 24 #[derive(Debug, Error)] 25 pub enum AndroidError { 26 #[error("Activity for package '{0}' not found")] 27 ActivityNotFound(String), 28 29 #[error(transparent)] 30 Device(#[from] mozdevice::DeviceError), 31 32 #[error(transparent)] 33 IO(#[from] io::Error), 34 35 #[error("Package '{0}' not found")] 36 PackageNotFound(String), 37 38 #[error(transparent)] 39 Serde(#[from] yaml_rust::EmitError), 40 } 41 42 impl From<AndroidError> for WebDriverError { 43 fn from(value: AndroidError) -> WebDriverError { 44 WebDriverError::new(ErrorStatus::UnknownError, value.to_string()) 45 } 46 } 47 48 /// A remote Gecko instance. 49 /// 50 /// Host refers to the device running `geckodriver`. Target refers to the 51 /// Android device running Gecko in a GeckoView-based vehicle. 52 #[derive(Debug)] 53 pub struct AndroidProcess { 54 pub device: Device, 55 pub package: String, 56 pub activity: String, 57 } 58 59 impl AndroidProcess { 60 pub fn new( 61 device: Device, 62 package: String, 63 activity: String, 64 ) -> mozdevice::Result<AndroidProcess> { 65 Ok(AndroidProcess { 66 device, 67 package, 68 activity, 69 }) 70 } 71 } 72 73 #[derive(Debug)] 74 pub struct AndroidHandler { 75 pub config: UnixPathBuf, 76 pub options: AndroidOptions, 77 pub process: AndroidProcess, 78 pub profile: UnixPathBuf, 79 pub test_root: UnixPathBuf, 80 81 // Port forwarding for Marionette: host => target 82 pub marionette_host_port: u16, 83 pub marionette_target_port: u16, 84 85 pub system_access: bool, 86 87 // Port forwarding for WebSocket connections (WebDriver BiDi and CDP) 88 pub websocket_port: Option<u16>, 89 } 90 91 impl Drop for AndroidHandler { 92 fn drop(&mut self) { 93 // Try to clean up various settings 94 let clear_command = format!("am clear-debug-app {}", self.process.package); 95 match self 96 .process 97 .device 98 .execute_host_shell_command(&clear_command) 99 { 100 Ok(_) => debug!("Disabled reading from configuration file"), 101 Err(e) => error!("Failed disabling from configuration file: {}", e), 102 } 103 104 match self.process.device.remove(&self.config) { 105 Ok(_) => debug!("Deleted GeckoView configuration file"), 106 Err(e) => error!("Failed deleting GeckoView configuration file: {}", e), 107 } 108 109 match self.process.device.remove(&self.test_root) { 110 Ok(_) => debug!("Deleted test root folder: {}", &self.test_root.display()), 111 Err(e) => error!("Failed deleting test root folder: {}", e), 112 } 113 114 debug!( 115 "Stop forwarding Marionette port ({} -> {})", 116 &self.marionette_host_port, &self.marionette_target_port 117 ); 118 match self 119 .process 120 .device 121 .kill_forward_port(self.marionette_host_port) 122 { 123 Ok(_) => {} 124 Err(e) => error!( 125 "Failed to stop forwarding Marionette port ({} -> {}): {}", 126 &self.marionette_host_port, &self.marionette_target_port, e 127 ), 128 } 129 130 if let Some(port) = self.websocket_port { 131 debug!( 132 "Stop forwarding WebSocket port ({} -> {})", 133 &self.marionette_host_port, &self.marionette_target_port 134 ); 135 match self.process.device.kill_forward_port(port) { 136 Ok(_) => {} 137 Err(e) => error!( 138 "Failed to stop forwarding WebSocket port ({0} -> {0}): {1}", 139 &port, e 140 ), 141 } 142 } 143 } 144 } 145 146 impl AndroidHandler { 147 pub fn new( 148 options: &AndroidOptions, 149 marionette_host_port: u16, 150 system_access: bool, 151 websocket_port: Option<u16>, 152 ) -> Result<AndroidHandler> { 153 // We need to push profile.pathbuf to a safe space on the device. 154 // Make it per-Android package to avoid clashes and confusion. 155 // This naming scheme follows GeckoView's configuration file naming scheme, 156 // see bug 1533385. 157 158 let host = Host { 159 host: None, 160 port: None, 161 read_timeout: Some(time::Duration::from_millis(5000)), 162 write_timeout: Some(time::Duration::from_millis(5000)), 163 }; 164 165 let mut device = host.device_or_default(options.device_serial.as_ref(), options.storage)?; 166 167 // Set up port forwarding for Marionette. 168 debug!( 169 "Start forwarding Marionette port ({} -> {})", 170 marionette_host_port, MARIONETTE_TARGET_PORT 171 ); 172 device.forward_port(marionette_host_port, MARIONETTE_TARGET_PORT)?; 173 174 if let Some(port) = websocket_port { 175 // Set up port forwarding for WebSocket connections (WebDriver BiDi, and CDP). 176 debug!("Start forwarding WebSocket port ({} -> {})", port, port); 177 device.forward_port(port, port)?; 178 } 179 180 let test_root = match device.storage { 181 AndroidStorage::App => { 182 device.run_as_package = Some(options.package.to_owned()); 183 let mut buf = UnixPathBuf::from("/data/data"); 184 buf.push(&options.package); 185 buf.push("test_root"); 186 buf 187 } 188 AndroidStorage::Internal => UnixPathBuf::from("/data/local/tmp/test_root"), 189 AndroidStorage::Sdcard => { 190 // We need to push the profile to a location on the device that can also 191 // be read and write by the application, and works for unrooted devices. 192 // The only location that meets this criteria is under: 193 // $EXTERNAL_STORAGE/Android/data/%options.package%/files 194 let response = device.execute_host_shell_command("echo $EXTERNAL_STORAGE")?; 195 let mut buf = UnixPathBuf::from(response.trim_end_matches('\n')); 196 buf.push("Android/data"); 197 buf.push(&options.package); 198 buf.push("files/test_root"); 199 buf 200 } 201 }; 202 203 debug!( 204 "Connecting: options={:?}, storage={:?}) test_root={}, run_as_package={:?}", 205 options, 206 device.storage, 207 test_root.display(), 208 device.run_as_package 209 ); 210 211 let mut profile = test_root.clone(); 212 profile.push(format!("{}-geckodriver-profile", &options.package)); 213 214 // Check if the specified package is installed 215 let response = 216 device.execute_host_shell_command(&format!("pm list packages {}", &options.package))?; 217 let mut packages = response 218 .trim() 219 .split_terminator('\n') 220 .filter(|line| line.starts_with("package:")) 221 .map(|line| line.rsplit(':').next().expect("Package name found")); 222 if !packages.any(|x| x == options.package.as_str()) { 223 return Err(AndroidError::PackageNotFound(options.package.clone())); 224 } 225 226 let config = UnixPathBuf::from(format!( 227 "/data/local/tmp/{}-geckoview-config.yaml", 228 &options.package 229 )); 230 231 // If activity hasn't been specified default to the main activity of the package 232 let activity = match options.activity { 233 Some(ref activity) => activity.clone(), 234 None => { 235 let response = device.execute_host_shell_command(&format!( 236 "cmd package resolve-activity --brief {}", 237 &options.package 238 ))?; 239 let activities = response 240 .split_terminator('\n') 241 .filter(|line| line.starts_with(&options.package)) 242 .map(|line| line.rsplit('/').next().unwrap()) 243 .collect::<Vec<&str>>(); 244 if activities.is_empty() { 245 return Err(AndroidError::ActivityNotFound(options.package.clone())); 246 } 247 248 activities[0].to_owned() 249 } 250 }; 251 252 let process = AndroidProcess::new(device, options.package.clone(), activity)?; 253 254 Ok(AndroidHandler { 255 config, 256 process, 257 profile, 258 test_root, 259 marionette_host_port, 260 marionette_target_port: MARIONETTE_TARGET_PORT, 261 options: options.clone(), 262 system_access, 263 websocket_port, 264 }) 265 } 266 267 pub fn copy_minidumps_files(&self, save_path: &str) -> Result<()> { 268 let minidumps_path = self.profile.join("minidumps"); 269 270 match self.process.device.list_dir(&minidumps_path) { 271 Ok(entries) => { 272 for entry in entries { 273 if let RemoteMetadata::RemoteFile(_) = entry.metadata { 274 let file_path = minidumps_path.join(&entry.name); 275 276 let extension = file_path 277 .extension() 278 .and_then(|ext| ext.to_str()) 279 .map(|ext| ext.to_lowercase()) 280 .unwrap_or(String::from("")); 281 282 if extension == "dmp" || extension == "extra" { 283 let mut dest_path = PathBuf::from(save_path); 284 dest_path.push(&entry.name); 285 286 self.process 287 .device 288 .pull(&file_path, &mut File::create(dest_path.as_path())?)?; 289 290 debug!( 291 "Copied minidump file {:?} from the device to the local path {:?}.", 292 entry.name, save_path 293 ); 294 } 295 } 296 } 297 } 298 Err(_) => { 299 warn!( 300 "Couldn't read files from minidumps folder '{}'", 301 minidumps_path.display(), 302 ); 303 304 return Ok(()); 305 } 306 } 307 308 Ok(()) 309 } 310 311 pub fn generate_config_file<I, K, V>( 312 &self, 313 args: Option<Vec<String>>, 314 envs: I, 315 ) -> Result<String> 316 where 317 I: IntoIterator<Item = (K, V)>, 318 K: ToString, 319 V: ToString, 320 { 321 // To configure GeckoView, we use the automation techniques documented at 322 // https://mozilla.github.io/geckoview/consumer/docs/automation. 323 324 let args = { 325 let mut args_yaml = Vec::from([ 326 "--marionette".into(), 327 "--profile".into(), 328 self.profile.display().to_string(), 329 ]); 330 331 if self.system_access { 332 args_yaml.push("--remote-allow-system-access".into()); 333 } 334 args_yaml.append(&mut args.unwrap_or_default()); 335 args_yaml.into_iter().map(Yaml::String).collect() 336 }; 337 338 let mut env = Hash::new(); 339 340 for (key, value) in envs { 341 env.insert( 342 Yaml::String(key.to_string()), 343 Yaml::String(value.to_string()), 344 ); 345 } 346 347 env.insert( 348 Yaml::String("MOZ_CRASHREPORTER".to_owned()), 349 Yaml::String("1".to_owned()), 350 ); 351 env.insert( 352 Yaml::String("MOZ_CRASHREPORTER_NO_REPORT".to_owned()), 353 Yaml::String("1".to_owned()), 354 ); 355 env.insert( 356 Yaml::String("MOZ_CRASHREPORTER_SHUTDOWN".to_owned()), 357 Yaml::String("1".to_owned()), 358 ); 359 360 let config_yaml = { 361 let mut config = Hash::new(); 362 config.insert(Yaml::String("env".into()), Yaml::Hash(env)); 363 config.insert(Yaml::String("args".into()), Yaml::Array(args)); 364 365 let mut yaml = String::new(); 366 let mut emitter = yaml_rust::YamlEmitter::new(&mut yaml); 367 emitter.dump(&Yaml::Hash(config))?; 368 yaml 369 }; 370 371 Ok([CONFIG_FILE_HEADING, &*config_yaml].concat()) 372 } 373 374 pub fn prepare<I, K, V>( 375 &self, 376 profile: &Profile, 377 args: Option<Vec<String>>, 378 env: I, 379 ) -> Result<()> 380 where 381 I: IntoIterator<Item = (K, V)>, 382 K: ToString, 383 V: ToString, 384 { 385 self.process.device.clear_app_data(&self.process.package)?; 386 387 // These permissions, at least, are required to read profiles in /mnt/sdcard. 388 for perm in &["READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"] { 389 self.process.device.execute_host_shell_command(&format!( 390 "pm grant {} android.permission.{}", 391 &self.process.package, perm 392 ))?; 393 } 394 395 // Make sure to create the test root. 396 self.process.device.create_dir(&self.test_root)?; 397 self.process.device.chmod(&self.test_root, "777", true)?; 398 399 // Replace the profile 400 self.process.device.remove(&self.profile)?; 401 self.process 402 .device 403 .push_dir(&profile.path, &self.profile, 0o777)?; 404 405 let contents = self.generate_config_file(args, env)?; 406 debug!("Content of generated GeckoView config file:\n{}", contents); 407 let reader = &mut io::BufReader::new(contents.as_bytes()); 408 409 debug!( 410 "Pushing GeckoView configuration file to {}", 411 self.config.display() 412 ); 413 self.process.device.push(reader, &self.config, 0o777)?; 414 415 // Tell GeckoView to read configuration even when `android:debuggable="false"`. 416 self.process.device.execute_host_shell_command(&format!( 417 "am set-debug-app --persistent {}", 418 self.process.package 419 ))?; 420 421 Ok(()) 422 } 423 424 pub fn launch(&self) -> Result<()> { 425 // TODO: Remove the usage of intent arguments once Fennec is no longer 426 // supported. Packages which are using GeckoView always read the arguments 427 // via the YAML configuration file. 428 let mut intent_arguments = self 429 .options 430 .intent_arguments 431 .clone() 432 .unwrap_or_else(|| Vec::with_capacity(3)); 433 intent_arguments.push("--es".to_owned()); 434 intent_arguments.push("args".to_owned()); 435 intent_arguments.push(format!("--marionette --profile {}", self.profile.display())); 436 437 debug!( 438 "Launching {}/{}", 439 self.process.package, self.process.activity 440 ); 441 442 // A counter to how many times to try launching the package. 443 let max_start_attempts = 2; 444 let mut n = 0; 445 446 loop { 447 match self.process.device.launch( 448 &self.process.package, 449 &self.process.activity, 450 &intent_arguments, 451 ) { 452 Ok(_) => break, 453 Err(e) => { 454 n += 1; 455 if n < max_start_attempts 456 && e.to_string().contains("Resource temporarily unavailable") 457 { 458 debug!( 459 "Failed the {} attempt to launch Android {}/{}: {}, wait for 2 seconds and try starting again", 460 n, self.process.package, self.process.activity, e 461 ); 462 463 std::thread::sleep(std::time::Duration::from_secs(2)); 464 465 continue; 466 } else { 467 let message = format!( 468 "Could not launch Android {}/{}: {}", 469 self.process.package, self.process.activity, e 470 ); 471 return Err(AndroidError::from(mozdevice::DeviceError::Adb(message))); 472 } 473 } 474 } 475 } 476 477 Ok(()) 478 } 479 480 pub fn push_as_file(&self, content: &[u8], path: &str) -> Result<String> { 481 let mut dest = self.test_root.clone(); 482 dest.push(path); 483 484 let buffer = &mut io::Cursor::new(content); 485 self.process.device.push(buffer, &dest, 0o777)?; 486 487 Ok(dest.display().to_string()) 488 } 489 490 pub fn force_stop(&self) -> Result<()> { 491 debug!( 492 "Force stopping the Android package: {}", 493 &self.process.package 494 ); 495 self.process.device.force_stop(&self.process.package)?; 496 497 Ok(()) 498 } 499 } 500 501 #[cfg(test)] 502 mod test { 503 // To successfully run those tests the geckoview_example package needs to 504 // be installed on the device or emulator. After setting up the build 505 // environment (https://mzl.la/3muLv5M), the following mach commands have to 506 // be executed: 507 // 508 // $ ./mach build && ./mach install 509 // 510 // Currently the mozdevice API is not safe for multiple requests at the same 511 // time. It is recommended to run each of the unit tests on its own. Also adb 512 // specific tests cannot be run in CI yet. To check those locally, also run 513 // the ignored tests. 514 // 515 // Use the following command to accomplish that: 516 // 517 // $ cargo test -- --ignored --test-threads=1 518 519 use crate::android::AndroidHandler; 520 use crate::capabilities::AndroidOptions; 521 use mozdevice::{AndroidStorage, AndroidStorageInput, UnixPathBuf}; 522 523 fn run_handler_storage_test(package: &str, storage: AndroidStorageInput) { 524 let options = AndroidOptions::new(package.to_owned(), storage); 525 let handler = 526 AndroidHandler::new(&options, 4242, true, None).expect("has valid Android handler"); 527 528 assert_eq!(handler.options, options); 529 assert_eq!(handler.marionette_host_port, 4242); 530 assert_eq!(handler.process.package, package); 531 assert_eq!(handler.system_access, true); 532 assert_eq!(handler.websocket_port, None); 533 534 let expected_config_path = UnixPathBuf::from(format!( 535 "/data/local/tmp/{}-geckoview-config.yaml", 536 &package 537 )); 538 assert_eq!(handler.config, expected_config_path); 539 540 if handler.process.device.storage == AndroidStorage::App { 541 assert_eq!( 542 handler.process.device.run_as_package, 543 Some(package.to_owned()) 544 ); 545 } else { 546 assert_eq!(handler.process.device.run_as_package, None); 547 } 548 549 let test_root = match handler.process.device.storage { 550 AndroidStorage::App => { 551 let mut buf = UnixPathBuf::from("/data/data"); 552 buf.push(package); 553 buf.push("test_root"); 554 buf 555 } 556 AndroidStorage::Internal => UnixPathBuf::from("/data/local/tmp/test_root"), 557 AndroidStorage::Sdcard => { 558 let response = handler 559 .process 560 .device 561 .execute_host_shell_command("echo $EXTERNAL_STORAGE") 562 .unwrap(); 563 564 let mut buf = UnixPathBuf::from(response.trim_end_matches('\n')); 565 buf.push("Android/data/"); 566 buf.push(package); 567 buf.push("files/test_root"); 568 buf 569 } 570 }; 571 assert_eq!(handler.test_root, test_root); 572 573 let mut profile = test_root; 574 profile.push(format!("{}-geckodriver-profile", &package)); 575 assert_eq!(handler.profile, profile); 576 } 577 578 #[test] 579 #[ignore] 580 fn android_handler_storage_as_app() { 581 let package = "org.mozilla.geckoview_example"; 582 run_handler_storage_test(package, AndroidStorageInput::App); 583 } 584 585 #[test] 586 #[ignore] 587 fn android_handler_storage_as_auto() { 588 let package = "org.mozilla.geckoview_example"; 589 run_handler_storage_test(package, AndroidStorageInput::Auto); 590 } 591 592 #[test] 593 #[ignore] 594 fn android_handler_storage_as_internal() { 595 let package = "org.mozilla.geckoview_example"; 596 run_handler_storage_test(package, AndroidStorageInput::Internal); 597 } 598 599 #[test] 600 #[ignore] 601 fn android_handler_storage_as_sdcard() { 602 let package = "org.mozilla.geckoview_example"; 603 run_handler_storage_test(package, AndroidStorageInput::Sdcard); 604 } 605 }