diff --git a/tidevice/__main__.py b/tidevice/__main__.py index c2697eb..145328f 100644 --- a/tidevice/__main__.py +++ b/tidevice/__main__.py @@ -42,6 +42,9 @@ ulogger.disable(PROGRAM_NAME) +# ulogger.remove() +# ulogger.add(sys.stderr, level="INFO") + def _complete_udid(udid: Optional[str] = None) -> str: infos = um.device_list() @@ -166,8 +169,9 @@ def cmd_install(args: argparse.Namespace): bundle_id = d.app_install(args.filepath_or_url) if args.launch: - pid = d.instruments.app_launch(bundle_id) - logger.info("Launch %r, process pid: %d", bundle_id, pid) + with d.instruments_context() as ts: + pid = ts.app_launch(bundle_id) + logger.info("Launch %r, process pid: %d", bundle_id, pid) def cmd_uninstall(args: argparse.Namespace): @@ -314,13 +318,14 @@ def cmd_applist(args: argparse.Namespace): def cmd_energy(args: argparse.Namespace): d = _udid2device(args.udid) + ts = d.connect_instruments() try: - pid = d.instruments.app_launch(args.bundle_id, + pid = ts.app_launch(args.bundle_id, args=args.arguments, kill_running=args.kill) - d.instruments.start_energy_sampling(pid) + ts.start_energy_sampling(pid) while True: - ret = d.instruments.get_process_energy_stats(pid) + ret = ts.get_process_energy_stats(pid) if ret != None: print(json.dumps(ret)) time.sleep(1.0) @@ -330,10 +335,11 @@ def cmd_energy(args: argparse.Namespace): def cmd_launch(args: argparse.Namespace): d = _udid2device(args.udid) try: - pid = d.instruments.app_launch(args.bundle_id, - args=args.arguments, - kill_running=args.kill) - print("PID:", pid) + with d.instruments_context() as ts: + pid = ts.app_launch(args.bundle_id, + args=args.arguments, + kill_running=args.kill) + print("PID:", pid) except ServiceError as e: sys.exit(e) @@ -353,8 +359,9 @@ def cmd_kill(args: argparse.Namespace): def cmd_system_info(args): d = _udid2device(args.udid) - sinfo = d.instruments.system_info() - pprint(sinfo) + with d.instruments_context() as ts: + sinfo = ts.system_info() + pprint(sinfo) def cmd_battery(args: argparse.Namespace): @@ -485,7 +492,7 @@ def cmd_syslog(args: argparse.Namespace): def cmd_dump_fps(args): d = _udid2device(args.udid) - for data in d.instruments.iter_opengl_data(): + for data in d.connect_instruments().iter_opengl_data(): if isinstance(data, str): continue fps = data['CoreAnimationFramesPerSecond'] @@ -563,7 +570,7 @@ def cmd_fsync(args: argparse.Namespace): def cmd_ps(args: argparse.Namespace): d = _udid2device(args.udid) app_infos = list(d.installation.iter_installed(app_type=None)) - ps = list(d.instruments.app_process_list(app_infos)) + ps = list(d.connect_instruments().app_process_list(app_infos)) lens = defaultdict(int) json_data = [] diff --git a/tidevice/_device.py b/tidevice/_device.py index 7cc2c18..33c9e9c 100644 --- a/tidevice/_device.py +++ b/tidevice/_device.py @@ -671,18 +671,18 @@ def app_stop(self, pid_or_name: Union[int, str]) -> int: """ return pid killed """ - instruments = self.connect_instruments() - if isinstance(pid_or_name, int): - instruments.app_kill(pid_or_name) - return pid_or_name - elif isinstance(pid_or_name, str): - bundle_id = pid_or_name - app_infos = list(self.installation.iter_installed(app_type=None)) - ps = instruments.app_process_list(app_infos) - for p in ps: - if p['bundle_id'] == bundle_id: - instruments.app_kill(p['pid']) - return p['pid'] + with self.instruments_context() as ts: + if isinstance(pid_or_name, int): + ts.app_kill(pid_or_name) + return pid_or_name + elif isinstance(pid_or_name, str): + bundle_id = pid_or_name + app_infos = list(self.installation.iter_installed(app_type=None)) + ps = ts.app_process_list(app_infos) + for p in ps: + if p['bundle_id'] == bundle_id: + ts.app_kill(p['pid']) + return p['pid'] return None def app_kill(self, *args, **kwargs) -> int: @@ -698,10 +698,8 @@ def app_start(self, return pid """ - instruments = self.connect_instruments() - return instruments.app_launch(bundle_id, - args=args, - kill_running=kill_running) + with self.instruments_context() as ts: + return ts.app_launch(bundle_id, args=args, kill_running=kill_running) def app_install(self, file_or_url: Union[str, typing.IO]) -> str: """ @@ -795,11 +793,17 @@ def connect_instruments(self) -> ServiceInstruments: LockdownService.InstrumentsRemoteServerSecure) else: conn = self.start_service(LockdownService.InstrumentsRemoteServer) + return ServiceInstruments(conn) + - @property - def instruments(self) -> ServiceInstruments: - return self.connect_instruments() + @contextlib.contextmanager + def instruments_context(self) -> typing.Generator[ServiceInstruments, None, None]: + ts = self.connect_instruments() + try: + yield ts + finally: + ts.close() def _launch_app_runner(self, bundle_id: str, diff --git a/tidevice/_perf.py b/tidevice/_perf.py index cd9aa6c..cf9a8ae 100644 --- a/tidevice/_perf.py +++ b/tidevice/_perf.py @@ -12,6 +12,7 @@ import uuid from collections import defaultdict, namedtuple from typing import Any, Iterator, Optional, Tuple, Union +import weakref from ._device import BaseDevice from ._proto import * @@ -33,12 +34,13 @@ class RunningProcess: PID_UPDATE_DURATION = 5.0 def __init__(self, d: BaseDevice, bundle_id: str): - self._ins = d.instruments + self._ins = d.connect_instruments() self._bundle_id = bundle_id self._app_infos = list(d.installation.iter_installed(app_type=None)) self._next_update_time = 0.0 self._last_pid = None self._lock = threading.Lock() + weakref.finalize(self, self._ins.close) @property def bundle_id(self) -> str: @@ -100,19 +102,21 @@ def gen_stimestamp(seconds: Optional[float] = None) -> str: def iter_fps(d: BaseDevice) -> Iterator[Any]: - for data in d.instruments.iter_opengl_data(): - fps = data['CoreAnimationFramesPerSecond'] # fps from GPU - # print("FPS:", fps) - yield DataType.FPS, {"fps": fps, "time": time.time(), "value": fps} + with d.instruments_context() as ts: + for data in ts.iter_opengl_data(): + fps = data['CoreAnimationFramesPerSecond'] # fps from GPU + # print("FPS:", fps) + yield DataType.FPS, {"fps": fps, "time": time.time(), "value": fps} def iter_gpu(d: BaseDevice) -> Iterator[Any]: - for data in d.instruments.iter_opengl_data(): - device_utilization = data['Device Utilization %'] # Device Utilization - tiler_utilization = data['Tiler Utilization %'] # Tiler Utilization - renderer_utilization = data['Renderer Utilization %'] # Renderer Utilization - yield DataType.GPU, {"device": device_utilization, "renderer": renderer_utilization, - "tiler": tiler_utilization, "time": time.time(), "value": device_utilization} + with d.instruments_context() as ts: + for data in ts.iter_opengl_data(): + device_utilization = data['Device Utilization %'] # Device Utilization + tiler_utilization = data['Tiler Utilization %'] # Tiler Utilization + renderer_utilization = data['Renderer Utilization %'] # Renderer Utilization + yield DataType.GPU, {"device": device_utilization, "renderer": renderer_utilization, + "tiler": tiler_utilization, "time": time.time(), "value": device_utilization} def iter_screenshot(d: BaseDevice) -> Iterator[Tuple[DataType, dict]]: @@ -146,75 +150,76 @@ def _iter_complex_cpu_memory(d: BaseDevice, 'mem_rss': 130760704, 'pid': 1344} """ - for info in d.instruments.iter_cpu_memory(): - pid = rp.get_pid() - - if info is None or len(info) != 2: - continue - sinfo, pinfolist = info - if 'CPUCount' not in sinfo: - sinfo, pinfolist = pinfolist, sinfo - - if 'CPUCount' not in sinfo: - continue - - cpu_count = sinfo['CPUCount'] - - sys_cpu_usage = sinfo['SystemCPUUsage'] - cpu_total_load = sys_cpu_usage['CPU_TotalLoad'] - cpu_user = sys_cpu_usage['CPU_UserLoad'] - cpu_sys = sys_cpu_usage['CPU_SystemLoad'] - - if 'Processes' not in pinfolist: - continue - - # 这里的total_cpu_usage加起来的累计值大概在0.5~5.0之间 - total_cpu_usage = 0.0 - for attrs in pinfolist['Processes'].values(): - pinfo = ProcAttrs(*attrs) - if isinstance(pinfo.cpuUsage, float): # maybe NSNull - total_cpu_usage += pinfo.cpuUsage - - cpu_usage = 0.0 - attrs = pinfolist['Processes'].get(pid) - if attrs is None: # process is not running - # continue - # print('process not launched') - pass - else: - assert len(attrs) == len(SYSMON_PROC_ATTRS) - # print(ProcAttrs, attrs) - pinfo = ProcAttrs(*attrs) - cpu_usage = pinfo.cpuUsage - # next_list_process_time = time.time() + next_timeout - # cpu_usage, rss, mem_anon, pid = pinfo - - # 很诡异的计算方法,不过也就这种方法计算出来的CPU看起来正常一点 - # 计算后的cpuUsage范围 [0, 100] - # cpu_total_load /= cpu_count - # cpu_usage *= cpu_total_load - # if total_cpu_usage > 0: - # cpu_usage /= total_cpu_usage - - # print("cpuUsage: {}, total: {}".format(cpu_usage, total_cpu_usage)) - # print("memory: {} MB".format(pinfo.physFootprint / 1024 / 1024)) - yield dict( - type="process", - pid=pid, - phys_memory=pinfo.physFootprint, # 物理内存 - phys_memory_string="{:.1f} MiB".format(pinfo.physFootprint / 1024 / - 1024), - vss=pinfo.memVirtualSize, - rss=pinfo.memResidentSize, - anon=pinfo.memAnon, # 匿名内存? 这个是啥 - cpu_count=cpu_count, - cpu_usage=cpu_usage, # 理论上最高 100.0 (这里是除以过cpuCount的) - sys_cpu_usage=cpu_total_load, - attr_cpuUsage=pinfo.cpuUsage, - attr_cpuTotal=cpu_total_load, - attr_ctxSwitch=pinfo.ctxSwitch, - attr_intWakeups=pinfo.intWakeups, - attr_systemInfo=sys_cpu_usage) + with d.instruments_context() as ts: + for info in ts.iter_cpu_memory(): + pid = rp.get_pid() + + if info is None or len(info) != 2: + continue + sinfo, pinfolist = info + if 'CPUCount' not in sinfo: + sinfo, pinfolist = pinfolist, sinfo + + if 'CPUCount' not in sinfo: + continue + + cpu_count = sinfo['CPUCount'] + + sys_cpu_usage = sinfo['SystemCPUUsage'] + cpu_total_load = sys_cpu_usage['CPU_TotalLoad'] + cpu_user = sys_cpu_usage['CPU_UserLoad'] + cpu_sys = sys_cpu_usage['CPU_SystemLoad'] + + if 'Processes' not in pinfolist: + continue + + # 这里的total_cpu_usage加起来的累计值大概在0.5~5.0之间 + total_cpu_usage = 0.0 + for attrs in pinfolist['Processes'].values(): + pinfo = ProcAttrs(*attrs) + if isinstance(pinfo.cpuUsage, float): # maybe NSNull + total_cpu_usage += pinfo.cpuUsage + + cpu_usage = 0.0 + attrs = pinfolist['Processes'].get(pid) + if attrs is None: # process is not running + # continue + # print('process not launched') + pass + else: + assert len(attrs) == len(SYSMON_PROC_ATTRS) + # print(ProcAttrs, attrs) + pinfo = ProcAttrs(*attrs) + cpu_usage = pinfo.cpuUsage + # next_list_process_time = time.time() + next_timeout + # cpu_usage, rss, mem_anon, pid = pinfo + + # 很诡异的计算方法,不过也就这种方法计算出来的CPU看起来正常一点 + # 计算后的cpuUsage范围 [0, 100] + # cpu_total_load /= cpu_count + # cpu_usage *= cpu_total_load + # if total_cpu_usage > 0: + # cpu_usage /= total_cpu_usage + + # print("cpuUsage: {}, total: {}".format(cpu_usage, total_cpu_usage)) + # print("memory: {} MB".format(pinfo.physFootprint / 1024 / 1024)) + yield dict( + type="process", + pid=pid, + phys_memory=pinfo.physFootprint, # 物理内存 + phys_memory_string="{:.1f} MiB".format(pinfo.physFootprint / 1024 / + 1024), + vss=pinfo.memVirtualSize, + rss=pinfo.memResidentSize, + anon=pinfo.memAnon, # 匿名内存? 这个是啥 + cpu_count=cpu_count, + cpu_usage=cpu_usage, # 理论上最高 100.0 (这里是除以过cpuCount的) + sys_cpu_usage=cpu_total_load, + attr_cpuUsage=pinfo.cpuUsage, + attr_cpuTotal=cpu_total_load, + attr_ctxSwitch=pinfo.ctxSwitch, + attr_intWakeups=pinfo.intWakeups, + attr_systemInfo=sys_cpu_usage) def iter_cpu_memory(d: BaseDevice, rp: RunningProcess) -> Iterator[Any]: @@ -244,15 +249,16 @@ def set_interval(it: Iterator[Any], interval: float): def iter_network_flow(d: BaseDevice, rp: RunningProcess) -> Iterator[Any]: n = 0 - for nstat in d.instruments.iter_network(): - # if n < 2: - # n += 1 - # continue - yield DataType.NETWORK, { - "timestamp": gen_stimestamp(), - "downFlow": (nstat['rx.bytes'] or 0) / 1024, - "upFlow": (nstat['tx.bytes'] or 0) / 1024 - } + with d.instruments_context() as ts: + for nstat in ts.iter_network(): + # if n < 2: + # n += 1 + # continue + yield DataType.NETWORK, { + "timestamp": gen_stimestamp(), + "downFlow": (nstat['rx.bytes'] or 0) / 1024, + "upFlow": (nstat['tx.bytes'] or 0) / 1024 + } def append_data(wg: WaitGroup, stop_event: threading.Event, diff --git a/tidevice/_safe_socket.py b/tidevice/_safe_socket.py index 38b054f..71050e5 100644 --- a/tidevice/_safe_socket.py +++ b/tidevice/_safe_socket.py @@ -27,6 +27,7 @@ _id_numbers = [] def acquire_uid() -> int: + logger.info("Create new socket, total {}", len(_id_numbers) + 1) with _nlock: _n[0] += 1 _id_numbers.append(_n[0]) @@ -38,7 +39,7 @@ def release_uid(id: int): _id_numbers.remove(id) except ValueError: pass - logger.debug("Activate socket count {}", len(_id_numbers)) + logger.info("Release socket, total: {}", len(_id_numbers)) @@ -215,8 +216,8 @@ def recv_packet(self, header_size=None) -> dict: class PlistSocketProxy: def __init__(self, psock: typing.Union[PlistSocket, "PlistSocketProxy"]): if isinstance(psock, PlistSocketProxy): - self._psock = psock._psock psock._finalizer.detach() + self.__dict__.update(psock.__dict__) else: assert isinstance(psock, PlistSocket) self._psock = psock