PageRenderTime 180ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

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

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