Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(117)

Side by Side Diff: build/android/pylib/android_commands.py

Issue 21307002: [android] Push only updated files in PushIfNeeded when few files have changed. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: nits Created 7 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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()
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698