/build/mobile/devicemanager.py

http://github.com/zpao/v8monkey · Python · 583 lines · 450 code · 17 blank · 116 comment · 4 complexity · 10f865d3cf50b7cc10d4cdfe73772bab MD5 · raw file

  1. # ***** BEGIN LICENSE BLOCK *****
  2. # Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3. #
  4. # The contents of this file are subject to the Mozilla Public License Version
  5. # 1.1 (the "License"); you may not use this file except in compliance with
  6. # the License. You may obtain a copy of the License at
  7. # http://www.mozilla.org/MPL/
  8. #
  9. # Software distributed under the License is distributed on an "AS IS" basis,
  10. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11. # for the specific language governing rights and limitations under the
  12. # License.
  13. #
  14. # The Original Code is Test Automation Framework.
  15. #
  16. # The Initial Developer of the Original Code is Joel Maher.
  17. #
  18. # Portions created by the Initial Developer are Copyright (C) 2009
  19. # the Initial Developer. All Rights Reserved.
  20. #
  21. # Contributor(s):
  22. # Joel Maher <joel.maher@gmail.com> (Original Developer)
  23. # Clint Talbert <cmtalbert@gmail.com>
  24. # Mark Cote <mcote@mozilla.com>
  25. #
  26. # Alternatively, the contents of this file may be used under the terms of
  27. # either the GNU General Public License Version 2 or later (the "GPL"), or
  28. # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  29. # in which case the provisions of the GPL or the LGPL are applicable instead
  30. # of those above. If you wish to allow use of your version of this file only
  31. # under the terms of either the GPL or the LGPL, and not to allow others to
  32. # use your version of this file under the terms of the MPL, indicate your
  33. # decision by deleting the provisions above and replace them with the notice
  34. # and other provisions required by the GPL or the LGPL. If you do not delete
  35. # the provisions above, a recipient may use your version of this file under
  36. # the terms of any one of the MPL, the GPL or the LGPL.
  37. #
  38. # ***** END LICENSE BLOCK *****
  39. import time
  40. import hashlib
  41. import socket
  42. import os
  43. import re
  44. class FileError(Exception):
  45. " Signifies an error which occurs while doing a file operation."
  46. def __init__(self, msg = ''):
  47. self.msg = msg
  48. def __str__(self):
  49. return self.msg
  50. class DMError(Exception):
  51. "generic devicemanager exception."
  52. def __init__(self, msg= ''):
  53. self.msg = msg
  54. def __str__(self):
  55. return self.msg
  56. def abstractmethod(method):
  57. line = method.func_code.co_firstlineno
  58. filename = method.func_code.co_filename
  59. def not_implemented(*args, **kwargs):
  60. raise NotImplementedError('Abstract method %s at File "%s", line %s '
  61. 'should be implemented by a concrete class' %
  62. (repr(method), filename,line))
  63. return not_implemented
  64. class DeviceManager:
  65. @abstractmethod
  66. def pushFile(self, localname, destname):
  67. """
  68. external function
  69. returns:
  70. success: True
  71. failure: False
  72. """
  73. @abstractmethod
  74. def mkDir(self, name):
  75. """
  76. external function
  77. returns:
  78. success: directory name
  79. failure: None
  80. """
  81. @abstractmethod
  82. def mkDirs(self, filename):
  83. """
  84. make directory structure on the device
  85. external function
  86. returns:
  87. success: directory structure that we created
  88. failure: None
  89. """
  90. @abstractmethod
  91. def pushDir(self, localDir, remoteDir):
  92. """
  93. push localDir from host to remoteDir on the device
  94. external function
  95. returns:
  96. success: remoteDir
  97. failure: None
  98. """
  99. @abstractmethod
  100. def dirExists(self, dirname):
  101. """
  102. external function
  103. returns:
  104. success: True
  105. failure: False
  106. """
  107. @abstractmethod
  108. def fileExists(self, filepath):
  109. """
  110. Because we always have / style paths we make this a lot easier with some
  111. assumptions
  112. external function
  113. returns:
  114. success: True
  115. failure: False
  116. """
  117. @abstractmethod
  118. def listFiles(self, rootdir):
  119. """
  120. list files on the device, requires cd to directory first
  121. external function
  122. returns:
  123. success: array of filenames, ['file1', 'file2', ...]
  124. failure: None
  125. """
  126. @abstractmethod
  127. def removeFile(self, filename):
  128. """
  129. external function
  130. returns:
  131. success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt"
  132. failure: None
  133. """
  134. @abstractmethod
  135. def removeDir(self, remoteDir):
  136. """
  137. does a recursive delete of directory on the device: rm -Rf remoteDir
  138. external function
  139. returns:
  140. success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt"
  141. failure: None
  142. """
  143. @abstractmethod
  144. def getProcessList(self):
  145. """
  146. external function
  147. returns:
  148. success: array of process tuples
  149. failure: None
  150. """
  151. @abstractmethod
  152. def fireProcess(self, appname, failIfRunning=False):
  153. """
  154. external function
  155. returns:
  156. success: pid
  157. failure: None
  158. """
  159. @abstractmethod
  160. def launchProcess(self, cmd, outputFile = "process.txt", cwd = '', env = '', failIfRunning=False):
  161. """
  162. external function
  163. returns:
  164. success: output filename
  165. failure: None
  166. """
  167. def communicate(self, process, timeout = 600, interval = 5):
  168. """
  169. loops until 'process' has exited or 'timeout' seconds is reached
  170. loop sleeps for 'interval' seconds between iterations
  171. external function
  172. returns:
  173. success: [file contents, None]
  174. failure: [None, None]
  175. """
  176. timed_out = True
  177. if (timeout > 0):
  178. total_time = 0
  179. while total_time < timeout:
  180. time.sleep(interval)
  181. if self.processExist(process) == None:
  182. timed_out = False
  183. break
  184. total_time += interval
  185. if (timed_out == True):
  186. return [None, None]
  187. return [self.getFile(process, "temp.txt"), None]
  188. def processExist(self, appname):
  189. """
  190. iterates process list and returns pid if exists, otherwise None
  191. external function
  192. returns:
  193. success: pid
  194. failure: None
  195. """
  196. pid = None
  197. #filter out extra spaces
  198. parts = filter(lambda x: x != '', appname.split(' '))
  199. appname = ' '.join(parts)
  200. #filter out the quoted env string if it exists
  201. #ex: '"name=value;name2=value2;etc=..." process args' -> 'process args'
  202. parts = appname.split('"')
  203. if (len(parts) > 2):
  204. appname = ' '.join(parts[2:]).strip()
  205. pieces = appname.split(' ')
  206. parts = pieces[0].split('/')
  207. app = parts[-1]
  208. procList = self.getProcessList()
  209. if (procList == []):
  210. return None
  211. for proc in procList:
  212. procName = proc[1].split('/')[-1]
  213. if (procName == app):
  214. pid = proc[0]
  215. break
  216. return pid
  217. @abstractmethod
  218. def killProcess(self, appname):
  219. """
  220. external function
  221. returns:
  222. success: output from testagent
  223. failure: None
  224. """
  225. @abstractmethod
  226. def catFile(self, remoteFile):
  227. """
  228. external function
  229. returns:
  230. success: filecontents
  231. failure: None
  232. """
  233. @abstractmethod
  234. def pullFile(self, remoteFile):
  235. """
  236. external function
  237. returns:
  238. success: output of pullfile, string
  239. failure: None
  240. """
  241. @abstractmethod
  242. def getFile(self, remoteFile, localFile = ''):
  243. """
  244. copy file from device (remoteFile) to host (localFile)
  245. external function
  246. returns:
  247. success: output of pullfile, string
  248. failure: None
  249. """
  250. @abstractmethod
  251. def getDirectory(self, remoteDir, localDir, checkDir=True):
  252. """
  253. copy directory structure from device (remoteDir) to host (localDir)
  254. external function
  255. checkDir exists so that we don't create local directories if the
  256. remote directory doesn't exist but also so that we don't call isDir
  257. twice when recursing.
  258. returns:
  259. success: list of files, string
  260. failure: None
  261. """
  262. @abstractmethod
  263. def isDir(self, remotePath):
  264. """
  265. external function
  266. returns:
  267. success: True
  268. failure: False
  269. Throws a FileError exception when null (invalid dir/filename)
  270. """
  271. @abstractmethod
  272. def validateFile(self, remoteFile, localFile):
  273. """
  274. true/false check if the two files have the same md5 sum
  275. external function
  276. returns:
  277. success: True
  278. failure: False
  279. """
  280. @abstractmethod
  281. def getRemoteHash(self, filename):
  282. """
  283. return the md5 sum of a remote file
  284. internal function
  285. returns:
  286. success: MD5 hash for given filename
  287. failure: None
  288. """
  289. def getLocalHash(self, filename):
  290. """
  291. return the md5 sum of a file on the host
  292. internal function
  293. returns:
  294. success: MD5 hash for given filename
  295. failure: None
  296. """
  297. file = open(filename, 'rb')
  298. if (file == None):
  299. return None
  300. try:
  301. mdsum = hashlib.md5()
  302. except:
  303. return None
  304. while 1:
  305. data = file.read(1024)
  306. if not data:
  307. break
  308. mdsum.update(data)
  309. file.close()
  310. hexval = mdsum.hexdigest()
  311. if (self.debug >= 3): print "local hash returned: '" + hexval + "'"
  312. return hexval
  313. @abstractmethod
  314. def getDeviceRoot(self):
  315. """
  316. Gets the device root for the testing area on the device
  317. For all devices we will use / type slashes and depend on the device-agent
  318. to sort those out. The agent will return us the device location where we
  319. should store things, we will then create our /tests structure relative to
  320. that returned path.
  321. Structure on the device is as follows:
  322. /tests
  323. /<fennec>|<firefox> --> approot
  324. /profile
  325. /xpcshell
  326. /reftest
  327. /mochitest
  328. external
  329. returns:
  330. success: path for device root
  331. failure: None
  332. """
  333. @abstractmethod
  334. def getAppRoot(self):
  335. """
  336. Either we will have /tests/fennec or /tests/firefox but we will never have
  337. both. Return the one that exists
  338. TODO: ensure we can support org.mozilla.firefox
  339. external function
  340. returns:
  341. success: path for app root
  342. failure: None
  343. """
  344. def getTestRoot(self, type):
  345. """
  346. Gets the directory location on the device for a specific test type
  347. Type is one of: xpcshell|reftest|mochitest
  348. external function
  349. returns:
  350. success: path for test root
  351. failure: None
  352. """
  353. devroot = self.getDeviceRoot()
  354. if (devroot == None):
  355. return None
  356. if (re.search('xpcshell', type, re.I)):
  357. self.testRoot = devroot + '/xpcshell'
  358. elif (re.search('?(i)reftest', type)):
  359. self.testRoot = devroot + '/reftest'
  360. elif (re.search('?(i)mochitest', type)):
  361. self.testRoot = devroot + '/mochitest'
  362. return self.testRoot
  363. def signal(self, processID, signalType, signalAction):
  364. """
  365. Sends a specific process ID a signal code and action.
  366. For Example: SIGINT and SIGDFL to process x
  367. """
  368. #currently not implemented in device agent - todo
  369. pass
  370. def getReturnCode(self, processID):
  371. """Get a return code from process ending -- needs support on device-agent"""
  372. # TODO: make this real
  373. return 0
  374. @abstractmethod
  375. def unpackFile(self, filename):
  376. """
  377. external function
  378. returns:
  379. success: output of unzip command
  380. failure: None
  381. """
  382. @abstractmethod
  383. def reboot(self, ipAddr=None, port=30000):
  384. """
  385. external function
  386. returns:
  387. success: status from test agent
  388. failure: None
  389. """
  390. def validateDir(self, localDir, remoteDir):
  391. """
  392. validate localDir from host to remoteDir on the device
  393. external function
  394. returns:
  395. success: True
  396. failure: False
  397. """
  398. if (self.debug >= 2): print "validating directory: " + localDir + " to " + remoteDir
  399. for root, dirs, files in os.walk(localDir):
  400. parts = root.split(localDir)
  401. for file in files:
  402. remoteRoot = remoteDir + '/' + parts[1]
  403. remoteRoot = remoteRoot.replace('/', '/')
  404. if (parts[1] == ""): remoteRoot = remoteDir
  405. remoteName = remoteRoot + '/' + file
  406. if (self.validateFile(remoteName, os.path.join(root, file)) <> True):
  407. return False
  408. return True
  409. @abstractmethod
  410. def getInfo(self, directive=None):
  411. """
  412. Returns information about the device:
  413. Directive indicates the information you want to get, your choices are:
  414. os - name of the os
  415. id - unique id of the device
  416. uptime - uptime of the device
  417. systime - system time of the device
  418. screen - screen resolution
  419. memory - memory stats
  420. process - list of running processes (same as ps)
  421. disk - total, free, available bytes on disk
  422. power - power status (charge, battery temp)
  423. all - all of them - or call it with no parameters to get all the information
  424. returns:
  425. success: dict of info strings by directive name
  426. failure: None
  427. """
  428. @abstractmethod
  429. def installApp(self, appBundlePath, destPath=None):
  430. """
  431. external function
  432. returns:
  433. success: output from agent for inst command
  434. failure: None
  435. """
  436. @abstractmethod
  437. def uninstallAppAndReboot(self, appName, installPath=None):
  438. """
  439. external function
  440. returns:
  441. success: True
  442. failure: None
  443. """
  444. @abstractmethod
  445. def updateApp(self, appBundlePath, processName=None,
  446. destPath=None, ipAddr=None, port=30000):
  447. """
  448. external function
  449. returns:
  450. success: text status from command or callback server
  451. failure: None
  452. """
  453. @abstractmethod
  454. def getCurrentTime(self):
  455. """
  456. external function
  457. returns:
  458. success: time in ms
  459. failure: None
  460. """
  461. class NetworkTools:
  462. def __init__(self):
  463. pass
  464. # Utilities to get the local ip address
  465. def getInterfaceIp(self, ifname):
  466. if os.name != "nt":
  467. import fcntl
  468. import struct
  469. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  470. return socket.inet_ntoa(fcntl.ioctl(
  471. s.fileno(),
  472. 0x8915, # SIOCGIFADDR
  473. struct.pack('256s', ifname[:15])
  474. )[20:24])
  475. else:
  476. return None
  477. def getLanIp(self):
  478. try:
  479. ip = socket.gethostbyname(socket.gethostname())
  480. except socket.gaierror:
  481. ip = socket.gethostbyname(socket.gethostname() + ".local") # for Mac OS X
  482. if ip.startswith("127.") and os.name != "nt":
  483. interfaces = ["eth0","eth1","eth2","wlan0","wlan1","wifi0","ath0","ath1","ppp0"]
  484. for ifname in interfaces:
  485. try:
  486. ip = self.getInterfaceIp(ifname)
  487. break;
  488. except IOError:
  489. pass
  490. return ip
  491. # Gets an open port starting with the seed by incrementing by 1 each time
  492. def findOpenPort(self, ip, seed):
  493. try:
  494. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  495. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  496. connected = False
  497. if isinstance(seed, basestring):
  498. seed = int(seed)
  499. maxportnum = seed + 5000 # We will try at most 5000 ports to find an open one
  500. while not connected:
  501. try:
  502. s.bind((ip, seed))
  503. connected = True
  504. s.close()
  505. break
  506. except:
  507. if seed > maxportnum:
  508. print "Could not find open port after checking 5000 ports"
  509. raise
  510. seed += 1
  511. except:
  512. print "Socket error trying to find open port"
  513. return seed