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