Bug 1328301 - handle push/pull directory semantics changes in adb 1.0.36 for adb.py, r=gbrown.
authorBob Clary <bclary@bclary.com>
Sat, 21 Jan 2017 09:49:56 -0800
changeset 375545 e78066796efe05df7b2229e759fa9eb63dc3ec14
parent 375544 81824fdf77fb731a127aee0b55fdcbd00e084c25
child 375546 92f215ed4df341c2dc316ac7c6ff6fb4735fb95c
push id6996
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 20:48:21 +0000
treeherdermozilla-beta@d89512dab048 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgbrown
bugs1328301
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1328301 - handle push/pull directory semantics changes in adb 1.0.36 for adb.py, r=gbrown.
testing/mozbase/mozdevice/mozdevice/adb.py
--- a/testing/mozbase/mozdevice/mozdevice/adb.py
+++ b/testing/mozbase/mozdevice/mozdevice/adb.py
@@ -1,21 +1,23 @@
 # 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
@@ -144,25 +146,29 @@ class ADBCommand(object):
 
         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:
-            subprocess.Popen([adb, 'help'],
-                             stdout=subprocess.PIPE,
-                             stderr=subprocess.PIPE).communicate()
+            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)
@@ -1713,18 +1719,52 @@ class ADBDevice(ADBCommand):
             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
         :raises: * ADBTimeoutError
                  * ADBError
         """
-        self.command_output(["push", os.path.realpath(local), remote],
-                            timeout=timeout)
+        # remove trailing /
+        local = os.path.normpath(local)
+        remote = os.path.normpath(remote)
+        copy_required = False
+        if self._adb_version >= '1.0.36' and \
+           os.path.isdir(local) and self.is_dir(remote):
+            # See do_sync_push in
+            # https://android.googlesource.com/platform/system/core/+/master/adb/file_sync_client.cpp
+            # Work around change in behavior in adb 1.0.36 where if
+            # the remote destination directory exists, adb push will
+            # copy the source directory *into* the destination
+            # directory otherwise it will copy the source directory
+            # *onto* the destination directory.
+            #
+            # If the destination directory does exist, push to its
+            # parent directory.  If the source and destination leaf
+            # directory names are different, copy the source directory
+            # to a temporary directory with the same leaf name as the
+            # destination so that when we push to the parent, the
+            # source is copied onto the destination directory.
+            local_name = os.path.basename(local)
+            remote_name = os.path.basename(remote)
+            if local_name != remote_name:
+                copy_required = True
+                temp_parent = tempfile.mkdtemp()
+                new_local = os.path.join(temp_parent, remote_name)
+                dir_util.copy_tree(local, new_local)
+                local = new_local
+            remote = '/'.join(remote.rstrip('/').split('/')[:-1])
+        try:
+            self.command_output(["push", local, remote], timeout=timeout)
+        except:
+            raise
+        finally:
+            if copy_required:
+                shutil.rmtree(temp_parent)
 
     def pull(self, remote, local, timeout=None):
         """Pulls a file or directory from the device.
 
         :param str remote: The path of the remote file or
             directory.
         :param str local: The path of the local file or
             directory name.
@@ -1733,18 +1773,52 @@ class ADBDevice(ADBCommand):
             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
         :raises: * ADBTimeoutError
                  * ADBError
         """
-        self.command_output(["pull", remote, os.path.realpath(local)],
-                            timeout=timeout)
+        # remove trailing /
+        local = os.path.normpath(local)
+        remote = os.path.normpath(remote)
+        copy_required = False
+        original_local = local
+        if self._adb_version >= '1.0.36' and \
+           os.path.isdir(local) and self.is_dir(remote):
+            # See do_sync_pull in
+            # https://android.googlesource.com/platform/system/core/+/master/adb/file_sync_client.cpp
+            # Work around change in behavior in adb 1.0.36 where if
+            # the local destination directory exists, adb pull will
+            # copy the source directory *into* the destination
+            # directory otherwise it will copy the source directory
+            # *onto* the destination directory.
+            #
+            # If the destination directory does exist, pull to its
+            # parent directory. If the source and destination leaf
+            # directory names are different, pull the source directory
+            # into a temporary directory and then copy the temporary
+            # directory onto the destination.
+            local_name = os.path.basename(local)
+            remote_name = os.path.basename(remote)
+            if local_name != remote_name:
+                copy_required = True
+                temp_parent = tempfile.mkdtemp()
+                local = os.path.join(temp_parent, remote_name)
+            else:
+                local = '/'.join(local.rstrip('/').split('/')[:-1])
+        try:
+            self.command_output(["pull", remote, local], timeout=timeout)
+        except:
+            raise
+        finally:
+            if copy_required:
+                dir_util.copy_tree(local, original_local)
+                shutil.rmtree(temp_parent)
 
     def rm(self, path, recursive=False, force=False, timeout=None, root=False):
         """Delete files or directories on the device.
 
         :param str path: The path of the remote file or directory.
         :param bool recursive: Flag specifying if the command is
             to be applied recursively to the target. Default is False.
         :param bool force: Flag which if True will not raise an