Merge remote-tracking branch 'origin/main'
# Conflicts: # .idea/workspace.xml # Module/DeviceInfo.py # build.bat # resources/FlashLink.exe
This commit is contained in:
2
.idea/iOSAI.iml
generated
2
.idea/iOSAI.iml
generated
@@ -4,7 +4,7 @@
|
|||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Python 3.12 (IOS-AI)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -3,5 +3,5 @@
|
|||||||
<component name="Black">
|
<component name="Black">
|
||||||
<option name="sdkName" value="Python 3.12" />
|
<option name="sdkName" value="Python 3.12" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (IOS-AI)" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
|
||||||
</project>
|
</project>
|
||||||
99
.idea/workspace.xml
generated
99
.idea/workspace.xml
generated
@@ -5,81 +5,17 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
|
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
|
||||||
<change afterPath="$PROJECT_DIR$/Module/log/acList.json" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/Utils/SubprocessKit.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/resources/133bffc17635b7a3bd709492b7a519d96710a4a2/bgv.png" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/iOSAI.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/iOSAI.iml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Entity/DeviceModel.py" beforeDir="false" afterPath="$PROJECT_DIR$/Entity/DeviceModel.py" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Module/FlaskService.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/FlaskService.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Module/FlaskService.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/FlaskService.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/14.0.zip" beforeDir="false" />
|
<change beforePath="$PROJECT_DIR$/Module/FlaskSubprocessManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/FlaskSubprocessManager.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/14.1.zip" beforeDir="false" />
|
<change beforePath="$PROJECT_DIR$/Utils/ThreadManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ThreadManager.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/14.2.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/14.3.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/14.4.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/14.5.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/14.6.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/14.7.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/14.8.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/15.0.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/15.1.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/15.2.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/15.3.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/15.4.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/15.5.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/15.6.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/15.7.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/15.8.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/16.0.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/16.1.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/16.2.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/16.3.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/16.4.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/16.5.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/16.6.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/SupportFiles/16.7.zip" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/Utils/ControlUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ControlUtils.py" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/Utils/LogManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/LogManager.py" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/build-tidevice.bat" beforeDir="false" afterPath="$PROJECT_DIR$/build-tidevice.bat" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/build.bat" beforeDir="false" afterPath="$PROJECT_DIR$/build.bat" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/build.bat" beforeDir="false" afterPath="$PROJECT_DIR$/build.bat" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/resources/FlashLink.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/FlashLink.exe" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/resources/FlashLink.exe" beforeDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/resources/add.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/add.png" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/advertisement.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/advertisement.png" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/back.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/back.png" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/comment.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/comment.png" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/icon.ico" beforeDir="false" afterPath="$PROJECT_DIR$/resources/icon.ico" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevice_id.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevice_id.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicebackup.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicebackup.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicebackup2.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicebackup2.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicebtlogger.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicebtlogger.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicecrashreport.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicecrashreport.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicedate.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicedate.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicedebug.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicedebug.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicedebugserverproxy.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicedebugserverproxy.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicedevmodectl.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicedevmodectl.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicediagnostics.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicediagnostics.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/ideviceenterrecovery.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/ideviceenterrecovery.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/ideviceimagemounter.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/ideviceimagemounter.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/ideviceinfo.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/ideviceinfo.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicename.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicename.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicenotificationproxy.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicenotificationproxy.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicepair.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicepair.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/ideviceprovision.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/ideviceprovision.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicerestore.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicerestore.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicescreenshot.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicescreenshot.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicesetlocation.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicesetlocation.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicesyslog.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicesyslog.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/inetcat.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/inetcat.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/irecovery.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/irecovery.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/libcrypto-3.dll" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/libcrypto-3.dll" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/libcurl.dll" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/libcurl.dll" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/libssl-3.dll" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/libssl-3.dll" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/plistutil.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/plistutil.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/readline.dll" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/readline.dll" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/iproxy/tidevice.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/tidevice.exe" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/like.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/like.png" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/resources/search.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/search.png" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/script/ScriptManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/script/ScriptManager.py" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/tidevice_entry.py" beforeDir="false" afterPath="$PROJECT_DIR$/tidevice_entry.py" afterDir="false" />
|
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -137,10 +73,9 @@
|
|||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
"git-widget-placeholder": "main",
|
"git-widget-placeholder": "main",
|
||||||
"ignore.virus.scanning.warn.message": "true",
|
|
||||||
"javascript.nodejs.core.library.configured.version": "20.17.0",
|
"javascript.nodejs.core.library.configured.version": "20.17.0",
|
||||||
"javascript.nodejs.core.library.typings.version": "20.17.58",
|
"javascript.nodejs.core.library.typings.version": "20.17.58",
|
||||||
"last_opened_file_path": "C:/Users/zhangkai/Desktop/20250916部署的ios项目/iOSAI",
|
"last_opened_file_path": "F:/company code/AI item/20250820/iOSAI",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
@@ -262,7 +197,6 @@
|
|||||||
<option name="IS_MODULE_SDK" value="true" />
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
|
||||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/Utils/test.py" />
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/Utils/test.py" />
|
||||||
<option name="PARAMETERS" value="" />
|
<option name="PARAMETERS" value="" />
|
||||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
@@ -274,8 +208,8 @@
|
|||||||
</configuration>
|
</configuration>
|
||||||
<recent_temporary>
|
<recent_temporary>
|
||||||
<list>
|
<list>
|
||||||
<item itemvalue="Python.123" />
|
|
||||||
<item itemvalue="Python.test" />
|
<item itemvalue="Python.test" />
|
||||||
|
<item itemvalue="Python.123" />
|
||||||
<item itemvalue="Python.Test" />
|
<item itemvalue="Python.Test" />
|
||||||
<item itemvalue="Python.12" />
|
<item itemvalue="Python.12" />
|
||||||
</list>
|
</list>
|
||||||
@@ -284,8 +218,7 @@
|
|||||||
<component name="SharedIndexes">
|
<component name="SharedIndexes">
|
||||||
<attachedChunks>
|
<attachedChunks>
|
||||||
<set>
|
<set>
|
||||||
<option value="bundled-js-predefined-1d06a55b98c1-0b3e54e931b4-JavaScript-PY-241.18034.82" />
|
<option value="bundled-python-sdk-ce6832f46686-7b97d883f26b-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-252.25557.178" />
|
||||||
<option value="bundled-python-sdk-975db3bf15a3-2767605e8bc2-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-241.18034.82" />
|
|
||||||
</set>
|
</set>
|
||||||
</attachedChunks>
|
</attachedChunks>
|
||||||
</component>
|
</component>
|
||||||
@@ -351,12 +284,6 @@
|
|||||||
<workItem from="1757506636968" duration="5910000" />
|
<workItem from="1757506636968" duration="5910000" />
|
||||||
<workItem from="1757567423145" duration="16668000" />
|
<workItem from="1757567423145" duration="16668000" />
|
||||||
<workItem from="1757998910052" duration="3676000" />
|
<workItem from="1757998910052" duration="3676000" />
|
||||||
<workItem from="1758095627214" duration="795000" />
|
|
||||||
<workItem from="1758111876484" duration="4474000" />
|
|
||||||
<workItem from="1758116478749" duration="425000" />
|
|
||||||
<workItem from="1758116920643" duration="529000" />
|
|
||||||
<workItem from="1758117476093" duration="632000" />
|
|
||||||
<workItem from="1758118143217" duration="888000" />
|
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="ai 开始测试">
|
<task id="LOCAL-00001" summary="ai 开始测试">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -428,8 +355,8 @@
|
|||||||
<SUITE FILE_PATH="coverage/iOSAI$LogManager.coverage" NAME="LogManager 覆盖结果" MODIFIED="1756711414832" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/Utils" />
|
<SUITE FILE_PATH="coverage/iOSAI$LogManager.coverage" NAME="LogManager 覆盖结果" MODIFIED="1756711414832" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/Utils" />
|
||||||
<SUITE FILE_PATH="coverage/iOSAI$FlaskService.coverage" NAME="FlaskService 覆盖结果" MODIFIED="1756730187792" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/Module" />
|
<SUITE FILE_PATH="coverage/iOSAI$FlaskService.coverage" NAME="FlaskService 覆盖结果" MODIFIED="1756730187792" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/Module" />
|
||||||
<SUITE FILE_PATH="coverage/iOSAI$test.coverage" NAME="test 覆盖结果" MODIFIED="1756467664420" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
<SUITE FILE_PATH="coverage/iOSAI$test.coverage" NAME="test 覆盖结果" MODIFIED="1756467664420" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
<SUITE FILE_PATH="coverage/iOSAI$1352.coverage" NAME="1352 覆盖结果" MODIFIED="1757662777051" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
|
||||||
<SUITE FILE_PATH="coverage/iOSAI$windows_run.coverage" NAME="windows_run Coverage Results" MODIFIED="1756473558532" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
|
<SUITE FILE_PATH="coverage/iOSAI$windows_run.coverage" NAME="windows_run Coverage Results" MODIFIED="1756473558532" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
|
||||||
|
<SUITE FILE_PATH="coverage/iOSAI$1352.coverage" NAME="1352 覆盖结果" MODIFIED="1757662777051" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
<SUITE FILE_PATH="coverage/iOSAI$mac_wda_agent.coverage" NAME="mac_wda_agent Coverage Results" MODIFIED="1756473148639" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
|
<SUITE FILE_PATH="coverage/iOSAI$mac_wda_agent.coverage" NAME="mac_wda_agent Coverage Results" MODIFIED="1756473148639" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
|
||||||
<SUITE FILE_PATH="coverage/iOSAI$123456.coverage" NAME="123456 覆盖结果" MODIFIED="1757672582575" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
<SUITE FILE_PATH="coverage/iOSAI$123456.coverage" NAME="123456 覆盖结果" MODIFIED="1757672582575" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
<SUITE FILE_PATH="coverage/iOSAI$2111.coverage" NAME="2111 覆盖结果" MODIFIED="1757330714370" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
<SUITE FILE_PATH="coverage/iOSAI$2111.coverage" NAME="2111 覆盖结果" MODIFIED="1757330714370" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
@@ -437,7 +364,7 @@
|
|||||||
<SUITE FILE_PATH="coverage/iOSAI$123__1_.coverage" NAME="123 (1) 覆盖结果" MODIFIED="1756897091135" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
<SUITE FILE_PATH="coverage/iOSAI$123__1_.coverage" NAME="123 (1) 覆盖结果" MODIFIED="1756897091135" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
<SUITE FILE_PATH="coverage/iOSAI$tidevice_entry.coverage" NAME="tidevice_entry 覆盖结果" MODIFIED="1757061969626" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
<SUITE FILE_PATH="coverage/iOSAI$tidevice_entry.coverage" NAME="tidevice_entry 覆盖结果" MODIFIED="1757061969626" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
<SUITE FILE_PATH="coverage/iOSAI$ScriptManager.coverage" NAME="ScriptManager 覆盖结果" MODIFIED="1756896057801" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
|
<SUITE FILE_PATH="coverage/iOSAI$ScriptManager.coverage" NAME="ScriptManager 覆盖结果" MODIFIED="1756896057801" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
|
||||||
<SUITE FILE_PATH="coverage/iOSAI$Main.coverage" NAME="Main 覆盖结果" MODIFIED="1758119032647" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" />
|
<SUITE FILE_PATH="coverage/iOSAI$Main.coverage" NAME="Main 覆盖结果" MODIFIED="1758002271600" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" />
|
||||||
<SUITE FILE_PATH="coverage/iOSAI$123.coverage" NAME="123 覆盖结果" MODIFIED="1758115088356" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
<SUITE FILE_PATH="coverage/iOSAI$123.coverage" NAME="123 覆盖结果" MODIFIED="1758002344317" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -3,24 +3,38 @@ import os
|
|||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
import wda
|
import wda
|
||||||
import threading
|
import threading
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
from tidevice import Usbmux, ConnectionType
|
from tidevice import Usbmux, ConnectionType
|
||||||
from tidevice._device import BaseDevice
|
from tidevice._device import BaseDevice
|
||||||
from Entity.DeviceModel import DeviceModel
|
from Entity.DeviceModel import DeviceModel
|
||||||
from Entity.Variables import WdaAppBundleId
|
from Entity.Variables import WdaAppBundleId
|
||||||
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
||||||
from Utils.LogManager import LogManager
|
from Utils.LogManager import LogManager
|
||||||
|
from Utils.SubprocessKit import check_output as sp_check_output, popen as sp_popen
|
||||||
|
|
||||||
|
|
||||||
class Deviceinfo(object):
|
class Deviceinfo(object):
|
||||||
"""设备生命周期管理:以 deviceModelList 为唯一真理源"""
|
"""设备生命周期管理:以 deviceModelList 为唯一真理源"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
...
|
||||||
|
# ✅ 新增:连接线程池(最大 6 并发)
|
||||||
|
self._connect_pool = ThreadPoolExecutor(max_workers=6)
|
||||||
|
...
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
self._si = subprocess.STARTUPINFO()
|
||||||
|
self._si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
self._si.wShowWindow = subprocess.SW_HIDE # 0
|
||||||
|
else:
|
||||||
|
self._si = None
|
||||||
|
|
||||||
self.deviceIndex = 0
|
self.deviceIndex = 0
|
||||||
self.screenProxy = 9110
|
self.screenProxy = 9110
|
||||||
self.pidList: List[Dict] = [] # 仅记录 iproxy 进程
|
self.pidList: List[Dict] = [] # 仅记录 iproxy 进程
|
||||||
@@ -30,14 +44,12 @@ class Deviceinfo(object):
|
|||||||
|
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._model_index: Dict[str, DeviceModel] = {} # udid -> model
|
self._model_index: Dict[str, DeviceModel] = {} # udid -> model
|
||||||
self._miss_count: Dict[str, int] = {} # udid -> 连续未扫描到次数
|
# ✅ 1. 失踪时间戳记录(替代原来的 miss_count)
|
||||||
self._port_pool: List[int] = [] # 端口回收池
|
self._last_seen: Dict[str, float] = {}
|
||||||
self._port_in_use: set[int] = set() # 正在使用的端口
|
self._port_pool: List[int] = []
|
||||||
|
self._port_in_use: set[int] = set()
|
||||||
|
|
||||||
# 🔥1. 启动 WDA 健康检查线程
|
# region iproxy 初始化(原逻辑不变)
|
||||||
# threading.Thread(target=self._wda_health_checker, daemon=True).start()
|
|
||||||
|
|
||||||
# region iproxy 初始化
|
|
||||||
try:
|
try:
|
||||||
self.iproxy_path = self._iproxy_path()
|
self.iproxy_path = self._iproxy_path()
|
||||||
self.iproxy_dir = self.iproxy_path.parent
|
self.iproxy_dir = self.iproxy_path.parent
|
||||||
@@ -48,13 +60,14 @@ class Deviceinfo(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
self._creationflags = 0x08000000 if os.name == "nt" else 0
|
self._creationflags = 0x08000000 if os.name == "nt" else 0
|
||||||
|
|
||||||
self._popen_kwargs = dict(
|
self._popen_kwargs = dict(
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
cwd=str(self.iproxy_dir),
|
cwd=str(self.iproxy_dir),
|
||||||
shell=False,
|
shell=False,
|
||||||
text=True,
|
text=True,
|
||||||
creationflags=self._creationflags,
|
creationflags=0x08000000 if os.name == "nt" else 0, # CREATE_NO_WINDOW
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
bufsize=1,
|
bufsize=1,
|
||||||
)
|
)
|
||||||
@@ -85,7 +98,11 @@ class Deviceinfo(object):
|
|||||||
LogManager.error(f"初始化 iproxy 失败:{e}")
|
LogManager.error(f"初始化 iproxy 失败:{e}")
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 主监听循环 → 只负责“发现”和“提交任务”
|
||||||
|
# ------------------------------------------------------------------
|
||||||
def startDeviceListener(self):
|
def startDeviceListener(self):
|
||||||
|
MISS_WINDOW = 5.0
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
lists = Usbmux().device_list()
|
lists = Usbmux().device_list()
|
||||||
@@ -95,50 +112,55 @@ class Deviceinfo(object):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
now_udids = {d.udid for d in lists if d.conn_type == ConnectionType.USB}
|
now_udids = {d.udid for d in lists if d.conn_type == ConnectionType.USB}
|
||||||
|
usb_sn_set = self._usb_enumerate_sn()
|
||||||
|
|
||||||
# 1. 失踪登记 & 累加
|
# 1. 失踪判定(同旧逻辑)
|
||||||
need_remove = None # ← 新增:放锁外记录
|
need_remove = []
|
||||||
with self._lock:
|
with self._lock:
|
||||||
for udid in list(self._model_index.keys()):
|
for udid in list(self._model_index.keys()):
|
||||||
if udid not in now_udids:
|
if udid not in now_udids:
|
||||||
self._miss_count[udid] = self._miss_count.get(udid, 0) + 1
|
last = self._last_seen.get(udid, time.time())
|
||||||
if self._miss_count[udid] >= 3:
|
if time.time() - last > MISS_WINDOW and udid not in usb_sn_set:
|
||||||
self._miss_count.pop(udid, None)
|
need_remove.append(udid)
|
||||||
need_remove = udid # ← 只记录,不调用
|
|
||||||
else:
|
else:
|
||||||
self._miss_count.pop(udid, None)
|
self._last_seen[udid] = time.time()
|
||||||
|
for udid in need_remove:
|
||||||
|
self._remove_model(udid)
|
||||||
|
|
||||||
# 🔓 锁已释放,再删设备(不会重入)
|
# 2. 发现新设备 → 并发连接
|
||||||
if need_remove:
|
with self._lock:
|
||||||
self._remove_model(need_remove)
|
new_udids = [d.udid for d in lists
|
||||||
|
if d.conn_type == ConnectionType.USB and
|
||||||
# 2. 全新插入(只处理未在线且信任且未满)
|
d.udid not in self._model_index and
|
||||||
for d in lists:
|
len(self.deviceModelList) < self.maxDeviceCount]
|
||||||
if d.conn_type != ConnectionType.USB:
|
if new_udids:
|
||||||
continue
|
futures = {self._connect_pool.submit(self._connect_device_task, udid): udid
|
||||||
udid = d.udid
|
for udid in new_udids}
|
||||||
with self._lock:
|
for f in as_completed(futures, timeout=10):
|
||||||
if udid in self._model_index:
|
udid = futures[f]
|
||||||
continue
|
try:
|
||||||
if not self.is_device_trusted(udid):
|
f.result(timeout=8) # 单台 8 s 硬截止
|
||||||
continue
|
except Exception as e:
|
||||||
if len(self.deviceModelList) >= self.maxDeviceCount:
|
LogManager.error(f"连接任务超时/失败: {e}", udid)
|
||||||
continue
|
|
||||||
try:
|
|
||||||
self.connectDevice(udid)
|
|
||||||
except Exception as e:
|
|
||||||
LogManager.error(f"连接设备失败 {udid}: {e}", udid)
|
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
# 🔥2. WDA 健康检查
|
# ------------------------------------------------------------------
|
||||||
|
# ✅ 3. USB 层枚举 SN(跨平台)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _usb_enumerate_sn(self) -> set[str]:
|
||||||
|
try:
|
||||||
|
out = sp_check_output(["idevice_id", "-l"], text=True, timeout=3)
|
||||||
|
return {line.strip() for line in out.splitlines() if line.strip()}
|
||||||
|
except Exception:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
# ===================== 以下代码与原文件完全一致 =====================
|
||||||
def _wda_health_checker(self):
|
def _wda_health_checker(self):
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
print(len(self.deviceModelList))
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
online = [m for m in self.deviceModelList if m.ready] # ← 只检查就绪的
|
online = [m for m in self.deviceModelList if m.ready]
|
||||||
print(len(online))
|
|
||||||
for model in online:
|
for model in online:
|
||||||
udid = model.deviceId
|
udid = model.deviceId
|
||||||
if not self._wda_ok(udid):
|
if not self._wda_ok(udid):
|
||||||
@@ -147,32 +169,24 @@ class Deviceinfo(object):
|
|||||||
self._remove_model(udid)
|
self._remove_model(udid)
|
||||||
self.connectDevice(udid)
|
self.connectDevice(udid)
|
||||||
|
|
||||||
# 🔥3. 真正做 health-check 的地方
|
|
||||||
def _wda_ok(self, udid: str) -> bool:
|
def _wda_ok(self, udid: str) -> bool:
|
||||||
"""返回 True 表示 WDA 活着,False 表示已死"""
|
|
||||||
try:
|
try:
|
||||||
# 用 2 秒超时快速探测
|
|
||||||
c = wda.USBClient(udid, 8100)
|
c = wda.USBClient(udid, 8100)
|
||||||
# 下面这句就是“xctest launched but check failed” 的触发点
|
|
||||||
# 如果 status 里返回了 WebDriverAgent 运行信息就认为 OK
|
|
||||||
st = c.status()
|
st = c.status()
|
||||||
if st.get("state") != "success":
|
if st.get("state") != "success":
|
||||||
return False
|
return False
|
||||||
# 你也可以再苛刻一点,多做一次 /wda/healthcheck
|
|
||||||
# c.http.get("/wda/healthcheck")
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 任何异常(连接拒绝、超时、json 解析失败)都认为已死
|
|
||||||
LogManager.error(f"WDA health-check 异常:{e}", udid)
|
LogManager.error(f"WDA health-check 异常:{e}", udid)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# region ===================== 增删改查唯一入口(线程安全) =====================
|
# -------------------- 增删改查唯一入口(未改动) --------------------
|
||||||
def _has_model(self, udid: str) -> bool:
|
def _has_model(self, udid: str) -> bool:
|
||||||
return udid in self._model_index
|
return udid in self._model_index
|
||||||
|
|
||||||
def _add_model(self, model: DeviceModel):
|
def _add_model(self, model: DeviceModel):
|
||||||
if model.deviceId in self._model_index:
|
if model.deviceId in self._model_index:
|
||||||
return # 防重复
|
return
|
||||||
model.ready = True
|
model.ready = True
|
||||||
self.deviceModelList.append(model)
|
self.deviceModelList.append(model)
|
||||||
self._model_index[model.deviceId] = model
|
self._model_index[model.deviceId] = model
|
||||||
@@ -180,60 +194,48 @@ class Deviceinfo(object):
|
|||||||
self.manager.send(model.toDict())
|
self.manager.send(model.toDict())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.warning(f"{model.deviceId} 发送上线事件失败:{e}")
|
LogManager.warning(f"{model.deviceId} 发送上线事件失败:{e}")
|
||||||
LogManager.method_info(f"{model.deviceId} 加入设备成功,当前在线数:{len(self.deviceModelList)}",method="device_count")
|
LogManager.method_info(f"{model.deviceId} 加入设备成功,当前在线数:{len(self.deviceModelList)}", method="device_count")
|
||||||
|
|
||||||
|
|
||||||
# 删除设备
|
|
||||||
def _remove_model(self, udid: str):
|
def _remove_model(self, udid: str):
|
||||||
print(f"【删】进入删除方法 udid={udid}")
|
print(f"【删】进入删除方法 udid={udid}")
|
||||||
LogManager.method_info(f"【删】进入删除方法 udid={udid}", method="device_count")
|
LogManager.method_info(f"【删】进入删除方法 udid={udid}", method="device_count")
|
||||||
# 1. 纯内存临界区——毫秒级
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
print(f"【删】拿到锁 udid={udid}")
|
print(f"【删】拿到锁 udid={udid}")
|
||||||
LogManager.method_info(f"【删】拿到锁 udid={udid}",
|
LogManager.method_info(f"【删】拿到锁 udid={udid}", method="device_count")
|
||||||
method="device_count")
|
|
||||||
model = self._model_index.pop(udid, None)
|
model = self._model_index.pop(udid, None)
|
||||||
if not model:
|
if not model:
|
||||||
print(f"【删】模型已空,直接返回 udid={udid}")
|
print(f"【删】模型已空,直接返回 udid={udid}")
|
||||||
LogManager.method_info(f"【删】模型已空,直接返回 udid={udid}",method="device_count")
|
LogManager.method_info(f"【删】模型已空,直接返回 udid={udid}", method="device_count")
|
||||||
return
|
return
|
||||||
if model.deleting:
|
if model.deleting:
|
||||||
print(f"【删】正在删除中,幂等返回 udid={udid}")
|
print(f"【删】正在删除中,幂等返回 udid={udid}")
|
||||||
LogManager.method_info(method="device_count", text=f"【删】正在删除中,幂等返回 udid={udid}")
|
LogManager.method_info(method="device_count", text=f"【删】正在删除中,幂等返回 udid={udid}")
|
||||||
return
|
return
|
||||||
model.deleting = True
|
model.deleting = True
|
||||||
# 标记维删除设备
|
|
||||||
model.type = 2
|
model.type = 2
|
||||||
print(f"【删】标记 deleting=True udid={udid}")
|
print(f"【删】标记 deleting=True udid={udid}")
|
||||||
LogManager.method_info("【删】标记 deleting=True udid={udid}","device_count")
|
LogManager.method_info("【删】标记 deleting=True udid={udid}", "device_count")
|
||||||
# 过滤列表
|
|
||||||
before = len(self.deviceModelList)
|
before = len(self.deviceModelList)
|
||||||
self.deviceModelList = [m for m in self.deviceModelList if m.deviceId != udid]
|
self.deviceModelList = [m for m in self.deviceModelList if m.deviceId != udid]
|
||||||
after = len(self.deviceModelList)
|
after = len(self.deviceModelList)
|
||||||
print(f"【删】列表过滤 before={before} → after={after} udid={udid}")
|
print(f"【删】列表过滤 before={before} → after={after} udid={udid}")
|
||||||
LogManager.method_info(f"【删】列表过滤 before={before} → after={after} udid={udid}","device_count")
|
LogManager.method_info(f"【删】列表过滤 before={before} → after={after} udid={udid}", "device_count")
|
||||||
|
|
||||||
# 端口
|
|
||||||
self._port_in_use.discard(model.screenPort)
|
self._port_in_use.discard(model.screenPort)
|
||||||
self._port_pool.append(model.screenPort)
|
self._port_pool.append(model.screenPort)
|
||||||
print(f"【删】回收端口 port={model.screenPort} udid={udid}")
|
print(f"【删】回收端口 port={model.screenPort} udid={udid}")
|
||||||
LogManager.method_info(f"【删】回收端口 port={model.screenPort} udid={udid}", method="device_count")
|
LogManager.method_info(f"【删】回收端口 port={model.screenPort} udid={udid}", method="device_count")
|
||||||
|
|
||||||
# 进程
|
|
||||||
to_kill = [item for item in self.pidList if item.get("id") == udid]
|
to_kill = [item for item in self.pidList if item.get("id") == udid]
|
||||||
self.pidList = [item for item in self.pidList if item.get("id") != udid]
|
self.pidList = [item for item in self.pidList if item.get("id") != udid]
|
||||||
print(f"【删】待杀进程数 count={len(to_kill)} udid={udid}")
|
print(f"【删】待杀进程数 count={len(to_kill)} udid={udid}")
|
||||||
LogManager.method_info(f"【删】待杀进程数 count={len(to_kill)} udid={udid}", method="device_count")
|
LogManager.method_info(f"【删】待杀进程数 count={len(to_kill)} udid={udid}", method="device_count")
|
||||||
|
|
||||||
# 2. IO 区无锁
|
|
||||||
for idx, item in enumerate(to_kill, 1):
|
for idx, item in enumerate(to_kill, 1):
|
||||||
print(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}")
|
print(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}")
|
||||||
LogManager.method_error(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}", method="device_count")
|
LogManager.method_info(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}", method="device_count")
|
||||||
self._terminate_proc(item.get("target"))
|
self._terminate_proc(item.get("target"))
|
||||||
print(f"【删】进程清理完成 udid={udid}")
|
print(f"【删】进程清理完成 udid={udid}")
|
||||||
LogManager.method_info(f"【删】进程清理完成 udid={udid}", method="device_count")
|
LogManager.method_info(f"【删】进程清理完成 udid={udid}", method="device_count")
|
||||||
|
|
||||||
# 3. 网络 IO
|
|
||||||
retry = 3
|
retry = 3
|
||||||
while retry:
|
while retry:
|
||||||
try:
|
try:
|
||||||
@@ -255,7 +257,7 @@ class Deviceinfo(object):
|
|||||||
print(len(self.deviceModelList))
|
print(len(self.deviceModelList))
|
||||||
LogManager.method_info(f"当前剩余设备数量:{len(self.deviceModelList)}", method="device_count")
|
LogManager.method_info(f"当前剩余设备数量:{len(self.deviceModelList)}", method="device_count")
|
||||||
|
|
||||||
# region ===================== 端口分配与回收 =====================
|
# -------------------- 端口分配与回收(未改动) --------------------
|
||||||
def _alloc_port(self) -> int:
|
def _alloc_port(self) -> int:
|
||||||
if self._port_pool:
|
if self._port_pool:
|
||||||
port = self._port_pool.pop()
|
port = self._port_pool.pop()
|
||||||
@@ -269,14 +271,14 @@ class Deviceinfo(object):
|
|||||||
if port in self._port_in_use:
|
if port in self._port_in_use:
|
||||||
self._port_in_use.remove(port)
|
self._port_in_use.remove(port)
|
||||||
self._port_pool.append(port)
|
self._port_pool.append(port)
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region ===================== 单台设备连接 =====================
|
# ------------------------------------------------------------------
|
||||||
def connectDevice(self, udid: str):
|
# 线程池里真正干活的地方(原 connectDevice 逻辑搬过来)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _connect_device_task(self, udid: str):
|
||||||
if not self.is_device_trusted(udid):
|
if not self.is_device_trusted(udid):
|
||||||
LogManager.warning("设备未信任,跳过 WDA 启动", udid)
|
LogManager.warning("设备未信任,跳过 WDA 启动", udid)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
d = wda.USBClient(udid, 8100)
|
d = wda.USBClient(udid, 8100)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -293,23 +295,34 @@ class Deviceinfo(object):
|
|||||||
|
|
||||||
port = self._alloc_port()
|
port = self._alloc_port()
|
||||||
model = DeviceModel(udid, port, width, height, scale, type=1)
|
model = DeviceModel(udid, port, width, height, scale, type=1)
|
||||||
self._add_model(model)
|
|
||||||
|
|
||||||
|
# 先做完所有 IO,再抢锁写内存
|
||||||
try:
|
try:
|
||||||
d.app_start(WdaAppBundleId)
|
d.app_start(WdaAppBundleId)
|
||||||
d.home()
|
d.home()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.warning(f"启动/切回桌面失败:{e}", udid)
|
LogManager.warning(f"启动/切回桌面失败:{e}", udid)
|
||||||
|
|
||||||
time.sleep(2)
|
time.sleep(2) # 原逻辑保留
|
||||||
|
|
||||||
# 先清旧进程再启动新进程
|
|
||||||
self.pidList = [item for item in self.pidList if item.get("id") != udid]
|
|
||||||
target = self.relayDeviceScreenPort(udid, port)
|
target = self.relayDeviceScreenPort(udid, port)
|
||||||
if target:
|
|
||||||
self.pidList.append({"target": target, "id": udid})
|
|
||||||
|
|
||||||
# region ===================== 工具方法 =====================
|
# 毫秒级临界区
|
||||||
|
with self._lock:
|
||||||
|
if udid in self._model_index: # 并发防重
|
||||||
|
return
|
||||||
|
self._add_model(model)
|
||||||
|
if target:
|
||||||
|
self.pidList.append({"target": target, "id": udid})
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 原函数保留(改名即可)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def connectDevice(self, udid: str):
|
||||||
|
"""对外保留接口,实际走线程池"""
|
||||||
|
self._connect_pool.submit(self._connect_device_task, udid)
|
||||||
|
|
||||||
|
# -------------------- 工具方法(未改动) --------------------
|
||||||
def is_device_trusted(self, udid: str) -> bool:
|
def is_device_trusted(self, udid: str) -> bool:
|
||||||
try:
|
try:
|
||||||
d = BaseDevice(udid)
|
d = BaseDevice(udid)
|
||||||
@@ -319,22 +332,16 @@ class Deviceinfo(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def relayDeviceScreenPort(self, udid: str, port: int) -> Optional[subprocess.Popen]:
|
def relayDeviceScreenPort(self, udid: str, port: int) -> Optional[subprocess.Popen]:
|
||||||
"""启动 iproxy 前:端口若仍被占用则先杀掉占用者,再启动"""
|
|
||||||
if not self._spawn_iproxy:
|
if not self._spawn_iproxy:
|
||||||
LogManager.error("iproxy 启动器未就绪", udid)
|
LogManager.error("iproxy 启动器未就绪", udid)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# --- 新增:端口冲突检查 + 强制清理 ---
|
|
||||||
while self._port_in_use and self._is_port_open(port):
|
while self._port_in_use and self._is_port_open(port):
|
||||||
# 先查是哪个进程占用
|
|
||||||
pid = self._get_pid_by_port(port)
|
pid = self._get_pid_by_port(port)
|
||||||
if pid and pid != os.getpid():
|
if pid and pid != os.getpid():
|
||||||
LogManager.warning(f"端口 {port} 仍被 PID {pid} 占用,尝试释放", udid)
|
LogManager.warning(f"端口 {port} 仍被 PID {pid} 占用,尝试释放", udid)
|
||||||
self._kill_pid_gracefully(pid)
|
self._kill_pid_gracefully(pid)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
# -------------------------------------
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
p = self._spawn_iproxy(udid, port, 9100)
|
p = self._spawn_iproxy(udid, port, 9100)
|
||||||
self._port_in_use.add(port)
|
self._port_in_use.add(port)
|
||||||
@@ -344,30 +351,25 @@ class Deviceinfo(object):
|
|||||||
LogManager.error(f"启动 iproxy 失败:{e}", udid)
|
LogManager.error(f"启动 iproxy 失败:{e}", udid)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ------------------- 新增三个小工具 -------------------
|
|
||||||
def _is_port_open(self, port: int) -> bool:
|
def _is_port_open(self, port: int) -> bool:
|
||||||
import socket
|
import socket
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
return s.connect_ex(("127.0.0.1", port)) == 0
|
return s.connect_ex(("127.0.0.1", port)) == 0
|
||||||
|
|
||||||
def _get_pid_by_port(self, port: int) -> Optional[int]:
|
def _get_pid_by_port(self, port: int) -> Optional[int]:
|
||||||
"""跨平台根据端口号查 PID,失败返回 None"""
|
|
||||||
try:
|
try:
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
cmd = ["netstat", "-ano", "-p", "tcp"]
|
out = sp_check_output(["netstat", "-ano", "-p", "tcp"], text=True)
|
||||||
out = subprocess.check_output(cmd, text=True)
|
|
||||||
for line in out.splitlines():
|
for line in out.splitlines():
|
||||||
if f"127.0.0.1:{port}" in line and "LISTENING" in line:
|
if f"127.0.0.1:{port}" in line and "LISTENING" in line:
|
||||||
return int(line.strip().split()[-1])
|
return int(line.strip().split()[-1])
|
||||||
else:
|
else:
|
||||||
cmd = ["lsof", "-t", f"-iTCP:{port}", "-sTCP:LISTEN"]
|
out = sp_check_output(["lsof", "-t", f"-iTCP:{port}", "-sTCP:LISTEN"], text=True)
|
||||||
out = subprocess.check_output(cmd, text=True)
|
|
||||||
return int(out.strip().split()[0])
|
return int(out.strip().split()[0])
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _kill_pid_gracefully(self, pid: int):
|
def _kill_pid_gracefully(self, pid: int):
|
||||||
"""先 terminate 再 kill -9"""
|
|
||||||
try:
|
try:
|
||||||
os.kill(pid, signal.SIGTERM)
|
os.kill(pid, signal.SIGTERM)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@@ -375,7 +377,6 @@ class Deviceinfo(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _terminate_proc(self, p: Optional[subprocess.Popen]):
|
def _terminate_proc(self, p: Optional[subprocess.Popen]):
|
||||||
if not p or p.poll() is not None:
|
if not p or p.poll() is not None:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -266,8 +266,9 @@ def watchLiveForGrowth():
|
|||||||
def stopScript():
|
def stopScript():
|
||||||
body = request.get_json()
|
body = request.get_json()
|
||||||
udid = body.get("udid")
|
udid = body.get("udid")
|
||||||
code, massage = ThreadManager.stop(udid)
|
LogManager.method_info(f"接口收到 /stopScript udid={udid}", method="task")
|
||||||
return ResultData(code=code, data="", massage=massage).toJson()
|
code, msg = ThreadManager.stop(udid)
|
||||||
|
return ResultData(code=code, data="", massage=msg).toJson()
|
||||||
|
|
||||||
|
|
||||||
# 关注打招呼
|
# 关注打招呼
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Union, Dict, List
|
from typing import Optional, Union, Dict, List
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
|
||||||
from Utils.LogManager import LogManager
|
from Utils.LogManager import LogManager
|
||||||
|
|
||||||
|
|
||||||
@@ -28,9 +30,39 @@ class FlaskSubprocessManager:
|
|||||||
self.comm_port = 34566
|
self.comm_port = 34566
|
||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
self._monitor_thread: Optional[threading.Thread] = None
|
self._monitor_thread: Optional[threading.Thread] = None
|
||||||
|
# 新增:启动前先把可能残留的 Flask 干掉
|
||||||
|
self._kill_orphan_flask()
|
||||||
atexit.register(self.stop)
|
atexit.register(self.stop)
|
||||||
LogManager.info("FlaskSubprocessManager 单例已初始化", udid="system")
|
LogManager.info("FlaskSubprocessManager 单例已初始化", udid="system")
|
||||||
|
|
||||||
|
def _kill_orphan_flask(self):
|
||||||
|
"""根据端口 34566 把遗留进程全部杀掉"""
|
||||||
|
try:
|
||||||
|
if os.name == "nt":
|
||||||
|
# Windows
|
||||||
|
out = subprocess.check_output(
|
||||||
|
["netstat", "-ano"],
|
||||||
|
text=True, startupinfo=self._si
|
||||||
|
)
|
||||||
|
for line in out.splitlines():
|
||||||
|
if f"127.0.0.1:{self.comm_port}" in line and "LISTENING" in line:
|
||||||
|
pid = int(line.strip().split()[-1])
|
||||||
|
if pid != os.getpid():
|
||||||
|
subprocess.run(["taskkill", "/F", "/PID", str(pid)],
|
||||||
|
startupinfo=self._si,
|
||||||
|
capture_output=True)
|
||||||
|
else:
|
||||||
|
# macOS / Linux
|
||||||
|
out = subprocess.check_output(
|
||||||
|
["lsof", "-t", f"-iTCP:{self.comm_port}", "-sTCP:LISTEN"],
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
for pid in map(int, out.split()):
|
||||||
|
if pid != os.getpid():
|
||||||
|
os.kill(pid, 9)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# ---------- 启动 ----------
|
# ---------- 启动 ----------
|
||||||
def start(self):
|
def start(self):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -108,27 +140,24 @@ class FlaskSubprocessManager:
|
|||||||
# ---------- 停止 ----------
|
# ---------- 停止 ----------
|
||||||
def stop(self):
|
def stop(self):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if getattr(self, 'process', None) is None:
|
if not self.process:
|
||||||
LogManager.info("无子进程需要停止", udid="system")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
pid = self.process.pid
|
pid = self.process.pid
|
||||||
LogManager.info(f"正在停止 Flask 子进程 PID={pid}", udid="system")
|
LogManager.info(f"正在停止 Flask 子进程 PID={pid}", udid="system")
|
||||||
try:
|
try:
|
||||||
self.process.terminate()
|
# 1. 杀整棵树(Windows 也适用)
|
||||||
try:
|
parent = psutil.Process(pid)
|
||||||
self.process.wait(timeout=3)
|
for child in parent.children(recursive=True):
|
||||||
except subprocess.TimeoutExpired:
|
child.kill()
|
||||||
LogManager.warning("软杀超时,强制杀进程树", udid="system")
|
parent.kill()
|
||||||
import psutil
|
gone, alive = psutil.wait_procs([parent] + parent.children(), timeout=3)
|
||||||
parent = psutil.Process(pid)
|
for p in alive:
|
||||||
for child in parent.children(recursive=True):
|
p.kill() # 保险再补一刀
|
||||||
child.kill()
|
self.process.wait()
|
||||||
parent.kill()
|
except psutil.NoSuchProcess:
|
||||||
self.process.wait()
|
pass
|
||||||
LogManager.info("Flask 子进程已停止", udid="system")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.error(f"停止子进程时异常:{e}", udid="system")
|
LogManager.error(f"停止子进程异常:{e}", udid="system")
|
||||||
finally:
|
finally:
|
||||||
self.process = None
|
self.process = None
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
|
|||||||
24
Utils/SubprocessKit.py
Normal file
24
Utils/SubprocessKit.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
__all__ = ['check_output', 'popen', 'PIPE']
|
||||||
|
|
||||||
|
# 模块级单例,导入时只创建一次
|
||||||
|
if os.name == "nt":
|
||||||
|
_si = subprocess.STARTUPINFO()
|
||||||
|
_si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
_si.wShowWindow = subprocess.SW_HIDE
|
||||||
|
else:
|
||||||
|
_si = None
|
||||||
|
|
||||||
|
PIPE = subprocess.PIPE
|
||||||
|
|
||||||
|
def check_output(cmd, **kw):
|
||||||
|
if os.name == "nt":
|
||||||
|
kw.setdefault('startupinfo', _si)
|
||||||
|
return subprocess.check_output(cmd, **kw)
|
||||||
|
|
||||||
|
def popen(*args, **kw):
|
||||||
|
if os.name == "nt":
|
||||||
|
kw.setdefault('startupinfo', _si)
|
||||||
|
return subprocess.Popen(*args, **kw)
|
||||||
@@ -1,33 +1,126 @@
|
|||||||
from threading import Thread, Event
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import psutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from threading import Event, Thread
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from Utils.LogManager import LogManager
|
from Utils.LogManager import LogManager
|
||||||
from script.ScriptManager import ScriptManager
|
|
||||||
|
|
||||||
|
|
||||||
class ThreadManager():
|
class ThreadManager:
|
||||||
threads = {}
|
"""
|
||||||
|
对调用方完全透明:
|
||||||
|
add(udid, thread_obj, stop_event) 保持原签名
|
||||||
|
stop(udid) 保持原签名
|
||||||
|
但内部把 thread_obj 当成“壳”,真正拉起的是子进程。
|
||||||
|
"""
|
||||||
|
_pool: Dict[str, psutil.Process] = {}
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add(cls, udid, t: Thread, stopEvent: Event):
|
def add(cls, udid: str, dummy_thread, dummy_event: Event) -> None:
|
||||||
if udid in cls.threads:
|
LogManager.method_info(f"【1】入口 udid={udid} 长度={len(udid)}", method="task")
|
||||||
print("▲ 线程已存在")
|
if udid in cls._pool:
|
||||||
return
|
LogManager.method_warning(f"{udid} 仍在运行,先强制清理旧任务", method="task")
|
||||||
cls.threads[udid] = {"thread": t, "stopEvent": stopEvent}
|
cls.stop(udid)
|
||||||
|
LogManager.method_info(f"【2】判断旧任务后 udid={udid} 长度={len(udid)}", method="task")
|
||||||
|
port = cls._find_free_port()
|
||||||
|
LogManager.method_info(f"【3】找端口后 udid={udid} 长度={len(udid)}", method="task")
|
||||||
|
proc = cls._start_worker_process(udid, port)
|
||||||
|
LogManager.method_info(f"【4】子进程启动后 udid={udid} 长度={len(udid)}", method="task")
|
||||||
|
cls._pool[udid] = proc
|
||||||
|
LogManager.method_info(f"【5】已写入字典,udid={udid} 长度={len(udid)}", method="task")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def stop(cls, udid):
|
def stop(cls, udid: str) -> tuple[int, str]:
|
||||||
try:
|
with cls._lock: # 类级锁
|
||||||
info = cls.threads[udid]
|
proc = cls._pool.get(udid) # 1. 只读,不删
|
||||||
if info:
|
if proc is None:
|
||||||
info["stopEvent"].set() # 停止线程
|
return 1001, f"无此任务 {udid}"
|
||||||
info["thread"].join(timeout=3) # 等待线程退出
|
|
||||||
del cls.threads[udid]
|
try:
|
||||||
LogManager.info("停止线程成功", udid)
|
proc.terminate()
|
||||||
return 200, "停止线程成功 " + udid
|
gone, alive = psutil.wait_procs([proc], timeout=3)
|
||||||
else:
|
if alive:
|
||||||
LogManager.info("无此线程,无需关闭", udid)
|
for p in alive:
|
||||||
return 1001, "无此线程,无需关闭 " + udid
|
for child in p.children(recursive=True):
|
||||||
except KeyError as e:
|
child.kill()
|
||||||
LogManager.info("无此线程,无需关闭", udid)
|
p.kill()
|
||||||
return 1001, "停止脚本失败 " + udid
|
psutil.wait_procs(alive, timeout=2)
|
||||||
|
|
||||||
|
# 正常退出
|
||||||
|
cls._pool.pop(udid)
|
||||||
|
LogManager.method_info("任务停止成功", method="task")
|
||||||
|
return 200, f"停止线程成功 {udid}"
|
||||||
|
|
||||||
|
except psutil.NoSuchProcess: # 精准捕获
|
||||||
|
cls._pool.pop(udid)
|
||||||
|
LogManager.method_info("进程已自然退出", method="task")
|
||||||
|
return 200, f"进程已退出 {udid}"
|
||||||
|
|
||||||
|
except Exception as e: # 真正的异常
|
||||||
|
LogManager.method_error(f"停止异常: {e}", method="task")
|
||||||
|
return 1002, f"停止异常 {udid}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# 以下全是内部工具,外部无需调用
|
||||||
|
# ------------------------------------------------------
|
||||||
|
@staticmethod
|
||||||
|
def _find_free_port(start: int = 50000) -> int:
|
||||||
|
"""找个随机空闲端口,给子进程当通信口(可选)"""
|
||||||
|
import socket
|
||||||
|
for p in range(start, start + 1000):
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
if s.connect_ex(("127.0.0.1", p)) != 0:
|
||||||
|
return p
|
||||||
|
raise RuntimeError("无可用端口")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _start_worker_process(udid: str, port: int) -> psutil.Process:
|
||||||
|
"""
|
||||||
|
真正拉起子进程:
|
||||||
|
打包环境:exe --udid=xxx
|
||||||
|
源码环境:python -m Module.Worker --udid=xxx
|
||||||
|
"""
|
||||||
|
exe_path = Path(sys.executable).resolve()
|
||||||
|
is_frozen = exe_path.suffix.lower() == ".exe" and exe_path.exists()
|
||||||
|
|
||||||
|
if is_frozen:
|
||||||
|
# 打包后
|
||||||
|
cmd = [str(exe_path), "--role=worker", f"--udid={udid}", f"--port={port}"]
|
||||||
|
cwd = str(exe_path.parent)
|
||||||
|
else:
|
||||||
|
# 源码运行
|
||||||
|
cmd = [sys.executable, "-u", "-m", "Module.Worker", f"--udid={udid}", f"--port={port}"]
|
||||||
|
cwd = str(Path(__file__).resolve().parent.parent)
|
||||||
|
|
||||||
|
# 核心:CREATE_NO_WINDOW + 独立会话,父进程死也不影响
|
||||||
|
creation_flags = 0x08000000 if os.name == "nt" else 0
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="replace",
|
||||||
|
bufsize=1,
|
||||||
|
cwd=cwd,
|
||||||
|
start_new_session=True, # 独立进程组
|
||||||
|
creationflags=creation_flags
|
||||||
|
)
|
||||||
|
# 守护线程:把子进程 stdout 实时打到日志
|
||||||
|
Thread(target=lambda: ThreadManager._log_stdout(proc, udid), daemon=True).start()
|
||||||
|
return psutil.Process(proc.pid)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _log_stdout(proc: subprocess.Popen, udid: str):
|
||||||
|
for line in iter(proc.stdout.readline, ""):
|
||||||
|
if line:
|
||||||
|
LogManager.info(line.rstrip(), udid)
|
||||||
|
proc.stdout.close()
|
||||||
12
build.bat
12
build.bat
@@ -1,9 +1,9 @@
|
|||||||
python -m nuitka "C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\Module\Main.py" ^
|
python -m nuitka "Module/Main.py" ^
|
||||||
--standalone ^
|
--standalone ^
|
||||||
--msvc=latest ^
|
--msvc=latest ^
|
||||||
--windows-console-mode=disable ^
|
--windows-console-mode=disable ^
|
||||||
--remove-output ^
|
--remove-output ^
|
||||||
--output-dir="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\out" ^
|
--output-dir="F:/company code/AI item/20250820/iOSAI/out" ^
|
||||||
--output-filename=IOSAI ^
|
--output-filename=IOSAI ^
|
||||||
--include-package=Module,Utils,Entity,script ^
|
--include-package=Module,Utils,Entity,script ^
|
||||||
--include-module=flask ^
|
--include-module=flask ^
|
||||||
@@ -18,7 +18,7 @@ python -m nuitka "C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\Mod
|
|||||||
--include-module=urllib3 ^
|
--include-module=urllib3 ^
|
||||||
--include-module=certifi ^
|
--include-module=certifi ^
|
||||||
--include-module=idna ^
|
--include-module=idna ^
|
||||||
--include-data-dir="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\SupportFiles=SupportFiles" ^
|
--include-data-dir="F:/company code/AI item/20250820/iOSAI/SupportFiles=SupportFiles" ^
|
||||||
--include-data-dir="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\resources=resources" ^
|
--include-data-dir="F:/company code/AI item/20250820/iOSAI/resources=resources" ^
|
||||||
--include-data-files="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\resources\iproxy\*=resources/iproxy/" ^
|
--include-data-files="F:/company code/AI item/20250820/iOSAI/resources/iproxy/*=resources/iproxy/" ^
|
||||||
--windows-icon-from-ico="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\resources\icon.ico"
|
--windows-icon-from-ico="F:/company code/AI item/20250820/iOSAI/resources/icon.ico"
|
||||||
|
|||||||
BIN
resources/133bffc17635b7a3bd709492b7a519d96710a4a2/bgv.png
Normal file
BIN
resources/133bffc17635b7a3bd709492b7a519d96710a4a2/bgv.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
Reference in New Issue
Block a user