Skip to content
This repository was archived by the owner on Jul 8, 2025. It is now read-only.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*pyc
*#
.#*
8 changes: 6 additions & 2 deletions backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,15 @@ def get_app_bundleId(self, bundleId, version=None):
if r.status_code == 200:
appsDict = json.loads(r.text)
if len(appsDict) == 1:
return appsDict.values()[0]
if not 'minimumOsVersion' in appsDict:
minimumOsVersion = '0'
else:
minimumOsVersion = appsDict['minimumOsVersion']
return (appsDict.values()[0], minimumOsVersion)
logger.debug('%s returned %s results' % (url, len(appsDict)))
else:
logger.error('%s request failed: %s %s' % (url, r.status_code, r.text))
return None
return (None, None)


def get_app_archive(self, appId, archivePath):
Expand Down
89 changes: 77 additions & 12 deletions device.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ def device_info_dict(self):
''' raw device information as dict
'''
if (len(self.deviceDict) == 0):
output = subprocess.check_output(["ideviceinfo", "--xml", "--udid", self.udid])
try:
output = subprocess.check_output(["ideviceinfo", "--xml", "--udid", self.udid])
except subprocess.CalledProcessError as e:
try:
output = subprocess.check_output(["ideviceinfo", "--xml", "--uuid", self.udid])
except:
raise

self.deviceDict = plistlib.readPlistFromString(output)
return self.deviceDict

Expand All @@ -78,7 +85,14 @@ def locale(self):
''' the devices locale setting
'''
if (self.locale_val == ""):
self.locale_val = subprocess.check_output(["ideviceinfo", "--udid", self.udid, "--domain", "com.apple.international", "--key", "Locale"]).strip()
try:
self.locale_val = subprocess.check_output(["ideviceinfo", "--udid", self.udid, "--domain", "com.apple.international", "--key", "Locale"]).strip()
except subprocess.CalledProcessError as e:
try:
self.locale_val = subprocess.check_output(["ideviceinfo", "--uuid", self.udid, "--domain", "com.apple.international", "--key", "Locale"]).strip()
except:
raise

return self.locale_val


Expand All @@ -97,7 +111,15 @@ def base_url(self):
def free_bytes(self):
''' get the free space left on the device in bytes
'''
output = subprocess.check_output(["ideviceinfo", "--udid", self.udid, "--domain", "com.apple.disk_usage", "--key", "TotalDataAvailable"])
try:
output = subprocess.check_output(["ideviceinfo", "--udid", self.udid, "--domain", "com.apple.disk_usage", "--key", "TotalDataAvailable"])

except subprocess.CalledProcessError as e:
try:
output = subprocess.check_output(["ideviceinfo", "--uuid", self.udid, "--domain", "com.apple.disk_usage", "--key", "TotalDataAvailable"])
except:
raise

free_bytes = 0
try:
free_bytes = long(output)
Expand All @@ -111,7 +133,14 @@ def account_info_dict(self):
''' get raw account info from device as dict.
'''
if (len(self.accountDict) == 0):
output = subprocess.check_output(["ideviceinfo", "--xml", "--udid", self.udid, "--domain", "com.apple.mobile.iTunes.store", "--key", "KnownAccounts"])
try:
output = subprocess.check_output(["ideviceinfo", "--xml", "--udid", self.udid, "--domain", "com.apple.mobile.iTunes.store", "--key", "KnownAccounts"])
except subprocess.CalledProcessError as e:
try:
output = subprocess.check_output(["ideviceinfo", "--xml", "--uuid", self.udid, "--domain", "com.apple.mobile.iTunes.store", "--key", "KnownAccounts"])
except:
raise

if len(output) > 0:
self.accountDict = plistlib.readPlistFromString(output)
else:
Expand Down Expand Up @@ -163,7 +192,14 @@ def accounts(self):
def installed_apps(self):
''' list all installed apps as dict.
'''
output = subprocess.check_output(["ideviceinstaller", "--udid", self.udid, "--list-apps", "-o", "list_user", "-o", "xml"])
try:
output = subprocess.check_output(["ideviceinstaller", "--udid", self.udid, "--list-apps", "-o", "list_user", "-o", "xml"])
except subprocess.CalledProcessError as e:
try:
output = subprocess.check_output(["ideviceinstaller", "--uuid", self.udid, "--list-apps", "-o", "list_user", "-o", "xml"])
except:
raise

if (len(output)==0):
return {}

Expand All @@ -173,7 +209,14 @@ def installed_apps(self):
plist = plistlib.readPlistFromString(output)
except Exception:
logger.warning("Failed to parse installed apps via xml output. Try to extract data via regex.")
output = subprocess.check_output(["ideviceinstaller", "--udid", self.udid, "--list-apps", "-o", "list_user"])
try:
output = subprocess.check_output(["ideviceinstaller", "--udid", self.udid, "--list-apps", "-o", "list_user", "-o"])
except subprocess.CalledProcessError as e:
try:
output = subprocess.check_output(["ideviceinstaller", "--uuid", self.udid, "--list-apps", "-o", "list_user"])
except:
raise

regex = re.compile("^(?P<bundleId>.*) - (?P<name>.*) (?P<version>(\d+\.*)+)$",re.MULTILINE)
# r = regex.search(output)
for i in regex.finditer(output):
Expand Down Expand Up @@ -216,7 +259,13 @@ def install(self, app_archive_path):
'''
result=True
try:
output = subprocess.check_output(["ideviceinstaller", "--udid", self.udid, "--install", app_archive_path])
try:
output = subprocess.check_output(["ideviceinstaller", "--udid", self.udid, "--install", app_archive_path])
except subprocess.CalledProcessError as e:
try:
output = subprocess.check_output(["ideviceinstaller", "--uuid", self.udid, "--install", app_archive_path])
except:
raise
logger.debug('output: %s' % output)
if (len(output)==0):
result=False
Expand All @@ -231,7 +280,14 @@ def uninstall(self, bundleId):
'''
result=True
try:
output = subprocess.check_output(["ideviceinstaller", "--udid", self.udid, "--uninstall", bundleId])
try:
output = subprocess.check_output(["ideviceinstaller", "--udid", self.udid, "--uninstall", bundleId])
except subprocess.CalledProcessError as e:
try:
output = subprocess.check_output(["ideviceinstaller", "--uuid", self.udid, "--uninstall", bundleId])
except:
raise

logger.debug('output: %s' % output)
if (len(output)==0):
result=False
Expand All @@ -244,11 +300,15 @@ def archive(self, bundleId, app_archive_folder, app_only=True, uninstall=True):
''' archives an app to `app_archive_folder`
returns True or False
'''
options = ["ideviceinstaller", "--udid", self.udid, "--archive", bundleId, "-o", "copy="+app_archive_folder, "-o", "remove"]
options = ["ideviceinstaller", "--udid", self.udid, "--archive", bundleId, "-o", "copy="+app_archive_folder, "-o", "remove"]
options_alt = ["ideviceinstaller", "--uuid", self.udid, "--archive", bundleId, "-o", "copy="+app_archive_folder, "-o", "remove"]

if app_only:
options.extend(["-o", "app_only"])
options_alt.extend(["-o", "app_only"])
if uninstall:
options.extend(["-o", "uninstall"])
options_alt.extend(["-o", "uninstall"])

if not os.path.exists(app_archive_folder):
os.makedirs(app_archive_folder)
Expand All @@ -260,7 +320,12 @@ def archive(self, bundleId, app_archive_folder, app_only=True, uninstall=True):
if (len(output)==0):
result=False
except subprocess.CalledProcessError as e:
logger.error('archiving app %s failed with: %s <output: %s>', bundleId, e, output)
result=False
try:
output = subprocess.check_output(options_alt)
logger.debug('output: %s' % output)
if (len(output)==0):
result=False
except subprocess.CalledProcessError as e:
logger.error('archiving app %s failed with: %s <output: %s>', bundleId, e, output)
result=False
return result

62 changes: 59 additions & 3 deletions job.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import base64
import time
from distutils.version import StrictVersion

from enum import Enum
from store import AppStore, AppStoreException
Expand All @@ -13,6 +14,9 @@
class JobExecutionError(Exception):
pass

class ProductVersionError(Exception):
pass


class Job(object):

Expand Down Expand Up @@ -80,6 +84,8 @@ def _install_app(self, pilot):
if 'version' in jobInfo:
version = jobInfo['version']

productVersion = self.device.device_info_dict()['ProductVersion']

#check app type
if 'AppStoreApp' == jobInfo['appType']:
logger.debug('installing appstore app %s' % bundleId)
Expand All @@ -100,7 +106,8 @@ def _install_app(self, pilot):
alreadyInstalled = True

# check the backend for already existing app
app = self.backend.get_app_bundleId(bundleId, version)
(app, minimumOsVersion) = self.backend.get_app_bundleId(bundleId, version)

logger.debug('backend result for bundleId %s: %s' % (bundleId, app))
if app and '_id' in app:
self.appId = app['_id']
Expand All @@ -115,6 +122,13 @@ def _install_app(self, pilot):
elif self.appId:
# install from backend

productVersion_alt = int(''.join(productVersion.split('.')))
# has to be > 99 to compare with e.g. ProductVersion 7.1.2
if productVersion_alt < 100:
productVersion_alt *= 10
if int(minimumOsVersion) > productVersion_alt:
raise ProductVersionError(minimumOsVersion)

# dirty check for ipa-size < ~50MB
if app and 'fileSizeBytes' in app:
size = 0
Expand Down Expand Up @@ -156,22 +170,41 @@ def _install_app(self, pilot):
storeCountry = 'de'
if 'storeCountry' in jobInfo:
storeCountry = jobInfo['storeCountry']
if 'DeviceClass' in self.device.device_info_dict():
deviceClass = self.device.device_info_dict()['DeviceClass']
else:
deviceClass = 'iPhone'

logger.debug('User-Agent: %s' % deviceClass)

## get appInfo
logger.debug('fetch appInfo from iTunesStore')
store = AppStore(storeCountry)
store = AppStore(storeCountry, deviceClass)
trackId = 0
appInfo = {}
try:
trackId = store.get_trackId_for_bundleId(bundleId)
appInfo = store.get_app_info(trackId)
except AppStoreException as e:
if deviceClass == 'iPad':
device_type = 0b100
elif deviceClass == 'iPhone':
device_type = 0b10
else:
device_type == 0b1

self.jobDict['compatible_devices'] ^= device_type
logger.error('unable to get appInfo: %s ', e)
raise JobExecutionError('unable to get appInfo: AppStoreException')
if self.jobDict['compatible_devices'] != 0:
raise
else:
raise JobExecutionError('unable to get appInfo: AppStoreException')

self.jobDict['appInfo'] = appInfo
logger.debug('using appInfo: %s' % str(appInfo))

if StrictVersion(appInfo['minimum-os-version']) > StrictVersion(productVersion):
raise ProductVersionError(appInfo['minimum-os-version'])

## get account
accountId = ''
Expand Down Expand Up @@ -254,7 +287,29 @@ def execute(self):
except JobExecutionError, e:
logger.error("Job execution failed: %s" % str(e))
backendJobData['state'] = Job.STATE.FAILED
backendJobData['error_message'] = str(e)
result = False
except ProductVersionError, e:
logger.warn("Job execution aborted: iOS version to low")
backendJobData['state'] = Job.STATE.PENDING
backendJobData['worker'] = None
backendJobData['device'] = None

# has to be > 99 to compare with e.g. ProductVersion 7.1.2
minimumOSVersion = int(''.join(str(e).split('.')))
if minimumOSVersion < 100:
minimumOSVersion *= 10
backendJobData['jobInfo']['minimumOSVersion'] = str(minimumOSVersion)
self.backend.post_job(backendJobData)
return False
except AppStoreException:
logger.warn("Job execution aborted: Job seems to be not compatible with device")
backendJobData['state'] = Job.STATE.PENDING
backendJobData['worker'] = None
backendJobData['device'] = None
backendJobData['compatible_devices'] = self.jobDict['compatible_devices']
self.backend.post_job(backendJobData)
return False

## set job finished
if self.jobId:
Expand Down Expand Up @@ -385,6 +440,7 @@ def execute(self):
except JobExecutionError, e:
logger.error("Job execution failed: %s" % str(e))
backendJobData['state'] = Job.STATE.FAILED
backendJobData['error_message'] = str(e)
self.backend.post_job(backendJobData)
return False

Expand Down
Loading