class AutomatorServer(object): """start and quit rpc server on device. """ __jar_files = { "bundle.jar": "libs/bundle.jar", "uiautomator-stub.jar": "libs/uiautomator-stub.jar" } __apk_files = ["libs/app-uiautomator.apk", "libs/app-uiautomator-test.apk"] # Used for check if installed __apk_vercode = 2 __apk_pkgname = 'com.github.uiautomator' __apk_pkgname_test = 'com.github.uiautomator.test' __sdk = 0 __httpsession = requests.Session() # use a standalone session handlers = NotFoundHandler() # handler UI Not Found exception def __init__(self, serial=None, local_port=None, device_port=None, adb_server_host=None, adb_server_port=None): self.uiautomator_process = None self.adb = Adb(serial=serial, adb_server_host=adb_server_host, adb_server_port=adb_server_port) self.device_port = int(device_port) if device_port else DEVICE_PORT self.__local_port = local_port def get_forwarded_port(self): for s, lp, rp in self.adb.forward_list(): if s == self.adb.device_serial( ) and rp == 'tcp:%d' % self.device_port: return int(lp[4:]) return None @property def local_port(self): if self.__local_port: return self.__local_port for i in range(10): # Max retry 10 times forwarded_port = self.get_forwarded_port() if forwarded_port: self.__local_port = forwarded_port return self.__local_port port = next_local_port(self.adb.adb_server_host) self.adb.forward(port, self.device_port, rebind=False) raise RuntimeError("Error run: adb forward tcp:<any> tcp:%d" % self.device_port) def push(self): base_dir = os.path.dirname(__file__) for jar, url in self.__jar_files.items(): filename = os.path.join(base_dir, url) self.adb.run_cmd("push", filename, "/data/local/tmp/") return list(self.__jar_files.keys()) def need_install(self): pkginfo = self.adb.package_info(self.__apk_pkgname) if pkginfo is None: return True if pkginfo['version_code'] != self.__apk_vercode: return True if self.adb.package_info(self.__apk_pkgname_test) is None: return True return False def install(self): base_dir = os.path.dirname(__file__) if self.need_install(): debug_print("install apks", self.__apk_files) for apk in self.__apk_files: self.adb.cmd("install", "-r", os.path.join(base_dir, apk)).wait() else: debug_print("already installed, skip") @property def jsonrpc(self): return self.jsonrpc_wrap( timeout=int(os.environ.get("jsonrpc_timeout", 90))) def jsonrpc_wrap(self, timeout): server = self error_code_base = -32000 def _JsonRPCMethod(url, method, timeout, restart=True): _method_obj = JsonRPCMethod(url, method, timeout) _URLError = requests.exceptions.ConnectionError def wrapper(*args, **kwargs): try: return _method_obj(*args, **kwargs) except (_URLError, socket.error) as e: if restart: debug_print('restart') server.stop() server.start(timeout=30) return _JsonRPCMethod(url, method, timeout, False)(*args, **kwargs) else: raise except JsonRPCError as e: debug_print('rpc error', e.code, e.message) raise # if e.code >= error_code_base - 1: # server.stop() # server.start(timeout=10) # return _method_obj(*args, **kwargs) # elif e.code == error_code_base - 2 and self.handlers['on']: # Not Found # try: # self.handlers['on'] = False # # any handler returns True will break the left handlers # any(handler(self.handlers.get('device', None)) for handler in self.handlers['handlers']) # finally: # self.handlers['on'] = True # return _method_obj(*args, **kwargs) # raise return wrapper return JsonRPCClient(self.rpc_uri, timeout=timeout, method_class=_JsonRPCMethod) def __jsonrpc(self): return JsonRPCClient(self.rpc_uri, timeout=int(os.environ.get("JSONRPC_TIMEOUT", 90))) def sdk_version(self): '''sdk version of connected device.''' if self.__sdk == 0: try: self.__sdk = int( self.adb.cmd("shell", "getprop", "ro.build.version.sdk").communicate() [0].decode("utf-8").strip()) except: pass return self.__sdk def ro_product(self): return self.adb.shell("getprop", "ro.build.product").strip() def ro_manufacturer(self): return self.adb.shell("getprop", "ro.product.manufacturer").strip().lower() def start(self, timeout=5): # 对应关系列表 # http://www.cnblogs.com/lipeineng/archive/2017/01/06/6257859.html # Android 4.3 (sdk=18) # Android 5.0 (sdk=21) debug_print('sdk version(instrument>=18)', self.sdk_version()) debug_print('product', self.ro_product()) debug_print('manufacturer', self.ro_manufacturer()) if self.sdk_version() >= 100: self.install() cmd = [ "shell", "am", "instrument", "-w", "-e", "debug", "false", "-e", "class", "com.github.uiautomator.stub.Stub", "com.github.uiautomator.test/android.support.test.runner.AndroidJUnitRunner" ] else: files = self.push() cmd = list( itertools.chain(["shell", "uiautomator", "runtest"], files, ["-c", "com.github.uiautomatorstub.Stub"])) debug_print('$ ' + subprocess.list2cmdline(list(cmd))) self.uiautomator_process = self.adb.cmd(*cmd) self.adb.forward(self.local_port, self.device_port) while not self.alive and timeout > 0: time.sleep(0.2) timeout -= 0.2 debug_print('poll', self.uiautomator_process.poll()) if self.uiautomator_process.poll() is not None: stdout = self.uiautomator_process.stdout.read() raise IOError("uiautomator start failed: " + stdout) if not self.alive: raise IOError("RPC server not started!") def ping(self): try: return self.__jsonrpc().ping() except: return None def info(self): try: return self.__jsonrpc().deviceInfo() except: return False @property def alive(self): '''Check if the rpc server is alive.''' return self.ping() == "pong" and self.info() # return self.ping() == "pong" def stop(self): '''Stop the rpc server.''' if self.uiautomator_process and self.uiautomator_process.poll( ) is None: res = None try: res = requests.get(self.stop_uri) self.uiautomator_process.wait() except: self.uiautomator_process.kill() finally: if res is not None: res.close() self.uiautomator_process = None try: out = self.adb.cmd("shell", "ps", "-C", "uiautomator").communicate()[0].decode( "utf-8").strip().splitlines() if out: index = out[0].split().index("PID") for line in out[1:]: if len(line.split()) > index: self.adb.cmd("shell", "kill", "-9", line.split()[index]).wait() except: pass @property def stop_uri(self): return "http://%s:%d/stop" % (self.adb.adb_server_host, self.local_port) @property def rpc_uri(self): return "http://%s:%d/jsonrpc/0" % (self.adb.adb_server_host, self.local_port) @property def screenshot_uri(self): return "http://%s:%d/screenshot/0" % (self.adb.adb_server_host, self.local_port) def screenshot(self, filename=None, scale=1.0, quality=100): # since sdk version is always great 18, so no check here # also can not use requests.Session, this will break /jsonrpc/0 requests try: r = self.__httpsession.get(self.screenshot_uri, params=dict(scale=scale, quality=quality), timeout=30) if filename: with open(filename, 'wb') as f: f.write(r.content) return filename else: return r.content except: pass
class AutomatorServer(object): """start and quit rpc server on device. """ __jar_files = { "bundle.jar": "libs/bundle.jar", "uiautomator-stub.jar": "libs/uiautomator-stub.jar" } __apk_files = ["libs/app-uiautomator.apk", "libs/app-uiautomator-test.apk"] # Used for check if installed # 2: fix bug # 3: screenrecord support __apk_vercode = 3 __apk_pkgname = 'com.github.uiautomator' __apk_pkgname_test = 'com.github.uiautomator.test' __sdk = 0 __httpsession = requests.Session() # use a standalone session handlers = NotFoundHandler() # handler UI Not Found exception def __init__(self, serial=None, local_port=None, device_port=None, adb_server_host=None, adb_server_port=None): self.uiautomator_process = None self.adb = Adb(serial=serial, adb_server_host=adb_server_host, adb_server_port=adb_server_port) self.device_port = int(device_port) if device_port else DEVICE_PORT self.__local_port = local_port def get_forwarded_port(self): for s, lp, rp in self.adb.forward_list(): if s == self.adb.device_serial() and rp == 'tcp:%d' % self.device_port: return int(lp[4:]) return None @property def local_port(self): if self.__local_port: return self.__local_port for i in range(10): # Max retry 10 times forwarded_port = self.get_forwarded_port() if forwarded_port: self.__local_port = forwarded_port return self.__local_port port = next_local_port(self.adb.adb_server_host) self.adb.forward(port, self.device_port, rebind=False) raise RuntimeError("Error run: adb forward tcp:<any> tcp:%d" % self.device_port) def push(self): base_dir = os.path.dirname(__file__) for jar, url in self.__jar_files.items(): filename = os.path.join(base_dir, url) self.adb.run_cmd("push", filename, "/data/local/tmp/") return list(self.__jar_files.keys()) def need_install(self): pkginfo = self.adb.package_info(self.__apk_pkgname) if pkginfo is None: return True if pkginfo['version_code'] != self.__apk_vercode: return True if self.adb.package_info(self.__apk_pkgname_test) is None: return True return False def install(self): base_dir = os.path.dirname(__file__) if self.need_install(): debug_print("install apks", self.__apk_files) for apk in self.__apk_files: self.adb.cmd("install", "-r", os.path.join(base_dir, apk)).wait() else: debug_print("already installed, skip") def uninstall(self): self.adb.cmd("uninstall", self.__apk_pkgname) self.adb.cmd("uninstall", self.__apk_pkgname_test) @property def jsonrpc(self): return self.jsonrpc_wrap(timeout=int(os.environ.get("jsonrpc_timeout", 90))) def jsonrpc_wrap(self, timeout): server = self error_code_base = -32000 def _JsonRPCMethod(url, method, timeout, restart=True): _method_obj = JsonRPCMethod(url, method, timeout) _URLError = requests.exceptions.ConnectionError def wrapper(*args, **kwargs): try: return _method_obj(*args, **kwargs) except (_URLError, socket.error) as e: if restart: debug_print('restart') server.stop() server.start(timeout=30) return _JsonRPCMethod(url, method, timeout, False)(*args, **kwargs) else: raise except JsonRPCError as e: debug_print('rpc error', e.code, e.message) if 'UiAutomation not connected' in e.message: self.uninstall() return _JsonRPCMethod(url, method, timeout, False)(*args, **kwargs) else: # if e.message raise # if e.code >= error_code_base - 1: # server.stop() # server.start(timeout=10) # return _method_obj(*args, **kwargs) # elif e.code == error_code_base - 2 and self.handlers['on']: # Not Found # try: # self.handlers['on'] = False # # any handler returns True will break the left handlers # any(handler(self.handlers.get('device', None)) for handler in self.handlers['handlers']) # finally: # self.handlers['on'] = True # return _method_obj(*args, **kwargs) # raise return wrapper return JsonRPCClient(self.rpc_uri, timeout=timeout, method_class=_JsonRPCMethod) def __jsonrpc(self): return JsonRPCClient(self.rpc_uri, timeout=int(os.environ.get("JSONRPC_TIMEOUT", 90))) def sdk_version(self): '''sdk version of connected device.''' if self.__sdk == 0: try: self.__sdk = int(self.adb.cmd("shell", "getprop", "ro.build.version.sdk").communicate()[0].decode("utf-8").strip()) except: pass return self.__sdk def ro_product(self): return self.adb.shell("getprop", "ro.build.product").strip() def ro_manufacturer(self): return self.adb.shell("getprop", "ro.product.manufacturer").strip().lower() def start(self, timeout=5): # 对应关系列表 # http://www.cnblogs.com/lipeineng/archive/2017/01/06/6257859.html # Android 4.3 (sdk=18) # Android 5.0 (sdk=21) debug_print('sdk version(instrument>=18)', self.sdk_version()) debug_print('product', self.ro_product()) debug_print('manufacturer', self.ro_manufacturer()) if self.sdk_version() >= 18: self.install() cmd = ["shell", "am", "instrument", "-w", "-r", "-e", "debug", "false", "-e", "class", "com.github.uiautomator.stub.Stub", "com.github.uiautomator.test/android.support.test.runner.AndroidJUnitRunner"] else: files = self.push() cmd = list(itertools.chain( ["shell", "uiautomator", "runtest"], files, ["-c", "com.github.uiautomatorstub.Stub"] )) debug_print('$ ' + subprocess.list2cmdline(list(cmd))) self.uiautomator_process = self.adb.cmd(*cmd) self.adb.forward(self.local_port, self.device_port) while not self.alive and timeout > 0: time.sleep(0.2) timeout -= 0.2 debug_print('poll', self.uiautomator_process.poll()) if self.uiautomator_process.poll() is not None: stdout = self.uiautomator_process.stdout.read() raise IOError("uiautomator start failed: " + str(stdout)) if not self.alive: raise IOError("RPC server not started!") def ping(self): try: return self.__jsonrpc().ping() except: return None def info(self): try: return self.__jsonrpc().deviceInfo() except: return False @property def alive(self): '''Check if the rpc server is alive.''' return self.ping() == "pong" and self.info() # return self.ping() == "pong" def stop(self): '''Stop the rpc server.''' if self.uiautomator_process and self.uiautomator_process.poll() is None: res = None try: res = requests.get(self.stop_uri) self.uiautomator_process.wait() except: self.uiautomator_process.kill() finally: if res is not None: res.close() self.uiautomator_process = None try: out = self.adb.cmd("shell", "ps", "-C", "uiautomator").communicate()[0].decode("utf-8").strip().splitlines() if out: index = out[0].split().index("PID") for line in out[1:]: if len(line.split()) > index: self.adb.cmd("shell", "kill", "-9", line.split()[index]).wait() except: pass @property def stop_uri(self): return "http://%s:%d/stop" % (self.adb.adb_server_host, self.local_port) @property def rpc_uri(self): return "http://%s:%d/jsonrpc/0" % (self.adb.adb_server_host, self.local_port) @property def screenshot_uri(self): return "http://%s:%d/screenshot/0" % (self.adb.adb_server_host, self.local_port) def screenshot(self, filename=None, scale=1.0, quality=100): # since sdk version is always great 18, so no check here # also can not use requests.Session, this will break /jsonrpc/0 requests try: r = self.__httpsession.get(self.screenshot_uri, params=dict(scale=scale, quality=quality), timeout=30) if filename: with open(filename, 'wb') as f: f.write(r.content) return filename else: return r.content except: pass
def test_forward(self): adb = Adb() adb.cmd = MagicMock() adb.forward(90, 91) adb.cmd.assert_called_once_with("forward", "tcp:90", "tcp:91") adb.cmd.return_value.wait.assert_called_once_with()
class AutomatorServer(object): """start and quit rpc server on device. """ __jar_files = { "bundle.jar": "libs/bundle.jar", "uiautomator-stub.jar": "libs/uiautomator-stub.jar" } __apk_files = ["libs/app-uiautomator.apk", "libs/app-uiautomator-test.apk"] # Used for check if installed __apk_vercode = 1 __apk_pkgname = 'com.github.uiautomator' __apk_pkgname_test = 'com.github.uiautomator.test' __sdk = 0 handlers = NotFoundHandler() # handler UI Not Found exception def __init__(self, serial=None, local_port=None, device_port=None, adb_server_host=None, adb_server_port=None): self.uiautomator_process = None self.adb = Adb(serial=serial, adb_server_host=adb_server_host, adb_server_port=adb_server_port) self.device_port = int(device_port) if device_port else DEVICE_PORT if local_port: self.local_port = local_port else: try: # first we will try to use the local port already adb forwarded for s, lp, rp in self.adb.forward_list(): if s == self.adb.device_serial( ) and rp == 'tcp:%d' % self.device_port: self.local_port = int(lp[4:]) break else: self.local_port = next_local_port(adb_server_host) except: self.local_port = next_local_port(adb_server_host) def push(self): base_dir = os.path.dirname(__file__) for jar, url in self.__jar_files.items(): filename = os.path.join(base_dir, url) self.adb.cmd("push", filename, "/data/local/tmp/").wait() return list(self.__jar_files.keys()) def need_install(self): pkginfo = self.adb.package_info(self.__apk_pkgname) if pkginfo is None: return True if pkginfo['version_code'] != self.__apk_vercode: return True if self.adb.package_info(self.__apk_pkgname_test) is None: return True return False def install(self): base_dir = os.path.dirname(__file__) if self.need_install(): for apk in self.__apk_files: self.adb.cmd("install", "-rt", os.path.join(base_dir, apk)).wait() @property def jsonrpc(self): return self.jsonrpc_wrap( timeout=int(os.environ.get("jsonrpc_timeout", 90))) def jsonrpc_wrap(self, timeout): server = self error_code_base = -32000 def _JsonRPCMethod(url, method, timeout, restart=True): _method_obj = JsonRPCMethod(url, method, timeout) _URLError = requests.exceptions.ConnectionError def wrapper(*args, **kwargs): try: return _method_obj(*args, **kwargs) except (_URLError, socket.error, HTTPException) as e: if restart: server.stop() server.start(timeout=30) return _JsonRPCMethod(url, method, timeout, False)(*args, **kwargs) else: raise except JsonRPCError as e: if e.code >= error_code_base - 1: server.stop() server.start() return _method_obj(*args, **kwargs) elif e.code == error_code_base - 2 and self.handlers[ 'on']: # Not Found try: self.handlers['on'] = False # any handler returns True will break the left handlers any( handler(self.handlers.get('device', None)) for handler in self.handlers['handlers']) finally: self.handlers['on'] = True return _method_obj(*args, **kwargs) raise return wrapper return JsonRPCClient(self.rpc_uri, timeout=timeout, method_class=_JsonRPCMethod) def __jsonrpc(self): return JsonRPCClient(self.rpc_uri, timeout=int(os.environ.get("JSONRPC_TIMEOUT", 90))) def sdk_version(self): '''sdk version of connected device.''' if self.__sdk == 0: try: self.__sdk = int( self.adb.cmd("shell", "getprop", "ro.build.version.sdk").communicate() [0].decode("utf-8").strip()) except: pass return self.__sdk def start(self, timeout=5): # always use uiautomator runtest # This is because use am instrument can not found a way to stop it # this will stuck when Automator not started error occurs if True or self.sdk_version() < 18: # FIXME(ssx): hot fix here files = self.push() cmd = list( itertools.chain(["shell", "uiautomator", "runtest"], files, ["-c", "com.github.uiautomatorstub.Stub"])) else: self.install() cmd = [ "shell", "am", "instrument", "-w", "com.github.uiautomator.test/android.support.test.runner.AndroidJUnitRunner" ] self.uiautomator_process = self.adb.cmd(*cmd) self.adb.forward(self.local_port, self.device_port) while not self.alive and timeout > 0: time.sleep(0.1) timeout -= 0.1 if not self.alive: raise IOError("RPC server not started!") def ping(self): try: return self.__jsonrpc().ping() except: return None @property def alive(self): '''Check if the rpc server is alive.''' return self.ping() == "pong" def stop(self): '''Stop the rpc server.''' if self.uiautomator_process and self.uiautomator_process.poll( ) is None: res = None try: res = urllib2.urlopen(self.stop_uri) self.uiautomator_process.wait() except: self.uiautomator_process.kill() finally: if res is not None: res.close() self.uiautomator_process = None try: out = self.adb.cmd("shell", "ps", "-C", "uiautomator").communicate()[0].decode( "utf-8").strip().splitlines() if out: index = out[0].split().index("PID") for line in out[1:]: if len(line.split()) > index: self.adb.cmd("shell", "kill", "-9", line.split()[index]).wait() except: pass @property def stop_uri(self): return "http://%s:%d/stop" % (self.adb.adb_server_host, self.local_port) @property def rpc_uri(self): return "http://%s:%d/jsonrpc/0" % (self.adb.adb_server_host, self.local_port) @property def screenshot_uri(self): return "http://%s:%d/screenshot/0" % (self.adb.adb_server_host, self.local_port) def screenshot(self, filename=None, scale=1.0, quality=100): if self.sdk_version() >= 18: try: req = urllib2.Request("%s?scale=%f&quality=%f" % (self.screenshot_uri, scale, quality)) result = urllib2.urlopen(req, timeout=30) if filename: with open(filename, 'wb') as f: f.write(result.read()) return filename else: return result.read() except: pass return None