This repository has been archived by the owner on Aug 14, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathadb.py
2262 lines (1981 loc) · 92.2 KB
/
adb.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
import os
import posixpath
import re
import shutil
import subprocess
import tempfile
import time
import traceback
from abc import ABCMeta, abstractmethod
from distutils import dir_util
class ADBProcess(object):
"""ADBProcess encapsulates the data related to executing the adb process."""
def __init__(self, args):
#: command argument argument list.
self.args = args
#: Temporary file handle to be used for stdout.
self.stdout_file = tempfile.TemporaryFile()
#: boolean indicating if the command timed out.
self.timedout = None
#: exitcode of the process.
self.exitcode = None
#: subprocess Process object used to execute the command.
self.proc = subprocess.Popen(args,
stdout=self.stdout_file,
stderr=subprocess.STDOUT)
@property
def stdout(self):
"""Return the contents of stdout."""
if not self.stdout_file or self.stdout_file.closed:
content = ""
else:
self.stdout_file.seek(0, os.SEEK_SET)
content = self.stdout_file.read().rstrip()
return content
def __str__(self):
return ('args: %s, exitcode: %s, stdout: %s' % (
' '.join(self.args), self.exitcode, self.stdout))
# ADBError, ADBRootError, and ADBTimeoutError are treated
# differently in order that unhandled ADBRootErrors and
# ADBTimeoutErrors can be handled distinctly from ADBErrors.
class ADBError(Exception):
"""ADBError is raised in situations where a command executed on a
device either exited with a non-zero exitcode or when an
unexpected error condition has occurred. Generally, ADBErrors can
be handled and the device can continue to be used.
"""
pass
class ADBListDevicesError(ADBError):
"""ADBListDevicesError is raised when errors are found listing the
devices, typically not any permissions.
The devices information is stocked with the *devices* member.
"""
def __init__(self, msg, devices):
ADBError.__init__(self, msg)
self.devices = devices
class ADBRootError(Exception):
"""ADBRootError is raised when a shell command is to be executed as
root but the device does not support it. This error is fatal since
there is no recovery possible by the script. You must either root
your device or change your scripts to not require running as root.
"""
pass
class ADBTimeoutError(Exception):
"""ADBTimeoutError is raised when either a host command or shell
command takes longer than the specified timeout to execute. The
timeout value is set in the ADBCommand constructor and is 300 seconds by
default. This error is typically fatal since the host is having
problems communicating with the device. You may be able to recover
by rebooting, but this is not guaranteed.
Recovery options are:
* Killing and restarting the adb server via
::
adb kill-server; adb start-server
* Rebooting the device manually.
* Rebooting the host.
"""
pass
class ADBCommand(object):
"""ADBCommand provides a basic interface to adb commands
which is used to provide the 'command' methods for the
classes ADBHost and ADBDevice.
ADBCommand should only be used as the base class for other
classes and should not be instantiated directly. To enforce this
restriction calling ADBCommand's constructor will raise a
NonImplementedError exception.
::
from mozdevice import ADBCommand
try:
adbcommand = ADBCommand()
except NotImplementedError:
print "ADBCommand can not be instantiated."
"""
def __init__(self,
adb='adb',
adb_host=None,
adb_port=None,
logger_name='adb',
timeout=300,
verbose=False):
"""Initializes the ADBCommand object.
:param str adb: path to adb executable. Defaults to 'adb'.
:param adb_host: host of the adb server.
:type adb_host: str or None
:param adb_port: port of the adb server.
:type adb_port: integer or None
:param str logger_name: logging logger name. Defaults to 'adb'.
:raises: * ADBError
* ADBTimeoutError
"""
if self.__class__ == ADBCommand:
raise NotImplementedError
self._logger = self._get_logger(logger_name)
self._verbose = verbose
self._adb_path = adb
self._adb_host = adb_host
self._adb_port = adb_port
self._timeout = timeout
self._polling_interval = 0.1
self._adb_version = ''
self._logger.debug("%s: %s" % (self.__class__.__name__,
self.__dict__))
# catch early a missing or non executable adb command
# and get the adb version while we are at it.
try:
output = subprocess.Popen([adb, 'version'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE).communicate()
re_version = re.compile(r'Android Debug Bridge version (.*)')
self._adb_version = re_version.match(output[0]).group(1)
except Exception as exc:
raise ADBError('%s: %s is not executable.' % (exc, adb))
def _get_logger(self, logger_name):
logger = None
try:
import mozlog
logger = mozlog.get_default_logger(logger_name)
except ImportError:
pass
if logger is None:
import logging
logger = logging.getLogger(logger_name)
return logger
# Host Command methods
def command(self, cmds, device_serial=None, timeout=None):
"""Executes an adb command on the host.
:param list cmds: The command and its arguments to be
executed.
:param device_serial: The device's
serial number if the adb command is to be executed against
a specific device.
:type device_serial: str or None
:param timeout: The maximum time in
seconds for any spawned adb process to complete before
throwing an ADBTimeoutError. This timeout is per adb call. The
total time spent may exceed this value. If it is not
specified, the value set in the ADBCommand constructor is used.
:type timeout: integer or None
:returns: :class:`mozdevice.ADBProcess`
command() provides a low level interface for executing
commands on the host via adb.
command() executes on the host in such a fashion that stdout
of the adb process is a file handle on the host and
the exit code is available as the exit code of the adb
process.
The caller provides a list containing commands, as well as a
timeout period in seconds.
A subprocess is spawned to execute adb with stdout and stderr
directed to a temporary file. If the process takes longer than
the specified timeout, the process is terminated.
It is the caller's responsibilty to clean up by closing
the stdout temporary file.
"""
args = [self._adb_path]
if self._adb_host:
args.extend(['-H', self._adb_host])
if self._adb_port:
args.extend(['-P', str(self._adb_port)])
if device_serial:
args.extend(['-s', device_serial, 'wait-for-device'])
args.extend(cmds)
adb_process = ADBProcess(args)
if timeout is None:
timeout = self._timeout
start_time = time.time()
adb_process.exitcode = adb_process.proc.poll()
while ((time.time() - start_time) <= timeout and
adb_process.exitcode is None):
time.sleep(self._polling_interval)
adb_process.exitcode = adb_process.proc.poll()
if adb_process.exitcode is None:
adb_process.proc.kill()
adb_process.timedout = True
adb_process.exitcode = adb_process.proc.poll()
adb_process.stdout_file.seek(0, os.SEEK_SET)
return adb_process
def command_output(self, cmds, device_serial=None, timeout=None):
"""Executes an adb command on the host returning stdout.
:param list cmds: The command and its arguments to be
executed.
:param device_serial: The device's
serial number if the adb command is to be executed against
a specific device.
:type device_serial: str or None
:param timeout: The maximum time in seconds
for any spawned adb process to complete before throwing
an ADBTimeoutError.
This timeout is per adb call. The total time spent
may exceed this value. If it is not specified, the value
set in the ADBCommand constructor is used.
:type timeout: integer or None
:returns: string - content of stdout.
:raises: * ADBTimeoutError
* ADBError
"""
adb_process = None
try:
# Need to force the use of the ADBCommand class's command
# since ADBDevice will redefine command and call its
# own version otherwise.
adb_process = ADBCommand.command(self, cmds,
device_serial=device_serial,
timeout=timeout)
if adb_process.timedout:
raise ADBTimeoutError("%s" % adb_process)
elif adb_process.exitcode:
raise ADBError("%s" % adb_process)
output = adb_process.stdout_file.read().rstrip()
if self._verbose:
self._logger.debug('command_output: %s, '
'timeout: %s, '
'timedout: %s, '
'exitcode: %s, output: %s' %
(' '.join(adb_process.args),
timeout,
adb_process.timedout,
adb_process.exitcode,
output))
return output
finally:
if adb_process and isinstance(adb_process.stdout_file, file):
adb_process.stdout_file.close()
class ADBHost(ADBCommand):
"""ADBHost provides a basic interface to adb host commands
which do not target a specific device.
::
from mozdevice import ADBHost
adbhost = ADBHost()
adbhost.start_server()
"""
def __init__(self,
adb='adb',
adb_host=None,
adb_port=None,
logger_name='adb',
timeout=300,
verbose=False):
"""Initializes the ADBHost object.
:param str adb: path to adb executable. Defaults to 'adb'.
:param adb_host: host of the adb server.
:type adb_host: str or None
:param adb_port: port of the adb server.
:type adb_port: integer or None
:param str logger_name: logging logger name. Defaults to 'adb'.
:raises: * ADBError
* ADBTimeoutError
"""
ADBCommand.__init__(self, adb=adb, adb_host=adb_host,
adb_port=adb_port, logger_name=logger_name,
timeout=timeout, verbose=verbose)
def command(self, cmds, timeout=None):
"""Executes an adb command on the host.
:param list cmds: The command and its arguments to be
executed.
:param timeout: The maximum time in
seconds for any spawned adb process to complete before
throwing an ADBTimeoutError. This timeout is per adb call. The
total time spent may exceed this value. If it is not
specified, the value set in the ADBHost constructor is used.
:type timeout: integer or None
:returns: :class:`mozdevice.ADBProcess`
command() provides a low level interface for executing
commands on the host via adb.
command() executes on the host in such a fashion that stdout
of the adb process is a file handle on the host and
the exit code is available as the exit code of the adb
process.
The caller provides a list containing commands, as well as a
timeout period in seconds.
A subprocess is spawned to execute adb with stdout and stderr
directed to a temporary file. If the process takes longer than
the specified timeout, the process is terminated.
It is the caller's responsibilty to clean up by closing
the stdout temporary file.
"""
return ADBCommand.command(self, cmds, timeout=timeout)
def command_output(self, cmds, timeout=None):
"""Executes an adb command on the host returning stdout.
:param list cmds: The command and its arguments to be
executed.
:param timeout: The maximum time in seconds
for any spawned adb process to complete before throwing
an ADBTimeoutError.
This timeout is per adb call. The total time spent
may exceed this value. If it is not specified, the value
set in the ADBHost constructor is used.
:type timeout: integer or None
:returns: string - content of stdout.
:raises: * ADBTimeoutError
* ADBError
"""
return ADBCommand.command_output(self, cmds, timeout=timeout)
def start_server(self, timeout=None):
"""Starts the adb server.
:param timeout: The maximum time in
seconds for any spawned adb process to complete before
throwing an ADBTimeoutError. This timeout is per adb call. The
total time spent may exceed this value. If it is not
specified, the value set in the ADBHost constructor is used.
:type timeout: integer or None
:raises: * ADBTimeoutError
* ADBError
Attempting to use start_server with any adb_host value other than None
will fail with an ADBError exception.
You will need to start the server on the remote host via the command:
.. code-block:: shell
adb -a fork-server server
If you wish the remote adb server to restart automatically, you can
enclose the command in a loop as in:
.. code-block:: shell
while true; do
adb -a fork-server server
done
"""
self.command_output(["start-server"], timeout=timeout)
def kill_server(self, timeout=None):
"""Kills the adb server.
:param timeout: The maximum time in
seconds for any spawned adb process to complete before
throwing an ADBTimeoutError. This timeout is per adb call. The
total time spent may exceed this value. If it is not
specified, the value set in the ADBHost constructor is used.
:type timeout: integer or None
:raises: * ADBTimeoutError
* ADBError
"""
self.command_output(["kill-server"], timeout=timeout)
def devices(self, timeout=None):
"""Executes adb devices -l and returns a list of objects describing attached devices.
:param timeout: The maximum time in
seconds for any spawned adb process to complete before
throwing an ADBTimeoutError. This timeout is per adb call. The
total time spent may exceed this value. If it is not
specified, the value set in the ADBHost constructor is used.
:type timeout: integer or None
:returns: an object contain
:raises: * ADBTimeoutError
* ADBListDevicesError
* ADBError
The output of adb devices -l ::
$ adb devices -l
List of devices attached
b313b945 device usb:1-7 product:d2vzw model:SCH_I535 device:d2vzw
is parsed and placed into an object as in
[{'device_serial': 'b313b945', 'state': 'device', 'product': 'd2vzw',
'usb': '1-7', 'device': 'd2vzw', 'model': 'SCH_I535' }]
"""
# b313b945 device usb:1-7 product:d2vzw model:SCH_I535 device:d2vzw
# from Android system/core/adb/transport.c statename()
re_device_info = re.compile(
r"([^\s]+)\s+(offline|bootloader|device|host|recovery|sideload|"
"no permissions|unauthorized|unknown)")
devices = []
lines = self.command_output(["devices", "-l"], timeout=timeout).splitlines()
for line in lines:
if line == 'List of devices attached ':
continue
match = re_device_info.match(line)
if match:
device = {
'device_serial': match.group(1),
'state': match.group(2)
}
remainder = line[match.end(2):].strip()
if remainder:
try:
device.update(dict([j.split(':')
for j in remainder.split(' ')]))
except ValueError:
self._logger.warning('devices: Unable to parse '
'remainder for device %s' % line)
devices.append(device)
for device in devices:
if device['state'] == 'no permissions':
raise ADBListDevicesError(
"No permissions to detect devices. You should restart the"
" adb server as root:\n"
"\n# adb kill-server\n# adb start-server\n"
"\nor maybe configure your udev rules.",
devices)
return devices
class ADBDevice(ADBCommand):
"""ADBDevice is an abstract base class which provides methods which
can be used to interact with the associated Android or B2G based
device. It must be used via one of the concrete implementations in
:class:`ADBAndroid` or :class:`ADBB2G`.
"""
__metaclass__ = ABCMeta
def __init__(self,
device=None,
adb='adb',
adb_host=None,
adb_port=None,
test_root='',
logger_name='adb',
timeout=300,
verbose=False,
device_ready_retry_wait=20,
device_ready_retry_attempts=3):
"""Initializes the ADBDevice object.
:param device: When a string is passed, it is interpreted as the
device serial number. This form is not compatible with
devices containing a ":" in the serial; in this case
ValueError will be raised.
When a dictionary is passed it must have one or both of
the keys "device_serial" and "usb". This is compatible
with the dictionaries in the list returned by
ADBHost.devices(). If the value of device_serial is a
valid serial not containing a ":" it will be used to
identify the device, otherwise the value of the usb key,
prefixed with "usb:" is used.
If None is passed and there is exactly one device attached
to the host, that device is used. If there is more than one
device attached, ValueError is raised. If no device is
attached the constructor will block until a device is
attached or the timeout is reached.
:type device: dict, str or None
:param adb_host: host of the adb server to connect to.
:type adb_host: str or None
:param adb_port: port of the adb server to connect to.
:type adb_port: integer or None
:param str logger_name: logging logger name. Defaults to 'adb'.
:param integer device_ready_retry_wait: number of seconds to wait
between attempts to check if the device is ready after a
reboot.
:param integer device_ready_retry_attempts: number of attempts when
checking if a device is ready.
:raises: * ADBError
* ADBTimeoutError
* ValueError
"""
ADBCommand.__init__(self, adb=adb, adb_host=adb_host,
adb_port=adb_port, logger_name=logger_name,
timeout=timeout, verbose=verbose)
self._device_serial = self._get_device_serial(device)
self._initial_test_root = test_root
self._test_root = None
self._device_ready_retry_wait = device_ready_retry_wait
self._device_ready_retry_attempts = device_ready_retry_attempts
self._have_root_shell = False
self._have_su = False
self._have_android_su = False
# Catch exceptions due to the potential for segfaults
# calling su when using an improperly rooted device.
# Note this check to see if adbd is running is performed on
# the device in the state it exists in when the ADBDevice is
# initialized. It may be the case that it has been manipulated
# since its last boot and that its current state does not
# match the state the device will have immediately after a
# reboot. For example, if adb root was called manually prior
# to ADBDevice being initialized, then self._have_root_shell
# will not reflect the state of the device after it has been
# rebooted again. Therefore this check will need to be
# performed again after a reboot.
self._check_adb_root(timeout=timeout)
uid = 'uid=0'
# Do we have a 'Superuser' sh like su?
try:
if self.shell_output("su -c id", timeout=timeout).find(uid) != -1:
self._have_su = True
self._logger.info("su -c supported")
except ADBError:
self._logger.debug("Check for su -c failed")
# Do we have Android's su?
try:
if self.shell_output("su 0 id", timeout=timeout).find(uid) != -1:
self._have_android_su = True
self._logger.info("su 0 supported")
except ADBError:
self._logger.debug("Check for su 0 failed")
self._mkdir_p = None
# Force the use of /system/bin/ls or /system/xbin/ls in case
# there is /sbin/ls which embeds ansi escape codes to colorize
# the output. Detect if we are using busybox ls. We want each
# entry on a single line and we don't want . or ..
if self.shell_bool("/system/bin/ls /data/local/tmp", timeout=timeout):
self._ls = "/system/bin/ls"
elif self.shell_bool("/system/xbin/ls /data/local/tmp", timeout=timeout):
self._ls = "/system/xbin/ls"
else:
raise ADBError("ADBDevice.__init__: ls not found")
try:
self.shell_output("%s -1A /data/local/tmp" % self._ls, timeout=timeout)
self._ls += " -1A"
except ADBError:
self._ls += " -a"
self._logger.info("%s supported" % self._ls)
# Do we have cp?
self._have_cp = self.shell_bool("type cp", timeout=timeout)
self._logger.info("Native cp support: %s" % self._have_cp)
# Do we have chmod -R?
try:
self._chmod_R = False
re_recurse = re.compile(r'[-]R')
chmod_output = self.shell_output("chmod --help", timeout=timeout)
match = re_recurse.search(chmod_output)
if match:
self._chmod_R = True
except (ADBError, ADBTimeoutError) as e:
self._logger.debug('Check chmod -R: %s' % e)
match = re_recurse.search(e.message)
if match:
self._chmod_R = True
self._logger.info("Native chmod -R support: %s" % self._chmod_R)
self._logger.debug("ADBDevice: %s" % self.__dict__)
def _get_device_serial(self, device):
if device is None:
devices = ADBHost(adb=self._adb_path, adb_host=self._adb_host,
adb_port=self._adb_port).devices()
if len(devices) > 1:
raise ValueError("ADBDevice called with multiple devices "
"attached and no device specified")
elif len(devices) == 0:
# We could error here, but this way we'll wait-for-device before we next
# run a command, which seems more friendly
return
device = devices[0]
def is_valid_serial(serial):
return ":" not in serial or serial.startswith("usb:")
if isinstance(device, (str, unicode)):
# Treat this as a device serial
if not is_valid_serial(device):
raise ValueError("Device serials containing ':' characters are "
"invalid. Pass the output from "
"ADBHost.devices() for the device instead")
return device
serial = device.get("device_serial")
if serial is not None and is_valid_serial(serial):
return serial
usb = device.get("usb")
if usb is not None:
return "usb:%s" % usb
raise ValueError("Unable to get device serial")
def _check_adb_root(self, timeout=None):
self._have_root_shell = False
uid = 'uid=0'
# Is shell already running as root?
try:
if self.shell_output("id", timeout=timeout).find(uid) != -1:
self._have_root_shell = True
self._logger.info("adbd running as root")
except ADBError:
self._logger.debug("Check for root shell failed")
# Do we need to run adb root to get a root shell?
try:
if (not self._have_root_shell and self.command_output(
["root"],
timeout=timeout).find("cannot run as root") == -1):
self._have_root_shell = True
self._logger.info("adbd restarted as root")
except ADBError:
self._logger.debug("Check for root adbd failed")
@staticmethod
def _escape_command_line(cmd):
"""Utility function to return escaped and quoted version of command
line.
"""
quoted_cmd = []
for arg in cmd:
arg.replace('&', r'\&')
needs_quoting = False
for char in [' ', '(', ')', '"', '&']:
if arg.find(char) >= 0:
needs_quoting = True
break
if needs_quoting:
arg = "'%s'" % arg
quoted_cmd.append(arg)
return " ".join(quoted_cmd)
@staticmethod
def _get_exitcode(file_obj):
"""Get the exitcode from the last line of the file_obj for shell
commands.
"""
file_obj.seek(0, os.SEEK_END)
line = ''
length = file_obj.tell()
offset = 1
while length - offset >= 0:
file_obj.seek(-offset, os.SEEK_END)
char = file_obj.read(1)
if not char:
break
if char != '\r' and char != '\n':
line = char + line
elif line:
# we have collected everything up to the beginning of the line
break
offset += 1
match = re.match(r'rc=([0-9]+)', line)
if match:
exitcode = int(match.group(1))
file_obj.seek(-1, os.SEEK_CUR)
file_obj.truncate()
else:
exitcode = None
return exitcode
@property
def test_root(self):
"""
The test_root property returns the directory on the device where
temporary test files are stored.
The first time test_root it is called it determines and caches a value
for the test root on the device. It determines the appropriate test
root by attempting to create a 'dummy' directory on each of a list of
directories and returning the first successful directory as the
test_root value.
The default list of directories checked by test_root are:
- /storage/sdcard0/tests
- /storage/sdcard1/tests
- /sdcard/tests
- /mnt/sdcard/tests
- /data/local/tests
You may override the default list by providing a test_root argument to
the :class:`ADBDevice` constructor which will then be used when
attempting to create the 'dummy' directory.
:raises: * ADBTimeoutError
* ADBRootError
* ADBError
"""
if self._test_root is not None:
return self._test_root
if self._initial_test_root:
paths = [self._initial_test_root]
else:
paths = ['/storage/sdcard0/tests',
'/storage/sdcard1/tests',
'/sdcard/tests',
'/mnt/sdcard/tests',
'/data/local/tests']
max_attempts = 3
for attempt in range(1, max_attempts + 1):
for test_root in paths:
self._logger.debug("Setting test root to %s attempt %d of %d" %
(test_root, attempt, max_attempts))
if self._try_test_root(test_root):
self._test_root = test_root
return self._test_root
self._logger.debug('_setup_test_root: '
'Attempt %d of %d failed to set test_root to %s' %
(attempt, max_attempts, test_root))
if attempt != max_attempts:
time.sleep(20)
raise ADBError("Unable to set up test root using paths: [%s]"
% ", ".join(paths))
def _try_test_root(self, test_root):
base_path, sub_path = posixpath.split(test_root)
if not self.is_dir(base_path):
return False
try:
dummy_dir = posixpath.join(test_root, 'dummy')
if self.is_dir(dummy_dir):
self.rm(dummy_dir, recursive=True)
self.mkdir(dummy_dir, parents=True)
except ADBError:
self._logger.debug("%s is not writable" % test_root)
return False
return True
# Host Command methods
def command(self, cmds, timeout=None):
"""Executes an adb command on the host against the device.
:param list cmds: The command and its arguments to be
executed.
:param timeout: The maximum time in
seconds for any spawned adb process to complete before
throwing an ADBTimeoutError. This timeout is per adb call. The
total time spent may exceed this value. If it is not
specified, the value set in the ADBDevice constructor is used.
:type timeout: integer or None
:returns: :class:`mozdevice.ADBProcess`
command() provides a low level interface for executing
commands for a specific device on the host via adb.
command() executes on the host in such a fashion that stdout
of the adb process are file handles on the host and
the exit code is available as the exit code of the adb
process.
For executing shell commands on the device, use
ADBDevice.shell(). The caller provides a list containing
commands, as well as a timeout period in seconds.
A subprocess is spawned to execute adb for the device with
stdout and stderr directed to a temporary file. If the process
takes longer than the specified timeout, the process is
terminated.
It is the caller's responsibilty to clean up by closing
the stdout temporary file.
"""
return ADBCommand.command(self, cmds,
device_serial=self._device_serial,
timeout=timeout)
def command_output(self, cmds, timeout=None):
"""Executes an adb command on the host against the device returning
stdout.
:param list cmds: The command and its arguments to be executed.
:param timeout: The maximum time in seconds
for any spawned adb process to complete before throwing
an ADBTimeoutError.
This timeout is per adb call. The total time spent
may exceed this value. If it is not specified, the value
set in the ADBDevice constructor is used.
:type timeout: integer or None
:returns: string - content of stdout.
:raises: * ADBTimeoutError
* ADBError
"""
return ADBCommand.command_output(self, cmds,
device_serial=self._device_serial,
timeout=timeout)
# Port forwarding methods
def _validate_port(self, port, is_local=True):
"""Validate a port forwarding specifier. Raises ValueError on failure.
:param str port: The port specifier to validate
:param bool is_local: Flag indicating whether the port represents a local port.
"""
prefixes = ["tcp", "localabstract", "localreserved", "localfilesystem", "dev"]
if not is_local:
prefixes += ["jdwp"]
parts = port.split(":", 1)
if len(parts) != 2 or parts[0] not in prefixes:
raise ValueError("Invalid forward specifier %s" % port)
def forward(self, local, remote, allow_rebind=True, timeout=None):
"""Forward a local port to a specific port on the device.
Ports are specified in the form:
tcp:<port>
localabstract:<unix domain socket name>
localreserved:<unix domain socket name>
localfilesystem:<unix domain socket name>
dev:<character device name>
jdwp:<process pid> (remote only)
:param str local: Local port to forward
:param str remote: Remote port to which to forward
:param bool allow_rebind: Don't error if the local port is already forwarded
:param timeout: The maximum time in seconds
for any spawned adb process to complete before throwing
an ADBTimeoutError. If it is not specified, the value
set in the ADBDevice constructor is used.
:type timeout: integer or None
:raises: * ValueError
* ADBTimeoutError
* ADBError
"""
for port, is_local in [(local, True), (remote, False)]:
self._validate_port(port, is_local=is_local)
cmd = ["forward", local, remote]
if not allow_rebind:
cmd.insert(1, "--no-rebind")
self.command_output(cmd, timeout=timeout)
def list_forwards(self, timeout=None):
"""Return a list of tuples specifying active forwards
Return values are of the form (device, local, remote).
:param timeout: The maximum time in seconds
for any spawned adb process to complete before throwing
an ADBTimeoutError. If it is not specified, the value
set in the ADBDevice constructor is used.
:type timeout: integer or None
:raises: * ADBTimeoutError
* ADBError
"""
forwards = self.command_output(["forward", "--list"], timeout=timeout)
return [tuple(line.split(" ")) for line in forwards.splitlines() if line.strip()]
def remove_forwards(self, local=None, timeout=None):
"""Remove existing port forwards.
:param local: local port specifier as for ADBDevice.forward. If local
is not specified removes all forwards.
:type local: str or None
:param timeout: The maximum time in seconds
for any spawned adb process to complete before throwing
an ADBTimeoutError. If it is not specified, the value
set in the ADBDevice constructor is used.
:type timeout: integer or None
:raises: * ValueError
* ADBTimeoutError
* ADBError
"""
cmd = ["forward"]
if local is None:
cmd.extend(["--remove-all"])
else:
self._validate_port(local, is_local=True)
cmd.extend(["--remove", local])
self.command_output(cmd, timeout=timeout)
# Device Shell methods
def shell(self, cmd, env=None, cwd=None, timeout=None, root=False):
"""Executes a shell command on the device.
:param str cmd: The command to be executed.
:param env: Contains the environment variables and
their values.
:type env: dict or None
:param cwd: The directory from which to execute.
:type cwd: str or None
:param timeout: The maximum time in
seconds for any spawned adb process to complete before
throwing an ADBTimeoutError. This timeout is per adb call. The
total time spent may exceed this value. If it is not
specified, the value set in the ADBDevice constructor is used.
:type timeout: integer or None
:param bool root: Flag specifying if the command should
be executed as root.
:returns: :class:`mozdevice.ADBProcess`
:raises: ADBRootError
shell() provides a low level interface for executing commands
on the device via adb shell.
shell() executes on the host in such as fashion that stdout
contains the stdout and stderr of the host abd process
combined with the stdout and stderr of the shell command
on the device. The exit code of shell() is the exit code of
the adb command if it was non-zero or the extracted exit code
from the output of the shell command executed on the
device.
The caller provides a flag indicating if the command is to be
executed as root, a string for any requested working