Skip to content

Commit 61b977c

Browse files
authored
Merge pull request #4 from CERBSim/clean_app_teardown
some fixes for clean websocket teardown
2 parents 7d909a7 + 0a780a6 commit 61b977c

4 files changed

Lines changed: 66 additions & 9 deletions

File tree

webgpu/link/base.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,6 @@ def __init__(self):
480480
super().__init__()
481481
self._send_loop = asyncio.new_event_loop()
482482
self._callback_loop = asyncio.new_event_loop()
483-
self._callback_queue = asyncio.Queue()
484-
485483
self._callback_thread = threading.Thread(target=self._start_callback_thread, daemon=True)
486484
self._callback_thread.start()
487485

@@ -516,7 +514,13 @@ def _send_data(self, metadata, data, key=None):
516514
event = threading.Event()
517515
self._requests[request_id] = event, key
518516

519-
asyncio.run_coroutine_threadsafe(self._send_async(data), self._send_loop)
517+
try:
518+
asyncio.run_coroutine_threadsafe(self._send_async(data), self._send_loop)
519+
except RuntimeError:
520+
# Event loop is closed — connection is dead, clean up and bail.
521+
if event:
522+
self._requests.pop(request_id, None)
523+
return None
520524
if event:
521525
event.wait()
522526
return self._requests.pop(request_id)
@@ -530,15 +534,19 @@ async def handle_callbacks():
530534
try:
531535
func, args = await self._callback_queue.get()
532536
func(*args)
537+
except asyncio.CancelledError:
538+
break
539+
except RuntimeError:
540+
break
533541
except asyncio.QueueEmpty:
534542
pass
535543
except Exception as e:
536544
print("error in callback", type(e), str(e))
537-
# await asyncio.sleep(0.01)
538545

539546
try:
540547
self._callback_loop = asyncio.new_event_loop()
541548
asyncio.set_event_loop(self._callback_loop)
549+
self._callback_queue = asyncio.Queue()
542550
self._callback_loop.create_task(handle_callbacks())
543551
self._callback_loop.run_forever()
544552
except Exception as e:

webgpu/link/websocket.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,16 @@ async def start_websocket():
122122

123123
def stop(self):
124124
self._send_loop.call_soon_threadsafe(self._stop.set_result, None)
125+
126+
# Stop the callback event loop so the _callback_thread exits.
127+
try:
128+
self._callback_loop.call_soon_threadsafe(self._callback_loop.stop)
129+
except RuntimeError:
130+
pass # Event loop already closed
131+
132+
# Unblock any threads stuck waiting for websocket RPC responses.
133+
for rid, val in list(self._requests.items()):
134+
if isinstance(val, tuple):
135+
event, key = val
136+
if isinstance(event, threading.Event):
137+
event.set()

webgpu/platform.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,32 @@ def init_pyodide(link_):
206206

207207
LinkBase.register_serializer(BaseWebGPUHandle, lambda _, v: v.handle)
208208
LinkBase.register_serializer(BaseWebGPUObject, lambda _, v: v.__dict__ or None)
209+
210+
211+
def reset():
212+
"""Reset the platform globals so that init can be called again.
213+
214+
Used by test runners that start and stop multiple app instances
215+
in the same process.
216+
"""
217+
global js, websocket_server, link, create_proxy, destroy_proxy
218+
if websocket_server is not None:
219+
try:
220+
websocket_server.stop()
221+
except RuntimeError:
222+
pass # Event loop already closed
223+
js = None
224+
websocket_server = None
225+
link = None
226+
if not is_pyodide:
227+
create_proxy = None
228+
destroy_proxy = None
229+
230+
# Reset cached WebGPU device so it is re-requested on the new connection.
231+
from . import utils as _utils
232+
_utils._device = None
233+
234+
# Reset the cached font atlas — its GPU texture is tied to the old
235+
# connection and would deadlock when accessed on a new one.
236+
from . import font as _font
237+
_font._default_font_atlas = None

webgpu/scene.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ def init(self, canvas):
9797
self.options.timestamp = time.time()
9898
self.options.update_buffers()
9999
for obj in self.render_objects:
100-
obj._update_and_create_render_pipeline(self.options)
100+
try:
101+
obj._update_and_create_render_pipeline(self.options)
102+
except Exception as e:
103+
print(f'Warning: failed to init renderer {type(obj).__name__}: {e}')
104+
obj.active = False
101105

102106
camera = self.options.camera
103107
self._js_render = platform.create_proxy(self._render_direct)
@@ -308,8 +312,11 @@ def cleanup(self):
308312
self.options.camera._render_function = None
309313
self.options.camera._get_position_function = None
310314
self.input_handler.unregister_callbacks()
311-
platform.destroy_proxy(self._js_render)
312-
del self._js_render
313-
self.canvas._on_resize_callbacks.remove(self.render)
314-
self.canvas._on_update_html_canvas.remove(self.__on_update_html_canvas)
315+
if hasattr(self, '_js_render'):
316+
platform.destroy_proxy(self._js_render)
317+
del self._js_render
318+
if self.render in self.canvas._on_resize_callbacks:
319+
self.canvas._on_resize_callbacks.remove(self.render)
320+
if self.__on_update_html_canvas in self.canvas._on_update_html_canvas:
321+
self.canvas._on_update_html_canvas.remove(self.__on_update_html_canvas)
315322
self.canvas = None

0 commit comments

Comments
 (0)