OLD | NEW |
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Provides an interface to communicate with the device via the adb command. | 5 """Provides an interface to communicate with the device via the adb command. |
6 | 6 |
7 Assumes adb binary is currently on system path. | 7 Assumes adb binary is currently on system path. |
8 """ | 8 """ |
9 | 9 |
10 import collections | 10 import collections |
(...skipping 166 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
177 if isinstance(utc_offset, str) and len(utc_offset) == 5: | 177 if isinstance(utc_offset, str) and len(utc_offset) == 5: |
178 utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]), | 178 utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]), |
179 minutes=int(utc_offset[3:5])) | 179 minutes=int(utc_offset[3:5])) |
180 if utc_offset[0:1] == '-': | 180 if utc_offset[0:1] == '-': |
181 utc_delta = -utc_delta | 181 utc_delta = -utc_delta |
182 lastmod -= utc_delta | 182 lastmod -= utc_delta |
183 files[filename] = (int(file_match.group('size')), lastmod) | 183 files[filename] = (int(file_match.group('size')), lastmod) |
184 return files | 184 return files |
185 | 185 |
186 | 186 |
187 def _ComputeFileListHash(md5sum_output): | 187 def _ParseMd5SumOutput(md5sum_output): |
188 """Returns a list of tuples from the provided md5sum output. | 188 """Returns a list of tuples from the provided md5sum output. |
189 | 189 |
190 Args: | 190 Args: |
191 md5sum_output: output directly from md5sum binary. | 191 md5sum_output: output directly from md5sum binary. |
192 | 192 |
193 Returns: | 193 Returns: |
194 List of namedtuples (hash, path). | 194 List of namedtuples with attributes |hash| and |path|, where |path| is the |
| 195 absolute path to the file with an Md5Sum of |hash|. |
195 """ | 196 """ |
196 HashAndPath = collections.namedtuple('HashAndPath', ['hash', 'path']) | 197 HashAndPath = collections.namedtuple('HashAndPath', ['hash', 'path']) |
197 split_lines = [line.split(' ') for line in md5sum_output] | 198 split_lines = [line.split(' ') for line in md5sum_output] |
198 return [HashAndPath._make(s) for s in split_lines if len(s) == 2] | 199 return [HashAndPath._make(s) for s in split_lines if len(s) == 2] |
199 | 200 |
200 | 201 |
201 def _HasAdbPushSucceeded(command_output): | 202 def _HasAdbPushSucceeded(command_output): |
202 """Returns whether adb push has succeeded from the provided output.""" | 203 """Returns whether adb push has succeeded from the provided output.""" |
203 # TODO(frankf): We should look at the return code instead of the command | 204 # TODO(frankf): We should look at the return code instead of the command |
204 # output for many of the commands in this file. | 205 # output for many of the commands in this file. |
(...skipping 206 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
411 Args: | 412 Args: |
412 apk_path: Path to .apk file to install. | 413 apk_path: Path to .apk file to install. |
413 keep_data: Reinstalls instead of uninstalling first, preserving the | 414 keep_data: Reinstalls instead of uninstalling first, preserving the |
414 application data. | 415 application data. |
415 package_name: Package name (only needed if keep_data=False). | 416 package_name: Package name (only needed if keep_data=False). |
416 reboots_on_failure: number of time to reboot if package manager is frozen. | 417 reboots_on_failure: number of time to reboot if package manager is frozen. |
417 """ | 418 """ |
418 # Check if package is already installed and up to date. | 419 # Check if package is already installed and up to date. |
419 if package_name: | 420 if package_name: |
420 installed_apk_path = self.GetApplicationPath(package_name) | 421 installed_apk_path = self.GetApplicationPath(package_name) |
421 if installed_apk_path and self.CheckMd5Sum(apk_path, installed_apk_path): | 422 if (installed_apk_path and |
| 423 not self.GetFilesChanged(apk_path, installed_apk_path)): |
422 logging.info('Skipped install: identical %s APK already installed' % | 424 logging.info('Skipped install: identical %s APK already installed' % |
423 package_name) | 425 package_name) |
424 return | 426 return |
425 # Install. | 427 # Install. |
426 reboots_left = reboots_on_failure | 428 reboots_left = reboots_on_failure |
427 while True: | 429 while True: |
428 try: | 430 try: |
429 if not keep_data: | 431 if not keep_data: |
430 assert package_name | 432 assert package_name |
431 self.Uninstall(package_name) | 433 self.Uninstall(package_name) |
(...skipping 299 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
731 self.RunShellCommand('pm clear ' + package) | 733 self.RunShellCommand('pm clear ' + package) |
732 | 734 |
733 def SendKeyEvent(self, keycode): | 735 def SendKeyEvent(self, keycode): |
734 """Sends keycode to the device. | 736 """Sends keycode to the device. |
735 | 737 |
736 Args: | 738 Args: |
737 keycode: Numeric keycode to send (see "enum" at top of file). | 739 keycode: Numeric keycode to send (see "enum" at top of file). |
738 """ | 740 """ |
739 self.RunShellCommand('input keyevent %d' % keycode) | 741 self.RunShellCommand('input keyevent %d' % keycode) |
740 | 742 |
741 def CheckMd5Sum(self, local_path, device_path): | 743 def _RunMd5Sum(self, host_path, device_path): |
742 """Compares the md5sum of a local path against a device path. | 744 """Gets the md5sum of a host path and device path. |
743 | 745 |
744 Args: | 746 Args: |
745 local_path: Path (file or directory) on the host. | 747 host_path: Path (file or directory) on the host. |
746 device_path: Path on the device. | 748 device_path: Path on the device. |
747 | 749 |
748 Returns: | 750 Returns: |
749 True if the md5sums match. | 751 A tuple containing lists of the host and device md5sum results as |
| 752 created by _ParseMd5SumOutput(). |
750 """ | 753 """ |
751 if not self._md5sum_build_dir: | 754 if not self._md5sum_build_dir: |
752 default_build_type = os.environ.get('BUILD_TYPE', 'Debug') | 755 default_build_type = os.environ.get('BUILD_TYPE', 'Debug') |
753 build_dir = '%s/%s/' % ( | 756 build_dir = '%s/%s/' % ( |
754 cmd_helper.OutDirectory().get(), default_build_type) | 757 cmd_helper.OutDirectory().get(), default_build_type) |
755 md5sum_dist_path = '%s/md5sum_dist' % build_dir | 758 md5sum_dist_path = '%s/md5sum_dist' % build_dir |
756 if not os.path.exists(md5sum_dist_path): | 759 if not os.path.exists(md5sum_dist_path): |
757 build_dir = '%s/Release/' % cmd_helper.OutDirectory().get() | 760 build_dir = '%s/Release/' % cmd_helper.OutDirectory().get() |
758 md5sum_dist_path = '%s/md5sum_dist' % build_dir | 761 md5sum_dist_path = '%s/md5sum_dist' % build_dir |
759 assert os.path.exists(md5sum_dist_path), 'Please build md5sum.' | 762 assert os.path.exists(md5sum_dist_path), 'Please build md5sum.' |
760 command = 'push %s %s' % (md5sum_dist_path, MD5SUM_DEVICE_FOLDER) | 763 command = 'push %s %s' % (md5sum_dist_path, MD5SUM_DEVICE_FOLDER) |
761 assert _HasAdbPushSucceeded(self._adb.SendCommand(command)) | 764 assert _HasAdbPushSucceeded(self._adb.SendCommand(command)) |
762 self._md5sum_build_dir = build_dir | 765 self._md5sum_build_dir = build_dir |
763 | 766 |
764 cmd = (MD5SUM_LD_LIBRARY_PATH + ' ' + self._util_wrapper + ' ' + | 767 cmd = (MD5SUM_LD_LIBRARY_PATH + ' ' + self._util_wrapper + ' ' + |
765 MD5SUM_DEVICE_PATH + ' ' + device_path) | 768 MD5SUM_DEVICE_PATH + ' ' + device_path) |
766 device_hash_tuples = _ComputeFileListHash( | 769 device_hash_tuples = _ParseMd5SumOutput( |
767 self.RunShellCommand(cmd, timeout_time=2 * 60)) | 770 self.RunShellCommand(cmd, timeout_time=2 * 60)) |
768 assert os.path.exists(local_path), 'Local path not found %s' % local_path | 771 assert os.path.exists(host_path), 'Local path not found %s' % host_path |
769 md5sum_output = cmd_helper.GetCmdOutput( | 772 md5sum_output = cmd_helper.GetCmdOutput( |
770 ['%s/md5sum_bin_host' % self._md5sum_build_dir, local_path]) | 773 ['%s/md5sum_bin_host' % self._md5sum_build_dir, host_path]) |
771 host_hash_tuples = _ComputeFileListHash(md5sum_output.splitlines()) | 774 host_hash_tuples = _ParseMd5SumOutput(md5sum_output.splitlines()) |
| 775 return (host_hash_tuples, device_hash_tuples) |
| 776 |
| 777 def GetFilesChanged(self, host_path, device_path): |
| 778 """Compares the md5sum of a host path against a device path. |
| 779 |
| 780 Note: Ignores extra files on the device. |
| 781 |
| 782 Args: |
| 783 host_path: Path (file or directory) on the host. |
| 784 device_path: Path on the device. |
| 785 |
| 786 Returns: |
| 787 A list of tuples of the form (host_path, device_path) for files whose |
| 788 md5sums do not match. |
| 789 """ |
| 790 host_hash_tuples, device_hash_tuples = self._RunMd5Sum( |
| 791 host_path, device_path) |
772 | 792 |
773 # Ignore extra files on the device. | 793 # Ignore extra files on the device. |
774 if len(device_hash_tuples) > len(host_hash_tuples): | 794 if len(device_hash_tuples) > len(host_hash_tuples): |
775 host_files = [os.path.relpath(os.path.normpath(p.path), | 795 host_files = [os.path.relpath(os.path.normpath(p.path), |
776 os.path.normpath(local_path)) for p in host_hash_tuples] | 796 os.path.normpath(host_path)) for p in host_hash_tuples] |
777 | 797 |
778 def _host_has(fname): | 798 def HostHas(fname): |
779 return any(path in fname for path in host_files) | 799 return any(path in fname for path in host_files) |
780 | 800 |
781 hashes_on_device = [h.hash for h in device_hash_tuples if | 801 device_hash_tuples = [h for h in device_hash_tuples if HostHas(h.path)] |
782 _host_has(h.path)] | |
783 else: | |
784 hashes_on_device = [h.hash for h in device_hash_tuples] | |
785 | 802 |
786 # Compare md5sums between host and device files. | 803 # Constructs the target device path from a given host path. Don't use when |
787 hashes_on_host = [h.hash for h in host_hash_tuples] | 804 # only a single file is given as the base name given in device_path may |
788 hashes_on_device.sort() | 805 # differ from that in host_path. |
789 hashes_on_host.sort() | 806 def HostToDevicePath(host_file_path): |
790 return hashes_on_device == hashes_on_host | 807 return os.path.join(os.path.dirname(device_path), os.path.relpath( |
| 808 host_file_path, os.path.dirname(os.path.normpath(host_path)))) |
791 | 809 |
792 def PushIfNeeded(self, local_path, device_path): | 810 device_hashes = [h.hash for h in device_hash_tuples] |
793 """Pushes |local_path| to |device_path|. | 811 return [(t.path, HostToDevicePath(t.path) if os.path.isdir(host_path) else |
| 812 device_path) |
| 813 for t in host_hash_tuples if t.hash not in device_hashes] |
| 814 |
| 815 def PushIfNeeded(self, host_path, device_path): |
| 816 """Pushes |host_path| to |device_path|. |
794 | 817 |
795 Works for files and directories. This method skips copying any paths in | 818 Works for files and directories. This method skips copying any paths in |
796 |test_data_paths| that already exist on the device with the same hash. | 819 |test_data_paths| that already exist on the device with the same hash. |
797 | 820 |
798 All pushed files can be removed by calling RemovePushedFiles(). | 821 All pushed files can be removed by calling RemovePushedFiles(). |
799 """ | 822 """ |
800 assert os.path.exists(local_path), 'Local path not found %s' % local_path | 823 MAX_INDIVIDUAL_PUSHES = 50 |
801 size = int(cmd_helper.GetCmdOutput(['du', '-sb', local_path]).split()[0]) | 824 assert os.path.exists(host_path), 'Local path not found %s' % host_path |
| 825 |
| 826 def GetHostSize(path): |
| 827 return int(cmd_helper.GetCmdOutput(['du', '-sb', path]).split()[0]) |
| 828 |
| 829 size = GetHostSize(host_path) |
802 self._pushed_files.append(device_path) | 830 self._pushed_files.append(device_path) |
803 self._potential_push_size += size | 831 self._potential_push_size += size |
804 | 832 |
805 if self.CheckMd5Sum(local_path, device_path): | 833 changed_files = self.GetFilesChanged(host_path, device_path) |
| 834 if not changed_files: |
806 return | 835 return |
807 | 836 |
808 self._actual_push_size += size | 837 def Push(host, device): |
809 # They don't match, so remove everything first and then create it. | 838 # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout |
810 if os.path.isdir(local_path): | 839 # of 60 seconds which isn't sufficient for a lot of users of this method. |
811 self.RunShellCommand('rm -r %s' % device_path, timeout_time=2 * 60) | 840 push_command = 'push %s %s' % (host, device) |
812 self.RunShellCommand('mkdir -p %s' % device_path) | 841 self._LogShell(push_command) |
813 | 842 |
814 # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout of | 843 # Retry push with increasing backoff if the device is busy. |
815 # 60 seconds which isn't sufficient for a lot of users of this method. | 844 retry = 0 |
816 push_command = 'push %s %s' % (local_path, device_path) | 845 while True: |
817 self._LogShell(push_command) | 846 output = self._adb.SendCommand(push_command, timeout_time=30 * 60) |
| 847 if _HasAdbPushSucceeded(output): |
| 848 return |
| 849 if retry < 3: |
| 850 retry += 1 |
| 851 wait_time = 5 * retry |
| 852 logging.error('Push failed, retrying in %d seconds: %s' % |
| 853 (wait_time, output)) |
| 854 time.sleep(wait_time) |
| 855 else: |
| 856 raise Exception('Push failed: %s' % output) |
818 | 857 |
819 # Retry push with increasing backoff if the device is busy. | 858 diff_size = 0 |
820 retry = 0 | 859 if len(changed_files) <= MAX_INDIVIDUAL_PUSHES: |
821 while True: | 860 diff_size = sum(GetHostSize(f[0]) for f in changed_files) |
822 output = self._adb.SendCommand(push_command, timeout_time=30 * 60) | 861 |
823 if _HasAdbPushSucceeded(output): | 862 # TODO(craigdh): Replace this educated guess with a heuristic that |
824 return | 863 # approximates the push time for each method. |
825 if 'resource busy' in output and retry < 3: | 864 if len(changed_files) > MAX_INDIVIDUAL_PUSHES or diff_size > 0.5 * size: |
826 retry += 1 | 865 # We're pushing everything, remove everything first and then create it. |
827 wait_time = 5 * retry | 866 self._actual_push_size += size |
828 logging.error('Push failed, retrying in %d seconds: %s' % | 867 if os.path.isdir(host_path): |
829 (wait_time, output)) | 868 self.RunShellCommand('rm -r %s' % device_path, timeout_time=2 * 60) |
830 time.sleep(wait_time) | 869 self.RunShellCommand('mkdir -p %s' % device_path) |
831 else: | 870 Push(host_path, device_path) |
832 raise Exception('Push failed: %s' % output) | 871 else: |
| 872 for f in changed_files: |
| 873 Push(f[0], f[1]) |
| 874 self._actual_push_size += diff_size |
833 | 875 |
834 def GetPushSizeInfo(self): | 876 def GetPushSizeInfo(self): |
835 """Get total size of pushes to the device done via PushIfNeeded() | 877 """Get total size of pushes to the device done via PushIfNeeded() |
836 | 878 |
837 Returns: | 879 Returns: |
838 A tuple: | 880 A tuple: |
839 1. Total size of push requests to PushIfNeeded (MB) | 881 1. Total size of push requests to PushIfNeeded (MB) |
840 2. Total size that was actually pushed (MB) | 882 2. Total size that was actually pushed (MB) |
841 """ | 883 """ |
842 return (self._potential_push_size, self._actual_push_size) | 884 return (self._potential_push_size, self._actual_push_size) |
(...skipping 629 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1472 """ | 1514 """ |
1473 def __init__(self, output): | 1515 def __init__(self, output): |
1474 self._output = output | 1516 self._output = output |
1475 | 1517 |
1476 def write(self, data): | 1518 def write(self, data): |
1477 data = data.replace('\r\r\n', '\n') | 1519 data = data.replace('\r\r\n', '\n') |
1478 self._output.write(data) | 1520 self._output.write(data) |
1479 | 1521 |
1480 def flush(self): | 1522 def flush(self): |
1481 self._output.flush() | 1523 self._output.flush() |
OLD | NEW |