#!/usr/bin/env python3 # coding: utf-8 # import argparse import collections import glob import hashlib import json import os import pprint import re import shutil import socket import subprocess import sys import tempfile import time from concurrent.futures import ThreadPoolExecutor from functools import partial import apkutils2 as apkutils import requests import tornado.web from logzero import logger from tornado import gen, websocket from tornado.concurrent import run_on_executor from tornado.ioloop import IOLoop from tornado.web import RequestHandler from tornado.websocket import WebSocketHandler, websocket_connect import adbutils from adbutils import adb as adbclient from asyncadb import adb from device import STATUS_FAIL, STATUS_INIT, STATUS_OKAY, AndroidDevice from heartbeat import heartbeat_connect from core.utils import current_ip, fix_url, id_generator, update_recursive from core import fetching import uiautomator2 as u2 import settings __curdir__ = os.path.dirname(os.path.abspath(__file__)) hbconn = None udid2device = {} secret = id_generator(10) class CorsMixin(object): CORS_ORIGIN = '*' CORS_METHODS = 'GET,POST,OPTIONS' CORS_CREDENTIALS = True CORS_HEADERS = "x-requested-with,authorization" def set_default_headers(self): self.set_header("Access-Control-Allow-Origin", self.CORS_ORIGIN) self.set_header("Access-Control-Allow-Headers", self.CORS_HEADERS) self.set_header('Access-Control-Allow-Methods', self.CORS_METHODS) def options(self): # no body self.set_status(204) self.finish() class InstallError(Exception): def __init__(self, stage: str, reason): self.stage = stage self.reason = reason def app_install_local(serial: str, apk_path: str, launch: bool = False) -> str: """ install apk to device Returns: package name Raises: AdbInstallError, FileNotFoundError """ # 解析apk文件 device = adbclient.device(serial) try: apk = apkutils.APK(apk_path) except apkutils.apkfile.BadZipFile: raise InstallError("ApkParse", "Bad zip file") # 提前将重名包卸载 package_name = apk.manifest.package_name pkginfo = device.package_info(package_name) if pkginfo: logger.debug("uninstall: %s", package_name) device.uninstall(package_name) # 解锁手机,防止锁屏 # ud = u2.connect_usb(serial) # ud.open_identify() try: # 推送到手机 dst = "/data/local/tmp/tmp-%d.apk" % int(time.time() * 1000) logger.debug("push %s %s", apk_path, dst) device.sync.push(apk_path, dst) logger.debug("install-remote %s", dst) # 调用pm install安装 device.install_remote(dst) except adbutils.errors.AdbInstallError as e: raise InstallError("install", e.output) # finally: # 停止uiautomator2服务 # logger.debug("uiautomator2 stop") # ud.session().press("home") # ud.service("uiautomator").stop() # 启动应用 if launch: logger.debug("launch %s", package_name) device.app_start(package_name) return package_name class AppHandler(CorsMixin, tornado.web.RequestHandler): _install_executor = ThreadPoolExecutor(4) _download_executor = ThreadPoolExecutor(1) def cache_filepath(self, text: str) -> str: m = hashlib.md5() m.update(text.encode('utf-8')) return "cache-" + m.hexdigest() @run_on_executor(executor="_download_executor") def cache_download(self, url: str) -> str: """ download with local cache """ target_path = self.cache_filepath(url) logger.debug("Download %s to %s", url, target_path) if os.path.exists(target_path): logger.debug("Cache hited") return target_path # TODO: remove last for fname in glob.glob("cache-*"): logger.debug("Remove old cache: %s", fname) os.unlink(fname) tmp_path = target_path + ".tmp" r = requests.get(url, stream=True) r.raise_for_status() with open(tmp_path, "wb") as tfile: content_length = int(r.headers.get("content-length", 0)) if content_length: for chunk in r.iter_content(chunk_size=40960): tfile.write(chunk) else: shutil.copyfileobj(r.raw, tfile) os.rename(tmp_path, target_path) return target_path @run_on_executor(executor='_install_executor') def app_install_url(self, serial: str, apk_path: str, **kwargs): pkg_name = app_install_local(serial, apk_path, **kwargs) return { "success": True, "description": "Success", "packageName": pkg_name, } async def post(self, udid=None): udid = udid or self.get_argument("udid") device = udid2device[udid] url = self.get_argument("url") launch = self.get_argument("launch", "false") in ['true', 'True', 'TRUE', '1'] try: apk_path = await self.cache_download(url) ret = await self.app_install_url(device.serial, apk_path, launch=launch) self.write(ret) except InstallError as e: self.set_status(400) self.write({ "success": False, "description": "{}: {}".format(e.stage, e.reason) }) except Exception as e: self.set_status(500) self.write(str(e)) class ColdingHandler(tornado.web.RequestHandler): async def post(self, udid=None): """ 设备清理 """ udid = udid or self.get_argument("udid") logger.info("Receive colding request for %s", udid) request_secret = self.get_argument("secret") if secret != request_secret: logger.warning("secret not match, expect %s, got %s", secret, request_secret) return if udid not in udid2device: return device = udid2device[udid] await device.reset() await hbconn.device_update({ "udid": udid, "colding": False, "provider": device.addrs(), }) self.write({"success": True, "description": "Device colded"}) def make_app(): app = tornado.web.Application([ (r"/app/install", AppHandler), (r"/cold", ColdingHandler), ]) return app async def device_watch(allow_remote: bool = False): serial2udid = {} udid2serial = {} def callback(udid: str, status: str): if status == STATUS_OKAY: print("Good") async for event in adb.track_devices(): logger.debug("%s", event) # udid = event.serial # FIXME(ssx): fix later if not allow_remote: if re.match(r"(\d+)\.(\d+)\.(\d+)\.(\d+):(\d+)", event.serial): logger.debug("Skip remote device: %s", event) continue if event.present: try: udid = serial2udid[event.serial] = event.serial udid2serial[udid] = event.serial device = AndroidDevice(event.serial, partial(callback, udid)) await device.init() await device.open_identify() udid2device[udid] = device await hbconn.device_update({ # "private": False, # TODO "udid": udid, "platform": "android", "colding": False, "provider": device.addrs(), "properties": await device.properties(), }) # yapf: disable logger.info("Device:%s is ready", event.serial) except RuntimeError: logger.warning("Device:%s initialize failed", event.serial) except Exception as e: logger.error("Unknown error: %s", e) import traceback traceback.print_exc() else: udid = serial2udid[event.serial] if udid in udid2device: udid2device[udid].close() udid2device.pop(udid, None) await hbconn.device_update({ "udid": udid, "provider": None, # not present }) async def async_main(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter) # yapf: disable parser.add_argument('-s', '--server', default='localhost:4000', help='server address') parser.add_argument("--allow-remote", action="store_true", help="allow remote connect device") parser.add_argument('-t', '--test', action="store_true", help="run test code") parser.add_argument('-p', '--port', type=int, default=3500, help='listen port') parser.add_argument("--atx-agent-version", default=u2.version.__atx_agent_version__, help="set atx-agent version") parser.add_argument("--owner", type=str, help="provider owner email") parser.add_argument("--owner-file", type=argparse.FileType("r"), help="provider owner email from file") args = parser.parse_args() # yapf: enable settings.atx_agent_version = args.atx_agent_version owner_email = args.owner if args.owner_file: with args.owner_file as file: owner_email = file.read().strip() logger.info("Owner: %s", owner_email) if args.test: for apk_name in ("cloudmusic.apk", ): # , "apkinfo.exe"): apk_path = "testdata/" + apk_name logger.info("Install %s", apk_path) # apk_path = r"testdata/cloudmusic.apk" ret = app_install_local("6EB0217704000486", apk_path, launch=True) logger.info("Ret: %s", ret) return # start local server provider_url = "http://" + current_ip() + ":" + str(args.port) app = make_app() app.listen(args.port) logger.info("ProviderURL: %s", provider_url) fetching.get_all() # connect to atxserver2 global hbconn hbconn = await heartbeat_connect(args.server, secret=secret, self_url=provider_url, owner=owner_email) await device_watch(args.allow_remote) async def test_asyncadb(): devices = await adb.devices() print(devices) # output = await adb.shell("3578298f", "getprop ro.product.brand") # print(output) version = await adb.server_version() print("ServerVersion:", version) await adb.forward_remove() await adb.forward("3578298f", "tcp:8888", "tcp:7912") async for f in adb.forward_list(): print(f) if __name__ == '__main__': # if os.path.getsize(os.path.join(__curdir__, # "vendor/app-uiautomator.apk")) < 1000: # sys.exit("Did you forget run\n\tgit lfs install\n\tgit lfs pull") try: IOLoop.current().run_sync(async_main) except KeyboardInterrupt: logger.info("Interrupt catched")