44
55import asyncio
66import logging
7+ import os
78import socket
9+ import sys
810import threading
911import time
1012import webbrowser
13+ from collections .abc import Callable
1114from typing import Any
1215
1316from uipath .core .tracing import UiPathTraceManager
@@ -57,13 +60,19 @@ def __init__(
5760 host : str = "localhost" ,
5861 port : int = 8000 ,
5962 open_browser : bool = True ,
63+ factory_creator : Callable [[], UiPathRuntimeFactoryProtocol ] | None = None ,
6064 ) -> None :
6165 """Initialize the developer server."""
6266 self .runtime_factory = runtime_factory
6367 self .trace_manager = trace_manager
6468 self .host = host
6569 self .port = port
6670 self .open_browser = open_browser
71+ self .factory_creator = factory_creator
72+
73+ self ._watcher_task : asyncio .Task [None ] | None = None
74+ self ._watcher_stop : asyncio .Event | None = None
75+ self .reload_pending = False
6776
6877 from uipath .dev .server .ws .manager import ConnectionManager
6978
@@ -98,6 +107,7 @@ async def run_async(self) -> None:
98107 if not HAS_EXTRAS :
99108 raise ImportError (_MISSING_EXTRAS_MSG )
100109
110+ await self .run_service .apply_factory_settings ()
101111 self .port = self ._find_free_port (self .host , self .port )
102112 app = self .create_app ()
103113
@@ -110,6 +120,10 @@ async def run_async(self) -> None:
110120 daemon = True ,
111121 ).start ()
112122
123+ # Start file watcher if factory_creator is available
124+ if self .factory_creator is not None :
125+ self ._start_watcher ()
126+
113127 config = uvicorn .Config (
114128 app ,
115129 host = self .host ,
@@ -122,6 +136,7 @@ async def run_async(self) -> None:
122136 async def shutdown (self ) -> None :
123137 """Clean up resources before shutting down."""
124138 logger .info ("Shutting down server resources..." )
139+ self ._stop_watcher ()
125140 # Close any active WebSocket connections
126141 await self .connection_manager .disconnect_all ()
127142 # Give threads time to finish
@@ -134,6 +149,69 @@ def run(self) -> None:
134149 except KeyboardInterrupt :
135150 pass
136151
152+ # ------------------------------------------------------------------
153+ # Hot-reload support
154+ # ------------------------------------------------------------------
155+
156+ async def reload_factory (self ) -> None :
157+ """Dispose old factory, flush user modules, and recreate."""
158+ if self .factory_creator is None :
159+ return
160+
161+ # Dispose old factory if it supports it
162+ if hasattr (self .runtime_factory , "dispose" ):
163+ try :
164+ await self .runtime_factory .dispose ()
165+ except Exception :
166+ logger .debug ("Error disposing old factory" , exc_info = True )
167+
168+ # Flush user modules (files under cwd, excluding venvs/site-packages)
169+ cwd = os .getcwd ()
170+ to_remove = [
171+ name
172+ for name , mod in sys .modules .items ()
173+ if hasattr (mod , "__file__" )
174+ and mod .__file__ is not None
175+ and os .path .abspath (mod .__file__ ).startswith (cwd )
176+ and ".venv" not in mod .__file__
177+ and "site-packages" not in mod .__file__
178+ ]
179+ for name in to_remove :
180+ del sys .modules [name ]
181+ logger .debug ("Flushed %d user modules" , len (to_remove ))
182+
183+ # Recreate factory
184+ self .runtime_factory = self .factory_creator ()
185+ self .run_service .runtime_factory = self .runtime_factory
186+ await self .run_service .apply_factory_settings ()
187+ self .reload_pending = False
188+ logger .debug ("Factory reloaded successfully" )
189+
190+ def _start_watcher (self ) -> None :
191+ """Start the file watcher background task."""
192+ from uipath .dev .server .watcher import watch_python_files
193+
194+ self ._watcher_stop = asyncio .Event ()
195+ self ._watcher_task = asyncio .create_task (
196+ watch_python_files (
197+ on_change = self ._on_files_changed ,
198+ stop_event = self ._watcher_stop ,
199+ )
200+ )
201+
202+ def _stop_watcher (self ) -> None :
203+ """Stop the file watcher background task."""
204+ if self ._watcher_stop is not None :
205+ self ._watcher_stop .set ()
206+ if self ._watcher_task is not None :
207+ self ._watcher_task .cancel ()
208+ self ._watcher_task = None
209+
210+ def _on_files_changed (self , changed_files : list [str ]) -> None :
211+ """Handle file change events from the watcher."""
212+ self .reload_pending = True
213+ self .connection_manager .broadcast_reload (changed_files )
214+
137215 # ------------------------------------------------------------------
138216 # Internal callbacks
139217 # ------------------------------------------------------------------
0 commit comments