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.
- magnax/__init__.py +3 -0
- magnax/__main__.py +25 -0
- magnax/debug.py +65 -0
- magnax/public/__init__.py +1 -0
- magnax/public/adb/linux/adb +0 -0
- magnax/public/adb/linux_arm/adb +0 -0
- magnax/public/adb/mac/adb +0 -0
- magnax/public/adb/windows/AdbWinApi.dll +0 -0
- magnax/public/adb/windows/AdbWinUsbApi.dll +0 -0
- magnax/public/adb/windows/adb.exe +0 -0
- magnax/public/adb.py +96 -0
- magnax/public/android_fps.py +750 -0
- magnax/public/apm.py +1306 -0
- magnax/public/apm_pk.py +184 -0
- magnax/public/common.py +1598 -0
- magnax/public/config.json +1 -0
- magnax/public/ios_perf_adapter.py +790 -0
- magnax/public/report_template/android.html +526 -0
- magnax/public/report_template/ios.html +482 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/AdbWinApi.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/AdbWinUsbApi.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/SDL2.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/adb.exe +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/avcodec-60.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/avformat-60.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/avutil-58.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/icon.png +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/libusb-1.0.dll +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/open_a_terminal_here.bat +1 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-console.bat +2 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-noconsole.vbs +7 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy-server +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/scrcpy.exe +0 -0
- magnax/public/scrcpy/scrcpy-win32-v2.4/swresample-4.dll +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/AdbWinApi.dll +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/AdbWinUsbApi.dll +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/SDL2.dll +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/avformat-60.dll +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/avutil-58.dll +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/open_a_terminal_here.bat +1 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy-noconsole.vbs +7 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy-server +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/scrcpy.exe +0 -0
- magnax/public/scrcpy/scrcpy-win64-v2.4/swresample-4.dll +0 -0
- magnax/static/css/highlight.min.css +9 -0
- magnax/static/css/magnax-dark-theme.css +1237 -0
- magnax/static/css/select2-bootstrap-5-theme.min.css +3 -0
- magnax/static/css/select2-bootstrap-5-theme.rtl.min.css +3 -0
- magnax/static/css/select2.min.css +1 -0
- magnax/static/css/sweetalert2.min.css +1 -0
- magnax/static/css/tabler.demo.min.css +9 -0
- magnax/static/css/tabler.min.css +14 -0
- magnax/static/image/500.png +0 -0
- magnax/static/image/avatar.png +0 -0
- magnax/static/image/empty.png +0 -0
- magnax/static/image/readme/home.png +0 -0
- magnax/static/image/readme/pk.png +0 -0
- magnax/static/js/apexcharts.js +14 -0
- magnax/static/js/gray.js +16 -0
- magnax/static/js/highlight.min.js +1173 -0
- magnax/static/js/highstock.js +803 -0
- magnax/static/js/html2canvas.min.js +20 -0
- magnax/static/js/jquery.min.js +2 -0
- magnax/static/js/magnax-chart-theme.js +492 -0
- magnax/static/js/select2.min.js +2 -0
- magnax/static/js/sweetalert2.min.js +1 -0
- magnax/static/js/tabler.demo.min.js +9 -0
- magnax/static/js/tabler.min.js +9 -0
- magnax/static/logo/logo.png +0 -0
- magnax/templates/404.html +30 -0
- magnax/templates/analysis.html +1375 -0
- magnax/templates/analysis_compare.html +600 -0
- magnax/templates/analysis_pk.html +680 -0
- magnax/templates/base.html +365 -0
- magnax/templates/index.html +2471 -0
- magnax/templates/pk.html +743 -0
- magnax/templates/report.html +416 -0
- magnax/view/__init__.py +1 -0
- magnax/view/apis.py +952 -0
- magnax/view/pages.py +146 -0
- magnax/web.py +345 -0
- magnax-1.0.0.dist-info/METADATA +242 -0
- magnax-1.0.0.dist-info/RECORD +87 -0
- magnax-1.0.0.dist-info/WHEEL +5 -0
- magnax-1.0.0.dist-info/entry_points.txt +2 -0
- magnax-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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)
|