性能优化
This commit is contained in:
2
.idea/iOSAI.iml
generated
2
.idea/iOSAI.iml
generated
@@ -4,7 +4,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</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" />
|
||||
</component>
|
||||
</module>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -3,5 +3,5 @@
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12" />
|
||||
</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>
|
||||
10
.idea/workspace.xml
generated
10
.idea/workspace.xml
generated
@@ -5,9 +5,17 @@
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
|
||||
<change afterPath="$PROJECT_DIR$/Utils/SubprocessKit.py" 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$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Utils/ControlUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ControlUtils.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/FlaskSubprocessManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/FlaskSubprocessManager.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Utils/ThreadManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ThreadManager.py" 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" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
|
||||
@@ -3,24 +3,38 @@ import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
import wda
|
||||
import threading
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from tidevice import Usbmux, ConnectionType
|
||||
from tidevice._device import BaseDevice
|
||||
from Entity.DeviceModel import DeviceModel
|
||||
from Entity.Variables import WdaAppBundleId
|
||||
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
||||
from Utils.LogManager import LogManager
|
||||
from Utils.SubprocessKit import check_output as sp_check_output, popen as sp_popen
|
||||
|
||||
|
||||
class Deviceinfo(object):
|
||||
"""设备生命周期管理:以 deviceModelList 为唯一真理源"""
|
||||
|
||||
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.screenProxy = 9110
|
||||
self.pidList: List[Dict] = [] # 仅记录 iproxy 进程
|
||||
@@ -46,13 +60,14 @@ class Deviceinfo(object):
|
||||
pass
|
||||
|
||||
self._creationflags = 0x08000000 if os.name == "nt" else 0
|
||||
|
||||
self._popen_kwargs = dict(
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
cwd=str(self.iproxy_dir),
|
||||
shell=False,
|
||||
text=True,
|
||||
creationflags=self._creationflags,
|
||||
creationflags=0x08000000 if os.name == "nt" else 0, # CREATE_NO_WINDOW
|
||||
encoding="utf-8",
|
||||
bufsize=1,
|
||||
)
|
||||
@@ -84,10 +99,10 @@ class Deviceinfo(object):
|
||||
# endregion
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# ✅ 2. 主监听循环(已用“时间窗口+USB 层兜底”重写)
|
||||
# 主监听循环 → 只负责“发现”和“提交任务”
|
||||
# ------------------------------------------------------------------
|
||||
def startDeviceListener(self):
|
||||
MISS_WINDOW = 5.0 # 5 秒连续失踪才判死刑
|
||||
MISS_WINDOW = 5.0
|
||||
while True:
|
||||
try:
|
||||
lists = Usbmux().device_list()
|
||||
@@ -97,38 +112,36 @@ class Deviceinfo(object):
|
||||
continue
|
||||
|
||||
now_udids = {d.udid for d in lists if d.conn_type == ConnectionType.USB}
|
||||
# ✅ USB 层真断兜底
|
||||
usb_sn_set = self._usb_enumerate_sn()
|
||||
|
||||
need_remove = None
|
||||
# 1. 失踪判定(同旧逻辑)
|
||||
need_remove = []
|
||||
with self._lock:
|
||||
for udid in list(self._model_index.keys()):
|
||||
if udid not in now_udids:
|
||||
last = self._last_seen.get(udid, time.time())
|
||||
if time.time() - last > MISS_WINDOW and udid not in usb_sn_set:
|
||||
need_remove = udid
|
||||
need_remove.append(udid)
|
||||
else:
|
||||
self._last_seen[udid] = time.time()
|
||||
for udid in need_remove:
|
||||
self._remove_model(udid)
|
||||
|
||||
if need_remove:
|
||||
self._remove_model(need_remove)
|
||||
|
||||
# 新增设备(原逻辑不变)
|
||||
for d in lists:
|
||||
if d.conn_type != ConnectionType.USB:
|
||||
continue
|
||||
udid = d.udid
|
||||
with self._lock:
|
||||
if udid in self._model_index:
|
||||
continue
|
||||
if not self.is_device_trusted(udid):
|
||||
continue
|
||||
if len(self.deviceModelList) >= self.maxDeviceCount:
|
||||
continue
|
||||
try:
|
||||
self.connectDevice(udid)
|
||||
except Exception as e:
|
||||
LogManager.error(f"连接设备失败 {udid}: {e}", udid)
|
||||
# 2. 发现新设备 → 并发连接
|
||||
with self._lock:
|
||||
new_udids = [d.udid for d in lists
|
||||
if d.conn_type == ConnectionType.USB and
|
||||
d.udid not in self._model_index and
|
||||
len(self.deviceModelList) < self.maxDeviceCount]
|
||||
if new_udids:
|
||||
futures = {self._connect_pool.submit(self._connect_device_task, udid): udid
|
||||
for udid in new_udids}
|
||||
for f in as_completed(futures, timeout=10):
|
||||
udid = futures[f]
|
||||
try:
|
||||
f.result(timeout=8) # 单台 8 s 硬截止
|
||||
except Exception as e:
|
||||
LogManager.error(f"连接任务超时/失败: {e}", udid)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
@@ -137,7 +150,7 @@ class Deviceinfo(object):
|
||||
# ------------------------------------------------------------------
|
||||
def _usb_enumerate_sn(self) -> set[str]:
|
||||
try:
|
||||
out = subprocess.check_output(["idevice_id", "-l"], text=True, timeout=3)
|
||||
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()
|
||||
@@ -259,8 +272,10 @@ class Deviceinfo(object):
|
||||
self._port_in_use.remove(port)
|
||||
self._port_pool.append(port)
|
||||
|
||||
# -------------------- 单台设备连接(未改动) --------------------
|
||||
def connectDevice(self, udid: str):
|
||||
# ------------------------------------------------------------------
|
||||
# 线程池里真正干活的地方(原 connectDevice 逻辑搬过来)
|
||||
# ------------------------------------------------------------------
|
||||
def _connect_device_task(self, udid: str):
|
||||
if not self.is_device_trusted(udid):
|
||||
LogManager.warning("设备未信任,跳过 WDA 启动", udid)
|
||||
return
|
||||
@@ -269,6 +284,7 @@ class Deviceinfo(object):
|
||||
except Exception as e:
|
||||
LogManager.error(f"启动 WDA 失败: {e}", udid)
|
||||
return
|
||||
|
||||
width, height, scale = 0, 0, 1.0
|
||||
try:
|
||||
size = d.window_size()
|
||||
@@ -276,19 +292,35 @@ class Deviceinfo(object):
|
||||
scale = d.scale
|
||||
except Exception as e:
|
||||
LogManager.warning(f"读取屏幕信息失败:{e}", udid)
|
||||
|
||||
port = self._alloc_port()
|
||||
model = DeviceModel(udid, port, width, height, scale, type=1)
|
||||
self._add_model(model)
|
||||
|
||||
# 先做完所有 IO,再抢锁写内存
|
||||
try:
|
||||
d.app_start(WdaAppBundleId)
|
||||
d.home()
|
||||
except Exception as e:
|
||||
LogManager.warning(f"启动/切回桌面失败:{e}", udid)
|
||||
time.sleep(2)
|
||||
self.pidList = [item for item in self.pidList if item.get("id") != udid]
|
||||
|
||||
time.sleep(2) # 原逻辑保留
|
||||
|
||||
target = self.relayDeviceScreenPort(udid, port)
|
||||
if target:
|
||||
self.pidList.append({"target": target, "id": udid})
|
||||
|
||||
# 毫秒级临界区
|
||||
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:
|
||||
@@ -327,14 +359,12 @@ class Deviceinfo(object):
|
||||
def _get_pid_by_port(self, port: int) -> Optional[int]:
|
||||
try:
|
||||
if os.name == "nt":
|
||||
cmd = ["netstat", "-ano", "-p", "tcp"]
|
||||
out = subprocess.check_output(cmd, text=True)
|
||||
out = sp_check_output(["netstat", "-ano", "-p", "tcp"], text=True)
|
||||
for line in out.splitlines():
|
||||
if f"127.0.0.1:{port}" in line and "LISTENING" in line:
|
||||
return int(line.strip().split()[-1])
|
||||
else:
|
||||
cmd = ["lsof", "-t", f"-iTCP:{port}", "-sTCP:LISTEN"]
|
||||
out = subprocess.check_output(cmd, text=True)
|
||||
out = sp_check_output(["lsof", "-t", f"-iTCP:{port}", "-sTCP:LISTEN"], text=True)
|
||||
return int(out.strip().split()[0])
|
||||
except Exception:
|
||||
return None
|
||||
@@ -375,4 +405,4 @@ class Deviceinfo(object):
|
||||
for p in candidates:
|
||||
if p.exists():
|
||||
return p
|
||||
raise FileNotFoundError(f"iproxy not found, tried: {[str(c) for c in candidates]}")
|
||||
raise FileNotFoundError(f"iproxy not found, tried: {[str(c) for c in candidates]}")
|
||||
|
||||
@@ -266,8 +266,9 @@ def watchLiveForGrowth():
|
||||
def stopScript():
|
||||
body = request.get_json()
|
||||
udid = body.get("udid")
|
||||
code, massage = ThreadManager.stop(udid)
|
||||
return ResultData(code=code, data="", massage=massage).toJson()
|
||||
LogManager.method_info(f"接口收到 /stopScript udid={udid}", method="task")
|
||||
code, msg = ThreadManager.stop(udid)
|
||||
return ResultData(code=code, data="", massage=msg).toJson()
|
||||
|
||||
|
||||
# 关注打招呼
|
||||
|
||||
@@ -9,6 +9,8 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union, Dict, List
|
||||
|
||||
import psutil
|
||||
|
||||
from Utils.LogManager import LogManager
|
||||
|
||||
|
||||
@@ -28,9 +30,39 @@ class FlaskSubprocessManager:
|
||||
self.comm_port = 34566
|
||||
self._stop_event = threading.Event()
|
||||
self._monitor_thread: Optional[threading.Thread] = None
|
||||
# 新增:启动前先把可能残留的 Flask 干掉
|
||||
self._kill_orphan_flask()
|
||||
atexit.register(self.stop)
|
||||
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):
|
||||
with self._lock:
|
||||
@@ -108,27 +140,24 @@ class FlaskSubprocessManager:
|
||||
# ---------- 停止 ----------
|
||||
def stop(self):
|
||||
with self._lock:
|
||||
if getattr(self, 'process', None) is None:
|
||||
LogManager.info("无子进程需要停止", udid="system")
|
||||
if not self.process:
|
||||
return
|
||||
|
||||
pid = self.process.pid
|
||||
LogManager.info(f"正在停止 Flask 子进程 PID={pid}", udid="system")
|
||||
try:
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
LogManager.warning("软杀超时,强制杀进程树", udid="system")
|
||||
import psutil
|
||||
parent = psutil.Process(pid)
|
||||
for child in parent.children(recursive=True):
|
||||
child.kill()
|
||||
parent.kill()
|
||||
self.process.wait()
|
||||
LogManager.info("Flask 子进程已停止", udid="system")
|
||||
# 1. 杀整棵树(Windows 也适用)
|
||||
parent = psutil.Process(pid)
|
||||
for child in parent.children(recursive=True):
|
||||
child.kill()
|
||||
parent.kill()
|
||||
gone, alive = psutil.wait_procs([parent] + parent.children(), timeout=3)
|
||||
for p in alive:
|
||||
p.kill() # 保险再补一刀
|
||||
self.process.wait()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
except Exception as e:
|
||||
LogManager.error(f"停止子进程时异常:{e}", udid="system")
|
||||
LogManager.error(f"停止子进程异常:{e}", udid="system")
|
||||
finally:
|
||||
self.process = None
|
||||
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 script.ScriptManager import ScriptManager
|
||||
|
||||
|
||||
class ThreadManager():
|
||||
threads = {}
|
||||
class ThreadManager:
|
||||
"""
|
||||
对调用方完全透明:
|
||||
add(udid, thread_obj, stop_event) 保持原签名
|
||||
stop(udid) 保持原签名
|
||||
但内部把 thread_obj 当成“壳”,真正拉起的是子进程。
|
||||
"""
|
||||
_pool: Dict[str, psutil.Process] = {}
|
||||
_lock = threading.Lock()
|
||||
|
||||
@classmethod
|
||||
def add(cls, udid, t: Thread, stopEvent: Event):
|
||||
if udid in cls.threads:
|
||||
print("▲ 线程已存在")
|
||||
return
|
||||
cls.threads[udid] = {"thread": t, "stopEvent": stopEvent}
|
||||
|
||||
def add(cls, udid: str, dummy_thread, dummy_event: Event) -> None:
|
||||
LogManager.method_info(f"【1】入口 udid={udid} 长度={len(udid)}", method="task")
|
||||
if udid in cls._pool:
|
||||
LogManager.method_warning(f"{udid} 仍在运行,先强制清理旧任务", method="task")
|
||||
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
|
||||
def stop(cls, udid):
|
||||
try:
|
||||
info = cls.threads[udid]
|
||||
if info:
|
||||
info["stopEvent"].set() # 停止线程
|
||||
info["thread"].join(timeout=3) # 等待线程退出
|
||||
del cls.threads[udid]
|
||||
LogManager.info("停止线程成功", udid)
|
||||
return 200, "停止线程成功 " + udid
|
||||
else:
|
||||
LogManager.info("无此线程,无需关闭", udid)
|
||||
return 1001, "无此线程,无需关闭 " + udid
|
||||
except KeyError as e:
|
||||
LogManager.info("无此线程,无需关闭", udid)
|
||||
return 1001, "停止脚本失败 " + udid
|
||||
def stop(cls, udid: str) -> tuple[int, str]:
|
||||
with cls._lock: # 类级锁
|
||||
proc = cls._pool.get(udid) # 1. 只读,不删
|
||||
if proc is None:
|
||||
return 1001, f"无此任务 {udid}"
|
||||
|
||||
try:
|
||||
proc.terminate()
|
||||
gone, alive = psutil.wait_procs([proc], timeout=3)
|
||||
if alive:
|
||||
for p in alive:
|
||||
for child in p.children(recursive=True):
|
||||
child.kill()
|
||||
p.kill()
|
||||
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()
|
||||
@@ -3,7 +3,6 @@ python -m nuitka "Module/Main.py" ^
|
||||
--msvc=latest ^
|
||||
--windows-console-mode=disable ^
|
||||
--remove-output ^
|
||||
--output-dir="F:/company code/AI item/20250820/iOSAI/out" ^
|
||||
--output-filename=IOSAI ^
|
||||
--include-package=Module,Utils,Entity,script ^
|
||||
--include-module=flask ^
|
||||
@@ -18,7 +17,7 @@ python -m nuitka "Module/Main.py" ^
|
||||
--include-module=urllib3 ^
|
||||
--include-module=certifi ^
|
||||
--include-module=idna ^
|
||||
--include-data-dir="F:/company code/AI item/20250820/iOSAI/SupportFiles=SupportFiles" ^
|
||||
--include-data-dir="F:/company code/AI item/20250820/iOSAI/resources=resources" ^
|
||||
--include-data-files="F:/company code/AI item/20250820/iOSAI/resources/iproxy/*=resources/iproxy/" ^
|
||||
--windows-icon-from-ico="F:/company code/AI item/20250820/iOSAI/resources/icon.ico"
|
||||
--include-data-dir="E:/code/Python/iOSAI/SupportFiles=SupportFiles" ^
|
||||
--include-data-dir="E:/code/Python/iOSAI/resources=resources" ^
|
||||
--include-data-files="E:/code/Python/iOSAI/resources/iproxy/*=resources/iproxy/" ^
|
||||
--windows-icon-from-ico="E:/code/Python/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 |
Binary file not shown.
Reference in New Issue
Block a user