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

View file

@ -1,6 +1,8 @@
from argparse import Namespace from argparse import Namespace
from io import BytesIO 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 collections.abc import Callable
from datetime import datetime from datetime import datetime
import os import os
@ -12,7 +14,6 @@ import webbrowser
import multiprocessing import multiprocessing
import secrets import secrets
import string import string
import subprocess
import signal import signal
from PyQt6.QtCore import QTimer from PyQt6.QtCore import QTimer
@ -38,7 +39,7 @@ from yaml import dump, load, Loader, Dumper
from qrcode.main import QRCode from qrcode.main import QRCode
import platformdirs import platformdirs
from .client import default_config from .client import create_async_and_start_client, default_config
from .sources import available_sources from .sources import available_sources
@ -286,7 +287,7 @@ class SyngGui(QMainWindow):
if self.syng_client is not None: if self.syng_client is not None:
self.syng_client.terminate() self.syng_client.terminate()
self.syng_client.wait(1.0) self.syng_client.join(1.0)
self.syng_client.kill() self.syng_client.kill()
self.destroy() self.destroy()
@ -367,7 +368,9 @@ class SyngGui(QMainWindow):
self.setWindowIcon(self.qt_icon) self.setWindowIcon(self.qt_icon)
self.syng_server: Optional[Process] = None 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") self.configfile = os.path.join(platformdirs.user_config_dir("syng"), "config.yaml")
@ -416,7 +419,7 @@ class SyngGui(QMainWindow):
self.setCentralWidget(self.central_widget) 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 = QTimer()
self.timer.timeout.connect(self.check_if_client_is_running) self.timer.timeout.connect(self.check_if_client_is_running)
@ -440,15 +443,7 @@ class SyngGui(QMainWindow):
self.timer.stop() self.timer.stop()
return return
ret = self.syng_client.poll() if not self.syng_client.is_alive():
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()
self.syng_client = None self.syng_client = None
self.set_client_button_start() self.set_client_button_start()
else: else:
@ -461,15 +456,26 @@ class SyngGui(QMainWindow):
self.startbutton.setText("Save and Start") self.startbutton.setText("Save and Start")
def start_syng_client(self) -> None: 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.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.notification_label.setText("")
self.timer.start() self.timer.start(500)
self.set_client_button_stop() self.set_client_button_stop()
else: else:
self.syng_client.terminate() self.syng_client.terminate()
self.syng_client.wait(1.0) self.syng_client.join(1.0)
self.syng_client.kill() self.syng_client.kill()
self.set_client_button_start() self.set_client_button_start()
@ -527,6 +533,15 @@ class SyngGui(QMainWindow):
self.change_qr(syng_server + room) 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: def run_gui() -> None:
signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGINT, signal.SIG_DFL)

View file

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