Communication between GUI and client back to multiprocessing, since Popen yielded 100% CPU :/

This commit is contained in:
Christoph Stahl 2024-09-22 23:34:30 +02:00
parent 1f1c2c4f1e
commit 5468b39bc1
3 changed files with 52 additions and 24 deletions

View file

@ -15,6 +15,8 @@ be one of:
import asyncio
import datetime
import logging
from logging.handlers import QueueHandler
from multiprocessing import Queue
import secrets
import string
import tempfile
@ -192,7 +194,7 @@ async def handle_connect() -> None:
:rtype: None
"""
logging.info("Connected to server")
logger.info("Connected to server")
data = {
"queue": state.queue,
"waiting_room": state.waiting_room,
@ -309,6 +311,7 @@ async def handle_search(data: dict[str, Any]) -> None:
:type data: dict[str, Any]
:rtype: None
"""
logger.info(f"Searching for: {data['query']}")
query = data["query"]
sid = data["sid"]
results_list = await asyncio.gather(*[source.search(query) for source in sources.values()])
@ -343,7 +346,7 @@ async def handle_client_registered(data: dict[str, Any]) -> None:
:rtype: None
"""
if data["success"]:
logging.info("Registered")
logger.info("Registered")
print(f"Join here: {state.config['server']}/{data['room']}")
qr = QRCode(box_size=20, border=2)
qr.add_data(f"{state.config['server']}/{data['room']}")
@ -354,7 +357,7 @@ async def handle_client_registered(data: dict[str, Any]) -> None:
if state.current_source is None: # A possible race condition can occur here
await sio.emit("get-first")
else:
logging.warning("Registration failed")
logger.warning("Registration failed")
await sio.disconnect()
@ -451,14 +454,24 @@ async def start_client(config: dict[str, Any]) -> None:
state.current_source.player.kill()
def create_async_and_start_client(config: dict[str, Any]) -> None:
def create_async_and_start_client(
config: dict[str, Any], queue: Optional[Queue[logging.LogRecord]] = None
) -> None:
"""
Create an asyncio event loop and start the client.
If a multiprocessing queue is given, the client will log to the queue.
:param config: Config options for the client
:type config: dict[str, Any]
:param queue: A multiprocessing queue to log to
:type queue: Optional[Queue[LogRecord]]
:rtype: None
"""
if queue is not None:
logger.addHandler(QueueHandler(queue))
asyncio.run(start_client(config))

View file

@ -1,6 +1,8 @@
from argparse import Namespace
from io import BytesIO
from multiprocessing import Process
import logging
from logging.handlers import QueueListener
from multiprocessing import Process, Queue
from collections.abc import Callable
from datetime import datetime
import os
@ -12,7 +14,6 @@ import webbrowser
import multiprocessing
import secrets
import string
import subprocess
import signal
from PyQt6.QtCore import QTimer
@ -38,7 +39,7 @@ from yaml import dump, load, Loader, Dumper
from qrcode.main import QRCode
import platformdirs
from .client import default_config
from .client import create_async_and_start_client, default_config
from .sources import available_sources
@ -286,7 +287,7 @@ class SyngGui(QMainWindow):
if self.syng_client is not None:
self.syng_client.terminate()
self.syng_client.wait(1.0)
self.syng_client.join(1.0)
self.syng_client.kill()
self.destroy()
@ -367,7 +368,9 @@ class SyngGui(QMainWindow):
self.setWindowIcon(self.qt_icon)
self.syng_server: Optional[Process] = None
self.syng_client: Optional[subprocess.Popen[bytes]] = None
# self.syng_client: Optional[subprocess.Popen[bytes]] = None
self.syng_client: Optional[Process] = None
self.syng_client_logging_listener: Optional[QueueListener] = None
self.configfile = os.path.join(platformdirs.user_config_dir("syng"), "config.yaml")
@ -416,7 +419,7 @@ class SyngGui(QMainWindow):
self.setCentralWidget(self.central_widget)
# check every 100 ms if client is running
# check every 500 ms if client is running
self.timer = QTimer()
self.timer.timeout.connect(self.check_if_client_is_running)
@ -440,15 +443,7 @@ class SyngGui(QMainWindow):
self.timer.stop()
return
ret = self.syng_client.poll()
if ret is not None:
_, stderr = self.syng_client.communicate()
stderr_lines = stderr.decode("utf-8").strip().split("\n")
if stderr_lines and stderr_lines[-1].startswith("Warning"):
self.notification_label.setText(stderr_lines[-1])
else:
self.notification_label.setText("")
self.syng_client.wait()
if not self.syng_client.is_alive():
self.syng_client = None
self.set_client_button_start()
else:
@ -461,15 +456,26 @@ class SyngGui(QMainWindow):
self.startbutton.setText("Save and Start")
def start_syng_client(self) -> None:
if self.syng_client is None or self.syng_client.poll() is not None:
if self.syng_client is None or not self.syng_client.is_alive():
self.save_config()
self.syng_client = subprocess.Popen(["syng", "client"], stderr=subprocess.PIPE)
config = self.gather_config()
queue: Queue[logging.LogRecord] = multiprocessing.Queue()
self.syng_client_logging_listener = QueueListener(
queue, LoggingLabelHandler(self.notification_label)
)
self.syng_client_logging_listener.start()
self.syng_client = multiprocessing.Process(
target=create_async_and_start_client, args=[config, queue]
)
self.syng_client.start()
self.notification_label.setText("")
self.timer.start()
self.timer.start(500)
self.set_client_button_stop()
else:
self.syng_client.terminate()
self.syng_client.wait(1.0)
self.syng_client.join(1.0)
self.syng_client.kill()
self.set_client_button_start()
@ -527,6 +533,15 @@ class SyngGui(QMainWindow):
self.change_qr(syng_server + room)
class LoggingLabelHandler(logging.Handler):
def __init__(self, label: QLabel):
super().__init__()
self.label = label
def emit(self, record: logging.LogRecord) -> None:
self.label.setText(self.format(record))
def run_gui() -> None:
signal.signal(signal.SIGINT, signal.SIG_DFL)

View file

@ -352,7 +352,7 @@ class YoutubeSource(Source):
title=result.title,
artist=result.author,
album="YouTube",
duration=result.length,
duration=str(result.length),
)
for result in results
]