diff --git a/plugins/native-ui/listener.py b/plugins/native-ui/listener.py index a418086..ed56864 100644 --- a/plugins/native-ui/listener.py +++ b/plugins/native-ui/listener.py @@ -6,6 +6,7 @@ import logging from typing import * import pubsub.pub as pub +import wx import blcsdk import blcsdk.models as sdk_models @@ -26,7 +27,7 @@ async def init(): blc_rooms = await blcsdk.get_rooms() for blc_room in blc_rooms: if blc_room.room_id is not None: - _get_or_add_room(blc_room.room_key, blc_room.room_id) + wx.CallAfter(_get_or_add_room, blc_room.room_key, blc_room.room_id) except blcsdk.SdkError: pass @@ -38,7 +39,10 @@ def shut_down(): class MsgHandler(blcsdk.BaseHandler): def on_client_stopped(self, client: blcsdk.BlcPluginClient, exception: Optional[Exception]): logger.info('blivechat disconnected') - __main__.start_shut_down() + wx.CallAfter(__main__.start_shut_down) + + def handle(self, client: blcsdk.BlcPluginClient, command: dict): + wx.CallAfter(super().handle, client, command) def _on_open_plugin_admin_ui( self, client: blcsdk.BlcPluginClient, message: sdk_models.OpenPluginAdminUiMsg, extra: sdk_models.ExtraData diff --git a/plugins/native-ui/main.pyw b/plugins/native-ui/main.pyw index 0b23921..4fb57c4 100755 --- a/plugins/native-ui/main.pyw +++ b/plugins/native-ui/main.pyw @@ -1,62 +1,53 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- import asyncio +import concurrent.futures +import logging import logging.handlers import os import signal -import sys +import threading +from typing import * +import pubsub.pub as pub import wx import blcsdk +import blcsdk.models as sdk_models import config import listener -import ui.app +import ui.room_config_dialog +import ui.room_frame +import ui.task_bar_icon logger = logging.getLogger('native-ui') - -async def main(): - try: - await init() - await run() - finally: - await shut_down() - return 0 +app: Optional['App'] = None -async def init(): +def main(): init_signal_handlers() init_logging() config.init() - await blcsdk.init() - if not blcsdk.is_sdk_version_compatible(): - raise RuntimeError('SDK version is not compatible') + global app + app = App() - ui.app.init() - await listener.init() + logger.info('Running event loop') + app.MainLoop() def init_signal_handlers(): - signums = (signal.SIGINT, signal.SIGTERM) - try: - loop = asyncio.get_running_loop() - for signum in signums: - loop.add_signal_handler(signum, start_shut_down) - except NotImplementedError: - def signal_handler(*args): - asyncio.get_running_loop().call_soon(start_shut_down, *args) + def signal_handler(*_args): + wx.CallAfter(start_shut_down) - # 不太安全,但Windows只能用这个 - for signum in signums: - signal.signal(signum, signal_handler) + for signum in (signal.SIGINT, signal.SIGTERM): + signal.signal(signum, signal_handler) -def start_shut_down(*_args): - app = wx.GetApp() - if app is not None: +def start_shut_down(): + if app is not None and app.IsMainLoopRunning(): app.ExitMainLoop() else: wx.Exit() @@ -77,16 +68,120 @@ def init_logging(): ) -async def run(): - logger.info('Running event loop') - await wx.GetApp().MainLoop() - logger.info('Start to shut down') +class App(wx.App): + def __init__(self, *args, **kwargs): + self._network_worker = NetworkWorker() + + self._dummy_timer: Optional[wx.Timer] = None + self._task_bar_icon: Optional[ui.task_bar_icon.TaskBarIcon] = None + + self._key_room_frame_dict: Dict[sdk_models.RoomKey, ui.room_frame.RoomFrame] = {} + self._room_config_dialog: Optional[ui.room_config_dialog.RoomConfigDialog] = None + + super().__init__(*args, clearSigInt=False, **kwargs) + self.SetExitOnFrameDelete(False) + + def OnInit(self): + # 这个定时器只是为了及时响应信号,因为只有处理UI事件时才会唤醒主线程 + self._dummy_timer = wx.Timer(self) + self._dummy_timer.Start(1000) + self.Bind(wx.EVT_TIMER, lambda _event: None, self._dummy_timer) + + self._task_bar_icon = ui.task_bar_icon.TaskBarIcon() + + pub.subscribe(self._on_add_room, 'add_room') + pub.subscribe(self._on_del_room, 'del_room') + pub.subscribe(self._on_room_frame_close, 'room_frame_close') + pub.subscribe(self._on_add_room, 'open_room') + pub.subscribe(self._on_open_room_config_dialog, 'open_room_config_dialog') + + self._network_worker.init() + return True + + def OnExit(self): + logger.info('Start to shut down') + + self._network_worker.start_shut_down() + self._network_worker.join(10) + + return super().OnExit() + + def _on_add_room(self, room_key: sdk_models.RoomKey): + if room_key in self._key_room_frame_dict: + return + + room_frame = self._key_room_frame_dict[room_key] = ui.room_frame.RoomFrame(None, room_key) + room_frame.Show() + + def _on_del_room(self, room_key: sdk_models.RoomKey): + room_frame = self._key_room_frame_dict.pop(room_key, None) + if room_frame is not None: + room_frame.Close(True) + + def _on_room_frame_close(self, room_key: sdk_models.RoomKey): + self._key_room_frame_dict.pop(room_key, None) + + def _on_open_room_config_dialog(self): + if self._room_config_dialog is None or self._room_config_dialog.IsBeingDeleted(): + self._room_config_dialog = ui.room_config_dialog.RoomConfigDialog(None) + self._room_config_dialog.Show() -async def shut_down(): - listener.shut_down() - await blcsdk.shut_down() +class NetworkWorker: + def __init__(self): + self._worker_thread = threading.Thread( + target=asyncio.run, args=(self._worker_thread_func(),), daemon=True + ) + self._thread_init_future = concurrent.futures.Future() + + self._loop: Optional[asyncio.AbstractEventLoop] = None + self._shut_down_event: Optional[asyncio.Event] = None + + def init(self): + self._worker_thread.start() + self._thread_init_future.result(10) + + def start_shut_down(self): + if self._shut_down_event is not None: + self._loop.call_soon_threadsafe(self._shut_down_event.set) + + def join(self, timeout=None): + self._worker_thread.join(timeout) + return not self._worker_thread.is_alive() + + async def _worker_thread_func(self): + self._loop = asyncio.get_running_loop() + try: + try: + await self._init_in_worker_thread() + self._thread_init_future.set_result(None) + except BaseException as e: + self._thread_init_future.set_exception(e) + return + + await self._run() + finally: + await self._shut_down() + + async def _init_in_worker_thread(self): + await blcsdk.init() + if not blcsdk.is_sdk_version_compatible(): + raise RuntimeError('SDK version is not compatible') + + await listener.init() + + self._shut_down_event = asyncio.Event() + + async def _run(self): + logger.info('Running network thread event loop') + await self._shut_down_event.wait() + logger.info('Network thread start to shut down') + + @staticmethod + async def _shut_down(): + listener.shut_down() + await blcsdk.shut_down() if __name__ == '__main__': - sys.exit(asyncio.run(main())) + main() diff --git a/plugins/native-ui/requirements.txt b/plugins/native-ui/requirements.txt index 901a0fe..67c1418 100644 --- a/plugins/native-ui/requirements.txt +++ b/plugins/native-ui/requirements.txt @@ -1,4 +1,3 @@ PyPubSub==4.0.3 -wxasync==0.49 wxPython==4.2.1 XlsxWriter==3.2.0 diff --git a/plugins/native-ui/ui/app.py b/plugins/native-ui/ui/app.py deleted file mode 100644 index ab8b9d2..0000000 --- a/plugins/native-ui/ui/app.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -import logging -from typing import * - -import pubsub.pub as pub -import wxasync - -import blcsdk.models as sdk_models -import ui.room_config_dialog -import ui.room_frame -import ui.task_bar_icon - -logger = logging.getLogger('native-ui.' + __name__) - -_app: Optional['App'] = None - - -def init(): - global _app - _app = App() - - -class App(wxasync.WxAsyncApp): - def __init__(self, *args, **kwargs): - self._task_bar_icon: Optional[ui.task_bar_icon.TaskBarIcon] = None - - super().__init__(*args, clearSigInt=False, **kwargs) - self.SetExitOnFrameDelete(False) - - self._key_room_frame_dict: Dict[sdk_models.RoomKey, ui.room_frame.RoomFrame] = {} - self._room_config_dialog: Optional[ui.room_config_dialog.RoomConfigDialog] = None - - def OnInit(self): - self._task_bar_icon = ui.task_bar_icon.TaskBarIcon() - - pub.subscribe(self._on_add_room, 'add_room') - pub.subscribe(self._on_del_room, 'del_room') - pub.subscribe(self._on_room_frame_close, 'room_frame_close') - pub.subscribe(self._on_add_room, 'open_room') - pub.subscribe(self._on_open_room_config_dialog, 'open_room_config_dialog') - return True - - def _on_add_room(self, room_key: sdk_models.RoomKey): - if room_key in self._key_room_frame_dict: - return - - room_frame = self._key_room_frame_dict[room_key] = ui.room_frame.RoomFrame(None, room_key) - room_frame.Show() - - def _on_del_room(self, room_key: sdk_models.RoomKey): - room_frame = self._key_room_frame_dict.pop(room_key, None) - if room_frame is not None: - room_frame.Close(True) - - def _on_room_frame_close(self, room_key: sdk_models.RoomKey): - self._key_room_frame_dict.pop(room_key, None) - - def _on_open_room_config_dialog(self): - if self._room_config_dialog is None or self._room_config_dialog.IsBeingDeleted(): - self._room_config_dialog = ui.room_config_dialog.RoomConfigDialog(None) - self._room_config_dialog.Show()