__main__.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. # coding: utf-8
  2. #
  3. from __future__ import absolute_import, print_function
  4. import argparse
  5. import hashlib
  6. import logging
  7. import os
  8. import re
  9. import shutil
  10. import sys
  11. import tarfile
  12. import time
  13. import fire
  14. import humanize
  15. import progress.bar
  16. import requests
  17. from logzero import logger
  18. from retry import retry
  19. import ssat_sdk.uiautomator2 as u2
  20. from ssat_sdk.uiautomator2 import adbutils
  21. from ssat_sdk.uiautomator2.version import __apk_version__, __atx_agent_version__
  22. appdir = os.path.join(os.path.expanduser("~"), '.uiautomator2')
  23. logger.debug("use cache directory: %s", appdir)
  24. GITHUB_BASEURL = "https://github.com/openatx"
  25. class DownloadBar(progress.bar.Bar):
  26. message = "Downloading"
  27. suffix = '%(current_size)s / %(total_size)s'
  28. @property
  29. def total_size(self):
  30. return humanize.naturalsize(self.max, gnu=True)
  31. @property
  32. def current_size(self):
  33. return humanize.naturalsize(self.index, gnu=True)
  34. def cache_download(url, filename=None):
  35. """ return downloaded filepath """
  36. # check cache
  37. if not filename:
  38. filename = os.path.basename(url)
  39. storepath = os.path.join(appdir,
  40. hashlib.sha224(url.encode()).hexdigest(),
  41. filename)
  42. storedir = os.path.dirname(storepath)
  43. if not os.path.isdir(storedir):
  44. os.makedirs(storedir)
  45. if os.path.exists(storepath) and os.path.getsize(storepath) > 0:
  46. return storepath
  47. # download from url
  48. headers = {
  49. 'Accept': '*/*',
  50. 'Accept-Encoding': 'gzip, deflate, br',
  51. 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
  52. 'Connection': 'keep-alive',
  53. 'Origin': 'https://github.com',
  54. 'User-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
  55. } # yapf: disable
  56. r = requests.get(url, stream=True, headers=headers)
  57. if r.status_code != 200:
  58. raise Exception(url, "status code", r.status_code)
  59. file_size = int(r.headers.get("Content-Length"))
  60. bar = DownloadBar(filename, max=file_size)
  61. with open(storepath + '.tmp', 'wb') as f:
  62. chunk_length = 16 * 1024
  63. while 1:
  64. buf = r.raw.read(chunk_length)
  65. if not buf:
  66. break
  67. f.write(buf)
  68. bar.next(len(buf))
  69. bar.finish()
  70. shutil.move(storepath + '.tmp', storepath)
  71. return storepath
  72. class Initer():
  73. def __init__(self, device):
  74. logger.info(">>> Initial device %s", device)
  75. d = self._device = device
  76. self.sdk = d.getprop('ro.build.version.sdk')
  77. self.abi = d.getprop('ro.product.cpu.abi')
  78. self.pre = d.getprop('ro.build.version.preview_sdk')
  79. self.arch = d.getprop('ro.arch')
  80. self.abis = (d.getprop('ro.product.cpu.abilist').strip()
  81. or self.abi).split(",")
  82. self.server_addr = None
  83. def shell(self, *args):
  84. logger.debug("Shell: %s", args)
  85. return self._device.shell_output(*args)
  86. @property
  87. def apk_urls(self):
  88. for name in ["app-uiautomator.apk", "app-uiautomator-test.apk"]:
  89. yield "".join([
  90. GITHUB_BASEURL,
  91. "/android-uiautomator-server/releases/download/",
  92. __apk_version__, "/", name
  93. ])
  94. @property
  95. def atx_agent_url(self):
  96. files = {
  97. 'armeabi-v7a': 'atx-agent_{v}_linux_armv7.tar.gz',
  98. 'arm64-v8a': 'atx-agent_{v}_linux_armv7.tar.gz',
  99. 'armeabi': 'atx-agent_{v}_linux_armv6.tar.gz',
  100. 'x86': 'atx-agent_{v}_linux_386.tar.gz',
  101. }
  102. name = None
  103. for abi in self.abis:
  104. name = files.get(abi)
  105. if name:
  106. break
  107. if not name:
  108. raise Exception(
  109. "arch(%s) need to be supported yet, please report an issue in github"
  110. % self.abis)
  111. return GITHUB_BASEURL + '/atx-agent/releases/download/%s/%s' % (
  112. __atx_agent_version__, name.format(v=__atx_agent_version__))
  113. @property
  114. def minicap_urls(self):
  115. base_url = GITHUB_BASEURL + \
  116. "/stf-binaries/raw/master/node_modules/minicap-prebuilt/prebuilt/"
  117. sdk = self.sdk
  118. yield base_url + self.abi + "/lib/android-" + sdk + "/minicap.so"
  119. yield base_url + self.abi + "/bin/minicap"
  120. @property
  121. def minitouch_url(self):
  122. return ''.join([
  123. GITHUB_BASEURL + "/stf-binaries",
  124. "/raw/master/node_modules/minitouch-prebuilt/prebuilt/",
  125. self.abi + "/bin/minitouch"
  126. ])
  127. def push_url(self, url, dest=None, mode=0o755, tgz=False, extract_name=None): # yapf: disable
  128. path = cache_download(url, os.path.basename(url))
  129. if tgz:
  130. tar = tarfile.open(path, 'r:gz')
  131. path = os.path.join(os.path.dirname(path), extract_name)
  132. tar.extract(extract_name, os.path.dirname(path))
  133. if not dest:
  134. dest = "/data/local/tmp/" + os.path.basename(path)
  135. logger.debug("Push %s -> %s:0%o", url, dest, mode)
  136. self._device.sync.push(path, dest, mode=mode)
  137. return dest
  138. def is_apk_outdate(self):
  139. apk1 = self._device.package_info("com.github.uiautomator")
  140. if not apk1:
  141. return True
  142. if apk1['version_name'] != __apk_version__:
  143. return True
  144. if not self._device.package_info("com.github.uiautomator.test"):
  145. return True
  146. return False
  147. def install(self, server_addr=None):
  148. logger.info("Install minicap, minitouch")
  149. self.push_url(self.minitouch_url)
  150. if self.abi == "x86":
  151. logger.info(
  152. "abi:x86 seems to be android emulator, skip install minicap")
  153. else:
  154. for url in self.minicap_urls:
  155. self.push_url(url)
  156. logger.info(
  157. "Install com.github.uiautomator, com.github.uiautomator.test")
  158. if self.is_apk_outdate():
  159. self.shell("pm", "uninstall", "com.github.uiautomator")
  160. self.shell("pm", "uninstall", "com.github.uiautomator.test")
  161. for url in self.apk_urls:
  162. path = self.push_url(url, mode=0o644)
  163. self.shell("pm", "install", "-r", "-t", path)
  164. else:
  165. logger.info("Already installed com.github.uiautomator apks")
  166. logger.info("Install atx-agent")
  167. path = self.push_url(
  168. self.atx_agent_url, tgz=True, extract_name="atx-agent")
  169. args = [path, "server", "-d"]
  170. if server_addr:
  171. args.extend(['-t', server_addr])
  172. self.shell(path, "server", "--stop")
  173. self.shell(*args)
  174. logger.info("Check install")
  175. self.check_atx_agent_version()
  176. print("Successfully init %s" % self._device)
  177. @retry(
  178. (requests.ConnectionError, requests.ReadTimeout, requests.HTTPError),
  179. delay=.5,
  180. tries=10)
  181. def check_atx_agent_version(self):
  182. port = self._device.forward_port(7912)
  183. logger.debug("Forward: local:tcp:%d -> remote:tcp:%d", port, 7912)
  184. response = requests.get("http://127.0.0.1:%d/version" % port).text
  185. logger.debug("atx-agent version %s", response.strip())
  186. # def check_apk_installed(self, apk_version):
  187. # """ in OPPO device, if you check immediatelly, package_info will return None """
  188. # pkg_info = self.package_info("com.github.uiautomator")
  189. # if not pkg_info:
  190. # raise EnvironmentError(
  191. # "package com.github.uiautomator not installed")
  192. # if pkg_info['version_name'] != apk_version:
  193. # raise EnvironmentError(
  194. # "package com.github.uiautomator version expect \"%s\" got \"%s\""
  195. # % (apk_version, pkg_info['version_name']))
  196. # r = requests.get(
  197. # 'http://localhost:%d/version' % lport, timeout=10)
  198. # r.raise_for_status()
  199. # log.info("atx-agent version: %s", r.text)
  200. # # todo finish the retry logic
  201. # print("atx-agent output:", output.strip())
  202. # # open uiautomator2 github URL
  203. # self.shell("am", "start", "-a", "android.intent.action.VIEW",
  204. # "-d", "https://github.com/openatx/uiautomator2")
  205. class MyFire(object):
  206. def update_apk(self, ip):
  207. """ update com.github.uiautomator apk remotely """
  208. u = u2.connect(ip)
  209. apk_version = __apk_version__
  210. app_url = GITHUB_BASEURL + \
  211. '/android-uiautomator-server/releases/download/%s/app-uiautomator.apk' % apk_version
  212. app_test_url = GITHUB_BASEURL + \
  213. '/android-uiautomator-server/releases/download/%s/app-uiautomator-test.apk' % apk_version
  214. u.app_install(app_url)
  215. u.app_install(app_test_url)
  216. def clear_cache(self):
  217. logger.info("clear cache dir: %s", appdir)
  218. shutil.rmtree(appdir, ignore_errors=True)
  219. def install(self, arg1, arg2=None):
  220. """
  221. Example:
  222. install "http://some-host.apk"
  223. install "$serial" "http://some-host.apk"
  224. """
  225. if arg2 is None:
  226. device_ip, apk_url = None, arg1
  227. else:
  228. device_ip, apk_url = arg1, arg2
  229. u = u2.connect(device_ip)
  230. pkg_name = u.app_install(apk_url)
  231. print("Installed", pkg_name)
  232. def unlock(self, device_ip=None):
  233. u = u2.connect(device_ip)
  234. u.unlock()
  235. def app_stop_all(self, device_ip=None):
  236. u = u2.connect(device_ip)
  237. u.app_stop_all()
  238. def uninstall_all(self, device_ip=None):
  239. u = u2.connect(device_ip)
  240. u.app_uninstall_all(verbose=True)
  241. def identify(self, device_ip=None, theme='black'):
  242. u = u2.connect(device_ip)
  243. u.open_identify(theme)
  244. def screenshot(self, device_ip, filename):
  245. u = u2.connect(device_ip)
  246. u.screenshot(filename)
  247. def healthcheck(self, device_ip):
  248. u = u2.connect(device_ip)
  249. u.healthcheck()
  250. def cmd_init(args):
  251. if args.serial:
  252. device = adbutils.adb.device_with_serial(args.serial)
  253. init = Initer(device)
  254. init.install(args.server)
  255. else:
  256. for device in adbutils.adb.iter_device():
  257. init = Initer(device)
  258. init.install(args.server)
  259. def cmd_screenshot(args):
  260. raise NotImplementedError()
  261. _commands = [
  262. dict(
  263. action=cmd_init,
  264. command="init",
  265. help="install enssential resources to device",
  266. flags=[
  267. dict(args=['--serial', '-s'], type=str, help='serial number'),
  268. dict(name=['server'], type=str, help='atxserver address')
  269. ]),
  270. dict(
  271. action=cmd_screenshot,
  272. command="screenshot",
  273. help="not implemented",
  274. flags=[
  275. dict(args=['-o', '--output'], type=str, help="output filename")
  276. ])
  277. ]
  278. def main():
  279. # yapf: disable
  280. if True or len(sys.argv) >= 2 and sys.argv[1] in ('init', 'screenshot'):
  281. parser = argparse.ArgumentParser(
  282. formatter_class=argparse.ArgumentDefaultsHelpFormatter)
  283. parser.add_argument('-s', '--serial', type=str,
  284. help='device serial number')
  285. subparser = parser.add_subparsers(dest='subparser')
  286. actions = {}
  287. for c in _commands:
  288. cmd_name = c['command']
  289. actions[cmd_name] = c['action']
  290. sp = subparser.add_parser(cmd_name, help=c.get('help'),
  291. formatter_class=argparse.ArgumentDefaultsHelpFormatter)
  292. for f in c.get('flags', []):
  293. args = f.get('args')
  294. if not args:
  295. args = ['-'*min(2, len(n)) + n for n in f['name']]
  296. kwargs = f.copy()
  297. kwargs.pop('name', None)
  298. kwargs.pop('args', None)
  299. sp.add_argument(*args, **kwargs)
  300. args = parser.parse_args()
  301. print(args, args.subparser)
  302. if args.subparser:
  303. actions[args.subparser](args)
  304. return
  305. # yapf: enable
  306. fire.Fire(MyFire)
  307. if __name__ == '__main__':
  308. # import logzero
  309. # logzero.loglevel(logging.INFO)
  310. main()