| # 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 datetime |
| import logging |
| import moznetwork |
| import select |
| import socket |
| import time |
| import os |
| import re |
| import posixpath |
| import subprocess |
| import StringIO |
| from devicemanager import DeviceManager, DMError, _pop_last_line |
| import errno |
| from distutils.version import StrictVersion |
| |
| class DeviceManagerSUT(DeviceManager): |
| """ |
| Implementation of DeviceManager interface that speaks to a device over |
| TCP/IP using the "system under test" protocol. A software agent such as |
| Negatus (http://github.com/mozilla/Negatus) or the Mozilla Android SUTAgent |
| app must be present and listening for connections for this to work. |
| """ |
| |
| _base_prompt = '$>' |
| _base_prompt_re = '\$\>' |
| _prompt_sep = '\x00' |
| _prompt_regex = '.*(' + _base_prompt_re + _prompt_sep + ')' |
| _agentErrorRE = re.compile('^##AGENT-WARNING##\ ?(.*)') |
| |
| reboot_timeout = 600 |
| reboot_settling_time = 60 |
| |
| def __init__(self, host, port=20701, retryLimit=5, deviceRoot=None, |
| logLevel=logging.ERROR, **kwargs): |
| DeviceManager.__init__(self, logLevel=logLevel, |
| deviceRoot=deviceRoot) |
| self.host = host |
| self.port = port |
| self.retryLimit = retryLimit |
| self._sock = None |
| self._everConnected = False |
| |
| # Get version |
| verstring = self._runCmds([{ 'cmd': 'ver' }]) |
| ver_re = re.match('(\S+) Version (\S+)', verstring) |
| self.agentProductName = ver_re.group(1) |
| self.agentVersion = ver_re.group(2) |
| |
| def _cmdNeedsResponse(self, cmd): |
| """ Not all commands need a response from the agent: |
| * rebt obviously doesn't get a response |
| * uninstall performs a reboot to ensure starting in a clean state and |
| so also doesn't look for a response |
| """ |
| noResponseCmds = [re.compile('^rebt'), |
| re.compile('^uninst .*$'), |
| re.compile('^pull .*$')] |
| |
| for c in noResponseCmds: |
| if (c.match(cmd)): |
| return False |
| |
| # If the command is not in our list, then it gets a response |
| return True |
| |
| def _stripPrompt(self, data): |
| """ |
| take a data blob and strip instances of the prompt '$>\x00' |
| """ |
| promptre = re.compile(self._prompt_regex + '.*') |
| retVal = [] |
| lines = data.split('\n') |
| for line in lines: |
| foundPrompt = False |
| try: |
| while (promptre.match(line)): |
| foundPrompt = True |
| pieces = line.split(self._prompt_sep) |
| index = pieces.index('$>') |
| pieces.pop(index) |
| line = self._prompt_sep.join(pieces) |
| except(ValueError): |
| pass |
| |
| # we don't want to append lines that are blank after stripping the |
| # prompt (those are basically "prompts") |
| if not foundPrompt or line: |
| retVal.append(line) |
| |
| return '\n'.join(retVal) |
| |
| def _shouldCmdCloseSocket(self, cmd): |
| """ |
| Some commands need to close the socket after they are sent: |
| * rebt |
| * uninst |
| * quit |
| """ |
| socketClosingCmds = [re.compile('^quit.*'), |
| re.compile('^rebt.*'), |
| re.compile('^uninst .*$')] |
| |
| for c in socketClosingCmds: |
| if (c.match(cmd)): |
| return True |
| return False |
| |
| def _sendCmds(self, cmdlist, outputfile, timeout = None, retryLimit = None): |
| """ |
| Wrapper for _doCmds that loops up to retryLimit iterations |
| """ |
| # this allows us to move the retry logic outside of the _doCmds() to make it |
| # easier for debugging in the future. |
| # note that since cmdlist is a list of commands, they will all be retried if |
| # one fails. this is necessary in particular for pushFile(), where we don't want |
| # to accidentally send extra data if a failure occurs during data transmission. |
| |
| retryLimit = retryLimit or self.retryLimit |
| retries = 0 |
| while retries < retryLimit: |
| try: |
| self._doCmds(cmdlist, outputfile, timeout) |
| return |
| except DMError, err: |
| # re-raise error if it's fatal (i.e. the device got the command but |
| # couldn't execute it). retry otherwise |
| if err.fatal: |
| raise err |
| self._logger.debug(err) |
| retries += 1 |
| # if we lost the connection or failed to establish one, wait a bit |
| if retries < retryLimit and not self._sock: |
| sleep_time = 5 * retries |
| self._logger.info('Could not connect; sleeping for %d seconds.' % sleep_time) |
| time.sleep(sleep_time) |
| |
| raise DMError("Remote Device Error: unable to connect to %s after %s attempts" % (self.host, retryLimit)) |
| |
| def _runCmds(self, cmdlist, timeout = None, retryLimit = None): |
| """ |
| Similar to _sendCmds, but just returns any output as a string instead of |
| writing to a file |
| """ |
| retryLimit = retryLimit or self.retryLimit |
| outputfile = StringIO.StringIO() |
| self._sendCmds(cmdlist, outputfile, timeout, retryLimit=retryLimit) |
| outputfile.seek(0) |
| return outputfile.read() |
| |
| def _doCmds(self, cmdlist, outputfile, timeout): |
| promptre = re.compile(self._prompt_regex + '$') |
| shouldCloseSocket = False |
| |
| if not timeout: |
| # We are asserting that all commands will complete in this time unless otherwise specified |
| timeout = self.default_timeout |
| |
| if not self._sock: |
| try: |
| if self._everConnected: |
| self._logger.info("reconnecting socket") |
| self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| except socket.error, msg: |
| self._sock = None |
| raise DMError("Automation Error: unable to create socket: "+str(msg)) |
| |
| try: |
| self._sock.settimeout(float(timeout)) |
| self._sock.connect((self.host, int(self.port))) |
| self._everConnected = True |
| except socket.error, msg: |
| self._sock = None |
| raise DMError("Remote Device Error: Unable to connect socket: "+str(msg)) |
| |
| # consume prompt |
| try: |
| self._sock.recv(1024) |
| except socket.error, msg: |
| self._sock.close() |
| self._sock = None |
| raise DMError("Remote Device Error: Did not get prompt after connecting: " + str(msg), fatal=True) |
| |
| # future recv() timeouts are handled by select() calls |
| self._sock.settimeout(None) |
| |
| for cmd in cmdlist: |
| cmdline = '%s\r\n' % cmd['cmd'] |
| |
| try: |
| sent = self._sock.send(cmdline) |
| if sent != len(cmdline): |
| raise DMError("Remote Device Error: our cmd was %s bytes and we " |
| "only sent %s" % (len(cmdline), sent)) |
| if cmd.get('data'): |
| totalsent = 0 |
| while totalsent < len(cmd['data']): |
| sent = self._sock.send(cmd['data'][totalsent:]) |
| self._logger.debug("sent %s bytes of data payload" % sent) |
| if sent == 0: |
| raise DMError("Socket connection broken when sending data") |
| totalsent += sent |
| |
| self._logger.debug("sent cmd: %s" % cmd['cmd']) |
| except socket.error, msg: |
| self._sock.close() |
| self._sock = None |
| self._logger.error("Remote Device Error: Error sending data"\ |
| " to socket. cmd=%s; err=%s" % (cmd['cmd'], msg)) |
| return False |
| |
| # Check if the command should close the socket |
| shouldCloseSocket = self._shouldCmdCloseSocket(cmd['cmd']) |
| |
| # Handle responses from commands |
| if self._cmdNeedsResponse(cmd['cmd']): |
| foundPrompt = False |
| data = "" |
| timer = 0 |
| select_timeout = 1 |
| commandFailed = False |
| |
| while not foundPrompt: |
| socketClosed = False |
| errStr = '' |
| temp = '' |
| self._logger.debug("recv'ing...") |
| |
| # Get our response |
| try: |
| # Wait up to a second for socket to become ready for reading... |
| if select.select([self._sock], [], [], select_timeout)[0]: |
| temp = self._sock.recv(1024) |
| self._logger.debug(u"response: %s" % temp.decode('utf8', 'replace')) |
| timer = 0 |
| if not temp: |
| socketClosed = True |
| errStr = 'connection closed' |
| timer += select_timeout |
| if timer > timeout: |
| self._sock.close() |
| self._sock = None |
| raise DMError("Automation Error: Timeout in command %s" % cmd['cmd'], fatal=True) |
| except socket.error, err: |
| socketClosed = True |
| errStr = str(err) |
| # This error shows up with we have our tegra rebooted. |
| if err[0] == errno.ECONNRESET: |
| errStr += ' - possible reboot' |
| |
| if socketClosed: |
| self._sock.close() |
| self._sock = None |
| raise DMError("Automation Error: Error receiving data from socket. cmd=%s; err=%s" % (cmd, errStr)) |
| |
| data += temp |
| |
| # If something goes wrong in the agent it will send back a string that |
| # starts with '##AGENT-WARNING##' |
| if not commandFailed: |
| errorMatch = self._agentErrorRE.match(data) |
| if errorMatch: |
| # We still need to consume the prompt, so raise an error after |
| # draining the rest of the buffer. |
| commandFailed = True |
| |
| for line in data.splitlines(): |
| if promptre.match(line): |
| foundPrompt = True |
| data = self._stripPrompt(data) |
| break |
| |
| # periodically flush data to output file to make sure it doesn't get |
| # too big/unwieldly |
| if len(data) > 1024: |
| outputfile.write(data[0:1024]) |
| data = data[1024:] |
| |
| if commandFailed: |
| raise DMError("Automation Error: Error processing command '%s'; err='%s'" % |
| (cmd['cmd'], errorMatch.group(1)), fatal=True) |
| |
| # Write any remaining data to outputfile |
| outputfile.write(data) |
| |
| if shouldCloseSocket: |
| try: |
| self._sock.close() |
| self._sock = None |
| except: |
| self._sock = None |
| raise DMError("Automation Error: Error closing socket") |
| |
| def _setupDeviceRoot(self, deviceRoot): |
| if not deviceRoot: |
| deviceRoot = "%s/tests" % self._runCmds( |
| [{ 'cmd': 'testroot' }]).strip() |
| self.mkDir(deviceRoot) |
| |
| return deviceRoot |
| |
| 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 pushFile(self, localname, destname, retryLimit=None, createDir=True): |
| retryLimit = retryLimit or self.retryLimit |
| if createDir: |
| 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 mkDir(self, name): |
| if not self.dirExists(name): |
| self._runCmds([{ 'cmd': 'mkdr ' + name }]) |
| |
| def pushDir(self, localDir, remoteDir, retryLimit=None, timeout=None): |
| retryLimit = retryLimit or self.retryLimit |
| self._logger.info("pushing directory: %s to %s" % (localDir, remoteDir)) |
| |
| existentDirectories = [] |
| for root, dirs, files in os.walk(localDir, followlinks=True): |
| _, subpath = root.split(localDir) |
| subpath = subpath.lstrip('/') |
| remoteRoot = posixpath.join(remoteDir, subpath) |
| for f in files: |
| remoteName = posixpath.join(remoteRoot, f) |
| |
| if subpath == "": |
| remoteRoot = remoteDir |
| |
| parent = os.path.dirname(remoteName) |
| if parent not in existentDirectories: |
| self.mkDirs(remoteName) |
| existentDirectories.append(parent) |
| |
| self.pushFile(os.path.join(root, f), remoteName, retryLimit=retryLimit, createDir=False) |
| |
| def dirExists(self, remotePath): |
| ret = self._runCmds([{ 'cmd': 'isdir ' + remotePath }]).strip() |
| if not ret: |
| raise DMError('Automation Error: DeviceManager isdir returned null') |
| |
| return ret == 'TRUE' |
| |
| def fileExists(self, filepath): |
| # Because we always have / style paths we make this a lot easier with some |
| # assumptions |
| filepath = posixpath.normpath(filepath) |
| # / should always exist but we can use this to check for things like |
| # having access to the filesystem |
| if filepath == '/': |
| return self.dirExists(filepath) |
| (containingpath, filename) = posixpath.split(filepath) |
| return filename in self.listFiles(containingpath) |
| |
| def listFiles(self, rootdir): |
| rootdir = posixpath.normpath(rootdir) |
| if not self.dirExists(rootdir): |
| return [] |
| data = self._runCmds([{ 'cmd': 'cd ' + rootdir }, { 'cmd': 'ls' }]) |
| |
| files = filter(lambda x: x, data.splitlines()) |
| if len(files) == 1 and files[0] == '<empty>': |
| # special case on the agent: empty directories return just the |
| # string "<empty>" |
| return [] |
| return files |
| |
| def removeFile(self, filename): |
| self._logger.info("removing file: " + filename) |
| if self.fileExists(filename): |
| self._runCmds([{ 'cmd': 'rm ' + filename }]) |
| |
| def removeDir(self, remoteDir): |
| if self.dirExists(remoteDir): |
| self._runCmds([{ 'cmd': 'rmdr ' + remoteDir }]) |
| |
| def moveTree(self, source, destination): |
| self._runCmds([{ 'cmd': 'mv %s %s' % (source, destination) }]) |
| |
| def copyTree(self, source, destination): |
| self._runCmds([{ 'cmd': 'dd if=%s of=%s' % (source, destination) }]) |
| |
| def getProcessList(self): |
| data = self._runCmds([{ 'cmd': 'ps' }]) |
| |
| processTuples = [] |
| for line in data.splitlines(): |
| if line: |
| pidproc = line.strip().split() |
| try: |
| if (len(pidproc) == 2): |
| processTuples += [[pidproc[0], pidproc[1]]] |
| elif (len(pidproc) == 3): |
| # android returns <userID> <procID> <procName> |
| processTuples += [[int(pidproc[1]), pidproc[2], int(pidproc[0])]] |
| else: |
| # unexpected format |
| raise ValueError |
| except ValueError: |
| self._logger.error("Unable to parse process list (bug 805969)") |
| self._logger.error("Line: %s\nFull output of process list:\n%s" % (line, data)) |
| raise DMError("Invalid process line: %s" % line) |
| |
| return processTuples |
| |
| 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 |
| |
| def launchProcess(self, cmd, outputFile = "process.txt", cwd = '', env = '', failIfRunning=False): |
| """ |
| Launches a process, redirecting output to standard out |
| |
| Returns output filename |
| |
| 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 not cmd: |
| self._logger.warning("launchProcess called without command to run") |
| return None |
| |
| if cmd[0] == 'am' and hasattr(self, '_getExtraAmStartArgs'): |
| cmd = cmd[:2] + self._getExtraAmStartArgs() + cmd[2:] |
| |
| cmdline = subprocess.list2cmdline(cmd) |
| if outputFile == "process.txt" or outputFile is None: |
| outputFile += "%s/process.txt" % self.deviceRoot |
| cmdline += " > " + outputFile |
| |
| # Prepend our env to the command |
| cmdline = '%s %s' % (self._formatEnvString(env), cmdline) |
| |
| # fireProcess may trigger an exception, but we won't handle it |
| if cmd[0] == "am": |
| # Robocop tests spawn "am instrument". sutAgent's exec ensures that |
| # am has started before returning, so there is no point in having |
| # fireProcess wait for it to start. Also, since "am" does not show |
| # up in the process list while the test is running, waiting for it |
| # in fireProcess is difficult. |
| self.fireProcess(cmdline, failIfRunning, 0) |
| else: |
| self.fireProcess(cmdline, failIfRunning) |
| return outputFile |
| |
| def killProcess(self, appname, sig=None): |
| if sig: |
| pid = self.processExist(appname) |
| if pid and pid > 0: |
| try: |
| self.shellCheckOutput(['kill', '-%d' % sig, str(pid)], |
| root=True) |
| except DMError, err: |
| self._logger.warning("unable to kill -%d %s (pid %s)" % |
| (sig, appname, str(pid))) |
| self._logger.debug(err) |
| raise err |
| else: |
| self._logger.warning("unable to kill -%d %s -- not running?" % |
| (sig, appname)) |
| else: |
| retries = 0 |
| while retries < self.retryLimit: |
| try: |
| if self.processExist(appname): |
| self._runCmds([{ 'cmd': 'kill ' + appname }]) |
| return |
| except DMError, err: |
| retries += 1 |
| self._logger.warning("try %d of %d failed to kill %s" % |
| (retries, self.retryLimit, appname)) |
| self._logger.debug(err) |
| if retries >= self.retryLimit: |
| raise err |
| |
| def getTempDir(self): |
| return self._runCmds([{ 'cmd': 'tmpd' }]).strip() |
| |
| def pullFile(self, remoteFile, offset=None, length=None): |
| # The "pull" command is different from other commands in that DeviceManager |
| # has to read a certain number of bytes instead of just reading to the |
| # next prompt. This is more robust than the "cat" command, which will be |
| # confused if the prompt string exists within the file being catted. |
| # However it means we can't use the response-handling logic in sendCMD(). |
| |
| def err(error_msg): |
| err_str = 'DeviceManager: pull unsuccessful: %s' % error_msg |
| self._logger.error(err_str) |
| self._sock = None |
| raise DMError(err_str) |
| |
| # FIXME: We could possibly move these socket-reading functions up to |
| # the class level if we wanted to refactor sendCMD(). For now they are |
| # only used to pull files. |
| |
| def uread(to_recv, error_msg): |
| """ unbuffered read """ |
| try: |
| data = "" |
| if select.select([self._sock], [], [], self.default_timeout)[0]: |
| data = self._sock.recv(to_recv) |
| if not data: |
| # timed out waiting for response or error response |
| err(error_msg) |
| |
| return data |
| except: |
| err(error_msg) |
| |
| def read_until_char(c, buf, error_msg): |
| """ read until 'c' is found; buffer rest """ |
| while not c in buf: |
| data = uread(1024, error_msg) |
| buf += data |
| return buf.partition(c) |
| |
| def read_exact(total_to_recv, buf, error_msg): |
| """ read exact number of 'total_to_recv' bytes """ |
| while len(buf) < total_to_recv: |
| to_recv = min(total_to_recv - len(buf), 1024) |
| data = uread(to_recv, error_msg) |
| buf += data |
| return buf |
| |
| prompt = self._base_prompt + self._prompt_sep |
| buf = '' |
| |
| # expected return value: |
| # <filename>,<filesize>\n<filedata> |
| # or, if error, |
| # <filename>,-1\n<error message> |
| |
| # just send the command first, we read the response inline below |
| if offset is not None and length is not None: |
| cmd = 'pull %s %d %d' % (remoteFile, offset, length) |
| elif offset is not None: |
| cmd = 'pull %s %d' % (remoteFile, offset) |
| else: |
| cmd = 'pull %s' % remoteFile |
| |
| self._runCmds([{ 'cmd': cmd }]) |
| |
| # read metadata; buffer the rest |
| metadata, sep, buf = read_until_char('\n', buf, 'could not find metadata') |
| if not metadata: |
| return None |
| self._logger.debug('metadata: %s' % metadata) |
| |
| filename, sep, filesizestr = metadata.partition(',') |
| if sep == '': |
| err('could not find file size in returned metadata') |
| try: |
| filesize = int(filesizestr) |
| except ValueError: |
| err('invalid file size in returned metadata') |
| |
| if filesize == -1: |
| # read error message |
| error_str, sep, buf = read_until_char('\n', buf, 'could not find error message') |
| if not error_str: |
| err("blank error message") |
| # prompt should follow |
| read_exact(len(prompt), buf, 'could not find prompt') |
| # failures are expected, so don't use "Remote Device Error" or we'll RETRY |
| raise DMError("DeviceManager: pulling file '%s' unsuccessful: %s" % (remoteFile, error_str)) |
| |
| # read file data |
| total_to_recv = filesize + len(prompt) |
| buf = read_exact(total_to_recv, buf, 'could not get all file data') |
| if buf[-len(prompt):] != prompt: |
| err('no prompt found after file data--DeviceManager may be out of sync with agent') |
| return buf |
| return buf[:-len(prompt)] |
| |
| 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 getDirectory(self, remoteDir, localDir, checkDir=True): |
| self._logger.info("getting files in '%s'" % remoteDir) |
| if checkDir and not self.dirExists(remoteDir): |
| raise DMError("Automation Error: Error getting directory: %s not a directory" % |
| remoteDir) |
| |
| filelist = self.listFiles(remoteDir) |
| self._logger.debug(filelist) |
| if not os.path.exists(localDir): |
| os.makedirs(localDir) |
| |
| for f in filelist: |
| if f == '.' or f == '..': |
| continue |
| remotePath = remoteDir + '/' + f |
| localPath = os.path.join(localDir, f) |
| if self.dirExists(remotePath): |
| self.getDirectory(remotePath, localPath, False) |
| else: |
| self.getFile(remotePath, localPath) |
| |
| def validateFile(self, remoteFile, localFile): |
| remoteHash = self._getRemoteHash(remoteFile) |
| localHash = self._getLocalHash(localFile) |
| |
| if (remoteHash == None): |
| return False |
| |
| if (remoteHash == localHash): |
| return True |
| |
| return False |
| |
| def _getRemoteHash(self, filename): |
| data = self._runCmds([{ 'cmd': 'hash ' + filename }]).strip() |
| self._logger.debug("remote hash returned: '%s'" % data) |
| return data |
| |
| def unpackFile(self, filePath, destDir=None): |
| """ |
| Unzips a bundle to a location on the device |
| |
| If destDir is not specified, the bundle is extracted in the same directory |
| """ |
| # if no destDir is passed in just set it to filePath's folder |
| if not destDir: |
| destDir = posixpath.dirname(filePath) |
| |
| if destDir[-1] != '/': |
| destDir += '/' |
| |
| self._runCmds([{ 'cmd': 'unzp %s %s' % (filePath, destDir)}]) |
| |
| def _getRebootServerSocket(self, ipAddr): |
| serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| serverSocket.settimeout(60.0) |
| serverSocket.bind((ipAddr, 0)) |
| serverSocket.listen(1) |
| self._logger.debug('Created reboot callback server at %s:%d' % |
| serverSocket.getsockname()) |
| return serverSocket |
| |
| def _waitForRebootPing(self, serverSocket): |
| conn = None |
| data = None |
| startTime = datetime.datetime.now() |
| waitTime = datetime.timedelta(seconds=self.reboot_timeout) |
| while not data and datetime.datetime.now() - startTime < waitTime: |
| self._logger.info("Waiting for reboot callback ping from device...") |
| try: |
| if not conn: |
| conn, _ = serverSocket.accept() |
| # Receiving any data is good enough. |
| data = conn.recv(1024) |
| if data: |
| self._logger.info("Received reboot callback ping from device!") |
| conn.sendall('OK') |
| conn.close() |
| except socket.timeout: |
| pass |
| except socket.error, e: |
| if e.errno != errno.EAGAIN and e.errno != errno.EWOULDBLOCK: |
| raise |
| |
| if not data: |
| raise DMError('Timed out waiting for reboot callback.') |
| |
| self._logger.info("Sleeping for %s seconds to wait for device " |
| "to 'settle'" % self.reboot_settling_time) |
| time.sleep(self.reboot_settling_time) |
| |
| |
| def reboot(self, ipAddr=None, port=30000, wait=False): |
| # port ^^^ is here for backwards compatibility only, we now |
| # determine a port automatically and safely |
| wait = (wait or ipAddr) |
| |
| cmd = 'rebt' |
| |
| self._logger.info("Rebooting device") |
| |
| # if we're waiting, create a listening server and pass information on |
| # it to the device before rebooting (we do this instead of just polling |
| # to make sure the device actually rebooted -- yes, there are probably |
| # simpler ways of doing this like polling uptime, but this is what we're |
| # doing for now) |
| if wait: |
| if not ipAddr: |
| ipAddr = moznetwork.get_ip() |
| serverSocket = self._getRebootServerSocket(ipAddr) |
| # The update.info command tells the SUTAgent to send a TCP message |
| # after restarting. |
| destname = '/data/data/com.mozilla.SUTAgentAndroid/files/update.info' |
| data = "%s,%s\rrebooting\r" % serverSocket.getsockname() |
| self._runCmds([{'cmd': 'push %s %s' % (destname, len(data)), |
| 'data': data}]) |
| cmd += " %s %s" % serverSocket.getsockname() |
| |
| # actually reboot device |
| self._runCmds([{'cmd': cmd}]) |
| # if we're waiting, wait for a callback ping from the agent before |
| # continuing (and throw an exception if we don't receive said ping) |
| if wait: |
| self._waitForRebootPing(serverSocket) |
| |
| def getInfo(self, directive=None): |
| data = None |
| result = {} |
| collapseSpaces = re.compile(' +') |
| |
| directives = ['os','id','uptime','uptimemillis','systime','screen', |
| 'rotation','memory','process','disk','power','sutuserinfo', |
| 'temperature'] |
| if (directive in directives): |
| directives = [directive] |
| |
| for d in directives: |
| data = self._runCmds([{ 'cmd': 'info ' + d }]) |
| |
| data = collapseSpaces.sub(' ', data) |
| result[d] = data.split('\n') |
| |
| # Get rid of any 0 length members of the arrays |
| for k, v in result.iteritems(): |
| result[k] = filter(lambda x: x != '', result[k]) |
| |
| # Format the process output |
| if 'process' in result: |
| proclist = [] |
| for l in result['process']: |
| if l: |
| proclist.append(l.split('\t')) |
| result['process'] = proclist |
| |
| self._logger.debug("results: %s" % result) |
| return result |
| |
| 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 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 uninstallAppAndReboot(self, appName, installPath=None): |
| cmd = 'uninst ' + appName |
| if installPath: |
| cmd += ' ' + installPath |
| data = self._runCmds([{ 'cmd': cmd }]) |
| |
| self._logger.debug("uninstallAppAndReboot: %s" % data) |
| return |
| |
| def updateApp(self, appBundlePath, processName=None, destPath=None, |
| ipAddr=None, port=30000, wait=False): |
| # port ^^^ is here for backwards compatibility only, we now |
| # determine a port automatically and safely |
| wait = (wait or ipAddr) |
| |
| cmd = 'updt ' |
| if processName is None: |
| # Then we pass '' for processName |
| cmd += "'' " + appBundlePath |
| else: |
| cmd += processName + ' ' + appBundlePath |
| |
| if destPath: |
| cmd += " " + destPath |
| |
| if wait: |
| if not ipAddr: |
| ipAddr = moznetwork.get_ip() |
| serverSocket = self._getRebootServerSocket(ipAddr) |
| cmd += " %s %s" % serverSocket.getsockname() |
| |
| self._logger.debug("updateApp using command: " % cmd) |
| |
| self._runCmds([{'cmd': cmd}]) |
| |
| if wait: |
| self._waitForRebootPing(serverSocket) |
| |
| def getCurrentTime(self): |
| return int(self._runCmds([{ 'cmd': 'clok' }]).strip()) |
| |
| def _formatEnvString(self, env): |
| """ |
| Returns a properly formatted env string for the agent. |
| |
| Input - env, which is either None, '', or a dict |
| Output - a quoted string of the form: '"envvar1=val1,envvar2=val2..."' |
| If env is None or '' return '' (empty quoted string) |
| """ |
| if (env == None or env == ''): |
| return '' |
| |
| retVal = '"%s"' % ','.join(map(lambda x: '%s=%s' % (x[0], x[1]), env.iteritems())) |
| if (retVal == '""'): |
| return '' |
| |
| return retVal |
| |
| def adjustResolution(self, width=1680, height=1050, type='hdmi'): |
| """ |
| Adjust the screen resolution on the device, REBOOT REQUIRED |
| |
| NOTE: this only works on a tegra ATM |
| |
| supported resolutions: 640x480, 800x600, 1024x768, 1152x864, 1200x1024, 1440x900, 1680x1050, 1920x1080 |
| """ |
| if self.getInfo('os')['os'][0].split()[0] != 'harmony-eng': |
| self._logger.warning("unable to adjust screen resolution on non Tegra device") |
| return False |
| |
| results = self.getInfo('screen') |
| parts = results['screen'][0].split(':') |
| self._logger.debug("we have a current resolution of %s, %s" % (parts[1].split()[0], parts[2].split()[0])) |
| |
| #verify screen type is valid, and set it to the proper value (https://bugzilla.mozilla.org/show_bug.cgi?id=632895#c4) |
| screentype = -1 |
| if (type == 'hdmi'): |
| screentype = 5 |
| elif (type == 'vga' or type == 'crt'): |
| screentype = 3 |
| else: |
| return False |
| |
| #verify we have numbers |
| if not (isinstance(width, int) and isinstance(height, int)): |
| return False |
| |
| if (width < 100 or width > 9999): |
| return False |
| |
| if (height < 100 or height > 9999): |
| return False |
| |
| self._logger.debug("adjusting screen resolution to %s, %s and rebooting" % (width, height)) |
| |
| self._runCmds([{ 'cmd': "exec setprop persist.tegra.dpy%s.mode.width %s" % (screentype, width) }]) |
| self._runCmds([{ 'cmd': "exec setprop persist.tegra.dpy%s.mode.height %s" % (screentype, height) }]) |
| |
| def chmodDir(self, remoteDir, **kwargs): |
| self._runCmds([{ 'cmd': "chmod "+remoteDir }]) |