@@ -98,30 +98,39 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
9898 await self ._register ()
9999
100100 run_task = asyncio .create_task (self ._signalr_client .run ())
101-
102- # Set up a task to wait for cancellation
103101 cancel_task = asyncio .create_task (self ._cancel_event .wait ())
104-
105102 self ._keep_alive_task = asyncio .create_task (self ._keep_alive ())
106103
107- # Keep the runtime alive
108- # Wait for either the run to complete or cancellation
109- done , pending = await asyncio .wait (
110- [run_task , cancel_task ], return_when = asyncio .FIRST_COMPLETED
111- )
112-
113- # Cancel any pending tasks
114- for task in pending :
115- task .cancel ()
104+ try :
105+ # Wait for either the run to complete or cancellation
106+ done , pending = await asyncio .wait (
107+ [run_task , cancel_task ], return_when = asyncio .FIRST_COMPLETED
108+ )
109+ except KeyboardInterrupt :
110+ logger .info (
111+ "Received keyboard interrupt, shutting down gracefully..."
112+ )
113+ self ._cancel_event .set ()
114+ finally :
115+ # Cancel any pending tasks gracefully
116+ for task in [run_task , cancel_task , self ._keep_alive_task ]:
117+ if task and not task .done ():
118+ task .cancel ()
119+ try :
120+ await asyncio .wait_for (task , timeout = 2.0 )
121+ except (asyncio .CancelledError , asyncio .TimeoutError ):
122+ pass
116123
117124 output_result = {}
118125 if self ._session_output :
119126 output_result ["content" ] = self ._session_output
120127
121128 self .context .result = UiPathRuntimeResult (output = output_result )
122-
123129 return self .context .result
124130
131+ except KeyboardInterrupt :
132+ logger .info ("Keyboard interrupt received" )
133+ return None
125134 except Exception as e :
126135 if isinstance (e , UiPathMcpRuntimeError ):
127136 raise
@@ -133,7 +142,9 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
133142 UiPathErrorCategory .USER ,
134143 ) from e
135144 finally :
136- self .trace_provider .shutdown ()
145+ await self .cleanup ()
146+ if hasattr (self , "trace_provider" ) and self .trace_provider :
147+ self .trace_provider .shutdown ()
137148
138149 async def validate (self ) -> None :
139150 """Validate runtime inputs and load MCP server configuration."""
@@ -442,36 +453,49 @@ async def _keep_alive(self) -> None:
442453 """
443454 Heartbeat to keep the runtime available.
444455 """
445- while not self ._cancel_event .is_set ():
446- try :
456+ try :
457+ while not self ._cancel_event .is_set ():
458+ try :
459+
460+ async def on_keep_alive_response (
461+ response : CompletionMessage ,
462+ ) -> None :
463+ if response .error :
464+ logger .error (f"Error during keep-alive: { response .error } " )
465+ return
466+ session_ids = response .result
467+ logger .info (f"Active sessions: { session_ids } " )
468+ # If there are no active sessions and this is a sandbox environment
469+ # We need to cancel the runtime
470+ # eg: when user kills the agent that triggered the runtime, before we subscribe to events
471+ if (
472+ not session_ids
473+ and self .sandboxed
474+ and not self ._cancel_event .is_set ()
475+ ):
476+ logger .error (
477+ "No active sessions, cancelling sandboxed runtime..."
478+ )
479+ self ._cancel_event .set ()
447480
448- async def on_keep_alive_response (response : CompletionMessage ) -> None :
449- if response .error :
450- logger .error (f"Error during keep-alive: { response .error } " )
451- return
452- session_ids = response .result
453- logger .info (f"Active sessions: { session_ids } " )
454- # If there are no active sessions and this is a sandbox environment
455- # We need to cancel the runtime
456- # eg: when user kills the agent that triggered the runtime, before we subscribe to events
457- if (
458- not session_ids
459- and self .sandboxed
460- and not self ._cancel_event .is_set ()
461- ):
462- logger .error (
463- "No active sessions, cancelling sandboxed runtime..."
481+ if self ._signalr_client :
482+ await self ._signalr_client .send (
483+ method = "OnKeepAlive" ,
484+ arguments = [],
485+ on_invocation = on_keep_alive_response ,
464486 )
465- self ._cancel_event .set ()
487+ except Exception as e :
488+ if not self ._cancel_event .is_set ():
489+ logger .error (f"Error during keep-alive: { e } " )
466490
467- await self . _signalr_client . send (
468- method = "OnKeepAlive" ,
469- arguments = [],
470- on_invocation = on_keep_alive_response ,
471- )
472- except Exception as e :
473- logger .error ( f"Error during keep -alive: { e } " )
474- await asyncio . sleep ( 60 )
491+ try :
492+ await asyncio . wait_for ( self . _cancel_event . wait (), timeout = 60 )
493+ break
494+ except asyncio . TimeoutError :
495+ continue
496+ except asyncio . CancelledError :
497+ logger .info ( "Keep -alive task cancelled " )
498+ raise
475499
476500 async def _on_runtime_abort (self ) -> None :
477501 """
0 commit comments