adbutils.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. # coding: utf-8
  2. #
  3. # Refs: https://github.com/openatx/uiautomator2/blob/d08e2e8468/uiautomator2/adbutils.py
  4. from __future__ import print_function
  5. import datetime
  6. import os
  7. import re
  8. import socket
  9. import stat
  10. import struct
  11. import subprocess
  12. import time
  13. from collections import namedtuple
  14. from contextlib import contextmanager
  15. import six
  16. import whichcraft
  17. _OKAY = "OKAY"
  18. _FAIL = "FAIL"
  19. _DENT = "DENT" # Directory Entity
  20. _DONE = "DONE"
  21. DeviceItem = namedtuple("Device", ["serial", "status"])
  22. ForwardItem = namedtuple("ForwardItem", ["serial", "local", "remote"])
  23. FileInfo = namedtuple("FileInfo", ['mode', 'size', 'mtime', 'name'])
  24. def get_free_port():
  25. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  26. s.bind(('localhost', 0))
  27. try:
  28. return s.getsockname()[1]
  29. finally:
  30. s.close()
  31. def adb_path():
  32. path = whichcraft.which("adb")
  33. if path is None:
  34. raise EnvironmentError(
  35. "Can't find the adb, please install adb on your PC")
  36. return path
  37. class AdbError(Exception):
  38. """ adb error """
  39. class _AdbStreamConnection(object):
  40. def __init__(self, host=None, port=None):
  41. # assert isinstance(host, six.string_types)
  42. # assert isinstance(port, int)
  43. self.__host = host
  44. self.__port = port
  45. self.__conn = None
  46. self._connect()
  47. def _connect(self):
  48. adb_host = self.__host or os.environ.get("ANDROID_ADB_SERVER_HOST",
  49. "127.0.0.1")
  50. adb_port = self.__port or int(
  51. os.environ.get("ANDROID_ADB_SERVER_PORT", 5037))
  52. self.__conn
  53. s = self.__conn = socket.socket()
  54. s.connect((adb_host, adb_port))
  55. return self
  56. def close(self):
  57. self.conn.close()
  58. def __enter__(self):
  59. return self
  60. def __exit__(self, exc_type, exc, traceback):
  61. self.close()
  62. @property
  63. def conn(self):
  64. return self.__conn
  65. def send(self, cmd):
  66. assert isinstance(cmd, six.string_types)
  67. self.conn.send("{:04x}{}".format(len(cmd), cmd).encode("utf-8"))
  68. def read(self, n):
  69. assert isinstance(n, int)
  70. return self.conn.recv(n).decode()
  71. def read_raw(self, n):
  72. assert isinstance(n, int)
  73. t = n
  74. buffer = b''
  75. while t > 0:
  76. chunk = self.conn.recv(t)
  77. if not chunk:
  78. break
  79. buffer += chunk
  80. t = n - len(buffer)
  81. return buffer
  82. def read_string(self):
  83. size = int(self.read(4), 16)
  84. return self.read(size)
  85. def read_until_close(self):
  86. content = ""
  87. while True:
  88. chunk = self.read(4096)
  89. if not chunk:
  90. break
  91. content += chunk
  92. return content
  93. def check_okay(self):
  94. data = self.read(4)
  95. if data == _FAIL:
  96. raise AdbError(self.read_string())
  97. elif data == _OKAY:
  98. return
  99. raise AdbError("Unknown data: %s" % data)
  100. class AdbClient(object):
  101. def connect(self):
  102. return _AdbStreamConnection()
  103. def server_version(self):
  104. """ 40 will match 1.0.40
  105. Returns:
  106. int
  107. """
  108. with self.connect() as c:
  109. c.send("host:version")
  110. c.check_okay()
  111. return int(c.read_string(), 16)
  112. def shell(self, serial, command):
  113. """Run shell in android and return output
  114. Args:
  115. serial (str)
  116. command: list, tuple or str
  117. Returns:
  118. str
  119. """
  120. assert isinstance(serial, six.string_types)
  121. if isinstance(command, (list, tuple)):
  122. command = subprocess.list2cmdline(command)
  123. assert isinstance(command, six.string_types)
  124. with self.connect() as c:
  125. c.send("host:transport:" + serial)
  126. c.check_okay()
  127. c.send("shell:" + command)
  128. c.check_okay()
  129. return c.read_until_close()
  130. def forward_list(self, serial=None):
  131. list_cmd = "host:list-forward"
  132. if serial:
  133. list_cmd = "host-serial:{}:list-forward".format(serial)
  134. with self.connect() as c:
  135. c.send(list_cmd)
  136. c.check_okay()
  137. content = c.read_string()
  138. for line in content.splitlines():
  139. parts = line.split()
  140. if len(parts) != 3:
  141. continue
  142. yield ForwardItem(*parts)
  143. def forward(self, serial, local, remote, norebind=False):
  144. """
  145. Args:
  146. serial (str): device serial
  147. local, remote (str): tcp:<port> or localabstract:<name>
  148. norebind (bool): fail if already forwarded when set to true
  149. Raises:
  150. AdbError
  151. Protocol: 0036host-serial:6EB0217704000486:forward:tcp:5533;tcp:9182
  152. """
  153. with self.connect() as c:
  154. cmds = ["host-serial", serial, "forward"]
  155. if norebind:
  156. cmds.append("norebind")
  157. cmds.append(local + ";" + remote)
  158. c.send(":".join(cmds))
  159. c.check_okay()
  160. def iter_device(self):
  161. """
  162. Returns:
  163. list of DeviceItem
  164. """
  165. with self.connect() as c:
  166. c.send("host:devices")
  167. c.check_okay()
  168. output = c.read_string()
  169. for line in output.splitlines():
  170. parts = line.strip().split("\t")
  171. if len(parts) != 2:
  172. continue
  173. if parts[1] == 'device':
  174. yield AdbDevice(self, parts[0])
  175. def devices(self):
  176. return list(self.iter_device())
  177. def must_one_device(self):
  178. ds = self.devices()
  179. if len(ds) == 0:
  180. raise RuntimeError("Can't find any android device/emulator")
  181. if len(ds) > 1:
  182. raise RuntimeError(
  183. "more than one device/emulator, please specify the serial number"
  184. )
  185. return ds[0]
  186. def device_with_serial(self, serial=None):
  187. if not serial:
  188. return self.must_one_device()
  189. return AdbDevice(self, serial)
  190. def sync(self, serial):
  191. return Sync(self, serial)
  192. class AdbDevice(object):
  193. def __init__(self, client, serial):
  194. self._client = client
  195. self._serial = serial
  196. @property
  197. def serial(self):
  198. return self._serial
  199. def __repr__(self):
  200. return "AdbDevice(serial={})".format(self.serial)
  201. @property
  202. def sync(self):
  203. return Sync(self._client, self.serial)
  204. def adb_output(self, *args, **kwargs):
  205. """Run adb command and get its content
  206. Returns:
  207. string of output
  208. Raises:
  209. EnvironmentError
  210. """
  211. cmds = [adb_path(), '-s', self._serial
  212. ] if self._serial else [adb_path()]
  213. cmds.extend(args)
  214. cmdline = subprocess.list2cmdline(map(str, cmds))
  215. try:
  216. return subprocess.check_output(
  217. cmdline, stderr=subprocess.STDOUT, shell=True).decode('utf-8')
  218. except subprocess.CalledProcessError as e:
  219. if kwargs.get('raise_error', True):
  220. raise EnvironmentError(
  221. "subprocess", cmdline,
  222. e.output.decode('utf-8', errors='ignore'))
  223. def shell_output(self, *args):
  224. return self._client.shell(self._serial, subprocess.list2cmdline(args))
  225. def forward_port(self, remote_port):
  226. assert isinstance(remote_port, int)
  227. for f in self._client.forward_list(self._serial):
  228. if f.serial == self._serial and f.remote == 'tcp:' + str(
  229. remote_port) and f.local.startswith("tcp:"):
  230. return int(f.local[len("tcp:"):])
  231. local_port = get_free_port()
  232. self._client.forward(self._serial, "tcp:" + str(local_port),
  233. "tcp:" + str(remote_port))
  234. return local_port
  235. def push(self, local, remote):
  236. assert isinstance(local, six.string_types)
  237. assert isinstance(remote, six.string_types)
  238. self.adb_output("push", local, remote)
  239. def install(self, apk_path):
  240. """
  241. sdk = self.getprop('ro.build.version.sdk')
  242. sdk > 23 support -g
  243. """
  244. assert isinstance(apk_path, six.string_types)
  245. self.adb_output("install", "-r", apk_path)
  246. def uninstall(self, pkg_name):
  247. assert isinstance(pkg_name, six.string_types)
  248. self.adb_output("uninstall", pkg_name)
  249. def getprop(self, prop):
  250. assert isinstance(prop, six.string_types)
  251. return self.shell_output('getprop', prop).strip()
  252. def package_info(self, pkg_name):
  253. assert isinstance(pkg_name, six.string_types)
  254. output = self.shell_output('dumpsys', 'package', pkg_name)
  255. m = re.compile(r'versionName=(?P<name>[\d.]+)').search(output)
  256. version_name = m.group('name') if m else None
  257. m = re.search(r'PackageSignatures\{(.*?)\}', output)
  258. signature = m.group(1) if m else None
  259. if version_name is None and signature is None:
  260. return None
  261. return dict(version_name=version_name, signature=signature)
  262. class Sync():
  263. def __init__(self, adbclient, serial):
  264. self._adbclient = adbclient
  265. self._serial = serial
  266. # self._path = path
  267. @contextmanager
  268. def _prepare_sync(self, path, cmd):
  269. c = self._adbclient.connect()
  270. try:
  271. c.send(":".join(["host", "transport", self._serial]))
  272. c.check_okay()
  273. c.send("sync:")
  274. c.check_okay()
  275. # {COMMAND}{LittleEndianPathLength}{Path}
  276. c.conn.send(
  277. cmd.encode("utf-8") + struct.pack("<I", len(path)) +
  278. path.encode("utf-8"))
  279. yield c
  280. finally:
  281. c.close()
  282. def stat(self, path):
  283. assert isinstance(path, six.string_types)
  284. with self._prepare_sync(path, "STAT") as c:
  285. assert "STAT" == c.read(4)
  286. mode, size, mtime = struct.unpack("<III", c.conn.recv(12))
  287. return FileInfo(mode, size, datetime.datetime.fromtimestamp(mtime),
  288. path)
  289. def iter_directory(self, path):
  290. assert isinstance(path, six.string_types)
  291. with self._prepare_sync(path, "LIST") as c:
  292. while 1:
  293. response = c.read(4)
  294. if response == _DONE:
  295. break
  296. mode, size, mtime, namelen = struct.unpack(
  297. "<IIII", c.conn.recv(16))
  298. name = c.read(namelen)
  299. yield FileInfo(mode, size,
  300. datetime.datetime.fromtimestamp(mtime), name)
  301. def list(self, path):
  302. return list(self.iter_directory(path))
  303. def push(self, src, dst, mode=0o755):
  304. # IFREG: File Regular
  305. # IFDIR: File Directory
  306. assert isinstance(dst, six.string_types)
  307. path = dst + "," + str(stat.S_IFREG | mode)
  308. with self._prepare_sync(path, "SEND") as c:
  309. r = src if hasattr(src, "read") else open(src, "rb")
  310. try:
  311. while True:
  312. chunk = r.read(4096)
  313. if not chunk:
  314. mtime = int(time.time())
  315. c.conn.send(b"DONE" + struct.pack("<I", mtime))
  316. break
  317. c.conn.send(b"DATA" + struct.pack("<I", len(chunk)))
  318. c.conn.send(chunk)
  319. assert c.read(4) == _OKAY
  320. finally:
  321. if hasattr(r, "close"):
  322. r.close()
  323. def pull(self, path):
  324. assert isinstance(path, six.string_types)
  325. with self._prepare_sync(path, "RECV") as c:
  326. while True:
  327. cmd = c.read(4)
  328. if cmd == "DONE":
  329. break
  330. assert cmd == "DATA"
  331. chunk_size = struct.unpack("<I", c.read_raw(4))[0]
  332. chunk = c.read_raw(chunk_size)
  333. if len(chunk) != chunk_size:
  334. raise RuntimeError("read chunk missing")
  335. print("Chunk:", chunk)
  336. adb = AdbClient()
  337. if __name__ == "__main__":
  338. print("server version:", adb.server_version())
  339. print("devices:", adb.devices())
  340. d = adb.devices()[0]
  341. print(d.serial)
  342. for f in adb.sync(d.serial).iter_directory("/data/local/tmp"):
  343. print(f)
  344. # adb.listdir(d.serial, "/data/local/tmp/")
  345. finfo = adb.sync(d.serial).stat("/data/local/tmp")
  346. print(finfo)
  347. import io
  348. sync = adb.sync(d.serial)
  349. sync.push(
  350. io.BytesIO(b"hi5a4de5f4qa6we541fq6w1ef5a61f65ew1rf6we"),
  351. "/data/local/tmp/hi.txt", 0o644)
  352. # sync.mkdir("/data/local/tmp/foo")
  353. sync.pull("/data/local/tmp/hi.txt")