magnax 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. magnax/__init__.py +3 -0
  2. magnax/__main__.py +25 -0
  3. magnax/debug.py +65 -0
  4. magnax/public/__init__.py +1 -0
  5. magnax/public/adb/linux/adb +0 -0
  6. magnax/public/adb/linux_arm/adb +0 -0
  7. magnax/public/adb/mac/adb +0 -0
  8. magnax/public/adb/windows/AdbWinApi.dll +0 -0
  9. magnax/public/adb/windows/AdbWinUsbApi.dll +0 -0
  10. magnax/public/adb/windows/adb.exe +0 -0
  11. magnax/public/adb.py +96 -0
  12. magnax/public/android_fps.py +750 -0
  13. magnax/public/apm.py +1306 -0
  14. magnax/public/apm_pk.py +184 -0
  15. magnax/public/common.py +1598 -0
  16. magnax/public/config.json +1 -0
  17. magnax/public/ios_perf_adapter.py +790 -0
  18. magnax/public/report_template/android.html +526 -0
  19. magnax/public/report_template/ios.html +482 -0
  20. magnax/public/scrcpy/scrcpy-win32-v2.4/AdbWinApi.dll +0 -0
  21. magnax/public/scrcpy/scrcpy-win32-v2.4/AdbWinUsbApi.dll +0 -0
  22. magnax/public/scrcpy/scrcpy-win32-v2.4/SDL2.dll +0 -0
  23. magnax/public/scrcpy/scrcpy-win32-v2.4/adb.exe +0 -0
  24. magnax/public/scrcpy/scrcpy-win32-v2.4/avcodec-60.dll +0 -0
  25. magnax/public/scrcpy/scrcpy-win32-v2.4/avformat-60.dll +0 -0
  26. magnax/public/scrcpy/scrcpy-win32-v2.4/avutil-58.dll +0 -0
  27. magnax/public/scrcpy/scrcpy-win32-v2.4/icon.png +0 -0
  28. magnax/public/scrcpy/scrcpy-win32-v2.4/libusb-1.0.dll +0 -0
  29. magnax/public/scrcpy/scrcpy-win32-v2.4/open_a_terminal_here.bat +1 -0
  30. magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-console.bat +2 -0
  31. magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-noconsole.vbs +7 -0
  32. magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-server +0 -0
  33. magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy.exe +0 -0
  34. magnax/public/scrcpy/scrcpy-win32-v2.4/swresample-4.dll +0 -0
  35. magnax/public/scrcpy/scrcpy-win64-v2.4/AdbWinApi.dll +0 -0
  36. magnax/public/scrcpy/scrcpy-win64-v2.4/AdbWinUsbApi.dll +0 -0
  37. magnax/public/scrcpy/scrcpy-win64-v2.4/SDL2.dll +0 -0
  38. magnax/public/scrcpy/scrcpy-win64-v2.4/avformat-60.dll +0 -0
  39. magnax/public/scrcpy/scrcpy-win64-v2.4/avutil-58.dll +0 -0
  40. magnax/public/scrcpy/scrcpy-win64-v2.4/open_a_terminal_here.bat +1 -0
  41. magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy-noconsole.vbs +7 -0
  42. magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy-server +0 -0
  43. magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy.exe +0 -0
  44. magnax/public/scrcpy/scrcpy-win64-v2.4/swresample-4.dll +0 -0
  45. magnax/static/css/highlight.min.css +9 -0
  46. magnax/static/css/magnax-dark-theme.css +1237 -0
  47. magnax/static/css/select2-bootstrap-5-theme.min.css +3 -0
  48. magnax/static/css/select2-bootstrap-5-theme.rtl.min.css +3 -0
  49. magnax/static/css/select2.min.css +1 -0
  50. magnax/static/css/sweetalert2.min.css +1 -0
  51. magnax/static/css/tabler.demo.min.css +9 -0
  52. magnax/static/css/tabler.min.css +14 -0
  53. magnax/static/image/500.png +0 -0
  54. magnax/static/image/avatar.png +0 -0
  55. magnax/static/image/empty.png +0 -0
  56. magnax/static/image/readme/home.png +0 -0
  57. magnax/static/image/readme/pk.png +0 -0
  58. magnax/static/js/apexcharts.js +14 -0
  59. magnax/static/js/gray.js +16 -0
  60. magnax/static/js/highlight.min.js +1173 -0
  61. magnax/static/js/highstock.js +803 -0
  62. magnax/static/js/html2canvas.min.js +20 -0
  63. magnax/static/js/jquery.min.js +2 -0
  64. magnax/static/js/magnax-chart-theme.js +492 -0
  65. magnax/static/js/select2.min.js +2 -0
  66. magnax/static/js/sweetalert2.min.js +1 -0
  67. magnax/static/js/tabler.demo.min.js +9 -0
  68. magnax/static/js/tabler.min.js +9 -0
  69. magnax/static/logo/logo.png +0 -0
  70. magnax/templates/404.html +30 -0
  71. magnax/templates/analysis.html +1375 -0
  72. magnax/templates/analysis_compare.html +600 -0
  73. magnax/templates/analysis_pk.html +680 -0
  74. magnax/templates/base.html +365 -0
  75. magnax/templates/index.html +2471 -0
  76. magnax/templates/pk.html +743 -0
  77. magnax/templates/report.html +416 -0
  78. magnax/view/__init__.py +1 -0
  79. magnax/view/apis.py +952 -0
  80. magnax/view/pages.py +146 -0
  81. magnax/web.py +345 -0
  82. magnax-1.0.0.dist-info/METADATA +242 -0
  83. magnax-1.0.0.dist-info/RECORD +87 -0
  84. magnax-1.0.0.dist-info/WHEEL +5 -0
  85. magnax-1.0.0.dist-info/entry_points.txt +2 -0
  86. magnax-1.0.0.dist-info/licenses/LICENSE +21 -0
  87. magnax-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1598 @@
1
+ import json
2
+ import os
3
+ import platform
4
+ import re
5
+ import shutil
6
+ import time
7
+ import requests
8
+ from loguru import logger
9
+ from tqdm import tqdm
10
+ import socket
11
+ from urllib.request import urlopen
12
+ import ssl
13
+ import openpyxl
14
+ import psutil
15
+ import signal
16
+ import cv2
17
+ from functools import wraps
18
+ from jinja2 import Environment, FileSystemLoader
19
+
20
+ # 使用 pymobiledevice3 进行 iOS 设备控制
21
+ try:
22
+ from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux
23
+ from pymobiledevice3.usbmux import list_devices as pmd3_list_devices
24
+ PMD3_AVAILABLE = True
25
+ except ImportError:
26
+ LockdownClient = None
27
+ create_using_usbmux = None
28
+ pmd3_list_devices = None
29
+ PMD3_AVAILABLE = False
30
+ from magnax.public.adb import adb
31
+
32
+
33
+ def downsample_lttb(data: list, target_points: int) -> list:
34
+ """
35
+ LTTB (Largest Triangle Three Buckets) 降采样算法
36
+ 专为时序数据设计,保留视觉特征(峰值、谷值、趋势)
37
+
38
+ Args:
39
+ data: [{"x": timestamp, "y": value}, ...] 格式的时序数据
40
+ target_points: 目标数据点数量(建议 500-2000)
41
+
42
+ Returns:
43
+ 降采样后的数据列表,保留首尾点和关键特征点
44
+ """
45
+ n = len(data)
46
+ if n <= target_points or target_points < 3:
47
+ return data
48
+
49
+ # 始终保留第一个和最后一个点
50
+ sampled = [data[0]]
51
+
52
+ # 计算每个桶的大小
53
+ bucket_size = (n - 2) / (target_points - 2)
54
+
55
+ a = 0 # 上一个选中点的索引
56
+
57
+ for i in range(target_points - 2):
58
+ # 计算当前桶的范围
59
+ bucket_start = int((i + 1) * bucket_size) + 1
60
+ bucket_end = int((i + 2) * bucket_size) + 1
61
+ bucket_end = min(bucket_end, n - 1)
62
+
63
+ # 计算下一个桶的平均值(用于计算三角形面积)
64
+ next_start = int((i + 2) * bucket_size) + 1
65
+ next_end = int((i + 3) * bucket_size) + 1
66
+ next_end = min(next_end, n)
67
+
68
+ # 计算下一个桶的平均 x 和 y
69
+ if next_end > next_start:
70
+ avg_x = sum(j for j in range(next_start, next_end)) / (next_end - next_start)
71
+ avg_y = sum(data[j]['y'] for j in range(next_start, next_end)) / (next_end - next_start)
72
+ else:
73
+ avg_x = next_start
74
+ avg_y = data[min(next_start, n - 1)]['y']
75
+
76
+ # 在当前桶中找到与上一个点和平均点组成的三角形面积最大的点
77
+ max_area = -1
78
+ max_idx = bucket_start
79
+
80
+ for j in range(bucket_start, bucket_end):
81
+ # 计算三角形面积 (使用简化公式)
82
+ # 面积 = 0.5 * |x1(y2-y3) + x2(y3-y1) + x3(y1-y2)|
83
+ # 这里使用索引作为 x 坐标的近似
84
+ area = abs(
85
+ (a - avg_x) * (data[j]['y'] - data[a]['y']) -
86
+ (a - j) * (avg_y - data[a]['y'])
87
+ )
88
+ if area > max_area:
89
+ max_area = area
90
+ max_idx = j
91
+
92
+ sampled.append(data[max_idx])
93
+ a = max_idx
94
+
95
+ # 添加最后一个点
96
+ sampled.append(data[-1])
97
+
98
+ return sampled
99
+
100
+
101
+ def get_ios_lockdown_client_in_common(device_id):
102
+ """获取iOS设备的lockdown client (common.py专用版本)"""
103
+ try:
104
+ if create_using_usbmux is None:
105
+ logger.warning("pymobiledevice3 not available, some iOS features may not work")
106
+ return None
107
+
108
+ return create_using_usbmux(serial=device_id)
109
+ except Exception as e:
110
+ logger.error(f"Failed to create lockdown client for device {device_id}: {e}")
111
+ return None
112
+
113
+ def get_ios_device_udid_list():
114
+ """获取连接的iOS设备UDID列表"""
115
+ if not PMD3_AVAILABLE:
116
+ logger.warning("pymobiledevice3 not available")
117
+ return []
118
+ try:
119
+ devices = pmd3_list_devices()
120
+ return [device.serial for device in devices]
121
+ except Exception as e:
122
+ logger.error(f"Failed to get iOS device list: {e}")
123
+ return []
124
+
125
+ class Platform:
126
+ Android = 'Android'
127
+ iOS = 'iOS'
128
+ Mac = 'MacOS'
129
+ Windows = 'Windows'
130
+
131
+ class Devices:
132
+
133
+ def __init__(self, platform=Platform.Android):
134
+ self.platform = platform
135
+ self.adb = adb.adb_path
136
+
137
+ def execCmd(self, cmd):
138
+ """Execute the command to get the terminal print result"""
139
+ r = os.popen(cmd)
140
+ try:
141
+ text = r.buffer.read().decode(encoding='gbk').replace('\x1b[0m','').strip()
142
+ except UnicodeDecodeError:
143
+ text = r.buffer.read().decode(encoding='utf-8').replace('\x1b[0m','').strip()
144
+ finally:
145
+ r.close()
146
+ return text
147
+
148
+ def filterType(self):
149
+ """Select the pipe filtering method according to the system"""
150
+ filtertype = ('grep', 'findstr')[platform.system() == Platform.Windows]
151
+ return filtertype
152
+
153
+ def getDeviceIds(self):
154
+ """Get all connected device ids"""
155
+ Ids = list(os.popen(f"{self.adb} devices").readlines())
156
+ deviceIds = []
157
+ for i in range(1, len(Ids) - 1):
158
+ id, state = Ids[i].strip().split()
159
+ if state == 'device':
160
+ deviceIds.append(id)
161
+ return deviceIds
162
+
163
+ def getDevicesName(self, deviceId):
164
+ """Get the device name of the Android corresponding device ID"""
165
+ try:
166
+ devices_name = os.popen(f'{self.adb} -s {deviceId} shell getprop ro.product.model').readlines()[0].strip()
167
+ except Exception:
168
+ devices_name = os.popen(f'{self.adb} -s {deviceId} shell getprop ro.product.model').buffer.readlines()[0].decode("utf-8").strip()
169
+ return devices_name
170
+
171
+ def getDevices(self):
172
+ """Get all Android devices"""
173
+ DeviceIds = self.getDeviceIds()
174
+ Devices = [f'{id}({self.getDevicesName(id)})' for id in DeviceIds]
175
+ logger.info('Connected devices: {}'.format(Devices))
176
+ return Devices
177
+
178
+ def getIdbyDevice(self, deviceinfo, platform):
179
+ """Obtain the corresponding device id according to the Android device information"""
180
+ if platform == Platform.Android:
181
+ deviceId = re.sub(u"\\(.*?\\)|\\{.*?}|\\[.*?]", "", deviceinfo)
182
+ if deviceId not in self.getDeviceIds():
183
+ raise Exception('no device found')
184
+ else:
185
+ deviceId = deviceinfo
186
+ return deviceId
187
+
188
+ def getSdkVersion(self, deviceId):
189
+ version = adb.shell(cmd='getprop ro.build.version.sdk', deviceId=deviceId)
190
+ return version
191
+
192
+ def getCpuCores(self, deviceId):
193
+ """get Android cpu cores"""
194
+ cmd = 'cat /sys/devices/system/cpu/online'
195
+ result = adb.shell(cmd=cmd, deviceId=deviceId)
196
+ try:
197
+ nums = int(result.split('-')[1]) + 1
198
+ except:
199
+ nums = 1
200
+ return nums
201
+
202
+ def getPid(self, deviceId, pkgName):
203
+ """Get the pid corresponding to the Android package name"""
204
+ try:
205
+ sdkversion = self.getSdkVersion(deviceId)
206
+ if sdkversion and int(sdkversion) < 26:
207
+ result = os.popen(f"{self.adb} -s {deviceId} shell ps | {self.filterType()} {pkgName}").readlines()
208
+ processList = ['{}:{}'.format(process.split()[1],process.split()[8]) for process in result]
209
+ else:
210
+ result = os.popen(f"{self.adb} -s {deviceId} shell ps -ef | {self.filterType()} {pkgName}").readlines()
211
+ processList = ['{}:{}'.format(process.split()[1],process.split()[7]) for process in result]
212
+ for i in range(len(processList)):
213
+ if processList[i].count(':') == 1:
214
+ index = processList.index(processList[i])
215
+ processList.insert(0, processList.pop(index))
216
+ break
217
+ if len(processList) == 0:
218
+ logger.warning('{}: no pid found'.format(pkgName))
219
+ except Exception as e:
220
+ processList = []
221
+ logger.exception(e)
222
+ return processList
223
+
224
+ def checkPkgname(self, pkgname):
225
+ flag = True
226
+ replace_list = ['com.google']
227
+ for i in replace_list:
228
+ if i in pkgname:
229
+ flag = False
230
+ return flag
231
+
232
+ def getPkgname(self, deviceId):
233
+ """Get all package names of Android devices"""
234
+ pkginfo = os.popen(f"{self.adb} -s {deviceId} shell pm list packages --user 0")
235
+ pkglist = [p.lstrip('package').lstrip(":").strip() for p in pkginfo]
236
+ if pkglist.__len__() > 0:
237
+ return pkglist
238
+ else:
239
+ pkginfo = os.popen(f"{self.adb} -s {deviceId} shell pm list packages")
240
+ pkglist = [p.lstrip('package').lstrip(":").strip() for p in pkginfo]
241
+ return pkglist
242
+
243
+ def getDeviceInfoByiOS(self):
244
+ """Get a list of all successfully connected iOS devices"""
245
+ deviceInfo = get_ios_device_udid_list()
246
+ logger.info('Connected devices: {}'.format(deviceInfo))
247
+ return deviceInfo
248
+
249
+ def getPkgnameByiOS(self, udid):
250
+ """Get all package names of the corresponding iOS device"""
251
+ try:
252
+ lockdown_client = get_ios_lockdown_client_in_common(udid)
253
+ if lockdown_client is None:
254
+ logger.error("Failed to get lockdown client for iOS package list")
255
+ return []
256
+
257
+ from pymobiledevice3.services.installation_proxy import InstallationProxyService
258
+ installation = InstallationProxyService(lockdown=lockdown_client)
259
+
260
+ # 获取用户安装的应用列表(新版 API 返回字典,key 为 bundle identifier)
261
+ apps = installation.get_apps(application_type='User')
262
+ pkgNames = list(apps.keys())
263
+ return pkgNames
264
+ except Exception as e:
265
+ logger.error(f"Failed to get iOS package names: {e}")
266
+ return []
267
+
268
+ def get_pc_ip(self):
269
+ try:
270
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
271
+ s.connect(('8.8.8.8', 80))
272
+ ip = s.getsockname()[0]
273
+ except Exception:
274
+ logger.error('get local ip failed')
275
+ ip = '127.0.0.1'
276
+ finally:
277
+ s.close()
278
+ return ip
279
+
280
+ def get_device_ip(self, deviceId):
281
+ content = os.popen(f"{self.adb} -s {deviceId} shell ip addr show wlan0").read()
282
+ logger.info(content)
283
+ math_obj = re.search(r'inet\s(\d+\.\d+\.\d+\.\d+).*?wlan0', content)
284
+ if math_obj and math_obj.group(1):
285
+ return math_obj.group(1)
286
+ return None
287
+
288
+ def devicesCheck(self, platform, deviceid=None, pkgname=None):
289
+ """Check the device environment"""
290
+ match(platform):
291
+ case Platform.Android:
292
+ if len(self.getDeviceIds()) == 0:
293
+ raise Exception('no devices found')
294
+ if len(self.getPid(deviceId=deviceid, pkgName=pkgname)) == 0:
295
+ raise Exception('no process found')
296
+ case Platform.iOS:
297
+ if len(self.getDeviceInfoByiOS()) == 0:
298
+ raise Exception('no devices found')
299
+ case _:
300
+ raise Exception('platform must be Android or iOS')
301
+
302
+ def getDdeviceDetail(self, deviceId, platform):
303
+ result = dict()
304
+ match(platform):
305
+ case Platform.Android:
306
+ result['brand'] = adb.shell(cmd='getprop ro.product.brand', deviceId=deviceId)
307
+ result['name'] = adb.shell(cmd='getprop ro.product.model', deviceId=deviceId)
308
+ result['version'] = adb.shell(cmd='getprop ro.build.version.release', deviceId=deviceId)
309
+ result['serialno'] = adb.shell(cmd='getprop ro.serialno', deviceId=deviceId)
310
+ cmd = f'ip addr show wlan0 | {self.filterType()} link/ether'
311
+ wifiadr_content = adb.shell(cmd=cmd, deviceId=deviceId)
312
+ result['wifiadr'] = Method._index(wifiadr_content.split(), 1, '')
313
+ result['cpu_cores'] = self.getCpuCores(deviceId)
314
+ result['physical_size'] = adb.shell(cmd='wm size', deviceId=deviceId).replace('Physical size:','').strip()
315
+ case Platform.iOS:
316
+ try:
317
+ lockdown_client = get_ios_lockdown_client_in_common(deviceId)
318
+ if lockdown_client is None:
319
+ logger.error("Failed to get lockdown client for iOS device details")
320
+ return {'brand': '', 'name': '', 'version': '', 'serialno': deviceId, 'wifiadr': '', 'cpu_cores': 0, 'physical_size': ''}
321
+
322
+ # 从lockdown client获取设备信息
323
+ device_info = lockdown_client.all_values
324
+
325
+ result['brand'] = device_info.get("DeviceClass", "")
326
+ result['name'] = device_info.get("DeviceName", "")
327
+ result['version'] = device_info.get("ProductVersion", "")
328
+ result['serialno'] = deviceId
329
+ result['wifiadr'] = device_info.get("WiFiAddress", "")
330
+ result['cpu_cores'] = 0
331
+ result['physical_size'] = self.getPhysicalSzieOfiOS(deviceId)
332
+ except Exception as e:
333
+ logger.error(f"Failed to get iOS device details: {e}")
334
+ result = {'brand': '', 'name': '', 'version': '', 'serialno': deviceId, 'wifiadr': '', 'cpu_cores': 0, 'physical_size': ''}
335
+ case _:
336
+ raise Exception('{} is undefined'.format(platform))
337
+ return result
338
+
339
+ def getPhysicalSzieOfiOS(self, deviceId):
340
+ try:
341
+ lockdown_client = get_ios_lockdown_client_in_common(deviceId)
342
+ if lockdown_client is None:
343
+ logger.error("Failed to get lockdown client for iOS screen info")
344
+ return ''
345
+
346
+ # 获取屏幕信息
347
+ device_info = lockdown_client.all_values
348
+ screen_width = device_info.get('ScreenWidth', 0)
349
+ screen_height = device_info.get('ScreenHeight', 0)
350
+
351
+ if screen_width and screen_height:
352
+ PhysicalSzie = '{}x{}'.format(screen_width, screen_height)
353
+ else:
354
+ PhysicalSzie = ''
355
+ except Exception as e:
356
+ PhysicalSzie = ''
357
+ logger.exception(e)
358
+ return PhysicalSzie
359
+
360
+ def getCurrentActivity(self, deviceId):
361
+ result = adb.shell(cmd='dumpsys window | {} mCurrentFocus'.format(self.filterType()), deviceId=deviceId)
362
+ if result.__contains__('mCurrentFocus'):
363
+ activity = str(result).split(' ')[-1].replace('}','')
364
+ return activity
365
+ else:
366
+ raise Exception('No activity found')
367
+
368
+ def getStartupTimeByAndroid(self, activity, deviceId):
369
+ result = adb.shell(cmd='am start -W {}'.format(activity), deviceId=deviceId)
370
+ return result
371
+
372
+ def getStartupTimeByiOS(self, pkgname):
373
+ try:
374
+ import ios_device
375
+ except ImportError:
376
+ logger.error('py-ios-devices not found, please run [pip install py-ios-devices]')
377
+ result = self.execCmd('pyidevice instruments app_lifecycle -b {}'.format(pkgname))
378
+ return result
379
+
380
+ class File:
381
+
382
+ def __init__(self, fileroot='.'):
383
+ self.fileroot = fileroot
384
+ self.report_dir = self.get_repordir()
385
+
386
+ def _safe_remove_file(self, filepath, max_retries=5, retry_delay=1):
387
+ """安全删除文件,处理文件被占用的情况"""
388
+ for attempt in range(max_retries):
389
+ try:
390
+ if os.path.exists(filepath):
391
+ os.remove(filepath)
392
+ logger.info(f'文件删除成功: {filepath}')
393
+ return True
394
+
395
+ except PermissionError as e:
396
+ if attempt < max_retries - 1:
397
+ logger.warning(f'文件被占用,等待后重试删除 ({attempt + 1}/{max_retries}): {filepath}')
398
+ time.sleep(retry_delay)
399
+ retry_delay *= 2 # 指数退避
400
+ else:
401
+ logger.error(f'删除文件失败,已达到最大重试次数: {filepath}')
402
+ return False
403
+ except Exception as e:
404
+ logger.warning(f'删除文件时发生错误: {filepath}, 错误: {e}')
405
+ return False
406
+ return False
407
+
408
+ def clear_file(self):
409
+ logger.info('Clean up useless files ...')
410
+ if os.path.exists(self.report_dir):
411
+ files_to_remove = []
412
+ for f in os.listdir(self.report_dir):
413
+ filename = os.path.join(self.report_dir, f)
414
+ if f.split(".")[-1] in ['log', 'json', 'mkv']:
415
+ files_to_remove.append(filename)
416
+
417
+ # 先停止所有录屏进程,确保文件不被占用
418
+ Scrcpy.stop_record()
419
+
420
+ # 等待一些时间让进程完全结束
421
+ time.sleep(2)
422
+
423
+ # 安全删除文件
424
+ for filename in files_to_remove:
425
+ success = self._safe_remove_file(filename)
426
+ if not success:
427
+ logger.warning(f'无法删除文件,将在下次清理时重试: {filename}')
428
+
429
+ logger.info('Clean up useless files success')
430
+
431
+ def export_excel(self, platform, scene):
432
+ logger.info('Exporting excel ...')
433
+ android_log_file_list = ['cpu_app','cpu_sys','mem_total','mem_swap',
434
+ 'battery_level', 'battery_tem','upflow','downflow','fps','gpu']
435
+ ios_log_file_list = ['cpu_app','cpu_sys', 'mem_total', 'battery_tem', 'battery_current',
436
+ 'battery_voltage', 'battery_power','upflow','downflow','fps','gpu']
437
+ log_file_list = android_log_file_list if platform == 'Android' else ios_log_file_list
438
+ wb = openpyxl.Workbook()
439
+ # Remove the default sheet created by openpyxl
440
+ wb.remove(wb.active)
441
+ for name in log_file_list:
442
+ ws = wb.create_sheet(title=name)
443
+ ws.cell(row=1, column=1, value='Time')
444
+ ws.cell(row=1, column=2, value='Value')
445
+ row = 2 # start row (1-based, header is row 1)
446
+ if os.path.exists(f'{self.report_dir}/{scene}/{name}.log'):
447
+ with open(f'{self.report_dir}/{scene}/{name}.log', 'r', encoding='utf-8') as f:
448
+ for lines in f:
449
+ target = lines.split('=')
450
+ for i in range(len(target)):
451
+ ws.cell(row=row, column=i + 1, value=target[i])
452
+ row += 1
453
+ xlsx_path = os.path.join(self.report_dir, scene, f'{scene}.xlsx')
454
+ wb.save(xlsx_path)
455
+ logger.info('Exporting excel success : {}'.format(xlsx_path))
456
+ return xlsx_path
457
+
458
+ def make_android_html(self, scene, summary : dict, report_path=None):
459
+ logger.info('Generating HTML ...')
460
+ STATICPATH = os.path.dirname(os.path.realpath(__file__))
461
+ file_loader = FileSystemLoader(os.path.join(STATICPATH, 'report_template'))
462
+ env = Environment(loader=file_loader)
463
+ template = env.get_template('android.html')
464
+ if report_path:
465
+ html_path = report_path
466
+ else:
467
+ html_path = os.path.join(self.report_dir, scene, 'report.html')
468
+ with open(html_path,'w+') as fout:
469
+ html_content = template.render(devices=summary['devices'],app=summary['app'],
470
+ platform=summary['platform'],ctime=summary['ctime'],
471
+ cpu_app=summary['cpu_app'],cpu_sys=summary['cpu_sys'],
472
+ mem_total=summary['mem_total'],mem_swap=summary['mem_swap'],
473
+ fps=summary['fps'],jank=summary['jank'],level=summary['level'],
474
+ tem=summary['tem'],net_send=summary['net_send'],
475
+ net_recv=summary['net_recv'],cpu_charts=summary['cpu_charts'],
476
+ mem_charts=summary['mem_charts'],net_charts=summary['net_charts'],
477
+ battery_charts=summary['battery_charts'],fps_charts=summary['fps_charts'],
478
+ jank_charts=summary['jank_charts'],mem_detail_charts=summary['mem_detail_charts'],
479
+ gpu=summary['gpu'], gpu_charts=summary['gpu_charts'])
480
+
481
+ fout.write(html_content)
482
+ logger.info('Generating HTML success : {}'.format(html_path))
483
+ return html_path
484
+
485
+ def make_ios_html(self, scene, summary : dict, report_path=None):
486
+ logger.info('Generating HTML ...')
487
+ STATICPATH = os.path.dirname(os.path.realpath(__file__))
488
+ file_loader = FileSystemLoader(os.path.join(STATICPATH, 'report_template'))
489
+ env = Environment(loader=file_loader)
490
+ template = env.get_template('ios.html')
491
+ if report_path:
492
+ html_path = report_path
493
+ else:
494
+ html_path = os.path.join(self.report_dir, scene, 'report.html')
495
+ with open(html_path,'w+') as fout:
496
+ html_content = template.render(devices=summary['devices'],app=summary['app'],
497
+ platform=summary['platform'],ctime=summary['ctime'],
498
+ cpu_app=summary['cpu_app'],cpu_sys=summary['cpu_sys'],gpu=summary['gpu'],
499
+ mem_total=summary['mem_total'],fps=summary['fps'],
500
+ tem=summary['tem'],current=summary['current'],
501
+ voltage=summary['voltage'],power=summary['power'],
502
+ net_send=summary['net_send'],net_recv=summary['net_recv'],
503
+ cpu_charts=summary['cpu_charts'],mem_charts=summary['mem_charts'],
504
+ net_charts=summary['net_charts'],battery_charts=summary['battery_charts'],
505
+ fps_charts=summary['fps_charts'],gpu_charts=summary['gpu_charts'])
506
+ fout.write(html_content)
507
+ logger.info('Generating HTML success : {}'.format(html_path))
508
+ return html_path
509
+
510
+ def filter_secen(self, scene):
511
+ dirs = os.listdir(self.report_dir)
512
+ dir_list = list(reversed(sorted(dirs, key=lambda x: os.path.getmtime(os.path.join(self.report_dir, x)))))
513
+ dir_list.remove(scene)
514
+ return dir_list
515
+
516
+ def get_repordir(self):
517
+ report_dir = os.path.join(os.getcwd(), 'report')
518
+ if not os.path.exists(report_dir):
519
+ os.mkdir(report_dir)
520
+ return report_dir
521
+
522
+ def create_file(self, filename, content=''):
523
+ if not os.path.exists(self.report_dir):
524
+ os.mkdir(self.report_dir)
525
+ with open(os.path.join(self.report_dir, filename), 'a+', encoding="utf-8") as file:
526
+ file.write(content)
527
+
528
+ def add_log(self, path, log_time, value):
529
+ if value >= 0:
530
+ with open(path, 'a+', encoding="utf-8") as file:
531
+ file.write(f'{log_time}={str(value)}' + '\n')
532
+
533
+ def record_net(self, type, send , recv):
534
+ net_dict = dict()
535
+ match(type):
536
+ case 'pre':
537
+ net_dict['send'] = send
538
+ net_dict['recv'] = recv
539
+ content = json.dumps(net_dict)
540
+ self.create_file(filename='pre_net.json', content=content)
541
+ case 'end':
542
+ net_dict['send'] = send
543
+ net_dict['recv'] = recv
544
+ content = json.dumps(net_dict)
545
+ self.create_file(filename='end_net.json', content=content)
546
+ case _:
547
+ logger.error('record network data failed')
548
+
549
+ def _safe_move_file(self, src, dst, max_retries=5, retry_delay=1):
550
+ """安全移动文件,处理文件被占用的情况"""
551
+ for attempt in range(max_retries):
552
+ try:
553
+ # 检查目标路径,如果是目录则生成完整的目标文件路径
554
+ if os.path.isdir(dst):
555
+ dst_file = os.path.join(dst, os.path.basename(src))
556
+ else:
557
+ dst_file = dst
558
+
559
+ # 如果目标文件已存在,先尝试删除(可能是之前不完整的移动)
560
+ if os.path.exists(dst_file):
561
+ logger.warning(f'目标文件已存在,尝试删除: {dst_file}')
562
+ try:
563
+ os.remove(dst_file)
564
+ logger.info(f'成功删除已存在的目标文件: {dst_file}')
565
+ except Exception as delete_error:
566
+ logger.warning(f'无法删除已存在文件: {delete_error}')
567
+ # 如果无法删除,生成新的文件名
568
+ base, ext = os.path.splitext(dst_file)
569
+ counter = 1
570
+ while os.path.exists(dst_file):
571
+ dst_file = f"{base}_{counter}{ext}"
572
+ counter += 1
573
+ logger.info(f'生成新的目标文件名: {dst_file}')
574
+
575
+ shutil.move(src, dst_file)
576
+ logger.info(f'文件移动成功: {src} -> {dst_file}')
577
+ return True
578
+
579
+ except PermissionError as e:
580
+ if attempt < max_retries - 1:
581
+ logger.warning(f'文件被占用,等待后重试 ({attempt + 1}/{max_retries}): {src}')
582
+ time.sleep(retry_delay)
583
+ retry_delay *= 2 # 指数退避
584
+ else:
585
+ logger.error(f'移动文件失败,已达到最大重试次数: {src} -> {dst}')
586
+ raise e
587
+ except Exception as e:
588
+ logger.error(f'移动文件时发生错误: {src} -> {dst}, 错误: {e}')
589
+ # 对于非权限错误,不重试,直接抛出
590
+ raise e
591
+ return False
592
+
593
+ def make_report(self, app, devices, video, platform=Platform.Android, model='normal', cores=0):
594
+ logger.info('Generating test results ...')
595
+ current_time = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
596
+ result_dict = {
597
+ "app": app,
598
+ "icon": "",
599
+ "platform": platform,
600
+ "model": model,
601
+ "devices": devices,
602
+ "ctime": current_time,
603
+ "video": video,
604
+ "cores":cores
605
+ }
606
+ content = json.dumps(result_dict)
607
+ self.create_file(filename='result.json', content=content)
608
+ report_new_dir = os.path.join(self.report_dir, f'apm_{current_time}')
609
+ if not os.path.exists(report_new_dir):
610
+ os.mkdir(report_new_dir)
611
+
612
+ # 安全移动文件,特别处理可能被占用的录屏文件
613
+ moved_files = []
614
+ for f in os.listdir(self.report_dir):
615
+ filename = os.path.join(self.report_dir, f)
616
+ if f.split(".")[-1] in ['log', 'json', 'mkv']:
617
+ # 检查文件是否真实存在且不是目录
618
+ if not os.path.isfile(filename):
619
+ logger.warning(f'跳过非文件项: {filename}')
620
+ continue
621
+
622
+ try:
623
+ if f.endswith('.mkv'):
624
+ # 录屏文件可能被占用,使用安全移动方法
625
+ logger.info(f'移动录屏文件: {filename}')
626
+ self._safe_move_file(filename, report_new_dir)
627
+ else:
628
+ # 普通文件直接移动
629
+ dst_file = os.path.join(report_new_dir, f)
630
+ if os.path.exists(dst_file):
631
+ logger.warning(f'目标文件已存在,删除后重新移动: {dst_file}')
632
+ os.remove(dst_file)
633
+ shutil.move(filename, report_new_dir)
634
+ moved_files.append(f)
635
+ except Exception as e:
636
+ logger.error(f'移动文件失败: {filename}, 错误: {e}')
637
+ # 继续处理其他文件,不中断整个流程
638
+ continue
639
+
640
+ if moved_files:
641
+ logger.info(f'成功移动文件: {moved_files}')
642
+ else:
643
+ logger.info('没有文件需要移动')
644
+
645
+ logger.info('Generating test results success: {}'.format(report_new_dir))
646
+ return f'apm_{current_time}'
647
+
648
+ def instance_type(self, data):
649
+ if isinstance(data, float):
650
+ return 'float'
651
+ elif isinstance(data, int):
652
+ return 'int'
653
+ else:
654
+ return 'int'
655
+
656
+ def open_file(self, path, mode):
657
+ with open(path, mode) as f:
658
+ for line in f:
659
+ yield line
660
+
661
+ def readJson(self, scene):
662
+ path = os.path.join(self.report_dir,scene,'result.json')
663
+ result_json = open(file=path, mode='r').read()
664
+ result_dict = json.loads(result_json)
665
+ return result_dict
666
+
667
+ def readLog(self, scene, filename, max_points=0):
668
+ """
669
+ Read apmlog file data with optional downsampling
670
+
671
+ Args:
672
+ scene: 场景名称
673
+ filename: 日志文件名
674
+ max_points: 最大数据点数,0 表示不采样,默认 0
675
+
676
+ Returns:
677
+ (log_data_list, target_data_list, total_points) 三元组
678
+ - log_data_list: [{"x": timestamp, "y": value}, ...]
679
+ - target_data_list: [value, ...]
680
+ - total_points: 原始数据点总数
681
+ """
682
+ log_data_list = list()
683
+ target_data_list = list()
684
+ if os.path.exists(os.path.join(self.report_dir, scene, filename)):
685
+ lines = self.open_file(os.path.join(self.report_dir, scene, filename), "r")
686
+ for line in lines:
687
+ if isinstance(line.split('=')[1].strip(), int):
688
+ log_data_list.append({
689
+ "x": line.split('=')[0].strip(),
690
+ "y": int(line.split('=')[1].strip())
691
+ })
692
+ target_data_list.append(int(line.split('=')[1].strip()))
693
+ else:
694
+ log_data_list.append({
695
+ "x": line.split('=')[0].strip(),
696
+ "y": float(line.split('=')[1].strip())
697
+ })
698
+ target_data_list.append(float(line.split('=')[1].strip()))
699
+
700
+ # 记录原始数据点数量
701
+ total_points = len(log_data_list)
702
+
703
+ # 应用 LTTB 降采样
704
+ if max_points > 0 and len(log_data_list) > max_points:
705
+ log_data_list = downsample_lttb(log_data_list, max_points)
706
+ # 重建 target_data_list
707
+ target_data_list = [item['y'] for item in log_data_list]
708
+
709
+ return log_data_list, target_data_list, total_points
710
+
711
+ def getCpuLog(self, platform, scene, max_points=0):
712
+ targetDic = dict()
713
+ cpu_app_data, _, cpu_app_total = self.readLog(scene=scene, filename='cpu_app.log', max_points=max_points)
714
+ cpu_sys_data, _, cpu_sys_total = self.readLog(scene=scene, filename='cpu_sys.log', max_points=max_points)
715
+ targetDic['cpuAppData'] = cpu_app_data
716
+ targetDic['cpuSysData'] = cpu_sys_data
717
+ result = {
718
+ 'status': 1,
719
+ 'cpuAppData': targetDic['cpuAppData'],
720
+ 'cpuSysData': targetDic['cpuSysData'],
721
+ 'meta': {
722
+ 'sampled': max_points > 0 and max(cpu_app_total, cpu_sys_total) > max_points,
723
+ 'max_points': max_points,
724
+ 'total_points': max(cpu_app_total, cpu_sys_total)
725
+ }
726
+ }
727
+ return result
728
+
729
+ def getCpuLogCompare(self, platform, scene1, scene2, max_points=0):
730
+ targetDic = dict()
731
+ scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='cpu_app.log', max_points=max_points)
732
+ scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='cpu_app.log', max_points=max_points)
733
+ targetDic['scene1'] = scene1_data
734
+ targetDic['scene2'] = scene2_data
735
+ result = {
736
+ 'status': 1,
737
+ 'scene1': targetDic['scene1'],
738
+ 'scene2': targetDic['scene2'],
739
+ 'meta': {
740
+ 'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
741
+ 'max_points': max_points,
742
+ 'total_points': max(scene1_total, scene2_total)
743
+ }
744
+ }
745
+ return result
746
+
747
+ def getGpuLog(self, platform, scene, max_points=0):
748
+ targetDic = dict()
749
+ gpu_data, _, total_points = self.readLog(scene=scene, filename='gpu.log', max_points=max_points)
750
+ targetDic['gpu'] = gpu_data
751
+ result = {
752
+ 'status': 1,
753
+ 'gpu': targetDic['gpu'],
754
+ 'meta': {
755
+ 'sampled': max_points > 0 and total_points > max_points,
756
+ 'max_points': max_points,
757
+ 'total_points': total_points
758
+ }
759
+ }
760
+ return result
761
+
762
+ def getGpuLogCompare(self, platform, scene1, scene2, max_points=0):
763
+ targetDic = dict()
764
+ scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='gpu.log', max_points=max_points)
765
+ scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='gpu.log', max_points=max_points)
766
+ targetDic['scene1'] = scene1_data
767
+ targetDic['scene2'] = scene2_data
768
+ result = {
769
+ 'status': 1,
770
+ 'scene1': targetDic['scene1'],
771
+ 'scene2': targetDic['scene2'],
772
+ 'meta': {
773
+ 'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
774
+ 'max_points': max_points,
775
+ 'total_points': max(scene1_total, scene2_total)
776
+ }
777
+ }
778
+ return result
779
+
780
+ def getMemLog(self, platform, scene, max_points=0):
781
+ targetDic = dict()
782
+ mem_total_data, _, mem_total_total = self.readLog(scene=scene, filename='mem_total.log', max_points=max_points)
783
+ targetDic['memTotalData'] = mem_total_data
784
+ total_points = mem_total_total
785
+ if platform == Platform.Android:
786
+ mem_swap_data, _, mem_swap_total = self.readLog(scene=scene, filename='mem_swap.log', max_points=max_points)
787
+ targetDic['memSwapData'] = mem_swap_data
788
+ total_points = max(mem_total_total, mem_swap_total)
789
+ result = {
790
+ 'status': 1,
791
+ 'memTotalData': targetDic['memTotalData'],
792
+ 'memSwapData': targetDic['memSwapData'],
793
+ 'meta': {
794
+ 'sampled': max_points > 0 and total_points > max_points,
795
+ 'max_points': max_points,
796
+ 'total_points': total_points
797
+ }
798
+ }
799
+ else:
800
+ result = {
801
+ 'status': 1,
802
+ 'memTotalData': targetDic['memTotalData'],
803
+ 'meta': {
804
+ 'sampled': max_points > 0 and total_points > max_points,
805
+ 'max_points': max_points,
806
+ 'total_points': total_points
807
+ }
808
+ }
809
+ return result
810
+
811
+ def getMemDetailLog(self, platform, scene, max_points=0):
812
+ targetDic = dict()
813
+ total_points_list = []
814
+ for key, filename in [
815
+ ('java_heap', 'mem_java_heap.log'),
816
+ ('native_heap', 'mem_native_heap.log'),
817
+ ('code_pss', 'mem_code_pss.log'),
818
+ ('stack_pss', 'mem_stack_pss.log'),
819
+ ('graphics_pss', 'mem_graphics_pss.log'),
820
+ ('private_pss', 'mem_private_pss.log'),
821
+ ('system_pss', 'mem_system_pss.log')
822
+ ]:
823
+ data, _, total = self.readLog(scene=scene, filename=filename, max_points=max_points)
824
+ targetDic[key] = data
825
+ total_points_list.append(total)
826
+ max_total = max(total_points_list) if total_points_list else 0
827
+ result = {
828
+ 'status': 1,
829
+ 'memory_detail': targetDic,
830
+ 'meta': {
831
+ 'sampled': max_points > 0 and max_total > max_points,
832
+ 'max_points': max_points,
833
+ 'total_points': max_total
834
+ }
835
+ }
836
+ return result
837
+
838
+ def getCpuCoreLog(self, platform, scene, max_points=0):
839
+ targetDic = dict()
840
+ cores = self.readJson(scene=scene).get('cores', 0)
841
+ total_points_list = []
842
+ if int(cores) > 0:
843
+ for i in range(int(cores)):
844
+ data, _, total = self.readLog(scene=scene, filename='cpu{}.log'.format(i), max_points=max_points)
845
+ targetDic['cpu{}'.format(i)] = data
846
+ total_points_list.append(total)
847
+ max_total = max(total_points_list) if total_points_list else 0
848
+ result = {
849
+ 'status': 1,
850
+ 'cores': cores,
851
+ 'cpu_core': targetDic,
852
+ 'meta': {
853
+ 'sampled': max_points > 0 and max_total > max_points,
854
+ 'max_points': max_points,
855
+ 'total_points': max_total
856
+ }
857
+ }
858
+ return result
859
+
860
+ def getMemLogCompare(self, platform, scene1, scene2, max_points=0):
861
+ targetDic = dict()
862
+ scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='mem_total.log', max_points=max_points)
863
+ scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='mem_total.log', max_points=max_points)
864
+ targetDic['scene1'] = scene1_data
865
+ targetDic['scene2'] = scene2_data
866
+ result = {
867
+ 'status': 1,
868
+ 'scene1': targetDic['scene1'],
869
+ 'scene2': targetDic['scene2'],
870
+ 'meta': {
871
+ 'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
872
+ 'max_points': max_points,
873
+ 'total_points': max(scene1_total, scene2_total)
874
+ }
875
+ }
876
+ return result
877
+
878
+ def getBatteryLog(self, platform, scene, max_points=0):
879
+ targetDic = dict()
880
+ total_points_list = []
881
+ if platform == Platform.Android:
882
+ level_data, _, level_total = self.readLog(scene=scene, filename='battery_level.log', max_points=max_points)
883
+ tem_data, _, tem_total = self.readLog(scene=scene, filename='battery_tem.log', max_points=max_points)
884
+ targetDic['batteryLevel'] = level_data
885
+ targetDic['batteryTem'] = tem_data
886
+ total_points_list = [level_total, tem_total]
887
+ result = {
888
+ 'status': 1,
889
+ 'batteryLevel': targetDic['batteryLevel'],
890
+ 'batteryTem': targetDic['batteryTem'],
891
+ 'meta': {
892
+ 'sampled': max_points > 0 and max(total_points_list) > max_points,
893
+ 'max_points': max_points,
894
+ 'total_points': max(total_points_list)
895
+ }
896
+ }
897
+ else:
898
+ tem_data, _, tem_total = self.readLog(scene=scene, filename='battery_tem.log', max_points=max_points)
899
+ current_data, _, current_total = self.readLog(scene=scene, filename='battery_current.log', max_points=max_points)
900
+ voltage_data, _, voltage_total = self.readLog(scene=scene, filename='battery_voltage.log', max_points=max_points)
901
+ power_data, _, power_total = self.readLog(scene=scene, filename='battery_power.log', max_points=max_points)
902
+ targetDic['batteryTem'] = tem_data
903
+ targetDic['batteryCurrent'] = current_data
904
+ targetDic['batteryVoltage'] = voltage_data
905
+ targetDic['batteryPower'] = power_data
906
+ total_points_list = [tem_total, current_total, voltage_total, power_total]
907
+ result = {
908
+ 'status': 1,
909
+ 'batteryTem': targetDic['batteryTem'],
910
+ 'batteryCurrent': targetDic['batteryCurrent'],
911
+ 'batteryVoltage': targetDic['batteryVoltage'],
912
+ 'batteryPower': targetDic['batteryPower'],
913
+ 'meta': {
914
+ 'sampled': max_points > 0 and max(total_points_list) > max_points,
915
+ 'max_points': max_points,
916
+ 'total_points': max(total_points_list)
917
+ }
918
+ }
919
+ return result
920
+
921
+ def getBatteryLogCompare(self, platform, scene1, scene2, max_points=0):
922
+ targetDic = dict()
923
+ if platform == Platform.Android:
924
+ scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='battery_level.log', max_points=max_points)
925
+ scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='battery_level.log', max_points=max_points)
926
+ else:
927
+ scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='batteryPower.log', max_points=max_points)
928
+ scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='batteryPower.log', max_points=max_points)
929
+ targetDic['scene1'] = scene1_data
930
+ targetDic['scene2'] = scene2_data
931
+ result = {
932
+ 'status': 1,
933
+ 'scene1': targetDic['scene1'],
934
+ 'scene2': targetDic['scene2'],
935
+ 'meta': {
936
+ 'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
937
+ 'max_points': max_points,
938
+ 'total_points': max(scene1_total, scene2_total)
939
+ }
940
+ }
941
+ return result
942
+
943
+ def getFlowLog(self, platform, scene, max_points=0):
944
+ targetDic = dict()
945
+ up_data, _, up_total = self.readLog(scene=scene, filename='upflow.log', max_points=max_points)
946
+ down_data, _, down_total = self.readLog(scene=scene, filename='downflow.log', max_points=max_points)
947
+ targetDic['upFlow'] = up_data
948
+ targetDic['downFlow'] = down_data
949
+ max_total = max(up_total, down_total)
950
+ result = {
951
+ 'status': 1,
952
+ 'upFlow': targetDic['upFlow'],
953
+ 'downFlow': targetDic['downFlow'],
954
+ 'meta': {
955
+ 'sampled': max_points > 0 and max_total > max_points,
956
+ 'max_points': max_points,
957
+ 'total_points': max_total
958
+ }
959
+ }
960
+ return result
961
+
962
+ def getFlowSendLogCompare(self, platform, scene1, scene2, max_points=0):
963
+ targetDic = dict()
964
+ scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='upflow.log', max_points=max_points)
965
+ scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='upflow.log', max_points=max_points)
966
+ targetDic['scene1'] = scene1_data
967
+ targetDic['scene2'] = scene2_data
968
+ result = {
969
+ 'status': 1,
970
+ 'scene1': targetDic['scene1'],
971
+ 'scene2': targetDic['scene2'],
972
+ 'meta': {
973
+ 'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
974
+ 'max_points': max_points,
975
+ 'total_points': max(scene1_total, scene2_total)
976
+ }
977
+ }
978
+ return result
979
+
980
+ def getFlowRecvLogCompare(self, platform, scene1, scene2, max_points=0):
981
+ targetDic = dict()
982
+ scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='downflow.log', max_points=max_points)
983
+ scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='downflow.log', max_points=max_points)
984
+ targetDic['scene1'] = scene1_data
985
+ targetDic['scene2'] = scene2_data
986
+ result = {
987
+ 'status': 1,
988
+ 'scene1': targetDic['scene1'],
989
+ 'scene2': targetDic['scene2'],
990
+ 'meta': {
991
+ 'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
992
+ 'max_points': max_points,
993
+ 'total_points': max(scene1_total, scene2_total)
994
+ }
995
+ }
996
+ return result
997
+
998
+ def getFpsLog(self, platform, scene, max_points=0):
999
+ targetDic = dict()
1000
+ fps_data, _, fps_total = self.readLog(scene=scene, filename='fps.log', max_points=max_points)
1001
+ targetDic['fps'] = fps_data
1002
+ total_points = fps_total
1003
+ if platform == Platform.Android:
1004
+ jank_data, _, jank_total = self.readLog(scene=scene, filename='jank.log', max_points=max_points)
1005
+ targetDic['jank'] = jank_data
1006
+ total_points = max(fps_total, jank_total)
1007
+ result = {
1008
+ 'status': 1,
1009
+ 'fps': targetDic['fps'],
1010
+ 'jank': targetDic['jank'],
1011
+ 'meta': {
1012
+ 'sampled': max_points > 0 and total_points > max_points,
1013
+ 'max_points': max_points,
1014
+ 'total_points': total_points
1015
+ }
1016
+ }
1017
+ else:
1018
+ result = {
1019
+ 'status': 1,
1020
+ 'fps': targetDic['fps'],
1021
+ 'meta': {
1022
+ 'sampled': max_points > 0 and total_points > max_points,
1023
+ 'max_points': max_points,
1024
+ 'total_points': total_points
1025
+ }
1026
+ }
1027
+ return result
1028
+
1029
+ def getDiskLog(self, platform, scene, max_points=0):
1030
+ targetDic = dict()
1031
+ used_data, _, used_total = self.readLog(scene=scene, filename='disk_used.log', max_points=max_points)
1032
+ free_data, _, free_total = self.readLog(scene=scene, filename='disk_free.log', max_points=max_points)
1033
+ targetDic['used'] = used_data
1034
+ targetDic['free'] = free_data
1035
+ max_total = max(used_total, free_total)
1036
+ result = {
1037
+ 'status': 1,
1038
+ 'used': targetDic['used'],
1039
+ 'free': targetDic['free'],
1040
+ 'meta': {
1041
+ 'sampled': max_points > 0 and max_total > max_points,
1042
+ 'max_points': max_points,
1043
+ 'total_points': max_total
1044
+ }
1045
+ }
1046
+ return result
1047
+
1048
+ def analysisDisk(self, scene):
1049
+ initail_disk_list = list()
1050
+ current_disk_list = list()
1051
+ sum_init_disk = dict()
1052
+ sum_current_disk = dict()
1053
+ if os.path.exists(os.path.join(self.report_dir,scene,'initail_disk.log')):
1054
+ size_list = list()
1055
+ used_list = list()
1056
+ free_list = list()
1057
+ lines = self.open_file(os.path.join(self.report_dir,scene,'initail_disk.log'), "r")
1058
+ for line in lines:
1059
+ if 'Filesystem' not in line and line.strip() != '':
1060
+ disk_value_list = line.split()
1061
+ disk_dict = dict(
1062
+ filesystem = disk_value_list[0],
1063
+ blocks = disk_value_list[1],
1064
+ used = disk_value_list[2],
1065
+ available = disk_value_list[3],
1066
+ use_percent = disk_value_list[4],
1067
+ mounted = disk_value_list[5]
1068
+ )
1069
+ initail_disk_list.append(disk_dict)
1070
+ size_list.append(int(disk_value_list[1]))
1071
+ used_list.append(int(disk_value_list[2]))
1072
+ free_list.append(int(disk_value_list[3]))
1073
+ sum_init_disk['sum_size'] = int(sum(size_list) / 1024 / 1024)
1074
+ sum_init_disk['sum_used'] = int(sum(used_list) / 1024)
1075
+ sum_init_disk['sum_free'] = int(sum(free_list) / 1024)
1076
+
1077
+ if os.path.exists(os.path.join(self.report_dir,scene,'current_disk.log')):
1078
+ size_list = list()
1079
+ used_list = list()
1080
+ free_list = list()
1081
+ lines = self.open_file(os.path.join(self.report_dir,scene,'current_disk.log'), "r")
1082
+ for line in lines:
1083
+ if 'Filesystem' not in line and line.strip() != '':
1084
+ disk_value_list = line.split()
1085
+ disk_dict = dict(
1086
+ filesystem = disk_value_list[0],
1087
+ blocks = disk_value_list[1],
1088
+ used = disk_value_list[2],
1089
+ available = disk_value_list[3],
1090
+ use_percent = disk_value_list[4],
1091
+ mounted = disk_value_list[5]
1092
+ )
1093
+ current_disk_list.append(disk_dict)
1094
+ size_list.append(int(disk_value_list[1]))
1095
+ used_list.append(int(disk_value_list[2]))
1096
+ free_list.append(int(disk_value_list[3]))
1097
+ sum_current_disk['sum_size'] = int(sum(size_list) / 1024 / 1024)
1098
+ sum_current_disk['sum_used'] = int(sum(used_list) / 1024)
1099
+ sum_current_disk['sum_free'] = int(sum(free_list) / 1024)
1100
+
1101
+ return initail_disk_list, current_disk_list, sum_init_disk, sum_current_disk
1102
+
1103
+ def getFpsLogCompare(self, platform, scene1, scene2, max_points=0):
1104
+ targetDic = dict()
1105
+ scene1_data, _, scene1_total = self.readLog(scene=scene1, filename='fps.log', max_points=max_points)
1106
+ scene2_data, _, scene2_total = self.readLog(scene=scene2, filename='fps.log', max_points=max_points)
1107
+ targetDic['scene1'] = scene1_data
1108
+ targetDic['scene2'] = scene2_data
1109
+ result = {
1110
+ 'status': 1,
1111
+ 'scene1': targetDic['scene1'],
1112
+ 'scene2': targetDic['scene2'],
1113
+ 'meta': {
1114
+ 'sampled': max_points > 0 and max(scene1_total, scene2_total) > max_points,
1115
+ 'max_points': max_points,
1116
+ 'total_points': max(scene1_total, scene2_total)
1117
+ }
1118
+ }
1119
+ return result
1120
+
1121
+ def approximateSize(self, size, a_kilobyte_is_1024_bytes=True):
1122
+ '''
1123
+ convert a file size to human-readable form.
1124
+ Keyword arguments:
1125
+ size -- file size in bytes
1126
+ a_kilobyte_is_1024_bytes -- if True (default),use multiples of 1024
1127
+ if False, use multiples of 1000
1128
+ Returns: string
1129
+ '''
1130
+
1131
+ suffixes = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
1132
+ 1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}
1133
+
1134
+ if size < 0:
1135
+ raise ValueError('number must be non-negative')
1136
+
1137
+ multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
1138
+
1139
+ for suffix in suffixes[multiple]:
1140
+ size /= multiple
1141
+ if size < multiple:
1142
+ return '{0:.2f} {1}'.format(size, suffix)
1143
+
1144
+ def _setAndroidPerfs(self, scene):
1145
+ """Aggregate APM data for Android"""
1146
+
1147
+ app = self.readJson(scene=scene).get('app')
1148
+ devices = self.readJson(scene=scene).get('devices')
1149
+ platform = self.readJson(scene=scene).get('platform')
1150
+ ctime = self.readJson(scene=scene).get('ctime')
1151
+
1152
+ _, cpuAppData, _ = self.readLog(scene=scene, filename=f'cpu_app.log')
1153
+ _, cpuSystemData, _ = self.readLog(scene=scene, filename=f'cpu_sys.log')
1154
+ if cpuAppData.__len__() > 0 and cpuSystemData.__len__() > 0:
1155
+ cpuAppRate = f'{round(sum(cpuAppData) / len(cpuAppData), 2)}%'
1156
+ cpuSystemRate = f'{round(sum(cpuSystemData) / len(cpuSystemData), 2)}%'
1157
+ else:
1158
+ cpuAppRate, cpuSystemRate = 0, 0
1159
+
1160
+ _, batteryLevelData, _ = self.readLog(scene=scene, filename=f'battery_level.log')
1161
+ _, batteryTemlData, _ = self.readLog(scene=scene, filename=f'battery_tem.log')
1162
+ if batteryLevelData.__len__() > 0 and batteryTemlData.__len__() > 0:
1163
+ batteryLevel = f'{batteryLevelData[-1]}%'
1164
+ batteryTeml = f'{batteryTemlData[-1]}°C'
1165
+ else:
1166
+ batteryLevel, batteryTeml = 0, 0
1167
+
1168
+
1169
+ _, totalPassData, _ = self.readLog(scene=scene, filename=f'mem_total.log')
1170
+
1171
+ if totalPassData.__len__() > 0:
1172
+ _, swapPassData, _ = self.readLog(scene=scene, filename=f'mem_swap.log')
1173
+ totalPassAvg = f'{round(sum(totalPassData) / len(totalPassData), 2)}MB'
1174
+ swapPassAvg = f'{round(sum(swapPassData) / len(swapPassData), 2)}MB'
1175
+ else:
1176
+ totalPassAvg, swapPassAvg = 0, 0
1177
+
1178
+ _, fpsData, _ = self.readLog(scene=scene, filename=f'fps.log')
1179
+ _, jankData, _ = self.readLog(scene=scene, filename=f'jank.log')
1180
+ if fpsData.__len__() > 0:
1181
+ fpsAvg = f'{int(sum(fpsData) / len(fpsData))}HZ/s'
1182
+ jankAvg = f'{int(sum(jankData))}'
1183
+ else:
1184
+ fpsAvg, jankAvg = 0, 0
1185
+
1186
+ pre_net_path = os.path.join(self.report_dir, scene, 'pre_net.json')
1187
+ end_net_path = os.path.join(self.report_dir, scene, 'end_net.json')
1188
+ if os.path.exists(pre_net_path) and os.path.exists(end_net_path):
1189
+ with open(pre_net_path) as f_pre, open(end_net_path) as f_end:
1190
+ json_pre = json.loads(f_pre.read())
1191
+ json_end = json.loads(f_end.read())
1192
+ send = json_end['send'] - json_pre['send']
1193
+ recv = json_end['recv'] - json_pre['recv']
1194
+ else:
1195
+ send, recv = 0, 0
1196
+ flowSend = f'{round(float(send / 1024), 2)}MB'
1197
+ flowRecv = f'{round(float(recv / 1024), 2)}MB'
1198
+
1199
+ _, gpuData, _ = self.readLog(scene=scene, filename='gpu.log')
1200
+ if gpuData.__len__() > 0:
1201
+ gpu = round(sum(gpuData) / len(gpuData), 2)
1202
+ else:
1203
+ gpu = 0
1204
+
1205
+ mem_detail_flag = os.path.exists(os.path.join(self.report_dir,scene,'mem_java_heap.log'))
1206
+ disk_flag = os.path.exists(os.path.join(self.report_dir,scene,'disk_free.log'))
1207
+ thermal_flag = os.path.exists(os.path.join(self.report_dir,scene,'init_thermal_temp.json'))
1208
+ cpu_core_flag = os.path.exists(os.path.join(self.report_dir,scene,'cpu0.log'))
1209
+ apm_dict = dict()
1210
+ apm_dict['app'] = app
1211
+ apm_dict['devices'] = devices
1212
+ apm_dict['platform'] = platform
1213
+ apm_dict['ctime'] = ctime
1214
+ apm_dict['cpuAppRate'] = cpuAppRate
1215
+ apm_dict['cpuSystemRate'] = cpuSystemRate
1216
+ apm_dict['totalPassAvg'] = totalPassAvg
1217
+ apm_dict['swapPassAvg'] = swapPassAvg
1218
+ apm_dict['fps'] = fpsAvg
1219
+ apm_dict['jank'] = jankAvg
1220
+ apm_dict['flow_send'] = flowSend
1221
+ apm_dict['flow_recv'] = flowRecv
1222
+ apm_dict['batteryLevel'] = batteryLevel
1223
+ apm_dict['batteryTeml'] = batteryTeml
1224
+ apm_dict['mem_detail_flag'] = mem_detail_flag
1225
+ apm_dict['disk_flag'] = disk_flag
1226
+ apm_dict['gpu'] = gpu
1227
+ apm_dict['thermal_flag'] = thermal_flag
1228
+ apm_dict['cpu_core_flag'] = cpu_core_flag
1229
+
1230
+ if thermal_flag:
1231
+ init_thermal_temp = json.loads(open(os.path.join(self.report_dir,scene,'init_thermal_temp.json')).read())
1232
+ current_thermal_temp = json.loads(open(os.path.join(self.report_dir,scene,'current_thermal_temp.json')).read())
1233
+ apm_dict['init_thermal_temp'] = init_thermal_temp
1234
+ apm_dict['current_thermal_temp'] = current_thermal_temp
1235
+
1236
+ return apm_dict
1237
+
1238
+ def _setiOSPerfs(self, scene):
1239
+ """Aggregate APM data for iOS"""
1240
+
1241
+ app = self.readJson(scene=scene).get('app')
1242
+ devices = self.readJson(scene=scene).get('devices')
1243
+ platform = self.readJson(scene=scene).get('platform')
1244
+ ctime = self.readJson(scene=scene).get('ctime')
1245
+
1246
+ _, cpuAppData, _ = self.readLog(scene=scene, filename=f'cpu_app.log')
1247
+ _, cpuSystemData, _ = self.readLog(scene=scene, filename=f'cpu_sys.log')
1248
+ if cpuAppData.__len__() > 0 and cpuSystemData.__len__() > 0:
1249
+ cpuAppRate = f'{round(sum(cpuAppData) / len(cpuAppData), 2)}%'
1250
+ cpuSystemRate = f'{round(sum(cpuSystemData) / len(cpuSystemData), 2)}%'
1251
+ else:
1252
+ cpuAppRate, cpuSystemRate = 0, 0
1253
+
1254
+ _, totalPassData, _ = self.readLog(scene=scene, filename='mem_total.log')
1255
+ if totalPassData.__len__() > 0:
1256
+ totalPassAvg = f'{round(sum(totalPassData) / len(totalPassData), 2)}MB'
1257
+ else:
1258
+ totalPassAvg = 0
1259
+
1260
+ _, fpsData, _ = self.readLog(scene=scene, filename='fps.log')
1261
+ if fpsData.__len__() > 0:
1262
+ fpsAvg = f'{int(sum(fpsData) / len(fpsData))}HZ/s'
1263
+ else:
1264
+ fpsAvg = 0
1265
+
1266
+ _, flowSendData, _ = self.readLog(scene=scene, filename='upflow.log')
1267
+ _, flowRecvData, _ = self.readLog(scene=scene, filename='downflow.log')
1268
+ if flowSendData.__len__() > 0:
1269
+ flowSend = f'{round(float(sum(flowSendData) / 1024), 2)}MB'
1270
+ flowRecv = f'{round(float(sum(flowRecvData) / 1024), 2)}MB'
1271
+ else:
1272
+ flowSend, flowRecv = 0, 0
1273
+
1274
+ _, batteryTemlData, _ = self.readLog(scene=scene, filename='battery_tem.log')
1275
+ _, batteryCurrentData, _ = self.readLog(scene=scene, filename='battery_current.log')
1276
+ _, batteryVoltageData, _ = self.readLog(scene=scene, filename='battery_voltage.log')
1277
+ _, batteryPowerData, _ = self.readLog(scene=scene, filename='battery_power.log')
1278
+ if batteryTemlData.__len__() > 0:
1279
+ batteryTeml = int(batteryTemlData[-1])
1280
+ batteryCurrent = int(sum(batteryCurrentData) / len(batteryCurrentData))
1281
+ batteryVoltage = int(sum(batteryVoltageData) / len(batteryVoltageData))
1282
+ batteryPower = int(sum(batteryPowerData) / len(batteryPowerData))
1283
+ else:
1284
+ batteryTeml, batteryCurrent, batteryVoltage, batteryPower = 0, 0, 0, 0
1285
+
1286
+ _, gpuData, _ = self.readLog(scene=scene, filename='gpu.log')
1287
+ if gpuData.__len__() > 0:
1288
+ gpu = round(sum(gpuData) / len(gpuData), 2)
1289
+ else:
1290
+ gpu = 0
1291
+ disk_flag = os.path.exists(os.path.join(self.report_dir, scene, 'disk_free.log'))
1292
+ apm_dict = dict()
1293
+ apm_dict['app'] = app
1294
+ apm_dict['devices'] = devices
1295
+ apm_dict['platform'] = platform
1296
+ apm_dict['ctime'] = ctime
1297
+ apm_dict['cpuAppRate'] = cpuAppRate
1298
+ apm_dict['cpuSystemRate'] = cpuSystemRate
1299
+ apm_dict['totalPassAvg'] = totalPassAvg
1300
+ apm_dict['nativePassAvg'] = 0
1301
+ apm_dict['dalvikPassAvg'] = 0
1302
+ apm_dict['fps'] = fpsAvg
1303
+ apm_dict['jank'] = 0
1304
+ apm_dict['flow_send'] = flowSend
1305
+ apm_dict['flow_recv'] = flowRecv
1306
+ apm_dict['batteryTeml'] = batteryTeml
1307
+ apm_dict['batteryCurrent'] = batteryCurrent
1308
+ apm_dict['batteryVoltage'] = batteryVoltage
1309
+ apm_dict['batteryPower'] = batteryPower
1310
+ apm_dict['gpu'] = gpu
1311
+ apm_dict['disk_flag'] = disk_flag
1312
+ return apm_dict
1313
+
1314
+ def _setpkPerfs(self, scene):
1315
+ """Aggregate APM data for pk model"""
1316
+ _, cpuAppData1, _ = self.readLog(scene=scene, filename='cpu_app1.log')
1317
+ cpuAppRate1 = f'{round(sum(cpuAppData1) / len(cpuAppData1), 2)}%'
1318
+ _, cpuAppData2, _ = self.readLog(scene=scene, filename='cpu_app2.log')
1319
+ cpuAppRate2 = f'{round(sum(cpuAppData2) / len(cpuAppData2), 2)}%'
1320
+
1321
+ _, totalPassData1, _ = self.readLog(scene=scene, filename='mem1.log')
1322
+ totalPassAvg1 = f'{round(sum(totalPassData1) / len(totalPassData1), 2)}MB'
1323
+ _, totalPassData2, _ = self.readLog(scene=scene, filename='mem2.log')
1324
+ totalPassAvg2 = f'{round(sum(totalPassData2) / len(totalPassData2), 2)}MB'
1325
+
1326
+ _, fpsData1, _ = self.readLog(scene=scene, filename='fps1.log')
1327
+ fpsAvg1 = f'{int(sum(fpsData1) / len(fpsData1))}HZ/s'
1328
+ _, fpsData2, _ = self.readLog(scene=scene, filename='fps2.log')
1329
+ fpsAvg2 = f'{int(sum(fpsData2) / len(fpsData2))}HZ/s'
1330
+
1331
+ _, networkData1, _ = self.readLog(scene=scene, filename='network1.log')
1332
+ network1 = f'{round(float(sum(networkData1) / 1024), 2)}MB'
1333
+ _, networkData2, _ = self.readLog(scene=scene, filename='network2.log')
1334
+ network2 = f'{round(float(sum(networkData2) / 1024), 2)}MB'
1335
+
1336
+ apm_dict = dict()
1337
+ apm_dict['cpuAppRate1'] = cpuAppRate1
1338
+ apm_dict['cpuAppRate2'] = cpuAppRate2
1339
+ apm_dict['totalPassAvg1'] = totalPassAvg1
1340
+ apm_dict['totalPassAvg2'] = totalPassAvg2
1341
+ apm_dict['network1'] = network1
1342
+ apm_dict['network2'] = network2
1343
+ apm_dict['fpsAvg1'] = fpsAvg1
1344
+ apm_dict['fpsAvg2'] = fpsAvg2
1345
+ return apm_dict
1346
+
1347
+ class Method:
1348
+
1349
+ @classmethod
1350
+ def _request(cls, request, object):
1351
+ match(request.method):
1352
+ case 'POST':
1353
+ return request.form[object]
1354
+ case 'GET':
1355
+ return request.args[object]
1356
+ case _:
1357
+ raise Exception('request method error')
1358
+
1359
+ @classmethod
1360
+ def _setValue(cls, value, default = 0):
1361
+ try:
1362
+ result = value
1363
+ except ZeroDivisionError :
1364
+ result = default
1365
+ except IndexError:
1366
+ result = default
1367
+ except Exception:
1368
+ result = default
1369
+ return result
1370
+
1371
+ @classmethod
1372
+ def _settings(cls, request):
1373
+ content = {}
1374
+ content['cpuWarning'] = (0, request.cookies.get('cpuWarning'))[request.cookies.get('cpuWarning') not in [None, 'NaN']]
1375
+ content['memWarning'] = (0, request.cookies.get('memWarning'))[request.cookies.get('memWarning') not in [None, 'NaN']]
1376
+ content['fpsWarning'] = (0, request.cookies.get('fpsWarning'))[request.cookies.get('fpsWarning') not in [None, 'NaN']]
1377
+ content['netdataRecvWarning'] = (0, request.cookies.get('netdataRecvWarning'))[request.cookies.get('netdataRecvWarning') not in [None, 'NaN']]
1378
+ content['netdataSendWarning'] = (0, request.cookies.get('netdataSendWarning'))[request.cookies.get('netdataSendWarning') not in [None, 'NaN']]
1379
+ content['betteryWarning'] = (0, request.cookies.get('betteryWarning'))[request.cookies.get('betteryWarning') not in [None, 'NaN']]
1380
+ content['gpuWarning'] = (0, request.cookies.get('gpuWarning'))[request.cookies.get('gpuWarning') not in [None, 'NaN']]
1381
+ content['duration'] = (0, request.cookies.get('duration'))[request.cookies.get('duration') not in [None, 'NaN']]
1382
+ content['magnax_host'] = ('', request.cookies.get('magnax_host'))[request.cookies.get('magnax_host') not in [None, 'NaN']]
1383
+ content['host_switch'] = request.cookies.get('host_switch')
1384
+ return content
1385
+
1386
+ @classmethod
1387
+ def _index(cls, target: list, index: int, default: any):
1388
+ try:
1389
+ return target[index]
1390
+ except IndexError:
1391
+ return default
1392
+
1393
+ class Install:
1394
+
1395
+ def uploadFile(self, file_path, file_obj):
1396
+ """save upload file"""
1397
+ try:
1398
+ file_obj.save(file_path)
1399
+ return True
1400
+ except Exception as e:
1401
+ logger.exception(e)
1402
+ return False
1403
+
1404
+ def downloadLink(self,filelink=None, path=None, name=None):
1405
+ try:
1406
+ logger.info('Install link : {}'.format(filelink))
1407
+ ssl._create_default_https_context = ssl._create_unverified_context
1408
+ file_size = int(urlopen(filelink).info().get('Content-Length', -1))
1409
+ header = {"Range": "bytes=%s-%s" % (0, file_size)}
1410
+ pbar = tqdm(
1411
+ total=file_size, initial=0,
1412
+ unit='B', unit_scale=True, desc=filelink.split('/')[-1])
1413
+ req = requests.get(filelink, headers=header, stream=True)
1414
+ with(open(os.path.join(path, name), 'ab')) as f:
1415
+ for chunk in req.iter_content(chunk_size=1024):
1416
+ if chunk:
1417
+ f.write(chunk)
1418
+ pbar.update(1024)
1419
+ pbar.close()
1420
+ return True
1421
+ except Exception as e:
1422
+ logger.exception(e)
1423
+ return False
1424
+
1425
+ def installAPK(self, path):
1426
+ result = adb.shell_noDevice(cmd='install -r {}'.format(path))
1427
+ if result == 0:
1428
+ os.remove(path)
1429
+ return True, result
1430
+ else:
1431
+ return False, result
1432
+
1433
+ def installIPA(self, path, device_id=None):
1434
+ """使用 pymobiledevice3 安装 IPA"""
1435
+ try:
1436
+ if not PMD3_AVAILABLE:
1437
+ logger.error("pymobiledevice3 not available, cannot install IPA")
1438
+ return False, -1
1439
+
1440
+ from pymobiledevice3.services.installation_proxy import InstallationProxyService
1441
+
1442
+ # 获取设备列表,如果没有指定设备则使用第一个
1443
+ if device_id is None:
1444
+ devices = pmd3_list_devices()
1445
+ if not devices:
1446
+ logger.error("No iOS device connected")
1447
+ return False, -1
1448
+ device_id = devices[0].serial
1449
+
1450
+ lockdown_client = create_using_usbmux(serial=device_id)
1451
+ if lockdown_client is None:
1452
+ logger.error("Failed to connect to device")
1453
+ return False, -1
1454
+
1455
+ # 使用 InstallationProxyService 安装 IPA
1456
+ installation = InstallationProxyService(lockdown=lockdown_client)
1457
+ installation.install_from_local(path)
1458
+
1459
+ logger.info(f"Successfully installed IPA: {path}")
1460
+ os.remove(path)
1461
+ return True, 0
1462
+
1463
+ except Exception as e:
1464
+ logger.error(f"Failed to install IPA: {e}")
1465
+ return False, -1
1466
+
1467
+ class Scrcpy:
1468
+
1469
+ STATICPATH = os.path.dirname(os.path.realpath(__file__))
1470
+ DEFAULT_SCRCPY_PATH = {
1471
+ "64": os.path.join(STATICPATH, "scrcpy", "scrcpy-win64-v2.4", "scrcpy.exe"),
1472
+ "32": os.path.join(STATICPATH, "scrcpy", "scrcpy-win32-v2.4", "scrcpy.exe"),
1473
+ "default":"scrcpy"
1474
+ }
1475
+
1476
+ @classmethod
1477
+ def scrcpy_path(cls):
1478
+ bit = platform.architecture()[0]
1479
+ path = cls.DEFAULT_SCRCPY_PATH["default"]
1480
+ if platform.system().lower().__contains__('windows'):
1481
+ if bit.__contains__('64'):
1482
+ path = cls.DEFAULT_SCRCPY_PATH["64"]
1483
+ elif bit.__contains__('32'):
1484
+ path = cls.DEFAULT_SCRCPY_PATH["32"]
1485
+ return path
1486
+
1487
+ @classmethod
1488
+ def start_record(cls, device):
1489
+ f = File()
1490
+ logger.info('start record screen')
1491
+ win_cmd = "start /b {scrcpy_path} -s {deviceId} --no-playback --no-power-on --record={video}".format(
1492
+ scrcpy_path = cls.scrcpy_path(),
1493
+ deviceId = device,
1494
+ video = os.path.join(f.report_dir, 'record.mkv')
1495
+ )
1496
+ mac_cmd = "nohup {scrcpy_path} -s {deviceId} --no-playback --no-power-on --record={video} &".format(
1497
+ scrcpy_path = cls.scrcpy_path(),
1498
+ deviceId = device,
1499
+ video = os.path.join(f.report_dir, 'record.mkv')
1500
+ )
1501
+ if platform.system().lower().__contains__('windows'):
1502
+ result = os.system(win_cmd)
1503
+ else:
1504
+ result = os.system(mac_cmd)
1505
+ if result == 0:
1506
+ logger.info("record screen success : {}".format(os.path.join(f.report_dir, 'record.mkv')))
1507
+ else:
1508
+ logger.error("magnax's scrcpy is incompatible with your PC")
1509
+ logger.info("Please install the software yourself : brew install scrcpy")
1510
+ return result
1511
+
1512
+ @classmethod
1513
+ def stop_record(cls):
1514
+ logger.info('stop scrcpy process')
1515
+ pids = psutil.pids()
1516
+ scrcpy_processes = []
1517
+
1518
+ # 首先找到所有scrcpy进程
1519
+ try:
1520
+ for pid in pids:
1521
+ try:
1522
+ p = psutil.Process(pid)
1523
+ if p.name().__contains__('scrcpy'):
1524
+ scrcpy_processes.append(p)
1525
+ logger.info(f'发现scrcpy进程: {pid}')
1526
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
1527
+ continue
1528
+ except Exception as e:
1529
+ logger.exception(e)
1530
+
1531
+ # 尝试温和地终止进程,给scrcpy足够时间完成MKV容器写入
1532
+ if scrcpy_processes:
1533
+ logger.info('尝试温和地停止scrcpy进程...')
1534
+ for process in scrcpy_processes:
1535
+ try:
1536
+ process.terminate() # 发送SIGTERM信号
1537
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
1538
+ continue
1539
+
1540
+ # 等待进程优雅退出,scrcpy需要时间来正确关闭MKV容器(写入索引和元数据)
1541
+ gone, alive = psutil.wait_procs(scrcpy_processes, timeout=15)
1542
+
1543
+ if gone:
1544
+ logger.info(f'scrcpy进程已优雅退出: {[p.pid for p in gone]}')
1545
+
1546
+ if alive:
1547
+ logger.warning(f'scrcpy进程超时未退出,强制终止: {[p.pid for p in alive]}')
1548
+ for process in alive:
1549
+ try:
1550
+ process.kill()
1551
+ logger.info(f'强制终止进程: {process.pid}')
1552
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
1553
+ continue
1554
+ time.sleep(1)
1555
+
1556
+ logger.info('scrcpy进程停止完成')
1557
+ else:
1558
+ logger.info('没有发现运行中的scrcpy进程')
1559
+
1560
+ @classmethod
1561
+ def cast_screen(cls, device):
1562
+ logger.info('start cast screen')
1563
+ win_cmd = "start /i {scrcpy_path} -s {deviceId} --no-power-on".format(
1564
+ scrcpy_path = cls.scrcpy_path(),
1565
+ deviceId = device
1566
+ )
1567
+ mac_cmd = "nohup {scrcpy_path} -s {deviceId} --no-power-on &".format(
1568
+ scrcpy_path = cls.scrcpy_path(),
1569
+ deviceId = device
1570
+ )
1571
+ if platform.system().lower().__contains__('windows'):
1572
+ result = os.system(win_cmd)
1573
+ else:
1574
+ result = os.system(mac_cmd)
1575
+ if result == 0:
1576
+ logger.info("cast screen success")
1577
+ else:
1578
+ logger.error("magnax's scrcpy is incompatible with your PC")
1579
+ logger.info("Please install the software yourself : brew install scrcpy")
1580
+ return result
1581
+
1582
+ @classmethod
1583
+ def play_video(cls, video):
1584
+ logger.info('start play video : {}'.format(video))
1585
+ cap = cv2.VideoCapture(video)
1586
+ while(cap.isOpened()):
1587
+ ret, frame = cap.read()
1588
+ if ret:
1589
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
1590
+ cv2.namedWindow("frame", 0)
1591
+ cv2.resizeWindow("frame", 430, 900)
1592
+ cv2.imshow('frame', gray)
1593
+ if cv2.waitKey(25) & 0xFF == ord('q') or not cv2.getWindowProperty("frame", cv2.WND_PROP_VISIBLE):
1594
+ break
1595
+ else:
1596
+ break
1597
+ cap.release()
1598
+ cv2.destroyAllWindows()