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 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 inconsistent with historical behavior. 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 getCurrentTime(self): """ Returns device time in milliseconds since the epoch """ timestr = self._runCmd(["shell", "date", "+%s"]).stdout.read().strip() if (not timestr or not timestr.isdigit()): raise DMError("Unable to get current time using date (got: '%s')" % timestr) return str(int(timestr)*1000)
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 verifyRoot(self): # a test to see if we have root privs files = self.listFiles("/data/data") if (len(files) == 0): print "NOT running as root" raise DMError("not running as root") self.haveRoot = True
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 shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False): cmdline = self._escapedCommandLine(cmd) if env: cmdline = '%s %s' % (self._formatEnvString(env), cmdline) # execcwd/execcwdsu currently unsupported in Negatus; see bug 824127. if cwd and self.agentProductName == 'SUTAgentNegatus': raise DMError("Negatus does not support execcwd/execcwdsu") haveExecSu = (self.agentProductName == 'SUTAgentNegatus' or StrictVersion(self.agentVersion) >= StrictVersion('1.13')) # Depending on agent version we send one of the following commands here: # * exec (run as normal user) # * execsu (run as privileged user) # * execcwd (run as normal user from specified directory) # * execcwdsu (run as privileged user from specified directory) cmd = "exec" if cwd: cmd += "cwd" if root and haveExecSu: cmd += "su" if cwd: self._sendCmds([{ 'cmd': '%s %s %s' % (cmd, cwd, cmdline) }], outputfile, timeout) else: if (not root) or haveExecSu: self._sendCmds([{ 'cmd': '%s %s' % (cmd, cmdline) }], outputfile, timeout) else: # need to manually inject su -c for backwards compatibility (this may # not work on ICS or above!!) # (FIXME: this backwards compatibility code is really ugly and should # be deprecated at some point in the future) self._sendCmds([ { 'cmd': '%s su -c "%s"' % (cmd, cmdline) }], outputfile, timeout) # dig through the output to get the return code lastline = _pop_last_line(outputfile) if lastline: m = re.search('return code \[([0-9]+)\]', lastline) if m: return int(m.group(1)) # woops, we couldn't find an end of line/return value raise DMError("Automation Error: Error finding end of line/return value when running '%s'" % cmdline)
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 _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 == 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 shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False): # FIXME: this function buffers all output of the command into memory, # always. :( # 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: cmdline = "su -c \"%s\"" % 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.deviceSerial: args.extend(['-s', self.deviceSerial]) args.extend(["shell", cmdline]) proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if timeout: timeout = int(timeout) start_time = time.time() ret_code = proc.poll() while ((time.time() - start_time) <= timeout) and ret_code == None: time.sleep(1) ret_code = proc.poll() if ret_code == None: proc.kill() raise DMError("Timeout exceeded for shell call") (stdout, stderr) = proc.communicate() outputfile.write(stdout.rstrip('\n')) lastline = _pop_last_line(outputfile) if lastline: m = re.search('([0-9]+)', lastline) if m: return_code = m.group(1) outputfile.seek(-2, 2) outputfile.truncate() # truncate off the return code return int(return_code) return None
def getFile(self, remoteFile, localFile): data = self.pullFile(remoteFile) fhandle = open(localFile, 'wb') fhandle.write(data) fhandle.close() if not self.validateFile(remoteFile, localFile): raise DMError("Automation Error: Failed to validate file when downloading %s" % remoteFile)
def installApp(self, appBundlePath, destPath=None): cmd = 'inst ' + appBundlePath if destPath: cmd += ' ' + destPath data = self._runCmds([{ 'cmd': cmd }]) if 'installation complete [0]' not in data: raise DMError("Remove Device Error: Error installing app. Error message: %s" % data)
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 sendCMD(self, cmdline, newline=True, ignoreAgentErrors=True): done = False while (not done): retVal = self._doCMD(cmdline, newline) if (retVal is None): self.retries += 1 else: self.retries = 0 if ignoreAgentErrors == False: if (self.agentErrorRE.match(retVal)): raise DMError("error on the agent executing '%s'" % cmdline) return retVal if (self.retries >= self.retrylimit): done = True raise DMError("unable to connect to %s after %s attempts" % (self.host, self.retrylimit))
def dirExists(self, remotePath): """ Return True if remotePath is an existing directory on the device. """ ret = self._runCmds([{'cmd': 'isdir ' + remotePath}]).strip() if not ret: raise DMError( 'Automation Error: DeviceManager isdir returned null') return ret == '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 pushFile(self, localname, destname, retryLimit = None): retryLimit = retryLimit or self.retryLimit self.mkDirs(destname) try: filesize = os.path.getsize(localname) with open(localname, 'rb') as f: remoteHash = self._runCmds([{ 'cmd': 'push ' + destname + ' ' + str(filesize), 'data': f.read() }], retryLimit=retryLimit).strip() except OSError: raise DMError("DeviceManager: Error reading file to push") self._logger.debug("push returned: %s" % remoteHash) localHash = self._getLocalHash(localname) if localHash != remoteHash: raise DMError("Automation Error: Push File failed to Validate! (localhash: %s, " "remotehash: %s)" % (localHash, remoteHash))
def installApp(self, appBundlePath, destPath=None): cmd = 'inst ' + appBundlePath if destPath: cmd += ' ' + destPath data = self._runCmds([{ 'cmd': cmd }]) f = re.compile('Failure') for line in data.split(): if (f.match(line)): raise DMError("Remove Device Error: Error installing app. Error message: %s" % data)
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) tempParent = tempfile.mkdtemp() remoteName = os.path.basename(remoteDir) newLocal = os.path.join(tempParent, remoteName) dir_util.copy_tree(localDir, newLocal) # 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 self._adb_version >= '1.0.36': remoteDir = '/'.join(remoteDir.rstrip('/').split('/')[:-1]) try: if self._checkCmd(["push", newLocal, remoteDir], retryLimit=retryLimit, timeout=timeout): raise DMError("failed to push %s (copy of %s) to %s" % (newLocal, localDir, remoteDir)) except: raise finally: mozfile.remove(tempParent)
def uninstallApp(self, appName, installPath=None): cmd = 'uninstall ' + appName if installPath: cmd += ' ' + installPath data = self._runCmds([{'cmd': cmd}]) status = data.split('\n')[0].strip() self._logger.debug("uninstallApp: '%s'" % status) if status == 'Success': return raise DMError("Remote Device Error: uninstall failed for %s" % appName)
def uninstallApp(self, appName, installPath=None): """ Uninstalls the named application from device and DOES NOT cause a reboot appName - the name of the application (e.g org.mozilla.fennec) installPath - the path to where the application was installed (optional) """ data = self._runCmd(["uninstall", appName]).stdout.read().strip() status = data.split('\n')[0].strip() if status != 'Success': raise DMError("uninstall failed for %s. Got: %s" % (appName, status))
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 verifyRoot(self): # a test to see if we have root privs p = self.runCmd(["shell", "id"]) response = p.stdout.readline() response = response.rstrip() response = response.split(' ') if (response[0].find('uid=0') < 0 or response[1].find('gid=0') < 0): print "NOT running as root ", response[0].find('uid=0') raise DMError("not running as root") self.haveRoot = True
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) if self._useRunAs: remoteTmpFile = self.getTempDir() + "/" + os.path.basename(localname) self._checkCmd(["push", os.path.realpath(localname), remoteTmpFile], retryLimit=retryLimit) self.shellCheckOutput(["dd", "if=" + remoteTmpFile, "of=" + destname]) self.shellCheckOutput(["rm", remoteTmpFile]) else: self._checkCmd(["push", os.path.realpath(localname), destname], retryLimit=retryLimit)
def killProcess(self, appname, sig=None): procs = self.getProcessList() for (pid, name, user) in procs: if name == appname: args = ["shell", "kill"] if sig: args.append("-%d" % sig) args.append(str(pid)) p = self._runCmd(args) if p.returncode != 0: raise DMError("Error killing process " "'%s': %s" % (appname, p.output))
def getAppRoot(self, packageName): devroot = self.getDeviceRoot() if (devroot == None): return None if (packageName and self.dirExists('/data/data/' + packageName)): self._packageName = packageName return '/data/data/' + packageName elif (self._packageName and self.dirExists('/data/data/' + self._packageName)): return '/data/data/' + self._packageName # Failure (either not installed or not a recognized platform) raise DMError("Failed to get application root for: %s" % packageName)
def killProcess(self, appname, forceKill=False): procs = self.getProcessList() for (pid, name, user) in procs: if name == appname: args = ["shell", "kill"] if forceKill: args.append("-9") args.append(str(pid)) p = self._runCmdAs(args) p.communicate() if p.returncode != 0: raise DMError("Error killing process " "'%s': %s" % (appname, p.stdout.read()))
def _verifyDevice(self): # If there is a device serial number, see if adb is connected to it if self._deviceSerial: deviceStatus = None proc = subprocess.Popen([self._adbPath, "devices"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) for line in proc.stdout: m = re.match('(.+)?\s+(.+)$', line) if m: if self._deviceSerial == m.group(1): deviceStatus = m.group(2) if deviceStatus == 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 try: self._checkCmd(["shell", "echo"]) except subprocess.CalledProcessError: raise DMError("unable to connect to device: is it plugged in?")
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 fireProcess(self, appname, failIfRunning=False, maxWaitTime=30): """ Starts a process returns: pid DEPRECATED: Use shell() or launchApplication() for new code """ if not appname: raise DMError( "Automation Error: fireProcess called with no command to run") self._logger.info("FIRE PROC: '%s'" % appname) if (self.processExist(appname) != None): self._logger.warning("process %s appears to be running already\n" % appname) if (failIfRunning): raise DMError("Automation Error: Process is already running") self._runCmds([{'cmd': 'exec ' + appname}]) # The 'exec' command may wait for the process to start and end, so checking # for the process here may result in process = None. # The normal case is to launch the process and return right away # There is one case with robotium (am instrument) where exec returns at the end pid = None waited = 0 while pid is None and waited < maxWaitTime: pid = self.processExist(appname) if pid: break time.sleep(1) waited += 1 self._logger.debug("got pid: %s for process: %s" % (pid, appname)) return pid