summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py')
-rw-r--r--testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py893
1 files changed, 893 insertions, 0 deletions
diff --git a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
new file mode 100644
index 000000000..74d0a8d23
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
@@ -0,0 +1,893 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import logging
+import re
+import os
+import tempfile
+import time
+import traceback
+
+from distutils import dir_util
+
+from devicemanager import DeviceManager, DMError
+from mozprocess import ProcessHandler
+import mozfile
+import version_codes
+
+
+class DeviceManagerADB(DeviceManager):
+ """
+ Implementation of DeviceManager interface that uses the Android "adb"
+ utility to communicate with the device. Normally used to communicate
+ with a device that is directly connected with the host machine over a USB
+ port.
+ """
+
+ _haveRootShell = None
+ _haveSu = None
+ _suModifier = None
+ _lsModifier = None
+ _useZip = False
+ _logcatNeedsRoot = False
+ _pollingInterval = 0.01
+ _packageName = None
+ _tempDir = None
+ _adb_version = None
+ _sdk_version = None
+ connected = False
+
+ def __init__(self, host=None, port=5555, retryLimit=5, packageName='fennec',
+ adbPath=None, deviceSerial=None, deviceRoot=None,
+ logLevel=logging.ERROR, autoconnect=True, runAdbAsRoot=False,
+ serverHost=None, serverPort=None, **kwargs):
+ DeviceManager.__init__(self, logLevel=logLevel,
+ deviceRoot=deviceRoot)
+ self.host = host
+ self.port = port
+ self.retryLimit = retryLimit
+
+ self._serverHost = serverHost
+ self._serverPort = serverPort
+
+ # the path to adb, or 'adb' to assume that it's on the PATH
+ self._adbPath = adbPath or 'adb'
+
+ # The serial number of the device to use with adb, used in cases
+ # where multiple devices are being managed by the same adb instance.
+ self._deviceSerial = deviceSerial
+
+ # Some devices do no start adb as root, if allowed you can use
+ # this to reboot adbd on the device as root automatically
+ self._runAdbAsRoot = runAdbAsRoot
+
+ if packageName == 'fennec':
+ if os.getenv('USER'):
+ self._packageName = 'org.mozilla.fennec_' + os.getenv('USER')
+ else:
+ self._packageName = 'org.mozilla.fennec_'
+ elif packageName:
+ self._packageName = packageName
+
+ # verify that we can run the adb command. can't continue otherwise
+ self._verifyADB()
+
+ if autoconnect:
+ self.connect()
+
+ def connect(self):
+ if not self.connected:
+ # try to connect to the device over tcp/ip if we have a hostname
+ if self.host:
+ self._connectRemoteADB()
+
+ # verify that we can connect to the device. can't continue
+ self._verifyDevice()
+
+ # Note SDK version
+ try:
+ proc = self._runCmd(["shell", "getprop", "ro.build.version.sdk"],
+ timeout=self.short_timeout)
+ self._sdk_version = int(proc.output[0])
+ except (OSError, ValueError):
+ self._sdk_version = 0
+ self._logger.info("Detected Android sdk %d" % self._sdk_version)
+
+ # Some commands require root to work properly, even with ADB (e.g.
+ # grabbing APKs out of /data). For these cases, we check whether
+ # we're running as root. If that isn't true, check for the
+ # existence of an su binary
+ self._checkForRoot()
+
+ # can we use zip to speed up some file operations? (currently not
+ # required)
+ try:
+ self._verifyZip()
+ except DMError:
+ pass
+
+ def __del__(self):
+ if self.host:
+ self._disconnectRemoteADB()
+
+ def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
+ # FIXME: this function buffers all output of the command into memory,
+ # always. :(
+
+ # If requested to run as root, check that we can actually do that
+ if root:
+ if self._haveRootShell is None and self._haveSu is None:
+ self._checkForRoot()
+ if not self._haveRootShell and not self._haveSu:
+ raise DMError(
+ "Shell command '%s' requested to run as root but root "
+ "is not available on this device. Root your device or "
+ "refactor the test/harness to not require root." %
+ self._escapedCommandLine(cmd))
+
+ # Getting the return code is more complex than you'd think because adb
+ # doesn't actually return the return code from a process, so we have to
+ # capture the output to get it
+ if root and self._haveSu:
+ cmdline = "su %s \"%s\"" % (self._suModifier,
+ self._escapedCommandLine(cmd))
+ else:
+ cmdline = self._escapedCommandLine(cmd)
+ cmdline += "; echo $?"
+
+ # prepend cwd and env to command if necessary
+ if cwd:
+ cmdline = "cd %s; %s" % (cwd, cmdline)
+ if env:
+ envstr = '; '.join(map(lambda x: 'export %s=%s' % (x[0], x[1]), env.iteritems()))
+ cmdline = envstr + "; " + cmdline
+
+ # all output should be in stdout
+ args = [self._adbPath]
+ if self._serverHost is not None:
+ args.extend(['-H', self._serverHost])
+ if self._serverPort is not None:
+ args.extend(['-P', str(self._serverPort)])
+ if self._deviceSerial:
+ args.extend(['-s', self._deviceSerial])
+ args.extend(["shell", cmdline])
+
+ def _timeout():
+ self._logger.error("Timeout exceeded for shell call '%s'" % ' '.join(args))
+
+ self._logger.debug("shell - command: %s" % ' '.join(args))
+ proc = ProcessHandler(args, processOutputLine=self._log, onTimeout=_timeout)
+
+ if not timeout:
+ # We are asserting that all commands will complete in this time unless
+ # otherwise specified
+ timeout = self.default_timeout
+
+ timeout = int(timeout)
+ proc.run(timeout)
+ proc.wait()
+ output = proc.output
+
+ if output:
+ lastline = output[-1]
+ if lastline:
+ m = re.search('([0-9]+)', lastline)
+ if m:
+ return_code = m.group(1)
+ for line in output:
+ outputfile.write(line + '\n')
+ outputfile.seek(-2, 2)
+ outputfile.truncate() # truncate off the return code
+ return int(return_code)
+
+ return None
+
+ def forward(self, local, remote):
+ """
+ Forward socket connections.
+
+ Forward specs are one of:
+ tcp:<port>
+ localabstract:<unix domain socket name>
+ localreserved:<unix domain socket name>
+ localfilesystem:<unix domain socket name>
+ dev:<character device name>
+ jdwp:<process pid> (remote only)
+ """
+ if not self._checkCmd(['forward', local, remote], timeout=self.short_timeout) == 0:
+ raise DMError("Failed to forward socket connection.")
+
+ def remove_forward(self, local=None):
+ """
+ Turn off forwarding of socket connection.
+ """
+ cmd = ['forward']
+ if local is None:
+ cmd.extend(['--remove-all'])
+ else:
+ cmd.extend(['--remove', local])
+ if not self._checkCmd(cmd, timeout=self.short_timeout) == 0:
+ raise DMError("Failed to remove connection forwarding.")
+
+ def remount(self):
+ "Remounts the /system partition on the device read-write."
+ return self._checkCmd(['remount'], timeout=self.short_timeout)
+
+ def devices(self):
+ "Return a list of connected devices as (serial, status) tuples."
+ proc = self._runCmd(['devices'])
+ proc.output.pop(0) # ignore first line of output
+ devices = []
+ for line in proc.output:
+ result = re.match('(.*?)\t(.*)', line)
+ if result:
+ devices.append((result.group(1), result.group(2)))
+ return devices
+
+ def _connectRemoteADB(self):
+ self._checkCmd(["connect", self.host + ":" + str(self.port)])
+
+ def _disconnectRemoteADB(self):
+ self._checkCmd(["disconnect", self.host + ":" + str(self.port)])
+
+ def pushFile(self, localname, destname, retryLimit=None, createDir=True):
+ # you might expect us to put the file *in* the directory in this case,
+ # but that would be different behaviour from devicemanagerSUT. Throw
+ # an exception so we have the same behaviour between the two
+ # implementations
+ retryLimit = retryLimit or self.retryLimit
+ if self.dirExists(destname):
+ raise DMError("Attempted to push a file (%s) to a directory (%s)!" %
+ (localname, destname))
+ if not os.access(localname, os.F_OK):
+ raise DMError("File not found: %s" % localname)
+
+ proc = self._runCmd(["push", os.path.realpath(localname), destname],
+ retryLimit=retryLimit)
+ if proc.returncode != 0:
+ raise DMError("Error pushing file %s -> %s; output: %s" %
+ (localname, destname, proc.output))
+
+ def mkDir(self, name):
+ result = self._runCmd(["shell", "mkdir", name], timeout=self.short_timeout).output
+ if len(result) and 'read-only file system' in result[0].lower():
+ raise DMError("Error creating directory: read only file system")
+
+ def pushDir(self, localDir, remoteDir, retryLimit=None, timeout=None):
+ # adb "push" accepts a directory as an argument, but if the directory
+ # contains symbolic links, the links are pushed, rather than the linked
+ # files; we either zip/unzip or re-copy the directory into a temporary
+ # one to get around this limitation
+ retryLimit = retryLimit or self.retryLimit
+ if self._useZip:
+ self.removeDir(remoteDir)
+ self.mkDirs(remoteDir + "/x")
+ try:
+ localZip = tempfile.mktemp() + ".zip"
+ remoteZip = remoteDir + "/adbdmtmp.zip"
+ proc = ProcessHandler(["zip", "-r", localZip, '.'], cwd=localDir,
+ processOutputLine=self._log)
+ proc.run()
+ proc.wait()
+ self.pushFile(localZip, remoteZip, retryLimit=retryLimit, createDir=False)
+ mozfile.remove(localZip)
+ data = self._runCmd(["shell", "unzip", "-o", remoteZip,
+ "-d", remoteDir]).output[0]
+ self._checkCmd(["shell", "rm", remoteZip],
+ retryLimit=retryLimit, timeout=self.short_timeout)
+ if re.search("unzip: exiting", data) or re.search("Operation not permitted", data):
+ raise Exception("unzip failed, or permissions error")
+ except:
+ self._logger.warning(traceback.format_exc())
+ self._logger.warning("zip/unzip failure: falling back to normal push")
+ self._useZip = False
+ self.pushDir(localDir, remoteDir, retryLimit=retryLimit, timeout=timeout)
+ else:
+ localDir = os.path.normpath(localDir)
+ remoteDir = os.path.normpath(remoteDir)
+ copyRequired = False
+ if self._adb_version >= '1.0.36' and \
+ os.path.isdir(localDir) and self.dirExists(remoteDir):
+ # See do_sync_push in
+ # https://android.googlesource.com/platform/system/core/+/master/adb/file_sync_client.cpp
+ # Work around change in behavior in adb 1.0.36 where if
+ # the remote destination directory exists, adb push will
+ # copy the source directory *into* the destination
+ # directory otherwise it will copy the source directory
+ # *onto* the destination directory.
+ #
+ # If the destination directory does exist, push to its
+ # parent directory. If the source and destination leaf
+ # directory names are different, copy the source directory
+ # to a temporary directory with the same leaf name as the
+ # destination so that when we push to the parent, the
+ # source is copied onto the destination directory.
+ localName = os.path.basename(localDir)
+ remoteName = os.path.basename(remoteDir)
+ if localName != remoteName:
+ copyRequired = True
+ tempParent = tempfile.mkdtemp()
+ newLocal = os.path.join(tempParent, remoteName)
+ dir_util.copy_tree(localDir, newLocal)
+ localDir = newLocal
+ remoteDir = '/'.join(remoteDir.rstrip('/').split('/')[:-1])
+ try:
+ self._checkCmd(["push", localDir, remoteDir],
+ retryLimit=retryLimit, timeout=timeout)
+ except:
+ raise
+ finally:
+ if copyRequired:
+ mozfile.remove(tempParent)
+
+ def dirExists(self, remotePath):
+ self._detectLsModifier()
+ data = self._runCmd(["shell", "ls", self._lsModifier, remotePath + '/'],
+ timeout=self.short_timeout).output
+
+ if len(data) == 1:
+ res = data[0]
+ if "Not a directory" in res or "No such file or directory" in res:
+ return False
+ return True
+
+ def fileExists(self, filepath):
+ self._detectLsModifier()
+ data = self._runCmd(["shell", "ls", self._lsModifier, filepath],
+ timeout=self.short_timeout).output
+ if len(data) == 1:
+ foundpath = data[0].decode('utf-8').rstrip()
+ if foundpath == filepath:
+ return True
+ return False
+
+ def removeFile(self, filename):
+ if self.fileExists(filename):
+ self._checkCmd(["shell", "rm", filename], timeout=self.short_timeout)
+
+ def removeDir(self, remoteDir):
+ if self.dirExists(remoteDir):
+ self._checkCmd(["shell", "rm", "-r", remoteDir], timeout=self.short_timeout)
+ else:
+ self.removeFile(remoteDir.strip())
+
+ def moveTree(self, source, destination):
+ self._checkCmd(["shell", "mv", source, destination], timeout=self.short_timeout)
+
+ def copyTree(self, source, destination):
+ self._checkCmd(["shell", "dd", "if=%s" % source, "of=%s" % destination])
+
+ def listFiles(self, rootdir):
+ self._detectLsModifier()
+ data = self._runCmd(["shell", "ls", self._lsModifier, rootdir],
+ timeout=self.short_timeout).output
+ data[:] = [item.rstrip('\r\n') for item in data]
+ if (len(data) == 1):
+ if (data[0] == rootdir):
+ return []
+ if (data[0].find("No such file or directory") != -1):
+ return []
+ if (data[0].find("Not a directory") != -1):
+ return []
+ if (data[0].find("Permission denied") != -1):
+ return []
+ if (data[0].find("opendir failed") != -1):
+ return []
+ if (data[0].find("Device or resource busy") != -1):
+ return []
+ return data
+
+ def getProcessList(self):
+ ret = []
+ p = self._runCmd(["shell", "ps"], timeout=self.short_timeout)
+ if not p or not p.output or len(p.output) < 1:
+ return ret
+ # first line is the headers
+ p.output.pop(0)
+ for proc in p.output:
+ els = proc.split()
+ # We need to figure out if this is "user pid name" or
+ # "pid user vsz stat command"
+ if els[1].isdigit():
+ ret.append(list([int(els[1]), els[len(els) - 1], els[0]]))
+ else:
+ ret.append(list([int(els[0]), els[len(els) - 1], els[1]]))
+ return ret
+
+ def fireProcess(self, appname, failIfRunning=False):
+ """
+ Starts a process
+
+ returns: pid
+
+ DEPRECATED: Use shell() or launchApplication() for new code
+ """
+ # strip out env vars
+ parts = appname.split('"')
+ if (len(parts) > 2):
+ parts = parts[2:]
+ return self.launchProcess(parts, failIfRunning)
+
+ def launchProcess(self, cmd, outputFile="process.txt", cwd='', env='', failIfRunning=False):
+ """
+ Launches a process, redirecting output to standard out
+
+ WARNING: Does not work how you expect on Android! The application's
+ own output will be flushed elsewhere.
+
+ DEPRECATED: Use shell() or launchApplication() for new code
+ """
+ if cmd[0] == "am":
+ self._checkCmd(["shell"] + cmd)
+ return outputFile
+
+ acmd = ["-W"]
+ cmd = ' '.join(cmd).strip()
+ i = cmd.find(" ")
+ # SUT identifies the URL by looking for :\\ -- another strategy to consider
+ re_url = re.compile('^[http|file|chrome|about].*')
+ last = cmd.rfind(" ")
+ uri = ""
+ args = ""
+ if re_url.match(cmd[last:].strip()):
+ args = cmd[i:last].strip()
+ uri = cmd[last:].strip()
+ else:
+ args = cmd[i:].strip()
+ acmd.append("-n")
+ acmd.append(cmd[0:i] + "/org.mozilla.gecko.BrowserApp")
+ if args != "":
+ acmd.append("--es")
+ acmd.append("args")
+ acmd.append(args)
+ if env != '' and env is not None:
+ envCnt = 0
+ # env is expected to be a dict of environment variables
+ for envkey, envval in env.iteritems():
+ acmd.append("--es")
+ acmd.append("env" + str(envCnt))
+ acmd.append(envkey + "=" + envval)
+ envCnt += 1
+ if uri != "":
+ acmd.append("-d")
+ acmd.append(uri)
+
+ acmd = ["shell", ' '.join(map(lambda x: '"' + x + '"', ["am", "start"] + acmd))]
+ self._logger.info(acmd)
+ self._checkCmd(acmd)
+ return outputFile
+
+ def killProcess(self, appname, sig=None):
+ shell_args = ["shell"]
+ if self._sdk_version >= version_codes.N:
+ # Bug 1334613 - force use of root
+ if self._haveRootShell is None and self._haveSu is None:
+ self._checkForRoot()
+ if not self._haveRootShell and not self._haveSu:
+ raise DMError(
+ "killProcess '%s' requested to run as root but root "
+ "is not available on this device. Root your device or "
+ "refactor the test/harness to not require root." %
+ appname)
+ if not self._haveRootShell:
+ shell_args.extend(["su", self._suModifier])
+
+ procs = self.getProcessList()
+ for (pid, name, user) in procs:
+ if name == appname:
+ args = list(shell_args)
+ args.append("kill")
+ if sig:
+ args.append("-%d" % sig)
+ args.append(str(pid))
+ p = self._runCmd(args, timeout=self.short_timeout)
+ if p.returncode != 0 and len(p.output) > 0 and \
+ 'No such process' not in p.output[0]:
+ raise DMError("Error killing process "
+ "'%s': %s" % (appname, p.output))
+
+ def _runPull(self, remoteFile, localFile):
+ """
+ Pulls remoteFile from device to host
+ """
+ try:
+ self._runCmd(["pull", remoteFile, localFile])
+ except (OSError, ValueError):
+ raise DMError("Error pulling remote file '%s' to '%s'" % (remoteFile, localFile))
+
+ def pullFile(self, remoteFile, offset=None, length=None):
+ # TODO: add debug flags and allow for printing stdout
+ with mozfile.NamedTemporaryFile() as tf:
+ self._runPull(remoteFile, tf.name)
+ # we need to reopen the file to get the written contents
+ with open(tf.name) as tf2:
+ # ADB pull does not support offset and length, but we can
+ # instead read only the requested portion of the local file
+ if offset is not None and length is not None:
+ tf2.seek(offset)
+ return tf2.read(length)
+ elif offset is not None:
+ tf2.seek(offset)
+ return tf2.read()
+ else:
+ return tf2.read()
+
+ def getFile(self, remoteFile, localFile):
+ self._runPull(remoteFile, localFile)
+
+ def getDirectory(self, remoteDir, localDir, checkDir=True):
+ localDir = os.path.normpath(localDir)
+ remoteDir = os.path.normpath(remoteDir)
+ copyRequired = False
+ originalLocal = localDir
+ if self._adb_version >= '1.0.36' and \
+ os.path.isdir(localDir) and self.dirExists(remoteDir):
+ # See do_sync_pull in
+ # https://android.googlesource.com/platform/system/core/+/master/adb/file_sync_client.cpp
+ # Work around change in behavior in adb 1.0.36 where if
+ # the local destination directory exists, adb pull will
+ # copy the source directory *into* the destination
+ # directory otherwise it will copy the source directory
+ # *onto* the destination directory.
+ #
+ # If the destination directory does exist, pull to its
+ # parent directory. If the source and destination leaf
+ # directory names are different, pull the source directory
+ # into a temporary directory and then copy the temporary
+ # directory onto the destination.
+ localName = os.path.basename(localDir)
+ remoteName = os.path.basename(remoteDir)
+ if localName != remoteName:
+ copyRequired = True
+ tempParent = tempfile.mkdtemp()
+ localDir = os.path.join(tempParent, remoteName)
+ else:
+ localDir = '/'.join(localDir.rstrip('/').split('/')[:-1])
+ self._runCmd(["pull", remoteDir, localDir]).wait()
+ if copyRequired:
+ dir_util.copy_tree(localDir, originalLocal)
+ mozfile.remove(tempParent)
+
+ def validateFile(self, remoteFile, localFile):
+ md5Remote = self._getRemoteHash(remoteFile)
+ md5Local = self._getLocalHash(localFile)
+ if md5Remote is None or md5Local is None:
+ return None
+ return md5Remote == md5Local
+
+ def _getRemoteHash(self, remoteFile):
+ """
+ Return the md5 sum of a file on the device
+ """
+ with tempfile.NamedTemporaryFile() as f:
+ self._runPull(remoteFile, f.name)
+
+ return self._getLocalHash(f.name)
+
+ def _setupDeviceRoot(self, deviceRoot):
+ # user-specified device root, create it and return it
+ if deviceRoot:
+ self.mkDir(deviceRoot)
+ return deviceRoot
+
+ # we must determine the device root ourselves
+ paths = [('/storage/sdcard0', 'tests'),
+ ('/storage/sdcard1', 'tests'),
+ ('/storage/sdcard', 'tests'),
+ ('/mnt/sdcard', 'tests'),
+ ('/sdcard', 'tests'),
+ ('/data/local', 'tests')]
+ for (basePath, subPath) in paths:
+ if self.dirExists(basePath):
+ root = os.path.join(basePath, subPath)
+ try:
+ self.mkDir(root)
+ return root
+ except:
+ pass
+
+ raise DMError("Unable to set up device root using paths: [%s]"
+ % ", ".join(["'%s'" % os.path.join(b, s) for b, s in paths]))
+
+ def getTempDir(self):
+ # Cache result to speed up operations depending
+ # on the temporary directory.
+ if not self._tempDir:
+ self._tempDir = "%s/tmp" % self.deviceRoot
+ self.mkDir(self._tempDir)
+
+ return self._tempDir
+
+ def reboot(self, wait=False, **kwargs):
+ self._checkCmd(["reboot"])
+ if wait:
+ self._checkCmd(["wait-for-device"])
+ if self._runAdbAsRoot:
+ self._adb_root()
+ self._checkCmd(["shell", "ls", "/sbin"], timeout=self.short_timeout)
+
+ def updateApp(self, appBundlePath, **kwargs):
+ return self._runCmd(["install", "-r", appBundlePath]).output
+
+ def getCurrentTime(self):
+ timestr = str(self._runCmd(["shell", "date", "+%s"], timeout=self.short_timeout).output[0])
+ if (not timestr or not timestr.isdigit()):
+ raise DMError("Unable to get current time using date (got: '%s')" % timestr)
+ return int(timestr) * 1000
+
+ def getInfo(self, directive=None):
+ directive = directive or "all"
+ ret = {}
+ if directive == "id" or directive == "all":
+ ret["id"] = self._runCmd(["get-serialno"], timeout=self.short_timeout).output[0]
+ if directive == "os" or directive == "all":
+ ret["os"] = self.shellCheckOutput(
+ ["getprop", "ro.build.display.id"], timeout=self.short_timeout)
+ if directive == "uptime" or directive == "all":
+ uptime = self.shellCheckOutput(["uptime"], timeout=self.short_timeout)
+ if not uptime:
+ raise DMError("error getting uptime")
+ m = re.match("up time: ((\d+) days, )*(\d{2}):(\d{2}):(\d{2})", uptime)
+ if m:
+ uptime = "%d days %d hours %d minutes %d seconds" % tuple(
+ [int(g or 0) for g in m.groups()[1:]])
+ ret["uptime"] = uptime
+ if directive == "process" or directive == "all":
+ data = self.shellCheckOutput(["ps"], timeout=self.short_timeout)
+ ret["process"] = data.split('\n')
+ if directive == "systime" or directive == "all":
+ ret["systime"] = self.shellCheckOutput(["date"], timeout=self.short_timeout)
+ if directive == "memtotal" or directive == "all":
+ meminfo = {}
+ for line in self.pullFile("/proc/meminfo").splitlines():
+ key, value = line.split(":")
+ meminfo[key] = value.strip()
+ ret["memtotal"] = meminfo["MemTotal"]
+ if directive == "disk" or directive == "all":
+ data = self.shellCheckOutput(
+ ["df", "/data", "/system", "/sdcard"], timeout=self.short_timeout)
+ ret["disk"] = data.split('\n')
+ self._logger.debug("getInfo: %s" % ret)
+ return ret
+
+ def uninstallApp(self, appName, installPath=None):
+ status = self._runCmd(["uninstall", appName]).output[0].strip()
+ if status != 'Success':
+ raise DMError("uninstall failed for %s. Got: %s" % (appName, status))
+
+ def uninstallAppAndReboot(self, appName, installPath=None):
+ self.uninstallApp(appName)
+ self.reboot()
+
+ def _runCmd(self, args, timeout=None, retryLimit=None):
+ """
+ Runs a command using adb
+ If timeout is specified, the process is killed after <timeout> seconds.
+
+ returns: instance of ProcessHandler
+ """
+ retryLimit = retryLimit or self.retryLimit
+ finalArgs = [self._adbPath]
+ if self._serverHost is not None:
+ finalArgs.extend(['-H', self._serverHost])
+ if self._serverPort is not None:
+ finalArgs.extend(['-P', str(self._serverPort)])
+ if self._deviceSerial:
+ finalArgs.extend(['-s', self._deviceSerial])
+ finalArgs.extend(args)
+ self._logger.debug("_runCmd - command: %s" % ' '.join(finalArgs))
+ if not timeout:
+ timeout = self.default_timeout
+
+ def _timeout():
+ self._logger.error("Timeout exceeded for _runCmd call '%s'" % ' '.join(finalArgs))
+
+ retries = 0
+ while retries < retryLimit:
+ proc = ProcessHandler(finalArgs, storeOutput=True,
+ processOutputLine=self._log, onTimeout=_timeout)
+ proc.run(timeout=timeout)
+ proc.returncode = proc.wait()
+ if proc.returncode is None:
+ proc.kill()
+ retries += 1
+ else:
+ return proc
+
+ # timeout is specified in seconds, and if no timeout is given,
+ # we will run until we hit the default_timeout specified in the __init__
+ def _checkCmd(self, args, timeout=None, retryLimit=None):
+ """
+ Runs a command using adb and waits for the command to finish.
+ If timeout is specified, the process is killed after <timeout> seconds.
+
+ returns: returncode from process
+ """
+ retryLimit = retryLimit or self.retryLimit
+ finalArgs = [self._adbPath]
+ if self._serverHost is not None:
+ finalArgs.extend(['-H', self._serverHost])
+ if self._serverPort is not None:
+ finalArgs.extend(['-P', str(self._serverPort)])
+ if self._deviceSerial:
+ finalArgs.extend(['-s', self._deviceSerial])
+ finalArgs.extend(args)
+ self._logger.debug("_checkCmd - command: %s" % ' '.join(finalArgs))
+ if not timeout:
+ # We are asserting that all commands will complete in this
+ # time unless otherwise specified
+ timeout = self.default_timeout
+
+ def _timeout():
+ self._logger.error("Timeout exceeded for _checkCmd call '%s'" % ' '.join(finalArgs))
+
+ timeout = int(timeout)
+ retries = 0
+ while retries < retryLimit:
+ proc = ProcessHandler(finalArgs, processOutputLine=self._log, onTimeout=_timeout)
+ proc.run(timeout=timeout)
+ ret_code = proc.wait()
+ if ret_code is None:
+ proc.kill()
+ retries += 1
+ else:
+ return ret_code
+
+ raise DMError("Timeout exceeded for _checkCmd call after %d retries." % retries)
+
+ def chmodDir(self, remoteDir, mask="777"):
+ if (self.dirExists(remoteDir)):
+ if '/sdcard' in remoteDir:
+ self._logger.debug("chmod %s -- skipped (/sdcard)" % remoteDir)
+ else:
+ files = self.listFiles(remoteDir.strip())
+ for f in files:
+ remoteEntry = remoteDir.strip() + "/" + f.strip()
+ if (self.dirExists(remoteEntry)):
+ self.chmodDir(remoteEntry)
+ else:
+ self._checkCmd(["shell", "chmod", mask, remoteEntry],
+ timeout=self.short_timeout)
+ self._logger.info("chmod %s" % remoteEntry)
+ self._checkCmd(["shell", "chmod", mask, remoteDir], timeout=self.short_timeout)
+ self._logger.debug("chmod %s" % remoteDir)
+ else:
+ self._checkCmd(["shell", "chmod", mask, remoteDir.strip()], timeout=self.short_timeout)
+ self._logger.debug("chmod %s" % remoteDir.strip())
+
+ def _verifyADB(self):
+ """
+ Check to see if adb itself can be executed.
+ """
+ if self._adbPath != 'adb':
+ if not os.access(self._adbPath, os.X_OK):
+ raise DMError("invalid adb path, or adb not executable: %s" % self._adbPath)
+
+ try:
+ re_version = re.compile(r'Android Debug Bridge version (.*)')
+ proc = self._runCmd(["version"], timeout=self.short_timeout)
+ self._adb_version = re_version.match(proc.output[0]).group(1)
+ self._logger.info("Detected adb %s" % self._adb_version)
+ except os.error as err:
+ raise DMError(
+ "unable to execute ADB (%s): ensure Android SDK is installed "
+ "and adb is in your $PATH" % err)
+
+ def _verifyDevice(self):
+ # If there is a device serial number, see if adb is connected to it
+ if self._deviceSerial:
+ deviceStatus = None
+ for line in self._runCmd(["devices"]).output:
+ m = re.match('(.+)?\s+(.+)$', line)
+ if m:
+ if self._deviceSerial == m.group(1):
+ deviceStatus = m.group(2)
+ if deviceStatus is None:
+ raise DMError("device not found: %s" % self._deviceSerial)
+ elif deviceStatus != "device":
+ raise DMError("bad status for device %s: %s" % (self._deviceSerial, deviceStatus))
+
+ # Check to see if we can connect to device and run a simple command
+ if not self._checkCmd(["shell", "echo"], timeout=self.short_timeout) == 0:
+ raise DMError("unable to connect to device")
+
+ def _checkForRoot(self):
+ self._haveRootShell = False
+ self._haveSu = False
+ # If requested to attempt to run adbd as root, do so before
+ # checking whether adbs is running as root.
+ if self._runAdbAsRoot:
+ self._adb_root()
+
+ # Check whether we _are_ root by default (some development boards work
+ # this way, this is also the result of some relatively rare rooting
+ # techniques)
+ proc = self._runCmd(["shell", "id"], timeout=self.short_timeout)
+ if proc.output and 'uid=0(root)' in proc.output[0]:
+ self._haveRootShell = True
+ # if this returns true, we don't care about su
+ return
+
+ # if root shell is not available, check if 'su' can be used to gain
+ # root
+ def su_id(su_modifier, timeout):
+ proc = self._runCmd(["shell", "su", su_modifier, "id"],
+ timeout=timeout)
+
+ # wait for response for maximum of 15 seconds, in case su
+ # prompts for a password or triggers the Android SuperUser
+ # prompt
+ start_time = time.time()
+ retcode = None
+ while (time.time() - start_time) <= 15 and retcode is None:
+ retcode = proc.poll()
+ if retcode is None: # still not terminated, kill
+ proc.kill()
+
+ if proc.output and 'uid=0(root)' in proc.output[0]:
+ return True
+ return False
+
+ if su_id('0', self.short_timeout):
+ self._haveSu = True
+ self._suModifier = '0'
+ elif su_id('-c', self.short_timeout):
+ self._haveSu = True
+ self._suModifier = '-c'
+
+ def _isUnzipAvailable(self):
+ data = self._runCmd(["shell", "unzip"]).output
+ for line in data:
+ if (re.search('Usage', line)):
+ return True
+ return False
+
+ def _isLocalZipAvailable(self):
+ def _noOutput(line):
+ # suppress output from zip ProcessHandler
+ pass
+ try:
+ proc = ProcessHandler(["zip", "-?"], storeOutput=False, processOutputLine=_noOutput)
+ proc.run()
+ proc.wait()
+ except:
+ return False
+ return True
+
+ def _verifyZip(self):
+ # If "zip" can be run locally, and "unzip" can be run remotely, then pushDir
+ # can use these to push just one file per directory -- a significant
+ # optimization for large directories.
+ self._useZip = False
+ if (self._isUnzipAvailable() and self._isLocalZipAvailable()):
+ self._logger.info("will use zip to push directories")
+ self._useZip = True
+ else:
+ raise DMError("zip not available")
+
+ def _adb_root(self):
+ """ Some devices require us to reboot adbd as root.
+ This function takes care of it.
+ """
+ if self.processInfo("adbd")[2] != "root":
+ self._checkCmd(["root"])
+ self._checkCmd(["wait-for-device"])
+ if self.processInfo("adbd")[2] != "root":
+ raise DMError("We tried rebooting adbd as root, however, it failed.")
+
+ def _detectLsModifier(self):
+ if self._lsModifier is None:
+ # Check if busybox -1A is required in order to get one
+ # file per line.
+ output = self._runCmd(["shell", "ls", "-1A", "/"],
+ timeout=self.short_timeout).output
+ output = ' '.join(output)
+ if 'error: device not found' in output:
+ raise DMError(output)
+ if "Unknown option '-1'. Aborting." in output:
+ self._lsModifier = "-a"
+ elif "No such file or directory" in output:
+ self._lsModifier = "-a"
+ else:
+ self._lsModifier = "-1A"