PyStand icon indicating copy to clipboard operation
PyStand copied to clipboard

执行 subprocess.Popen 会重复启动 Qt 主窗口直到内存耗尽

Open shihao-hub opened this issue 3 weeks ago • 5 comments

有多进程调用时,会重复启动Qt主窗口直到内存耗尽 #21

参考过上面的链接,但是上面是 multiprocessing.Process,我这个是 subprocess.Popen

# app.py
from nicegui import ui, app


@app.get("/health")
def health():
    return {"status": "ok"}


@ui.page("/")
async def page_index():
    ui.label("nicegui 已启动").classes("mx-auto")


if __name__ in {"__main__", "__mp_main__"}:
    ui.run(
        title="笔记管理系统",
        host="localhost",
        port=9999,
        native=False,
        show=False,
        reload=False
    )

# main.py
import sys
import subprocess
import atexit

from PySide6.QtWidgets import QApplication, QVBoxLayout, QWidget
from PySide6.QtCore import QTimer, QUrl, QObject
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply


def terminate_process_gracefully(proc, timeout=5):
    if proc and proc.poll() is None:
        print("Terminating backend process...")
        proc.terminate()
        try:
            proc.wait(timeout=timeout)
        except subprocess.TimeoutExpired:
            print("Backend did not terminate gracefully; killing it.")
            proc.kill()


class MainWindow(QWidget):
    def __init__(self, backend_process):
        super().__init__()
        self.backend_process = backend_process

        self.setWindowTitle("笔记管理系统")
        layout = QVBoxLayout(self)

        self.view = QWebEngineView()
        layout.addWidget(self.view)

        self.resize(1200, 900)

        # 初始化网络管理器
        self.network_manager = QNetworkAccessManager(self)
        self.network_manager.finished.connect(self.on_health_check_response)

        # 显示加载页
        self.show_loading_page()

        # 开始检查后端
        self._retry_count = 0
        QTimer.singleShot(100, self.check_backend_ready)

    def check_backend_ready(self):
        health_url = "http://127.0.0.1:9999/health"
        request = QNetworkRequest(QUrl(health_url))
        request.setTransferTimeout(1000)  # 1秒超时
        reply = self.network_manager.get(request)
        reply.setProperty("request_type", "health")  # 标记类型

    def on_health_check_response(self, reply):
        req_type = reply.property("request_type")
        print(f"Request type: {req_type}")
        if reply.error() == QNetworkReply.NetworkError.NoError:
            status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
            if status_code == 200:
                print("Backend is ready!")
                index_url = "http://127.0.0.1:9999/"
                self.view.load(QUrl(index_url))
                reply.deleteLater()
                return

        # 请求失败、超时、连接拒绝等
        print(f"Health check failed: {reply.errorString()}")
        reply.deleteLater()

        self._retry_count += 1
        if self._retry_count < 20:
            QTimer.singleShot(100, self.check_backend_ready)
        else:
            self.show_error_page()

    def show_loading_page(self):
        html = """
        <html>
        <head>
            <style>
                body {
                    margin: 0;
                    padding: 0;
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    height: 100vh;
                    background-color: #f5f5f5;
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                    color: #333;
                }
                .loading {
                    text-align: center;
                    font-size: 24px;
                }
                .spinner {
                    border: 4px solid rgba(0, 0, 0, 0.1);
                    border-left-color: #0078d7;
                    border-radius: 50%;
                    width: 40px;
                    height: 40px;
                    animation: spin 1s linear infinite;
                    margin: 0 auto 20px;
                }
                @keyframes spin {
                    to { transform: rotate(360deg); }
                }
            </style>
        </head>
        <body>
            <div class="loading">
                <div class="spinner"></div>
                <p>正在启动后端服务,请稍候...</p>
            </div>
        </body>
        </html>
        """
        self.view.setHtml(html)

    def show_error_page(self):
        html = """
        <html>
        <head>
            <style>
                body {
                    margin: 0;
                    padding: 0;
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    height: 100vh;
                    background-color: #fff5f5;
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                    color: #c00;
                }
                .error {
                    text-align: center;
                    font-size: 20px;
                    max-width: 600px;
                    padding: 20px;
                }
            </style>
        </head>
        <body>
            <div class="error">
                <h2>❌ 无法连接到后端服务</h2>
                <p>后端进程未能及时启动。请检查日志或手动重启应用。</p>
            </div>
        </body>
        </html>
        """
        self.view.setHtml(html)
        print("Backend did not start in time.")


def start_backend():
    cmd = [sys.executable, "app.py"]
    return subprocess.Popen(cmd, cwd=".")


def main():
    app = QApplication(sys.argv)

    backend_proc = start_backend()
    atexit.register(lambda: terminate_process_gracefully(backend_proc, timeout=3))

    window = MainWindow(backend_proc)
    window.show()

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

shihao-hub avatar Dec 08 '25 16:12 shihao-hub

你这个服务器代码,不要用 pystand 启动,用 gui 程序拉起 runtime 下面的 python 来运行它

skywind3000 avatar Dec 09 '25 06:12 skywind3000

哦,我懂了,subprocess.Popen([sys.executable, "app.py"]) 的 sys.executable 实际指向的是 pystand.exe,所以才会出现重复启动 Qt 主窗口的情况。subprocess.Popen 应该用 runtime/python.exe。

shihao-hub avatar Dec 09 '25 12:12 shihao-hub

只不过这样的话,runtime 目录中的 python312._pth 得改成这样:

python312.zip
.
../site-packages

# Uncomment to run site.main() automatically
import site

不然找不到第三方库

shihao-hub avatar Dec 09 '25 12:12 shihao-hub

那就改呗

skywind3000 avatar Dec 09 '25 12:12 skywind3000

只不过这样的话,runtime 目录中的 python312._pth 得改成这样:

python312.zip . ../site-packages

Uncomment to run site.main() automatically

import site 不然找不到第三方库

用pythonw就行了呀

mengdeer589 avatar Dec 11 '25 15:12 mengdeer589