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
magnax/view/pages.py ADDED
@@ -0,0 +1,146 @@
1
+ import json
2
+ import os
3
+ from flask import Blueprint
4
+ from flask import render_template
5
+ from flask import request
6
+ from logzero import logger
7
+ from solox.public.common import Devices,File,Method
8
+
9
+ page = Blueprint("page", __name__)
10
+ d = Devices()
11
+ m = Method()
12
+ f = File()
13
+
14
+ @page.app_errorhandler(404)
15
+ def page_404(e):
16
+ settings = m._settings(request)
17
+ return render_template('404.html', **locals()), 404
18
+
19
+ @page.app_errorhandler(500)
20
+ def page_500(e):
21
+ settings = m._settings(request)
22
+ return render_template('500.html', **locals()), 500
23
+
24
+ @page.route('/')
25
+ def index():
26
+ platform = request.args.get('platform')
27
+ lan = request.args.get('lan')
28
+ settings = m._settings(request)
29
+ return render_template('index.html', **locals())
30
+
31
+ @page.route('/pk')
32
+ def pk():
33
+ lan = request.args.get('lan')
34
+ model = request.args.get('model')
35
+ settings = m._settings(request)
36
+ return render_template('pk.html', **locals())
37
+
38
+ @page.route('/report')
39
+ def report():
40
+ lan = request.args.get('lan')
41
+ settings = m._settings(request)
42
+ report_dir = os.path.join(os.getcwd(), 'report')
43
+ if not os.path.exists(report_dir):
44
+ os.mkdir(report_dir)
45
+ dirs = os.listdir(report_dir)
46
+ dir_list = reversed(sorted(dirs, key=lambda x: os.path.getmtime(os.path.join(report_dir, x))))
47
+ apm_data = []
48
+ for dir in dir_list:
49
+ if dir.split(".")[-1] not in ['log', 'json', 'mkv']:
50
+ try:
51
+ fpath = open(os.path.join(report_dir, dir, 'result.json'))
52
+ json_data = json.loads(fpath.read())
53
+ dict_data = {
54
+ 'scene': dir,
55
+ 'app': json_data['app'],
56
+ 'platform': json_data['platform'],
57
+ 'model': json_data['model'],
58
+ 'devices': json_data['devices'],
59
+ 'ctime': json_data['ctime'],
60
+ 'video': json_data.get('video', 0)
61
+ }
62
+ fpath.close()
63
+ apm_data.append(dict_data)
64
+ except Exception as e:
65
+ logger.exception(e)
66
+ continue
67
+ apm_data_len = len(apm_data)
68
+ return render_template('report.html', **locals())
69
+
70
+ @page.route('/analysis', methods=['post', 'get'])
71
+ def analysis():
72
+ lan = request.args.get('lan')
73
+ scene = request.args.get('scene')
74
+ app = request.args.get('app')
75
+ platform = request.args.get('platform')
76
+ settings = m._settings(request)
77
+ report_dir = os.path.join(os.getcwd(), 'report')
78
+ dirs = os.listdir(report_dir)
79
+ filter_dir = f.filter_secen(scene)
80
+ apm_data = {}
81
+ # Initialize disk variables with defaults
82
+ initial_disk = []
83
+ current_disk = []
84
+ sum_init_disk = {'sum_size': 0}
85
+ sum_current_disk = {'sum_size': 0}
86
+ for dir in dirs:
87
+ if dir == scene:
88
+ try:
89
+ if platform == 'Android':
90
+ apm_data = f._setAndroidPerfs(scene)
91
+ disk = f.analysisDisk(scene)
92
+ if disk and len(disk) >= 4:
93
+ initial_disk = disk[0]
94
+ current_disk = disk[1]
95
+ sum_init_disk = disk[2]
96
+ sum_current_disk = disk[3]
97
+ else:
98
+ apm_data = f._setiOSPerfs(scene)
99
+ except ZeroDivisionError:
100
+ pass
101
+ except Exception as e:
102
+ logger.exception(e)
103
+ finally:
104
+ break
105
+ return render_template('analysis.html', **locals())
106
+
107
+ @page.route('/pk_analysis', methods=['post', 'get'])
108
+ def analysis_pk():
109
+ lan = request.args.get('lan')
110
+ scene = request.args.get('scene')
111
+ app = request.args.get('app')
112
+ model = request.args.get('model')
113
+ settings = m._settings(request)
114
+ report_dir = os.path.join(os.getcwd(), 'report')
115
+ dirs = os.listdir(report_dir)
116
+ apm_data = {}
117
+ for dir in dirs:
118
+ if dir == scene:
119
+ try:
120
+ apm_data = f._setpkPerfs(scene)
121
+ except Exception as e:
122
+ logger.exception(e)
123
+ finally:
124
+ break
125
+ return render_template('analysis_pk.html', **locals())
126
+
127
+ @page.route('/compare_analysis', methods=['post', 'get'])
128
+ def analysis_compare():
129
+ platform = request.args.get('platform')
130
+ lan = request.args.get('lan')
131
+ scene1 = request.args.get('scene1')
132
+ scene2 = request.args.get('scene2')
133
+ app = request.args.get('app')
134
+ settings = m._settings(request)
135
+ try:
136
+ if platform == 'Android':
137
+ apm_data1 = f._setAndroidPerfs(scene1)
138
+ apm_data2 = f._setAndroidPerfs(scene2)
139
+ elif platform == 'iOS':
140
+ apm_data1 = f._setiOSPerfs(scene1)
141
+ apm_data2 = f._setiOSPerfs(scene2)
142
+ except ZeroDivisionError:
143
+ pass
144
+ except Exception as e:
145
+ logger.exception(e)
146
+ return render_template('analysis_compare.html', **locals())
magnax/web.py ADDED
@@ -0,0 +1,345 @@
1
+ from __future__ import absolute_import
2
+ import multiprocessing
3
+ import subprocess
4
+ import time
5
+ import os
6
+ import platform
7
+ import re
8
+ import webbrowser
9
+ import requests
10
+ import socket
11
+ import sys
12
+ import psutil
13
+ import atexit
14
+ from logzero import logger
15
+ from threading import Lock
16
+ from flask_socketio import SocketIO, disconnect
17
+ from flask import Flask
18
+ from pyfiglet import Figlet
19
+ from solox.view.apis import api
20
+ from solox.view.pages import page
21
+ from solox import __version__
22
+
23
+ # Global reference to tunneld process
24
+ _tunneld_process = None
25
+
26
+ app = Flask(__name__, template_folder='templates', static_folder='static')
27
+ app.register_blueprint(api)
28
+ app.register_blueprint(page)
29
+
30
+ # socketio = SocketIO(app, cors_allowed_origins="*")
31
+ # thread = True
32
+ # thread_lock = Lock()
33
+
34
+
35
+ # @socketio.on('connect', namespace='/logcat')
36
+ # def connect():
37
+ # socketio.emit('start connect', {'data': 'Connected'}, namespace='/logcat')
38
+ # logDir = os.path.join(os.getcwd(),'adblog')
39
+ # if not os.path.exists(logDir):
40
+ # os.mkdir(logDir)
41
+ # global thread
42
+ # thread = True
43
+ # with thread_lock:
44
+ # if thread:
45
+ # thread = socketio.start_background_task(target=backgroundThread)
46
+
47
+
48
+ # def backgroundThread():
49
+ # global thread
50
+ # try:
51
+ # current_time = time.strftime("%Y%m%d%H", time.localtime())
52
+ # logPath = os.path.join(os.getcwd(),'adblog',f'{current_time}.log')
53
+ # logcat = subprocess.Popen(f'adb logcat *:E > {logPath}', stdout=subprocess.PIPE,
54
+ # shell=True)
55
+ # with open(logPath, "r") as f:
56
+ # while thread:
57
+ # socketio.sleep(1)
58
+ # for line in f.readlines():
59
+ # socketio.emit('message', {'data': line}, namespace='/logcat')
60
+ # if logcat.poll() == 0:
61
+ # thread = False
62
+ # except Exception:
63
+ # pass
64
+
65
+
66
+ # @socketio.on('disconnect_request', namespace='/logcat')
67
+ # def disconnect():
68
+ # global thread
69
+ # logger.warning('Logcat client disconnected')
70
+ # thread = False
71
+ # disconnect()
72
+
73
+ def ip() -> str:
74
+ try:
75
+ ip = socket.gethostbyname(socket.gethostname())
76
+ except:
77
+ logger.info('hostname:{}'.format(socket.gethostname()))
78
+ logger.warning('config [127.0.0.1 hostname] in /etc/hosts file')
79
+ ip = '127.0.0.1'
80
+ return ip
81
+
82
+
83
+ def listen(port):
84
+ net_connections = psutil.net_connections()
85
+ conn = [c for c in net_connections if c.status == "LISTEN" and c.laddr.port == port]
86
+ if conn:
87
+ pid = conn[0].pid
88
+ logger.warning('Port {} is used by process {}'.format(port, pid))
89
+ logger.info('you can start solox : python -m solox --host={ip} --port={port}')
90
+ return False
91
+ return True
92
+
93
+ def status(host: str, port: int):
94
+ r = requests.get('http://{}:{}'.format(host, port), timeout=2.0)
95
+ flag = (True, False)[r.status_code == 200]
96
+ return flag
97
+
98
+
99
+ def open_url(host: str, port: int):
100
+ flag = True
101
+ while flag:
102
+ logger.info('start solox service')
103
+ f = Figlet(font="slant", width=300)
104
+ print(f.renderText("SOLOX {}".format(__version__)))
105
+ flag = status(host, port)
106
+ try:
107
+ webbrowser.open('http://{}:{}/?platform=Android&lan=en'.format(host, port), new=2)
108
+ except Exception as e:
109
+ logger.exception(e)
110
+ logger.info('Running on http://{}:{}/?platform=Android&lan=en (Press CTRL+C to quit)'.format(host, port))
111
+
112
+
113
+ def start(host: str, port: int):
114
+ app.run(host=host, port=port, debug=False)
115
+
116
+
117
+ def check_ios17_device():
118
+ """Check if there are iOS 17+ devices connected."""
119
+ try:
120
+ from pymobiledevice3.usbmux import list_devices
121
+ from pymobiledevice3.lockdown import create_using_usbmux
122
+
123
+ devices = list_devices()
124
+ for device in devices:
125
+ try:
126
+ lockdown = create_using_usbmux(serial=device.serial)
127
+ version = lockdown.product_version
128
+ if version:
129
+ major = int(version.split('.')[0])
130
+ if major >= 17:
131
+ return True
132
+ except:
133
+ pass
134
+ return False
135
+ except ImportError:
136
+ return False
137
+ except Exception:
138
+ return False
139
+
140
+
141
+ def is_tunneld_running():
142
+ """Check if tunneld is already running."""
143
+ try:
144
+ from pymobiledevice3.tunneld.api import get_tunneld_devices
145
+ devices = get_tunneld_devices()
146
+ return len(devices) > 0
147
+ except:
148
+ return False
149
+
150
+
151
+ def _run_with_sudo_macos(cmd_args):
152
+ """Run command with sudo using macOS GUI password prompt."""
153
+ try:
154
+ # Use osascript to get admin privileges with GUI prompt
155
+ cmd_str = ' '.join(cmd_args)
156
+ apple_script = f'''
157
+ do shell script "{cmd_str} > /dev/null 2>&1 &" with administrator privileges
158
+ '''
159
+ result = subprocess.run(
160
+ ['osascript', '-e', apple_script],
161
+ capture_output=True,
162
+ timeout=60 # Give user time to enter password
163
+ )
164
+ return result.returncode == 0
165
+ except subprocess.TimeoutExpired:
166
+ logger.warning('[iOS] Password prompt timed out')
167
+ return False
168
+ except Exception as e:
169
+ logger.debug(f'[iOS] osascript failed: {e}')
170
+ return False
171
+
172
+
173
+ def _run_with_sudo_linux(cmd_args):
174
+ """Run command with sudo using Linux GUI password prompt (if available)."""
175
+ try:
176
+ # Try pkexec (PolicyKit) for GUI prompt
177
+ result = subprocess.run(
178
+ ['which', 'pkexec'],
179
+ capture_output=True
180
+ )
181
+ if result.returncode == 0:
182
+ subprocess.Popen(
183
+ ['pkexec'] + cmd_args,
184
+ stdout=subprocess.DEVNULL,
185
+ stderr=subprocess.DEVNULL,
186
+ start_new_session=True
187
+ )
188
+ return True
189
+ except:
190
+ pass
191
+
192
+ # Try zenity/kdialog for password input
193
+ try:
194
+ # Try zenity (GNOME)
195
+ result = subprocess.run(
196
+ ['zenity', '--password', '--title=SoloX - iOS tunneld'],
197
+ capture_output=True,
198
+ text=True,
199
+ timeout=60
200
+ )
201
+ if result.returncode == 0 and result.stdout:
202
+ password = result.stdout.strip()
203
+ proc = subprocess.Popen(
204
+ ['sudo', '-S'] + cmd_args,
205
+ stdin=subprocess.PIPE,
206
+ stdout=subprocess.DEVNULL,
207
+ stderr=subprocess.DEVNULL,
208
+ start_new_session=True
209
+ )
210
+ proc.stdin.write(f'{password}\n'.encode())
211
+ proc.stdin.close()
212
+ return True
213
+ except:
214
+ pass
215
+
216
+ return False
217
+
218
+
219
+ def start_tunneld():
220
+ """Start tunneld daemon for iOS 17+ devices."""
221
+ global _tunneld_process
222
+
223
+ # Only run on macOS/Linux
224
+ if platform.system() == 'Windows':
225
+ logger.warning('[iOS] tunneld is not supported on Windows')
226
+ return False
227
+
228
+ # Check if already running
229
+ if is_tunneld_running():
230
+ logger.info('[iOS] tunneld is already running')
231
+ return True
232
+
233
+ # Check if iOS 17+ device exists
234
+ if not check_ios17_device():
235
+ logger.debug('[iOS] No iOS 17+ device found, skipping tunneld')
236
+ return True
237
+
238
+ logger.info('[iOS] iOS 17+ device detected, starting tunneld...')
239
+
240
+ tunneld_cmd = [sys.executable, '-m', 'pymobiledevice3', 'remote', 'tunneld']
241
+
242
+ # Try to start tunneld
243
+ try:
244
+ # First try without sudo (might work if user has permissions)
245
+ _tunneld_process = subprocess.Popen(
246
+ tunneld_cmd,
247
+ stdout=subprocess.DEVNULL,
248
+ stderr=subprocess.DEVNULL,
249
+ start_new_session=True
250
+ )
251
+
252
+ # Wait a moment and check if it's working
253
+ time.sleep(2)
254
+ if is_tunneld_running():
255
+ logger.info('[iOS] tunneld started successfully')
256
+ return True
257
+
258
+ # If not working, kill it and try with sudo
259
+ if _tunneld_process:
260
+ try:
261
+ _tunneld_process.terminate()
262
+ except:
263
+ pass
264
+ _tunneld_process = None
265
+
266
+ # Try with GUI sudo prompt based on platform
267
+ logger.info('[iOS] Requesting administrator privileges for tunneld...')
268
+
269
+ if platform.system() == 'Darwin':
270
+ # macOS: Use osascript for GUI password prompt
271
+ if _run_with_sudo_macos(tunneld_cmd):
272
+ # Wait for tunneld to start
273
+ for _ in range(10):
274
+ time.sleep(1)
275
+ if is_tunneld_running():
276
+ logger.info('[iOS] tunneld started successfully')
277
+ return True
278
+ else:
279
+ # Linux: Try GUI methods
280
+ if _run_with_sudo_linux(tunneld_cmd):
281
+ for _ in range(10):
282
+ time.sleep(1)
283
+ if is_tunneld_running():
284
+ logger.info('[iOS] tunneld started successfully')
285
+ return True
286
+
287
+ # Last resort: try terminal sudo (will prompt in terminal)
288
+ logger.info('[iOS] Trying terminal sudo (enter password if prompted)...')
289
+ subprocess.Popen(
290
+ ['sudo'] + tunneld_cmd,
291
+ stdout=subprocess.DEVNULL,
292
+ stderr=subprocess.DEVNULL,
293
+ start_new_session=True
294
+ )
295
+
296
+ for _ in range(15):
297
+ time.sleep(1)
298
+ if is_tunneld_running():
299
+ logger.info('[iOS] tunneld started successfully')
300
+ return True
301
+
302
+ logger.warning('[iOS] Could not start tunneld automatically')
303
+ logger.warning('[iOS] Please run manually: sudo python3 -m pymobiledevice3 remote tunneld')
304
+ return False
305
+
306
+ except Exception as e:
307
+ logger.warning(f'[iOS] Failed to start tunneld: {e}')
308
+ logger.warning('[iOS] Please run manually: sudo python3 -m pymobiledevice3 remote tunneld')
309
+ return False
310
+
311
+
312
+ def stop_tunneld():
313
+ """Stop tunneld daemon if we started it."""
314
+ global _tunneld_process
315
+ if _tunneld_process:
316
+ try:
317
+ _tunneld_process.terminate()
318
+ _tunneld_process.wait(timeout=5)
319
+ except:
320
+ try:
321
+ _tunneld_process.kill()
322
+ except:
323
+ pass
324
+ _tunneld_process = None
325
+
326
+
327
+ # Register cleanup on exit
328
+ atexit.register(stop_tunneld)
329
+
330
+
331
+ def main(host=ip(), port=50003):
332
+ # Start tunneld for iOS 17+ devices if needed
333
+ start_tunneld()
334
+
335
+ try:
336
+ pool = multiprocessing.Pool(processes=2)
337
+ pool.apply_async(start, (host, port))
338
+ pool.apply_async(open_url, (host, port))
339
+ pool.close()
340
+ pool.join()
341
+ except KeyboardInterrupt:
342
+ logger.info('stop solox success')
343
+ sys.exit()
344
+ except Exception as e:
345
+ logger.exception(e)