PageRenderTime 56ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/build/mobile/devicemanagerSUT.py

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