~extension-hackers/globalmenu-extension/3.5

« back to all changes in this revision

Viewing changes to build/mobile/devicemanagerSUT.py

  • Committer: Chris Coulson
  • Date: 2011-08-05 17:37:02 UTC
  • Revision ID: chrisccoulson@ubuntu.com-20110805173702-n11ykbt0tdp5u07q
Refresh build system from FIREFOX_6_0b5_BUILD1

Show diffs side-by-side

added added

removed removed

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