├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── device_api-ios.gemspec ├── lib └── device_api │ ├── ios.rb │ └── ios │ ├── device.rb │ ├── idevice.rb │ ├── idevicedebug.rb │ ├── idevicediagnostics.rb │ ├── ideviceinstaller.rb │ ├── idevicename.rb │ ├── ideviceprovision.rb │ ├── idevicescreenshot.rb │ └── ipaddress.rb └── spec ├── README.md └── lib └── device_api ├── ios ├── device_spec.rb ├── idevice_spec.rb ├── ideviceinstaller_spec.rb └── ipaddress_spec.rb └── ios_spec.rb /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2 4 | 5 | script: bundle exec rspec 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased](https://github.com/bbc/device_api-ios/tree/HEAD) 4 | 5 | [Full Changelog](https://github.com/bbc/device_api-ios/compare/v1.0.5...HEAD) 6 | 7 | **Closed issues:** 8 | 9 | - Plistutil calls in device.rb should cache results [\#10](https://github.com/bbc/device_api-ios/issues/10) 10 | 11 | **Merged pull requests:** 12 | 13 | - Add list\_installed\_packages method [\#25](https://github.com/bbc/device_api-ios/pull/25) ([jrmhaig](https://github.com/jrmhaig)) 14 | - Added changelog [\#24](https://github.com/bbc/device_api-ios/pull/24) ([jonpwilson](https://github.com/jonpwilson)) 15 | - Not having the IP address package installed is no longer a blocker [\#23](https://github.com/bbc/device_api-ios/pull/23) ([jonpwilson](https://github.com/jonpwilson)) 16 | 17 | ## [v1.0.5](https://github.com/bbc/device_api-ios/tree/v1.0.5) (2016-02-18) 18 | [Full Changelog](https://github.com/bbc/device_api-ios/compare/v1.0.4...v1.0.5) 19 | 20 | **Merged pull requests:** 21 | 22 | - Remove signing [\#22](https://github.com/bbc/device_api-ios/pull/22) ([jonpwilson](https://github.com/jonpwilson)) 23 | 24 | ## [v1.0.4](https://github.com/bbc/device_api-ios/tree/v1.0.4) (2016-02-10) 25 | [Full Changelog](https://github.com/bbc/device_api-ios/compare/v1.0.3...v1.0.4) 26 | 27 | **Merged pull requests:** 28 | 29 | - Added in the ability to retrieve entitlements for an app [\#21](https://github.com/bbc/device_api-ios/pull/21) ([jonpwilson](https://github.com/jonpwilson)) 30 | 31 | ## [v1.0.3](https://github.com/bbc/device_api-ios/tree/v1.0.3) (2016-02-03) 32 | [Full Changelog](https://github.com/bbc/device_api-ios/compare/1.0.1...v1.0.3) 33 | 34 | **Merged pull requests:** 35 | 36 | - Updated version number [\#20](https://github.com/bbc/device_api-ios/pull/20) ([jonpwilson](https://github.com/jonpwilson)) 37 | - Return a devices' assigned name [\#19](https://github.com/bbc/device_api-ios/pull/19) ([jonpwilson](https://github.com/jonpwilson)) 38 | - Added Wifi mac address [\#18](https://github.com/bbc/device_api-ios/pull/18) ([jonpwilson](https://github.com/jonpwilson)) 39 | - Added minimum versions to the dependencies so that they will be recog… [\#17](https://github.com/bbc/device_api-ios/pull/17) ([jonpwilson](https://github.com/jonpwilson)) 40 | - Corrected Gemfile and updated Gemfile.lock [\#16](https://github.com/bbc/device_api-ios/pull/16) ([jonpwilson](https://github.com/jonpwilson)) 41 | - Removed internal URL from Gemfile [\#15](https://github.com/bbc/device_api-ios/pull/15) ([jonpwilson](https://github.com/jonpwilson)) 42 | - Changed the installation method to reduce duplicated code [\#14](https://github.com/bbc/device_api-ios/pull/14) ([jonpwilson](https://github.com/jonpwilson)) 43 | - Added .type to the device object based on the device\_class [\#13](https://github.com/bbc/device_api-ios/pull/13) ([jonpwilson](https://github.com/jonpwilson)) 44 | - idevicedebug wrapper [\#12](https://github.com/bbc/device_api-ios/pull/12) ([jonpwilson](https://github.com/jonpwilson)) 45 | - Added wrapper for ideviceprovision binary [\#11](https://github.com/bbc/device_api-ios/pull/11) ([jonpwilson](https://github.com/jonpwilson)) 46 | - Package signing [\#9](https://github.com/bbc/device_api-ios/pull/9) ([jonpwilson](https://github.com/jonpwilson)) 47 | - Added ios-devices as a dependency [\#8](https://github.com/bbc/device_api-ios/pull/8) ([jonpwilson](https://github.com/jonpwilson)) 48 | 49 | ## [1.0.1](https://github.com/bbc/device_api-ios/tree/1.0.1) (2015-05-22) 50 | **Closed issues:** 51 | 52 | - Readme & license need adding to the gemspec files [\#5](https://github.com/bbc/device_api-ios/issues/5) 53 | - Gemspec is out of date [\#4](https://github.com/bbc/device_api-ios/issues/4) 54 | - Public api methods need documentation [\#3](https://github.com/bbc/device_api-ios/issues/3) 55 | - License file and copywrite notice required [\#2](https://github.com/bbc/device_api-ios/issues/2) 56 | 57 | **Merged pull requests:** 58 | 59 | - Added comments - yard coverage is now 100% [\#7](https://github.com/bbc/device_api-ios/pull/7) ([jonpwilson](https://github.com/jonpwilson)) 60 | - Added MIT license, updated gemspec [\#6](https://github.com/bbc/device_api-ios/pull/6) ([jonpwilson](https://github.com/jonpwilson)) 61 | - Bring the iOS version in line with the Android version [\#1](https://github.com/bbc/device_api-ios/pull/1) ([jonpwilson](https://github.com/jonpwilson)) 62 | 63 | 64 | 65 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | device_api-ios (1.1.1) 5 | device_api (>= 1.0, < 2.0) 6 | ios-devices (>= 0.2) 7 | ox (>= 2.9.4) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | device_api (1.0.2) 13 | diff-lcs (1.3) 14 | ios-devices (0.2.6) 15 | ox (2.9.4) 16 | rspec (3.8.0) 17 | rspec-core (~> 3.8.0) 18 | rspec-expectations (~> 3.8.0) 19 | rspec-mocks (~> 3.8.0) 20 | rspec-core (3.8.0) 21 | rspec-support (~> 3.8.0) 22 | rspec-expectations (3.8.1) 23 | diff-lcs (>= 1.2.0, < 2.0) 24 | rspec-support (~> 3.8.0) 25 | rspec-mocks (3.8.0) 26 | diff-lcs (>= 1.2.0, < 2.0) 27 | rspec-support (~> 3.8.0) 28 | rspec-support (3.8.0) 29 | 30 | PLATFORMS 31 | ruby 32 | 33 | DEPENDENCIES 34 | device_api-ios! 35 | rspec (~> 3.5) 36 | 37 | BUNDLED WITH 38 | 1.16.2 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 BBC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeviceAPI-IOS 2 | 3 | 4 | *DeviceAPI-IOS* is the iOS implementation of *DeviceAPI* - an initiative to allow full automation of device activities. For a full list of release notes, see the [change log](CHANGELOG.md) 5 | 6 | ## Dependencies 7 | 8 | *DeviceAPI-IOS* shells out to a number of iOS command line tools. You will need to make sure that the libimobiledevice library is installed and the following commands are available on your path: 9 | * ideviceinfo 10 | 11 | ## Using the gem 12 | 13 | Add the device_api-ios gem to your gemfile - this will automatically bring in the device_api base gem on which the iOS gem is built. 14 | 15 | gem 'device_api-ios' 16 | 17 | You'll need to require the library in your code: 18 | 19 | require 'device_api/ios' 20 | 21 | Try connecting an iOS device with USB and run: 22 | 23 | devices = DeviceAPI::IOS.devices 24 | 25 | You might need to accept the 'Trust this computer' dialog on your device. 26 | 27 | ### Detecting devices 28 | 29 | There are two methods for detecting devices: 30 | DeviceAPI::IOS.devices 31 | This returns an array of objects representing the connected devices. You will get an empty array if there are no connected devices. 32 | DeviceAPI::IOS.device(serial_id) 33 | This looks for a device with a matching serial_id and returns a single device object. 34 | 35 | ### Device object 36 | 37 | When *DeviceAPI* detects a device, it returns a device object that lets you interact with and query the device with various iOS tools. 38 | 39 | For example: 40 | 41 | device = DeviceAPI::IOS.device(serial_id) 42 | device.serial # '50d9299992726df277bg6befdf88e1704f4f8f8b' 43 | device.model # 'iPad mini 3' 44 | 45 | ## License 46 | 47 | *DeviceAPI-IOS* is available to everyone under the terms of the MIT open source licence. Take a look at the LICENSE file in the code. 48 | 49 | Copyright (c) 2015 BBC 50 | -------------------------------------------------------------------------------- /device_api-ios.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'device_api-ios' 3 | s.version = '1.1.1' 4 | s.date = Time.now.strftime("%Y-%m-%d") 5 | s.summary = 'IOS Device Management API' 6 | s.description = 'iOS implementation of DeviceAPI' 7 | s.authors = ['BBC', 'Jon Wilson', 'Kedar Barde'] 8 | s.email = ['jon.wilson01@bbc.co.uk', 'kedar_barde@mindtree.com'] 9 | s.files = `git ls-files`.split "\n" 10 | s.homepage = 'https://github.com/bbc/device_api-ios' 11 | s.license = 'MIT' 12 | s.add_runtime_dependency 'device_api', '>=1.0', '<2.0' 13 | s.add_runtime_dependency 'ios-devices', '>=0.2' 14 | s.add_runtime_dependency 'ox', '>=2.9.4' 15 | s.add_development_dependency 'rspec', '~> 3.5' 16 | end 17 | -------------------------------------------------------------------------------- /lib/device_api/ios.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'device_api/ios/device' 3 | require 'device_api/ios/idevice' 4 | require 'device_api/ios/ideviceinstaller' 5 | require 'device_api/ios/idevicedebug' 6 | require 'device_api/ios/ipaddress' 7 | require 'device_api/ios/ideviceprovision' 8 | require 'device_api/ios/idevicename' 9 | 10 | module DeviceAPI 11 | module IOS 12 | 13 | # Returns an array of connected iOS devices 14 | def self.devices 15 | devs = IDevice.devices 16 | devs.keys.map do |serial| 17 | DeviceAPI::IOS::Device.new(qualifier: serial, display: devs[serial], state: 'ok') 18 | end 19 | end 20 | 21 | # Retrieve a Device object by serial ID 22 | def self.device(qualifier) 23 | if qualifier.to_s.empty? 24 | raise DeviceAPI::BadSerialString.new("Serial was '#{ qualifier.nil? ? 'nil' : qualifier }'") 25 | end 26 | DeviceAPI::IOS::Device.new(qualifier: qualifier, state: 'device') 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/device_api/ios/device.rb: -------------------------------------------------------------------------------- 1 | require 'device_api/device' 2 | require 'device_api/ios/device' 3 | require 'device_api/ios/idevice' 4 | require 'device_api/ios/idevicename' 5 | require 'device_api/ios/idevicescreenshot' 6 | require 'device_api/ios/idevicediagnostics' 7 | require 'ios/devices' 8 | 9 | # DeviceAPI - an interface to allow for automation of devices 10 | module DeviceAPI 11 | # iOS component of DeviceAPI 12 | module IOS 13 | # Namespace for the Device object. 14 | class Device < DeviceAPI::Device 15 | attr_accessor :qualifier 16 | def self.create options = {} 17 | self.new(options) 18 | end 19 | 20 | def initialize(options = {}) 21 | @qualifier = options[:qualifier] 22 | @serial = options[:serial] || options[:qualifier] 23 | @state = options[:state] 24 | end 25 | 26 | # Mapping of device status - used to provide a consistent status across platforms 27 | # @return (String) common status string 28 | def status 29 | { 30 | 'device' => :ok, 31 | 'no device' => :dead, 32 | 'offline' => :offline 33 | }[@state] 34 | end 35 | 36 | # Look up device name - i.e. Bob's iPhone 37 | # @return (String) iOS device name 38 | def name 39 | IDeviceName.name(serial) 40 | end 41 | 42 | # Look up device model using the ios-devices gem - changing 'iPad4,7' to 'iPad mini 3' 43 | # @return (String) human readable model and version (where applicable) 44 | def model 45 | Ios::Devices.search(get_prop('ProductType')).name 46 | end 47 | 48 | # Returns the devices iOS version number - i.e. 8.2 49 | # @return (String) iOS version number 50 | def version 51 | get_prop('ProductVersion') 52 | end 53 | 54 | # Return the device class - i.e. iPad, iPhone, etc 55 | # @return (String) iOS device class 56 | def device_class 57 | get_prop('DeviceClass') 58 | end 59 | 60 | # Capture screenshot on device 61 | def screenshot(args = {}) 62 | args[:device_id] = serial 63 | IDeviceScreenshot.capture(args) 64 | end 65 | 66 | # Get the IMEI number of the device 67 | # @return (String) IMEI number of current device 68 | def imei 69 | get_prop('InternationalMobileEquipmentIdentity') 70 | end 71 | 72 | # Has the 'Trust this device' dialog been accepted? 73 | # @return (Boolean) true if the device is trusted, otherwise false 74 | def trusted? 75 | IDevice.trusted?(serial) 76 | end 77 | 78 | # Get the IP Address from the device 79 | # @return [String] IP Address of current device 80 | def ip_address 81 | IPAddress.address(serial) 82 | end 83 | 84 | # Get the Wifi Mac address for the current device 85 | # @return [String] Mac address of current device 86 | def wifi_mac_address 87 | get_prop('WiFiAddress') 88 | end 89 | 90 | # Install a specified IPA 91 | # @param [String] ipa string containing path to the IPA to install 92 | # @return [Boolean, Exception] true when the IPA installed successfully, otherwise an error is raised 93 | def install(ipa) 94 | fail StandardError, 'No IPA or app specified.', caller if ipa.empty? 95 | 96 | res = install_ipa(ipa) 97 | 98 | fail StandardError, res, caller unless res 99 | true 100 | end 101 | 102 | # Uninstall a specified package 103 | # @param [String] package_name string containing name of package to uninstall 104 | # @return [Boolean, Exception] true when the package is uninstalled successfully, otherwise an error is raised 105 | def uninstall(package_name) 106 | res = uninstall_package(package_name) 107 | 108 | fail StandardError, res, caller unless res 109 | true 110 | end 111 | 112 | # Return whether or not the device is a tablet or mobile 113 | # @return [Symbol] :tablet or :mobile depending on device_class 114 | def type 115 | if device_class.downcase == 'ipad' 116 | :tablet 117 | else 118 | :mobile 119 | end 120 | end 121 | 122 | def list_installed_packages 123 | IDeviceInstaller.list_installed_packages(serial) 124 | end 125 | 126 | # Reboot the device 127 | def reboot 128 | restart 129 | end 130 | 131 | def restart 132 | IDeviceDiagnostics.restart(serial) 133 | end 134 | 135 | private 136 | 137 | def get_prop(key) 138 | if !@props || !@props[key] 139 | @props = IDevice.get_props(serial) 140 | end 141 | @props[key] 142 | end 143 | 144 | def install_ipa(ipa) 145 | IDeviceInstaller.install_ipa(ipa: ipa, serial: serial) 146 | end 147 | 148 | def uninstall_package(package_name) 149 | IDeviceInstaller.uninstall_package(package: package_name, serial: serial) 150 | end 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/device_api/ios/idevice.rb: -------------------------------------------------------------------------------- 1 | require 'device_api/execution' 2 | 3 | # DeviceAPI - an interface to allow for automation of devices 4 | module DeviceAPI 5 | # iOS component of DeviceAPI 6 | module IOS 7 | # Namespace for all methods encapsulating idevice calls 8 | class IDevice < Execution 9 | 10 | # Returns an array of hashes representing connected devices 11 | # @return (Array) Hash containing serial and device name 12 | def self.devices 13 | result = execute_with_timeout_and_retry('idevice_id -l') 14 | 15 | raise IDeviceCommandError.new(result.stderr) if result.exit != 0 16 | 17 | lines = result.stdout.split("\n") 18 | results = {} 19 | 20 | lines.each do |ln| 21 | if /[0-9a-zA-Z].*/.match(ln) 22 | results[ln] = execute_with_timeout_and_retry("ideviceinfo -u #{ln} -k DeviceName").stdout.strip 23 | end 24 | end 25 | results 26 | end 27 | 28 | # Check to see if device has trusted the computer 29 | # @param device_id uuid of the device 30 | # @return true if the device returns information to ideviceinfo, otherwise false 31 | def self.trusted?(device_id) 32 | result = execute("ideviceinfo -u '#{device_id}'") 33 | 34 | lines = result.stdout.split("\n") 35 | result.exit == 0 and lines.length > 0 and not lines[0].match('Usage') 36 | end 37 | 38 | # Returns a Hash containing properties of the specified device using idevice_id. 39 | # @param device_id uuid of the device 40 | # @return (Hash) key value pair of properties 41 | def self.get_props(device_id) 42 | result = execute("ideviceinfo -u '#{device_id}'") 43 | 44 | raise IDeviceCommandError.new(result.stderr) if result.exit != 0 45 | 46 | result = result.stdout 47 | props = {} 48 | unless result.start_with?('Usage:') 49 | prop_list = result.split("\n") 50 | prop_list.each do |line| 51 | matches = line.scan(/(.*): (.*)/) 52 | prop_name, prop_value = matches[0] 53 | props[prop_name.strip] = prop_value.strip 54 | end 55 | end 56 | 57 | props 58 | end 59 | end 60 | 61 | # Exception class to handle exceptions related to IDevice Class 62 | class IDeviceCommandError < StandardError 63 | def initialize(msg) 64 | super(msg) 65 | end 66 | end 67 | 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/device_api/ios/idevicedebug.rb: -------------------------------------------------------------------------------- 1 | # DeviceAPI - an interface to allow for automation of devices 2 | module DeviceAPI 3 | # iOS component of DeviceAPI 4 | module IOS 5 | # Namespace for all methods encapsulating idevice calls 6 | class IDeviceDebug < Execution 7 | 8 | # idevicedebug doesn't return until the app you are attempting to run 9 | # exits. By passing in a timeout value we can limit how long we wait 10 | # before terminating the debug session 11 | # @param [Hash] options options for debug running 12 | # @option options [String] :serial serial of the device run 13 | # @option options [String] :bundle_id ID of the app to run 14 | # @option options [Integer] :timeout Number of seconds before the debug session should be killed 15 | # @return [Hash] Returns the stdout of the debug session 16 | def self.run(options = {}) 17 | serial = options[:serial] 18 | bundle_id = options[:bundle_id] 19 | timeout = options[:timeout] || 10 20 | 21 | result = execute("doalarm () { perl -e 'alarm shift; exec @ARGV' \"$@\"; }; doalarm #{timeout} idevicedebug -u #{serial} -d run #{bundle_id}") 22 | 23 | raise IDeviceDebugError.new(result.stderr) unless [0, 255, 142].include?(result.exit) 24 | 25 | result.stdout.split("\r\n") 26 | end 27 | end 28 | 29 | # Error class for the IDeviceDebug class 30 | class IDeviceDebugError < StandardError 31 | def initialize(msg) 32 | super(msg) 33 | end 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /lib/device_api/ios/idevicediagnostics.rb: -------------------------------------------------------------------------------- 1 | # DeviceAPI - an interface to allow for automation of devices 2 | module DeviceAPI 3 | # iOS component of DeviceAPI 4 | module IOS 5 | # Namespace for all methods encapsulating idevicename calls 6 | class IDeviceDiagnostics < Execution 7 | 8 | # Reboot the device 9 | def self.reboot(device_id) 10 | self.restart(device_id) 11 | end 12 | 13 | def self.restart(device_id) 14 | result = execute("idevicediagnostics restart -u #{device_id}") 15 | end 16 | 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/device_api/ios/ideviceinstaller.rb: -------------------------------------------------------------------------------- 1 | require 'device_api/execution' 2 | 3 | # DeviceAPI - an interface to allow for automation of devices 4 | module DeviceAPI 5 | # iOS component of DeviceAPI 6 | module IOS 7 | # Namespace for all methods encapsulating idevice calls 8 | class IDeviceInstaller < Execution 9 | # Installs a given IPA to the specified device 10 | # @param [Hash] options options for installing the app 11 | # @option options [String] :ipa path to the IPA to install 12 | # @option options [String] :serial serial of the target device 13 | # @return [Boolean] true if successful, otherwise false 14 | def self.install_ipa(options = {}) 15 | options[:action] = :install 16 | change_package(options) 17 | end 18 | 19 | # Uninstalls a specified package from a device 20 | # @param [Hash] options options for uninstalling the app 21 | # @option options [String] :package bundle ID of the package to be uninstalled 22 | # @option options [String] :serial serial of the target device 23 | # @return [Boolean] true if successful, otherwise false 24 | def self.uninstall_package(options = {}) 25 | options[:action] = :uninstall 26 | change_package(options) 27 | end 28 | 29 | def self.change_package(options = {}) 30 | package = options[:package] 31 | ipa = options[:ipa] 32 | serial = options[:serial] 33 | action = options[:action] 34 | 35 | command = nil 36 | if action == :install 37 | command = "ideviceinstaller -u '#{serial}' -i '#{ipa}'" 38 | elsif action == :uninstall 39 | command = "ideviceinstaller -u '#{serial}' -U '#{package}'" 40 | end 41 | 42 | raise IDeviceInstallerError.new('No action specified') if command.nil? 43 | 44 | result = execute(command) 45 | 46 | raise IDeviceInstallerError.new(result.stderr) if result.exit != 0 47 | 48 | lines = result.stdout.encode('UTF-8', :invalid => :replace).split("\n").map { |line| line.gsub('-', '').strip } 49 | 50 | return true if lines.last.match('Complete') 51 | false 52 | end 53 | 54 | # Lists packages installed on the specified device 55 | # @param [String] serial serial of the target device 56 | # @return [Hash] hash containing installed packages 57 | def self.list_installed_packages(serial) 58 | result = execute("ideviceinstaller -u '#{serial}' -l") 59 | 60 | raise IDeviceInstallerError.new(result.stderr) if result.exit != 0 61 | 62 | lines = result.stdout.encode('UTF-8', :invalid => :replace).split("\n") 63 | lines.shift 64 | packages = {} 65 | lines.each do |line| 66 | if /(.*)\s+-\s+(.*)\s+(\d.*)/.match(line) 67 | packages[Regexp.last_match[2]] = { package_name: Regexp.last_match[1], version: Regexp.last_match[3] } 68 | end 69 | end 70 | packages 71 | end 72 | 73 | # Check to see if a package is installed 74 | # @param [Hash] options options for checking for installed package 75 | # @option options [String] :package package ID to check for 76 | # @option options [String] :serial serial of the target device 77 | # @return [Boolean] true if the package is installed, false otherwise 78 | def self.package_installed?(options = {}) 79 | package = options[:package] 80 | serial = options[:serial] 81 | 82 | installed_packages = list_installed_packages(serial) 83 | 84 | matches = installed_packages.select { |_, values| values[:package_name] == package } 85 | return !matches.empty? 86 | end 87 | end 88 | 89 | # Error class for IDeviceInstaller class 90 | class IDeviceInstallerError < StandardError 91 | def initialize(msg) 92 | super(msg) 93 | end 94 | end 95 | end 96 | end -------------------------------------------------------------------------------- /lib/device_api/ios/idevicename.rb: -------------------------------------------------------------------------------- 1 | # DeviceAPI - an interface to allow for automation of devices 2 | module DeviceAPI 3 | # iOS component of DeviceAPI 4 | module IOS 5 | # Namespace for all methods encapsulating idevicename calls 6 | class IDeviceName < Execution 7 | 8 | # Returns the device name based on the provided UUID 9 | # @param device_id uuid of the device 10 | # @return device name if device is connected 11 | def self.name(device_id) 12 | result = execute("idevicename -u #{device_id}") 13 | return IDeviceNameError.new(result.stderr) if result.exit != 0 14 | result.stdout.strip 15 | end 16 | end 17 | 18 | # Error class for the IDeviceName class 19 | class IDeviceNameError < StandardError 20 | def initialize(msg) 21 | super(msg) 22 | end 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /lib/device_api/ios/ideviceprovision.rb: -------------------------------------------------------------------------------- 1 | # DeviceAPI - an interface to allow for automation of devices 2 | module DeviceAPI 3 | # iOS component of DeviceAPI 4 | module IOS 5 | # Namespace for all methods encapsulating ideviceprovision calls 6 | class IDeviceProvision < Execution 7 | # Lists all profiles on the specified device 8 | # @param [String] serial serial of the device to check 9 | # @return [Hash] hash of profile name and UUID 10 | def self.list_profiles(serial) 11 | result = execute("ideviceprovision -u #{serial} list") 12 | 13 | raise IDeviceProvisionError.new(result.stderr) if result.exit != 0 14 | 15 | Hash[result.stdout.split("\n").map { |a| b = a.split(' - '); [b[0], b[1]] }[1..-1]] 16 | end 17 | 18 | # Checks to see if a profile is installed on the specified device 19 | # @param [Hash] options options used for checking profiles 20 | # @option options [String] :name name of the profile (optional when uuid provided) 21 | # @option options [String] :uuid UUID of the profile (optional when name provided) 22 | # @option options [String] :serial serial of the device to check 23 | # @return [Boolean] true if the profile is installed, false otherwise 24 | def self.has_profile?(options = {}) 25 | name = options[:name] 26 | uuid = options[:uuid] 27 | serial = options[:serial] 28 | 29 | profiles = list_profiles(serial) 30 | 31 | profiles.key?(uuid) || profiles.value?(name) 32 | end 33 | 34 | # Removes the specified profile from the device 35 | # @param [Hash] options options used for removing a profile 36 | # @option options [String] :uuid UUID of the profile to be removed 37 | # @option options [String] :serial serial of the device to remove the profile from 38 | # @return [Boolean, IDeviceProvisionError] true if the profile is removed from the device, an error otherwise 39 | def self.remove_profile(options = {}) 40 | uuid = options[:uuid] 41 | serial = options[:serial] 42 | 43 | return true unless has_profile?(serial: serial, uuid: uuid) 44 | 45 | result = execute("ideviceprovision -u #{serial} remove #{uuid}") 46 | 47 | raise IDeviceProvisionError.new(result.stderr) if result.exit != 0 48 | true 49 | end 50 | 51 | # Installs the specified profile to the device 52 | # @param [Hash] options options used for installing a profile 53 | # @option options [String] :file path to the provisioning profile 54 | # @option options [String] :serial serial of the device to install the profile to 55 | # @return [Boolean, IDeviceProvisionError] true if the profile is installed, an error otherwise 56 | def self.install_profile(options = {}) 57 | serial = options[:serial] 58 | file = options[:file] 59 | 60 | info = get_profile_info(file) 61 | 62 | # Check to see if the profile has already been added to the device 63 | return true if has_profile?(serial: serial, uuid: info['UUID']) 64 | 65 | result = execute("ideviceprovision -u #{serial} install #{file}") 66 | 67 | raise IDeviceProvisionError.new(result.stderr) if result.exit != 0 68 | true 69 | end 70 | 71 | # Gets information about a provisioning profile 72 | # @param [String] file path to the provisioning profile 73 | # @return [Hash] hash containing provisioning profile information 74 | def self.get_profile_info(file) 75 | result = execute("ideviceprovision dump #{file}") 76 | 77 | raise IDeviceProvisionError.new(result.stderr) if result.exit != 0 78 | 79 | lines = result.stdout.split("\n") 80 | 81 | info = {} 82 | lines.each do |l| 83 | if /(.*):\s+(.*)/.match(l) 84 | info[Regexp.last_match[1]] = Regexp.last_match[2] 85 | end 86 | end 87 | info 88 | end 89 | end 90 | 91 | # Provisioning error class 92 | class IDeviceProvisionError < StandardError 93 | def initialize(msg) 94 | super(msg) 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/device_api/ios/idevicescreenshot.rb: -------------------------------------------------------------------------------- 1 | # DeviceAPI - an interface to allow for automation of devices 2 | module DeviceAPI 3 | # iOS component of DeviceAPI 4 | module IOS 5 | # Namespace for all methods encapsulating idevicescreenshot calls 6 | class IDeviceScreenshot < Execution 7 | 8 | # Take a screenshot of the device based on the provided UUID 9 | # @param filename for the output file 10 | def self.capture(args) 11 | result = execute("idevicescreenshot #{args[:filename]} -u #{args[:device_id]}") 12 | raise IDeviceScreenshotError.new(result.stderr) if result.exit != 0 13 | end 14 | end 15 | 16 | # Error class for the IDeviceScreenshot class 17 | class IDeviceScreenshotError < StandardError 18 | def initialize(msg) 19 | super(msg) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/device_api/ios/ipaddress.rb: -------------------------------------------------------------------------------- 1 | require 'device_api/ios/idevicedebug' 2 | require 'device_api/ios/ideviceinstaller' 3 | 4 | # DeviceAPI - an interface to allow for automation of devices 5 | module DeviceAPI 6 | # iOS component of DeviceAPI 7 | module IOS 8 | # Namespace for all methods encapsulating idevice calls 9 | class IPAddress < Execution 10 | 11 | # Package name for the IP Address app 12 | def self.ipaddress_bundle_id 13 | 'uk.co.bbc.titan.IPAddress' 14 | end 15 | 16 | # Check to see if the IPAddress app is installed 17 | # @param [String] serial serial of the target device 18 | # @return [Boolean] returns true if the app is installed 19 | def self.installed?(serial) 20 | if DeviceAPI::IOS::IDeviceInstaller.package_installed?( serial: serial, package: ipaddress_bundle_id ) 21 | return true 22 | else 23 | warn IPAddressError.new('IP Address package not installed: Please see https://github.com/bbc/ios-test-helper') 24 | end 25 | end 26 | 27 | # Get the IP Address from the installed app 28 | # @param [String] serial serial of the target device 29 | # @return [String] IP Address if found 30 | def self.address(serial) 31 | return nil unless installed?(serial) 32 | result = IDeviceDebug.run(serial: serial, bundle_id: ipaddress_bundle_id ) 33 | 34 | ip_address = nil 35 | result.each do |line| 36 | if /"en0\/ipv4" = "(.*)"/.match(line) 37 | ip_address = Regexp.last_match[1] 38 | end 39 | end 40 | ip_address 41 | end 42 | end 43 | 44 | # Error class for the IPAddress class 45 | class IPAddressError < StandardError 46 | def initialize(msg) 47 | super(msg) 48 | end 49 | end 50 | end 51 | end -------------------------------------------------------------------------------- /spec/README.md: -------------------------------------------------------------------------------- 1 | The directory structure mirrors the directory structure of the `lib` directory. 2 | For example: 3 | 4 | | Module/Class | Lib File | Spec file | 5 | |---|---|--- 6 | | `DeviceAPI::IOS` | `lib/device_api/ios.rb` | `spec/lib/device_api/ios_spec.rb` | 7 | | `DeviceAPI::IOS:IDevice` | `lib/device_api/ios/idevice.rb` | `spec/lib/device_api/ios/idevice_spec.rb` | 8 | -------------------------------------------------------------------------------- /spec/lib/device_api/ios/device_spec.rb: -------------------------------------------------------------------------------- 1 | require 'device_api/ios/device' 2 | 3 | RSpec.describe DeviceAPI::IOS::Device do 4 | describe '.create' do 5 | it 'creates an instance of DeviceAPI::IOS::Device' do 6 | expect(DeviceAPI::IOS::Device.create({qualifier: '12345'})).to be_a DeviceAPI::IOS::Device 7 | end 8 | 9 | it 'sets the serial to be the qualifier' do 10 | expect(DeviceAPI::IOS::Device.create({qualifier: '12345'}).serial).to eq '12345' 11 | end 12 | 13 | it 'uses serial to override the qualifer if it is set' do 14 | expect(DeviceAPI::IOS::Device.create({qualifier: '12345', serial: '98765'}).serial).to eq '98765' 15 | end 16 | 17 | it 'sets the qualifier' do 18 | expect(DeviceAPI::IOS::Device.create({qualifier: '12345'}).qualifier).to eq '12345' 19 | end 20 | 21 | it 'does not override the qualifier with the serial' do 22 | expect(DeviceAPI::IOS::Device.create({qualifier: '12345', serial: '98765'}).qualifier).to eq '12345' 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/lib/device_api/ios/idevice_spec.rb: -------------------------------------------------------------------------------- 1 | require 'device_api/ios/idevice' 2 | 3 | RSpec.describe DeviceAPI::IOS::IDevice do 4 | describe '.devices' do 5 | it 'detects devices attached to device' do 6 | allow(Open3).to receive(:capture3).with('idevice_id -l').and_return( [ "12345678\n23451234\n", '', Struct.new(:exitstatus).new(0) ] ) 7 | allow(Open3).to receive(:capture3).with('ideviceinfo -u 12345678 -k DeviceName').and_return( [ "Device-1\n", '', Struct.new(:exitstatus).new(0) ] ) 8 | allow(Open3).to receive(:capture3).with('ideviceinfo -u 23451234 -k DeviceName').and_return( [ "Device-2\n", '', Struct.new(:exitstatus).new(0) ] ) 9 | 10 | expect(DeviceAPI::IOS::IDevice.devices).to match( 11 | { 12 | '12345678' => 'Device-1', 13 | '23451234' => 'Device-2' 14 | } 15 | ) 16 | end 17 | 18 | it 'detects an empty list of devices' do 19 | allow(Open3).to receive(:capture3).with('idevice_id -l').and_return( [ '', '', Struct.new(:exitstatus).new(0) ] ) 20 | 21 | expect(DeviceAPI::IOS::IDevice.devices).to match({}) 22 | end 23 | end 24 | 25 | describe '#trusted?' do 26 | it 'reports a connected device as trusted' do 27 | allow(Open3).to receive(:capture3).with("ideviceinfo -u '00000001'").and_return( [ "ActivationState: Activated\nActivationStateAcknowledged: true\nBasebandActivationTicketVersion: V2\nBasebandCertId: 2\n", '', Struct.new(:exitstatus).new(0) ] ) 28 | expect(DeviceAPI::IOS::IDevice.trusted?('00000001')).to be_truthy 29 | end 30 | 31 | it 'reports a connected device as not trusted' do 32 | allow(Open3).to receive(:capture3).with("ideviceinfo -u '00000001'").and_return( [ '', "ERROR: Could not connect to lockdownd, error code -19\n", Struct.new(:exitstatus).new(255) ] ) 33 | expect(DeviceAPI::IOS::IDevice.trusted?('00000001')).to be_falsey 34 | end 35 | 36 | it 'reports a not connected device as not trusted' do 37 | # So apparently calling ideviceinfo with an unknown id results in a success 38 | allow(Open3).to receive(:capture3).with("ideviceinfo -u '00000001'").and_return( [ "Usage: ideviceinfo [OPTIONS]\nShow information about a connected device.\n\n -d, --debug enable communication debugging\n", '', Struct.new(:exitstatus).new(0) ] ) 39 | expect(DeviceAPI::IOS::IDevice.trusted?('00000001')).to be_falsey 40 | end 41 | 42 | it 'reports a success with no output as not trusted' do 43 | # This is unlikely but can occur 44 | # Possibly due to a race condition 45 | allow(Open3).to receive(:capture3).with("ideviceinfo -u '00000001'").and_return( [ '', '', Struct.new(:exitstatus).new(0) ] ) 46 | expect(DeviceAPI::IOS::IDevice.trusted?('00000001')).to be_falsey 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/lib/device_api/ios/ideviceinstaller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'device_api/ios/ideviceinstaller' 2 | 3 | RSpec.describe DeviceAPI::IOS::IDeviceInstaller do 4 | describe '.list_installed_packages' do 5 | it 'returns a list of installed apps' do 6 | output = <