PageRenderTime 70ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py

https://bitbucket.org/thinker/mozilla-central
Python | 1240 lines | 1087 code | 47 blank | 106 comment | 70 complexity | bd4698af3d90677889a780f0713acfc5 MD5 | raw file
Possible License(s): JSON, 0BSD, LGPL-3.0, BSD-2-Clause, MIT, MPL-2.0-no-copyleft-exception, BSD-3-Clause, GPL-2.0, AGPL-1.0, MPL-2.0, Apache-2.0, LGPL-2.1
  1. # This Source Code Form is subject to the terms of the Mozilla Public
  2. # License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. # You can obtain one at http://mozilla.org/MPL/2.0/.
  4. import select
  5. import socket
  6. import SocketServer
  7. import time
  8. import os
  9. import re
  10. import posixpath
  11. import subprocess
  12. from threading import Thread
  13. import StringIO
  14. from devicemanager import DeviceManager, FileError, NetworkTools, _pop_last_line
  15. import errno
  16. from distutils.version import StrictVersion
  17. class AgentError(Exception):
  18. "SUTAgent-specific exception."
  19. def __init__(self, msg= '', fatal = False):
  20. self.msg = msg
  21. self.fatal = fatal
  22. def __str__(self):
  23. return self.msg
  24. class DeviceManagerSUT(DeviceManager):
  25. host = ''
  26. port = 0
  27. debug = 2
  28. retries = 0
  29. tempRoot = os.getcwd()
  30. base_prompt = '$>'
  31. base_prompt_re = '\$\>'
  32. prompt_sep = '\x00'
  33. prompt_regex = '.*(' + base_prompt_re + prompt_sep + ')'
  34. agentErrorRE = re.compile('^##AGENT-WARNING##\ ?(.*)')
  35. # TODO: member variable to indicate error conditions.
  36. # This should be set to a standard error from the errno module.
  37. # So, for example, when an error occurs because of a missing file/directory,
  38. # before returning, the function would do something like 'self.error = errno.ENOENT'.
  39. # The error would be set where appropriate--so sendCMD() could set socket errors,
  40. # pushFile() and other file-related commands could set filesystem errors, etc.
  41. def __init__(self, host, port = 20701, retrylimit = 5, deviceRoot = None):
  42. self.host = host
  43. self.port = port
  44. self.retrylimit = retrylimit
  45. self.retries = 0
  46. self._sock = None
  47. self.deviceRoot = deviceRoot
  48. if self.getDeviceRoot() == None:
  49. raise BaseException("Failed to connect to SUT Agent and retrieve the device root.")
  50. try:
  51. verstring = self.runCmds([{ 'cmd': 'ver' }])
  52. self.agentVersion = re.sub('SUTAgentAndroid Version ', '', verstring)
  53. except AgentError, err:
  54. raise BaseException("Failed to get SUTAgent version")
  55. def _cmdNeedsResponse(self, cmd):
  56. """ Not all commands need a response from the agent:
  57. * rebt obviously doesn't get a response
  58. * uninstall performs a reboot to ensure starting in a clean state and
  59. so also doesn't look for a response
  60. """
  61. noResponseCmds = [re.compile('^rebt'),
  62. re.compile('^uninst .*$'),
  63. re.compile('^pull .*$')]
  64. for c in noResponseCmds:
  65. if (c.match(cmd)):
  66. return False
  67. # If the command is not in our list, then it gets a response
  68. return True
  69. def _stripPrompt(self, data):
  70. '''
  71. internal function
  72. take a data blob and strip instances of the prompt '$>\x00'
  73. '''
  74. promptre = re.compile(self.prompt_regex + '.*')
  75. retVal = []
  76. lines = data.split('\n')
  77. for line in lines:
  78. foundPrompt = False
  79. try:
  80. while (promptre.match(line)):
  81. foundPrompt = True
  82. pieces = line.split(self.prompt_sep)
  83. index = pieces.index('$>')
  84. pieces.pop(index)
  85. line = self.prompt_sep.join(pieces)
  86. except(ValueError):
  87. pass
  88. # we don't want to append lines that are blank after stripping the
  89. # prompt (those are basically "prompts")
  90. if not foundPrompt or line:
  91. retVal.append(line)
  92. return '\n'.join(retVal)
  93. def _shouldCmdCloseSocket(self, cmd):
  94. """ Some commands need to close the socket after they are sent:
  95. * rebt
  96. * uninst
  97. * quit
  98. """
  99. socketClosingCmds = [re.compile('^quit.*'),
  100. re.compile('^rebt.*'),
  101. re.compile('^uninst .*$')]
  102. for c in socketClosingCmds:
  103. if (c.match(cmd)):
  104. return True
  105. return False
  106. def sendCmds(self, cmdlist, outputfile, timeout = None):
  107. '''
  108. a wrapper for _doCmds that loops up to self.retrylimit iterations.
  109. this allows us to move the retry logic outside of the _doCmds() to make it
  110. easier for debugging in the future.
  111. note that since cmdlist is a list of commands, they will all be retried if
  112. one fails. this is necessary in particular for pushFile(), where we don't want
  113. to accidentally send extra data if a failure occurs during data transmission.
  114. '''
  115. if timeout:
  116. raise NotImplementedError("'timeout' parameter is not yet supported")
  117. while self.retries < self.retrylimit:
  118. try:
  119. self._doCmds(cmdlist, outputfile, timeout)
  120. return
  121. except AgentError, err:
  122. # re-raise error if it's fatal (i.e. the device got the command but
  123. # couldn't execute it). retry otherwise
  124. if err.fatal:
  125. raise err
  126. if self.debug >= 2:
  127. print err
  128. self.retries += 1
  129. raise AgentError("unable to connect to %s after %s attempts" % (self.host, self.retrylimit))
  130. def runCmds(self, cmdlist, timeout = None):
  131. '''
  132. similar to sendCmds, but just returns any output as a string instead of
  133. writing to a file. this is normally what you want to call to send a set
  134. of commands to the agent
  135. '''
  136. outputfile = StringIO.StringIO()
  137. self.sendCmds(cmdlist, outputfile, timeout)
  138. outputfile.seek(0)
  139. return outputfile.read()
  140. def _doCmds(self, cmdlist, outputfile, timeout):
  141. promptre = re.compile(self.prompt_regex + '$')
  142. shouldCloseSocket = False
  143. recvGuard = 1000
  144. if not self._sock:
  145. try:
  146. if self.debug >= 1:
  147. print "reconnecting socket"
  148. self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  149. except socket.error, msg:
  150. self._sock = None
  151. raise AgentError("unable to create socket: "+str(msg))
  152. try:
  153. self._sock.connect((self.host, int(self.port)))
  154. self._sock.recv(1024)
  155. except socket.error, msg:
  156. self._sock.close()
  157. self._sock = None
  158. raise AgentError("unable to connect socket: "+str(msg))
  159. for cmd in cmdlist:
  160. cmdline = '%s\r\n' % cmd['cmd']
  161. try:
  162. sent = self._sock.send(cmdline)
  163. if sent != len(cmdline):
  164. raise AgentError("ERROR: our cmd was %s bytes and we "
  165. "only sent %s" % (len(cmdline), sent))
  166. if cmd.get('data'):
  167. sent = self._sock.send(cmd['data'])
  168. if sent != len(cmd['data']):
  169. raise AgentError("ERROR: we had %s bytes of data to send, but "
  170. "only sent %s" % (len(cmd['data']), sent))
  171. if (self.debug >= 4): print "sent cmd: " + str(cmd['cmd'])
  172. except socket.error, msg:
  173. self._sock.close()
  174. self._sock = None
  175. if self.debug >= 1:
  176. print "Error sending data to socket. cmd="+str(cmd['cmd'])+"; err="+str(msg)
  177. return False
  178. # Check if the command should close the socket
  179. shouldCloseSocket = self._shouldCmdCloseSocket(cmd['cmd'])
  180. # Handle responses from commands
  181. if (self._cmdNeedsResponse(cmd['cmd'])):
  182. found = False
  183. loopguard = 0
  184. data = ""
  185. while (found == False and (loopguard < recvGuard)):
  186. temp = ''
  187. if (self.debug >= 4): print "recv'ing..."
  188. # Get our response
  189. try:
  190. # Wait up to a second for socket to become ready for reading...
  191. if select.select([self._sock], [], [], 1)[0]:
  192. temp = self._sock.recv(1024)
  193. if (self.debug >= 4): print "response: " + str(temp)
  194. except socket.error, err:
  195. self._sock.close()
  196. self._sock = None
  197. # This error shows up with we have our tegra rebooted.
  198. if err[0] == errno.ECONNRESET:
  199. raise AgentError("Automation error: Error receiving data from socket (possible reboot). cmd=%s; err=%s" % (cmd, err))
  200. raise AgentError("Error receiving data from socket. cmd=%s; err=%s" % (cmd, err))
  201. data += temp
  202. # If something goes wrong in the agent it will send back a string that
  203. # starts with '##AGENT-WARNING##'
  204. errorMatch = self.agentErrorRE.match(data)
  205. if errorMatch:
  206. raise AgentError("Agent Error processing command '%s'; err='%s'" %
  207. (cmd['cmd'], errorMatch.group(1)), fatal=True)
  208. for line in data.splitlines():
  209. if promptre.match(line):
  210. found = True
  211. data = self._stripPrompt(data)
  212. break
  213. # periodically flush data to output file to make sure it doesn't get
  214. # too big/unwieldly
  215. if len(data) > 1024:
  216. outputfile.write(data[0:1024])
  217. data = data[1024:]
  218. # If we violently lose the connection to the device, this loop tends to spin,
  219. # this guard prevents that
  220. if (temp == ''):
  221. loopguard += 1
  222. # Write any remaining data to outputfile
  223. outputfile.write(data)
  224. if shouldCloseSocket:
  225. try:
  226. self._sock.close()
  227. self._sock = None
  228. except:
  229. self._sock = None
  230. raise AgentError("Error closing socket")
  231. # external function: executes shell command on device
  232. # returns:
  233. # success: <return code>
  234. # failure: None
  235. def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
  236. cmdline = self._escapedCommandLine(cmd)
  237. if env:
  238. cmdline = '%s %s' % (self.formatEnvString(env), cmdline)
  239. haveExecSu = (StrictVersion(self.agentVersion) >= StrictVersion('1.13'))
  240. # Depending on agent version we send one of the following commands here:
  241. # * exec (run as normal user)
  242. # * execsu (run as privileged user)
  243. # * execcwd (run as normal user from specified directory)
  244. # * execcwdsu (run as privileged user from specified directory)
  245. cmd = "exec"
  246. if cwd:
  247. cmd += "cwd"
  248. if root and haveExecSu:
  249. cmd += "su"
  250. try:
  251. if cwd:
  252. self.sendCmds([{ 'cmd': '%s %s %s' % (cmd, cwd, cmdline) }], outputfile, timeout)
  253. else:
  254. if (not root) or haveExecSu:
  255. self.sendCmds([{ 'cmd': '%s %s' % (cmd, cmdline) }], outputfile, timeout)
  256. else:
  257. # need to manually inject su -c for backwards compatibility (this may
  258. # not work on ICS or above!!)
  259. # (FIXME: this backwards compatibility code is really ugly and should
  260. # be deprecated at some point in the future)
  261. self.sendCmds([ { 'cmd': '%s su -c "%s"' % (cmd, cmdline) }], outputfile,
  262. timeout)
  263. except AgentError:
  264. return None
  265. # dig through the output to get the return code
  266. lastline = _pop_last_line(outputfile)
  267. if lastline:
  268. m = re.search('return code \[([0-9]+)\]', lastline)
  269. if m:
  270. return int(m.group(1))
  271. # woops, we couldn't find an end of line/return value
  272. return None
  273. # external function
  274. # returns:
  275. # success: True
  276. # failure: False
  277. def pushFile(self, localname, destname):
  278. if (os.name == "nt"):
  279. destname = destname.replace('\\', '/')
  280. if (self.debug >= 3): print "in push file with: " + localname + ", and: " + destname
  281. if (self.dirExists(destname)):
  282. if (not destname.endswith('/')):
  283. destname = destname + '/'
  284. destname = destname + os.path.basename(localname)
  285. if (self.validateFile(destname, localname) == True):
  286. if (self.debug >= 3): print "files are validated"
  287. return True
  288. if self.mkDirs(destname) == None:
  289. print "unable to make dirs: " + destname
  290. return False
  291. if (self.debug >= 3): print "sending: push " + destname
  292. filesize = os.path.getsize(localname)
  293. f = open(localname, 'rb')
  294. data = f.read()
  295. f.close()
  296. try:
  297. retVal = self.runCmds([{ 'cmd': 'push ' + destname + ' ' + str(filesize),
  298. 'data': data }])
  299. except AgentError, e:
  300. print "error pushing file: %s" % e.msg
  301. return False
  302. if (self.debug >= 3): print "push returned: " + str(retVal)
  303. validated = False
  304. if (retVal):
  305. retline = retVal.strip()
  306. if (retline == None):
  307. # Then we failed to get back a hash from agent, try manual validation
  308. validated = self.validateFile(destname, localname)
  309. else:
  310. # Then we obtained a hash from push
  311. localHash = self.getLocalHash(localname)
  312. if (str(localHash) == str(retline)):
  313. validated = True
  314. else:
  315. # We got nothing back from sendCMD, try manual validation
  316. validated = self.validateFile(destname, localname)
  317. if (validated):
  318. if (self.debug >= 3): print "Push File Validated!"
  319. return True
  320. else:
  321. if (self.debug >= 2): print "Push File Failed to Validate!"
  322. return False
  323. # external function
  324. # returns:
  325. # success: directory name
  326. # failure: None
  327. def mkDir(self, name):
  328. if (self.dirExists(name)):
  329. return name
  330. else:
  331. try:
  332. retVal = self.runCmds([{ 'cmd': 'mkdr ' + name }])
  333. except AgentError:
  334. retVal = None
  335. return retVal
  336. # push localDir from host to remoteDir on the device
  337. # external function
  338. # returns:
  339. # success: remoteDir
  340. # failure: None
  341. def pushDir(self, localDir, remoteDir):
  342. if (self.debug >= 2): print "pushing directory: %s to %s" % (localDir, remoteDir)
  343. for root, dirs, files in os.walk(localDir, followlinks=True):
  344. parts = root.split(localDir)
  345. for file in files:
  346. remoteRoot = remoteDir + '/' + parts[1]
  347. if (remoteRoot.endswith('/')):
  348. remoteName = remoteRoot + file
  349. else:
  350. remoteName = remoteRoot + '/' + file
  351. if (parts[1] == ""): remoteRoot = remoteDir
  352. if (self.pushFile(os.path.join(root, file), remoteName) == False):
  353. # retry once
  354. self.removeFile(remoteName)
  355. if (self.pushFile(os.path.join(root, file), remoteName) == False):
  356. return None
  357. return remoteDir
  358. # external function
  359. # returns:
  360. # success: True
  361. # failure: False
  362. def dirExists(self, dirname):
  363. match = ".*" + dirname.replace('^', '\^') + "$"
  364. dirre = re.compile(match)
  365. try:
  366. data = self.runCmds([ { 'cmd': 'cd ' + dirname }, { 'cmd': 'cwd' }])
  367. except AgentError:
  368. return False
  369. found = False
  370. for d in data.splitlines():
  371. if (dirre.match(d)):
  372. found = True
  373. return found
  374. # Because we always have / style paths we make this a lot easier with some
  375. # assumptions
  376. # external function
  377. # returns:
  378. # success: True
  379. # failure: False
  380. def fileExists(self, filepath):
  381. s = filepath.split('/')
  382. containingpath = '/'.join(s[:-1])
  383. listfiles = self.listFiles(containingpath)
  384. for f in listfiles:
  385. if (f == s[-1]):
  386. return True
  387. return False
  388. # list files on the device, requires cd to directory first
  389. # external function
  390. # returns:
  391. # success: array of filenames, ['file1', 'file2', ...]
  392. # failure: []
  393. def listFiles(self, rootdir):
  394. rootdir = rootdir.rstrip('/')
  395. if (self.dirExists(rootdir) == False):
  396. return []
  397. try:
  398. data = self.runCmds([{ 'cmd': 'cd ' + rootdir }, { 'cmd': 'ls' }])
  399. except AgentError:
  400. return []
  401. files = filter(lambda x: x, data.splitlines())
  402. if len(files) == 1 and files[0] == '<empty>':
  403. # special case on the agent: empty directories return just the string "<empty>"
  404. return []
  405. return files
  406. # external function
  407. # returns:
  408. # success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt"
  409. # failure: None
  410. def removeFile(self, filename):
  411. if (self.debug>= 2): print "removing file: " + filename
  412. try:
  413. retVal = self.runCmds([{ 'cmd': 'rm ' + filename }])
  414. except AgentError:
  415. return None
  416. return retVal
  417. # does a recursive delete of directory on the device: rm -Rf remoteDir
  418. # external function
  419. # returns:
  420. # success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt"
  421. # failure: None
  422. def removeDir(self, remoteDir):
  423. try:
  424. retVal = self.runCmds([{ 'cmd': 'rmdr ' + remoteDir }])
  425. except AgentError:
  426. return None
  427. return retVal
  428. # external function
  429. # returns:
  430. # success: array of process tuples
  431. # failure: []
  432. def getProcessList(self):
  433. try:
  434. data = self.runCmds([{ 'cmd': 'ps' }])
  435. except AgentError:
  436. return []
  437. files = []
  438. for line in data.splitlines():
  439. if line:
  440. pidproc = line.strip().split()
  441. if (len(pidproc) == 2):
  442. files += [[pidproc[0], pidproc[1]]]
  443. elif (len(pidproc) == 3):
  444. #android returns <userID> <procID> <procName>
  445. files += [[pidproc[1], pidproc[2], pidproc[0]]]
  446. return files
  447. # external function
  448. # DEPRECATED: Use shell() or launchApplication() for new code
  449. # returns:
  450. # success: pid
  451. # failure: None
  452. def fireProcess(self, appname, failIfRunning=False):
  453. if (not appname):
  454. if (self.debug >= 1): print "WARNING: fireProcess called with no command to run"
  455. return None
  456. if (self.debug >= 2): print "FIRE PROC: '" + appname + "'"
  457. if (self.processExist(appname) != None):
  458. print "WARNING: process %s appears to be running already\n" % appname
  459. if (failIfRunning):
  460. return None
  461. try:
  462. self.runCmds([{ 'cmd': 'exec ' + appname }])
  463. except AgentError:
  464. return None
  465. # The 'exec' command may wait for the process to start and end, so checking
  466. # for the process here may result in process = None.
  467. process = self.processExist(appname)
  468. if (self.debug >= 4): print "got pid: %s for process: %s" % (process, appname)
  469. return process
  470. # external function
  471. # DEPRECATED: Use shell() or launchApplication() for new code
  472. # returns:
  473. # success: output filename
  474. # failure: None
  475. def launchProcess(self, cmd, outputFile = "process.txt", cwd = '', env = '', failIfRunning=False):
  476. if not cmd:
  477. if (self.debug >= 1): print "WARNING: launchProcess called without command to run"
  478. return None
  479. cmdline = subprocess.list2cmdline(cmd)
  480. if (outputFile == "process.txt" or outputFile == None):
  481. outputFile = self.getDeviceRoot();
  482. if outputFile is None:
  483. return None
  484. outputFile += "/process.txt"
  485. cmdline += " > " + outputFile
  486. # Prepend our env to the command
  487. cmdline = '%s %s' % (self.formatEnvString(env), cmdline)
  488. if self.fireProcess(cmdline, failIfRunning) is None:
  489. return None
  490. return outputFile
  491. # external function
  492. # returns:
  493. # success: True
  494. # failure: False
  495. def killProcess(self, appname, forceKill=False):
  496. if forceKill:
  497. print "WARNING: killProcess(): forceKill parameter unsupported on SUT"
  498. try:
  499. self.runCmds([{ 'cmd': 'kill ' + appname }])
  500. except AgentError:
  501. return False
  502. return True
  503. # external function
  504. # returns:
  505. # success: tmpdir, string
  506. # failure: None
  507. def getTempDir(self):
  508. try:
  509. data = self.runCmds([{ 'cmd': 'tmpd' }])
  510. except AgentError:
  511. return None
  512. return data.strip()
  513. # external function
  514. # returns:
  515. # success: filecontents
  516. # failure: None
  517. def catFile(self, remoteFile):
  518. try:
  519. data = self.runCmds([{ 'cmd': 'cat ' + remoteFile }])
  520. except AgentError:
  521. return None
  522. return data
  523. # external function
  524. # returns:
  525. # success: output of pullfile, string
  526. # failure: None
  527. def pullFile(self, remoteFile):
  528. """Returns contents of remoteFile using the "pull" command.
  529. The "pull" command is different from other commands in that DeviceManager
  530. has to read a certain number of bytes instead of just reading to the
  531. next prompt. This is more robust than the "cat" command, which will be
  532. confused if the prompt string exists within the file being catted.
  533. However it means we can't use the response-handling logic in sendCMD().
  534. """
  535. def err(error_msg):
  536. err_str = 'error returned from pull: %s' % error_msg
  537. print err_str
  538. self._sock = None
  539. raise FileError(err_str)
  540. # FIXME: We could possibly move these socket-reading functions up to
  541. # the class level if we wanted to refactor sendCMD(). For now they are
  542. # only used to pull files.
  543. def uread(to_recv, error_msg):
  544. """ unbuffered read """
  545. try:
  546. data = self._sock.recv(to_recv)
  547. if not data:
  548. err(error_msg)
  549. return None
  550. return data
  551. except:
  552. err(error_msg)
  553. return None
  554. def read_until_char(c, buffer, error_msg):
  555. """ read until 'c' is found; buffer rest """
  556. while not '\n' in buffer:
  557. data = uread(1024, error_msg)
  558. if data == None:
  559. err(error_msg)
  560. return ('', '', '')
  561. buffer += data
  562. return buffer.partition(c)
  563. def read_exact(total_to_recv, buffer, error_msg):
  564. """ read exact number of 'total_to_recv' bytes """
  565. while len(buffer) < total_to_recv:
  566. to_recv = min(total_to_recv - len(buffer), 1024)
  567. data = uread(to_recv, error_msg)
  568. if data == None:
  569. return None
  570. buffer += data
  571. return buffer
  572. prompt = self.base_prompt + self.prompt_sep
  573. buffer = ''
  574. # expected return value:
  575. # <filename>,<filesize>\n<filedata>
  576. # or, if error,
  577. # <filename>,-1\n<error message>
  578. try:
  579. # just send the command first, we read the response inline below
  580. self.runCmds([{ 'cmd': 'pull ' + remoteFile }])
  581. except AgentError:
  582. return None
  583. # read metadata; buffer the rest
  584. metadata, sep, buffer = read_until_char('\n', buffer, 'could not find metadata')
  585. if not metadata:
  586. return None
  587. if self.debug >= 3:
  588. print 'metadata: %s' % metadata
  589. filename, sep, filesizestr = metadata.partition(',')
  590. if sep == '':
  591. err('could not find file size in returned metadata')
  592. return None
  593. try:
  594. filesize = int(filesizestr)
  595. except ValueError:
  596. err('invalid file size in returned metadata')
  597. return None
  598. if filesize == -1:
  599. # read error message
  600. error_str, sep, buffer = read_until_char('\n', buffer, 'could not find error message')
  601. if not error_str:
  602. return None
  603. # prompt should follow
  604. read_exact(len(prompt), buffer, 'could not find prompt')
  605. print "DeviceManager: error pulling file '%s': %s" % (remoteFile, error_str)
  606. return None
  607. # read file data
  608. total_to_recv = filesize + len(prompt)
  609. buffer = read_exact(total_to_recv, buffer, 'could not get all file data')
  610. if buffer == None:
  611. return None
  612. if buffer[-len(prompt):] != prompt:
  613. err('no prompt found after file data--DeviceManager may be out of sync with agent')
  614. return buffer
  615. return buffer[:-len(prompt)]
  616. # copy file from device (remoteFile) to host (localFile)
  617. # external function
  618. # returns:
  619. # success: output of pullfile, string
  620. # failure: None
  621. def getFile(self, remoteFile, localFile = ''):
  622. if localFile == '':
  623. localFile = os.path.join(self.tempRoot, "temp.txt")
  624. try:
  625. retVal = self.pullFile(remoteFile)
  626. except:
  627. return None
  628. if (retVal is None):
  629. return None
  630. fhandle = open(localFile, 'wb')
  631. fhandle.write(retVal)
  632. fhandle.close()
  633. if not self.validateFile(remoteFile, localFile):
  634. print 'failed to validate file when downloading %s!' % remoteFile
  635. return None
  636. return retVal
  637. # copy directory structure from device (remoteDir) to host (localDir)
  638. # external function
  639. # checkDir exists so that we don't create local directories if the
  640. # remote directory doesn't exist but also so that we don't call isDir
  641. # twice when recursing.
  642. # returns:
  643. # success: list of files, string
  644. # failure: None
  645. def getDirectory(self, remoteDir, localDir, checkDir=True):
  646. if (self.debug >= 2): print "getting files in '" + remoteDir + "'"
  647. if checkDir:
  648. try:
  649. is_dir = self.isDir(remoteDir)
  650. except FileError:
  651. return None
  652. if not is_dir:
  653. return None
  654. filelist = self.listFiles(remoteDir)
  655. if (self.debug >= 3): print filelist
  656. if not os.path.exists(localDir):
  657. os.makedirs(localDir)
  658. for f in filelist:
  659. if f == '.' or f == '..':
  660. continue
  661. remotePath = remoteDir + '/' + f
  662. localPath = os.path.join(localDir, f)
  663. try:
  664. is_dir = self.isDir(remotePath)
  665. except FileError:
  666. print 'isdir failed on file "%s"; continuing anyway...' % remotePath
  667. continue
  668. if is_dir:
  669. if (self.getDirectory(remotePath, localPath, False) == None):
  670. print 'failed to get directory "%s"' % remotePath
  671. return None
  672. else:
  673. # It's sometimes acceptable to have getFile() return None, such as
  674. # when the agent encounters broken symlinks.
  675. # FIXME: This should be improved so we know when a file transfer really
  676. # failed.
  677. if self.getFile(remotePath, localPath) == None:
  678. print 'failed to get file "%s"; continuing anyway...' % remotePath
  679. return filelist
  680. # external function
  681. # returns:
  682. # success: True
  683. # failure: False
  684. # Throws a FileError exception when null (invalid dir/filename)
  685. def isDir(self, remotePath):
  686. try:
  687. data = self.runCmds([{ 'cmd': 'isdir ' + remotePath }])
  688. except AgentError:
  689. # normally there should be no error here; a nonexistent file/directory will
  690. # return the string "<filename>: No such file or directory".
  691. # However, I've seen AGENT-WARNING returned before.
  692. return False
  693. retVal = data.strip()
  694. if not retVal:
  695. raise FileError('isdir returned null')
  696. return retVal == 'TRUE'
  697. # true/false check if the two files have the same md5 sum
  698. # external function
  699. # returns:
  700. # success: True
  701. # failure: False
  702. def validateFile(self, remoteFile, localFile):
  703. remoteHash = self.getRemoteHash(remoteFile)
  704. localHash = self.getLocalHash(localFile)
  705. if (remoteHash == None):
  706. return False
  707. if (remoteHash == localHash):
  708. return True
  709. return False
  710. # return the md5 sum of a remote file
  711. # internal function
  712. # returns:
  713. # success: MD5 hash for given filename
  714. # failure: None
  715. def getRemoteHash(self, filename):
  716. try:
  717. data = self.runCmds([{ 'cmd': 'hash ' + filename }])
  718. except AgentError:
  719. return None
  720. retVal = None
  721. if data:
  722. retVal = data.strip()
  723. if self.debug >= 3:
  724. print "remote hash returned: '%s'" % retVal
  725. return retVal
  726. # Gets the device root for the testing area on the device
  727. # For all devices we will use / type slashes and depend on the device-agent
  728. # to sort those out. The agent will return us the device location where we
  729. # should store things, we will then create our /tests structure relative to
  730. # that returned path.
  731. # Structure on the device is as follows:
  732. # /tests
  733. # /<fennec>|<firefox> --> approot
  734. # /profile
  735. # /xpcshell
  736. # /reftest
  737. # /mochitest
  738. #
  739. # external function
  740. # returns:
  741. # success: path for device root
  742. # failure: None
  743. def getDeviceRoot(self):
  744. if self.deviceRoot:
  745. deviceRoot = self.deviceRoot
  746. else:
  747. try:
  748. data = self.runCmds([{ 'cmd': 'testroot' }])
  749. except:
  750. return None
  751. deviceRoot = data.strip() + '/tests'
  752. if (not self.dirExists(deviceRoot)):
  753. if (self.mkDir(deviceRoot) == None):
  754. return None
  755. self.deviceRoot = deviceRoot
  756. return self.deviceRoot
  757. def getAppRoot(self, packageName):
  758. try:
  759. data = self.runCmds([{ 'cmd': 'getapproot '+packageName }])
  760. except:
  761. return None
  762. return data.strip()
  763. # external function
  764. # returns:
  765. # success: output of unzip command
  766. # failure: None
  767. def unpackFile(self, file_path, dest_dir=None):
  768. devroot = self.getDeviceRoot()
  769. if (devroot == None):
  770. return None
  771. # if no dest_dir is passed in just set it to file_path's folder
  772. if not dest_dir:
  773. dest_dir = posixpath.dirname(file_path)
  774. if dest_dir[-1] != '/':
  775. dest_dir += '/'
  776. try:
  777. data = self.runCmds([{ 'cmd': 'unzp %s %s' % (file_path, dest_dir)}])
  778. except AgentError:
  779. return None
  780. return data
  781. # external function
  782. # returns:
  783. # success: status from test agent
  784. # failure: None
  785. def reboot(self, ipAddr=None, port=30000):
  786. cmd = 'rebt'
  787. if (self.debug > 3): print "INFO: sending rebt command"
  788. if (ipAddr is not None):
  789. #create update.info file:
  790. try:
  791. destname = '/data/data/com.mozilla.SUTAgentAndroid/files/update.info'
  792. data = "%s,%s\rrebooting\r" % (ipAddr, port)
  793. self.runCmds([{ 'cmd': 'push %s %s' % (destname, len(data)),
  794. 'data': data }])
  795. except AgentError:
  796. return None
  797. ip, port = self.getCallbackIpAndPort(ipAddr, port)
  798. cmd += " %s %s" % (ip, port)
  799. # Set up our callback server
  800. callbacksvr = callbackServer(ip, port, self.debug)
  801. try:
  802. status = self.runCmds([{ 'cmd': cmd }])
  803. except AgentError:
  804. return None
  805. if (ipAddr is not None):
  806. status = callbacksvr.disconnect()
  807. if (self.debug > 3): print "INFO: rebt- got status back: " + str(status)
  808. return status
  809. # Returns information about the device:
  810. # Directive indicates the information you want to get, your choices are:
  811. # os - name of the os
  812. # id - unique id of the device
  813. # uptime - uptime of the device
  814. # uptimemillis - uptime of the device in milliseconds (SUTAgent 1.11+)
  815. # systime - system time of the device
  816. # screen - screen resolution
  817. # rotation - rotation of the device (in degrees)
  818. # memory - memory stats
  819. # process - list of running processes (same as ps)
  820. # disk - total, free, available bytes on disk
  821. # power - power status (charge, battery temp)
  822. # all - all of them - or call it with no parameters to get all the information
  823. # returns:
  824. # success: dict of info strings by directive name
  825. # failure: {}
  826. def getInfo(self, directive=None):
  827. data = None
  828. result = {}
  829. collapseSpaces = re.compile(' +')
  830. directives = ['os','id','uptime','uptimemillis','systime','screen',
  831. 'rotation','memory','process','disk','power']
  832. if (directive in directives):
  833. directives = [directive]
  834. for d in directives:
  835. try:
  836. data = self.runCmds([{ 'cmd': 'info ' + d }])
  837. except AgentError:
  838. return result
  839. if (data is None):
  840. continue
  841. data = collapseSpaces.sub(' ', data)
  842. result[d] = data.split('\n')
  843. # Get rid of any 0 length members of the arrays
  844. for k, v in result.iteritems():
  845. result[k] = filter(lambda x: x != '', result[k])
  846. # Format the process output
  847. if 'process' in result:
  848. proclist = []
  849. for l in result['process']:
  850. if l:
  851. proclist.append(l.split('\t'))
  852. result['process'] = proclist
  853. if (self.debug >= 3): print "results: " + str(result)
  854. return result
  855. """
  856. Installs the application onto the device
  857. Application bundle - path to the application bundle on the device
  858. Destination - destination directory of where application should be
  859. installed to (optional)
  860. Returns None for success, or output if known failure
  861. """
  862. # external function
  863. # returns:
  864. # success: output from agent for inst command
  865. # failure: None
  866. def installApp(self, appBundlePath, destPath=None):
  867. cmd = 'inst ' + appBundlePath
  868. if destPath:
  869. cmd += ' ' + destPath
  870. try:
  871. data = self.runCmds([{ 'cmd': cmd }])
  872. except AgentError:
  873. return None
  874. f = re.compile('Failure')
  875. for line in data.split():
  876. if (f.match(line)):
  877. return data
  878. return None
  879. """
  880. Uninstalls the named application from device and causes a reboot.
  881. Takes an optional argument of installation path - the path to where the application
  882. was installed.
  883. Returns True, but it doesn't mean anything other than the command was sent,
  884. the reboot happens and we don't know if this succeeds or not.
  885. """
  886. # external function
  887. # returns:
  888. # success: True
  889. # failure: None
  890. def uninstallAppAndReboot(self, appName, installPath=None):
  891. cmd = 'uninst ' + appName
  892. if installPath:
  893. cmd += ' ' + installPath
  894. try:
  895. data = self.runCmds([{ 'cmd': cmd }])
  896. except AgentError:
  897. return None
  898. if (self.debug > 3): print "uninstallAppAndReboot: " + str(data)
  899. return True
  900. """
  901. Updates the application on the device.
  902. Application bundle - path to the application bundle on the device
  903. Process name of application - used to end the process if the applicaiton is
  904. currently running
  905. Destination - Destination directory to where the application should be
  906. installed (optional)
  907. ipAddr - IP address to await a callback ping to let us know that the device has updated
  908. properly - defaults to current IP.
  909. port - port to await a callback ping to let us know that the device has updated properly
  910. defaults to 30000, and counts up from there if it finds a conflict
  911. Returns True if succeeds, False if not
  912. """
  913. # external function
  914. # returns:
  915. # success: text status from command or callback server
  916. # failure: None
  917. def updateApp(self, appBundlePath, processName=None, destPath=None, ipAddr=None, port=30000):
  918. status = None
  919. cmd = 'updt '
  920. if (processName == None):
  921. # Then we pass '' for processName
  922. cmd += "'' " + appBundlePath
  923. else:
  924. cmd += processName + ' ' + appBundlePath
  925. if (destPath):
  926. cmd += " " + destPath
  927. if (ipAddr is not None):
  928. ip, port = self.getCallbackIpAndPort(ipAddr, port)
  929. cmd += " %s %s" % (ip, port)
  930. # Set up our callback server
  931. callbacksvr = callbackServer(ip, port, self.debug)
  932. if (self.debug >= 3): print "INFO: updateApp using command: " + str(cmd)
  933. try:
  934. status = self.runCmds([{ 'cmd': cmd }])
  935. except AgentError:
  936. return None
  937. if ipAddr is not None:
  938. status = callbacksvr.disconnect()
  939. if (self.debug >= 3): print "INFO: updateApp: got status back: " + str(status)
  940. return status
  941. """
  942. return the current time on the device
  943. """
  944. # external function
  945. # returns:
  946. # success: time in ms
  947. # failure: None
  948. def getCurrentTime(self):
  949. try:
  950. data = self.runCmds([{ 'cmd': 'clok' }])
  951. except AgentError:
  952. return None
  953. return data.strip()
  954. """
  955. Connect the ipaddress and port for a callback ping. Defaults to current IP address
  956. And ports starting at 30000.
  957. NOTE: the detection for current IP address only works on Linux!
  958. """
  959. def getCallbackIpAndPort(self, aIp, aPort):
  960. ip = aIp
  961. nettools = NetworkTools()
  962. if (ip == None):
  963. ip = nettools.getLanIp()
  964. if (aPort != None):
  965. port = nettools.findOpenPort(ip, aPort)
  966. else:
  967. port = nettools.findOpenPort(ip, 30000)
  968. return ip, port
  969. """
  970. Returns a properly formatted env string for the agent.
  971. Input - env, which is either None, '', or a dict
  972. Output - a quoted string of the form: '"envvar1=val1,envvar2=val2..."'
  973. If env is None or '' return '' (empty quoted string)
  974. """
  975. def formatEnvString(self, env):
  976. if (env == None or env == ''):
  977. return ''
  978. retVal = '"%s"' % ','.join(map(lambda x: '%s=%s' % (x[0], x[1]), env.iteritems()))
  979. if (retVal == '""'):
  980. return ''
  981. return retVal
  982. """
  983. adjust the screen resolution on the device, REBOOT REQUIRED
  984. NOTE: this only works on a tegra ATM
  985. success: True
  986. failure: False
  987. supported resolutions: 640x480, 800x600, 1024x768, 1152x864, 1200x1024, 1440x900, 1680x1050, 1920x1080
  988. """
  989. def adjustResolution(self, width=1680, height=1050, type='hdmi'):
  990. if self.getInfo('os')['os'][0].split()[0] != 'harmony-eng':
  991. if (self.debug >= 2): print "WARNING: unable to adjust screen resolution on non Tegra device"
  992. return False
  993. results = self.getInfo('screen')
  994. parts = results['screen'][0].split(':')
  995. if (self.debug >= 3): print "INFO: we have a current resolution of %s, %s" % (parts[1].split()[0], parts[2].split()[0])
  996. #verify screen type is valid, and set it to the proper value (https://bugzilla.mozilla.org/show_bug.cgi?id=632895#c4)
  997. screentype = -1
  998. if (type == 'hdmi'):
  999. screentype = 5
  1000. elif (type == 'vga' or type == 'crt'):
  1001. screentype = 3
  1002. else:
  1003. return False
  1004. #verify we have numbers
  1005. if not (isinstance(width, int) and isinstance(height, int)):
  1006. return False
  1007. if (width < 100 or width > 9999):
  1008. return False
  1009. if (height < 100 or height > 9999):
  1010. return False
  1011. if (self.debug >= 3): print "INFO: adjusting screen resolution to %s, %s and rebooting" % (width, height)
  1012. try:
  1013. self.runCmds([{ 'cmd': "exec setprop persist.tegra.dpy%s.mode.width %s" % (screentype, width) }])
  1014. self.runCmds([{ 'cmd': "exec setprop persist.tegra.dpy%s.mode.height %s" % (screentype, height) }])
  1015. except AgentError:
  1016. return False
  1017. return True
  1018. # external function
  1019. # returns:
  1020. # success: True
  1021. # failure: False
  1022. def chmodDir(self, remoteDir):
  1023. try:
  1024. self.runCmds([{ 'cmd': "chmod "+remoteDir }])
  1025. except AgentError:
  1026. return False
  1027. return True
  1028. gCallbackData = ''
  1029. class myServer(SocketServer.TCPServer):
  1030. allow_reuse_address = True
  1031. class callbackServer():
  1032. def __init__(self, ip, port, debuglevel):
  1033. global gCallbackData
  1034. if (debuglevel >= 1): print "DEBUG: gCallbackData is: %s on port: %s" % (gCallbackData, port)
  1035. gCallbackData = ''
  1036. self.ip = ip
  1037. self.port = port
  1038. self.connected = False
  1039. self.debug = debuglevel
  1040. if (self.debug >= 3): print "Creating server with " + str(ip) + ":" + str(port)
  1041. self.server = myServer((ip, port), self.myhandler)
  1042. self.server_thread = Thread(target=self.server.serve_forever)
  1043. self.server_thread.setDaemon(True)
  1044. self.server_thread.start()
  1045. def disconnect(self, step = 60, timeout = 600):
  1046. t = 0
  1047. if (self.debug >= 3): print "Calling disconnect on callback server"
  1048. while t < timeout:
  1049. if (gCallbackData):
  1050. # Got the data back
  1051. if (self.debug >= 3): print "Got data back from agent: " + str(gCallbackData)
  1052. break
  1053. else:
  1054. if (self.debug >= 0): print '.',
  1055. time.sleep(step)
  1056. t += step
  1057. try:
  1058. if (self.debug >= 3): print "Shutting down server now"
  1059. self.server.shutdown()
  1060. except:
  1061. if (self.debug >= 1): print "Unable to shutdown callback server - check for a connection on port: " + str(self.port)
  1062. #sleep 1 additional step to ensure not only we are online, but all our services are online
  1063. time.sleep(step)
  1064. return gCallbackData
  1065. class myhandler(SocketServer.BaseRequestHandler):
  1066. def handle(self):
  1067. global gCallbackData
  1068. gCallbackData = self.request.recv(1024)
  1069. #print "Callback Handler got data: " + str(gCallbackData)
  1070. self.request.send("OK")