@@ -28,15 +28,9 @@ class FastCS:
2828 loop: Optional event loop to run the control system in
2929 """
3030
31- def __init__ (
32- self ,
33- controller : Controller ,
34- transports : Sequence [Transport ],
35- loop : asyncio .AbstractEventLoop | None = None ,
36- ):
31+ def __init__ (self , controller : Controller , transports : Sequence [Transport ]):
3732 self ._controller = controller
3833 self ._transports = transports
39- self ._loop = loop or asyncio .get_event_loop ()
4034
4135 self ._scan_coros : list [ScanCallback ] = []
4236 self ._initial_coros : list [ScanCallback ] = []
@@ -47,36 +41,25 @@ def run(self, interactive: bool = True):
4741 """Run the application
4842
4943 This is a convenience method to call `serve` in a synchronous context.
44+ To use in an async context, call `serve` directly.
5045
5146 Args:
5247 interactive: Whether to create an interactive IPython shell
5348
5449 """
55- serve = asyncio .ensure_future (self .serve (interactive = interactive ))
5650
57- if os . name != "nt" :
58- self . _loop . add_signal_handler ( signal . SIGINT , serve . cancel )
59- self . _loop . add_signal_handler ( signal . SIGTERM , serve . cancel )
60- self . _loop . run_until_complete ( serve )
51+ async def _serve () :
52+ """Wrapper to add signal handlers and call ` serve`"""
53+ loop = asyncio . get_running_loop ( )
54+ task = asyncio . current_task ( )
6155
62- async def _run_initial_coros ( self ) :
63- for coro in self . _initial_coros :
64- await coro ( )
56+ if os . name != "nt" and task is not None :
57+ loop . add_signal_handler ( signal . SIGINT , task . cancel )
58+ loop . add_signal_handler ( signal . SIGTERM , task . cancel )
6559
66- async def _start_scan_tasks (self ):
67- self ._scan_tasks = {self ._loop .create_task (coro ()) for coro in self ._scan_coros }
60+ await self .serve (interactive = interactive )
6861
69- def _stop_scan_tasks (self ):
70- for task in self ._scan_tasks :
71- if not task .done ():
72- try :
73- task .cancel ()
74- except (asyncio .CancelledError , RuntimeError ):
75- pass
76- except Exception as e :
77- raise RuntimeError ("Unhandled exception in stop scan tasks" ) from e
78-
79- self ._scan_tasks .clear ()
62+ asyncio .run (_serve ())
8063
8164 async def serve (self , interactive : bool = True ) -> None :
8265 """Serve the control system over the given transports on the current event loop
@@ -110,7 +93,7 @@ async def serve(self, interactive: bool = True) -> None:
11093
11194 coros : list [Coroutine ] = []
11295 for transport in self ._transports :
113- transport .connect (controller_api = self .controller_api , loop = self . _loop )
96+ transport .connect (controller_api = self .controller_api )
11497 coros .append (transport .serve ())
11598 common_context = context .keys () & transport .context .keys ()
11699 if common_context :
@@ -153,16 +136,30 @@ async def block_forever():
153136 self ._stop_scan_tasks ()
154137 await self ._controller .disconnect ()
155138
139+ async def _run_initial_coros (self ):
140+ for coro in self ._initial_coros :
141+ await coro ()
142+
143+ async def _start_scan_tasks (self ):
144+ self ._scan_tasks = {asyncio .create_task (coro ()) for coro in self ._scan_coros }
145+
156146 async def _interactive_shell (self , context : dict [str , Any ]):
157147 """Spawn interactive shell in another thread and wait for it to complete."""
148+ loop = asyncio .get_running_loop ()
158149
159150 def run (coro : Coroutine [None , None , None ]):
160151 """Run coroutine on FastCS event loop from IPython thread."""
161152
162153 def wrapper ():
163- asyncio .create_task (coro )
154+ task = asyncio .create_task (coro )
155+
156+ def _log_exception (t : asyncio .Task ):
157+ if not t .cancelled () and (exc := t .exception ()):
158+ logger .exception ("`run` task raised exception" , exc_info = exc )
164159
165- self ._loop .call_soon_threadsafe (wrapper )
160+ task .add_done_callback (_log_exception )
161+
162+ loop .call_soon_threadsafe (wrapper )
166163
167164 async def interactive_shell (
168165 context : dict [str , object ], stop_event : asyncio .Event
@@ -176,8 +173,24 @@ async def interactive_shell(
176173 context ["run" ] = run
177174
178175 stop_event = asyncio .Event ()
179- self ._loop .create_task (interactive_shell (context , stop_event ))
176+ shell_task = asyncio .create_task (interactive_shell (context , stop_event ))
177+
180178 await stop_event .wait ()
181179
180+ if not shell_task .cancelled () and (exc := shell_task .exception ()):
181+ logger .exception ("Interactive shell raised exception" , exc_info = exc )
182+
183+ def _stop_scan_tasks (self ):
184+ for task in self ._scan_tasks :
185+ if not task .done ():
186+ try :
187+ task .cancel ()
188+ except (asyncio .CancelledError , RuntimeError ):
189+ pass
190+ except Exception as e :
191+ raise RuntimeError ("Unhandled exception in stop scan tasks" ) from e
192+
193+ self ._scan_tasks .clear ()
194+
182195 def __del__ (self ):
183196 self ._stop_scan_tasks ()
0 commit comments