0
# File Watching & Server
1
2
The server component provides file watching capabilities and WebSocket-based hot reloading functionality. It monitors file system changes and communicates with browser clients to trigger automatic page reloads.
3
4
## Capabilities
5
6
### RebuildServer Class
7
8
The main server class that handles file watching and WebSocket connections for hot reloading.
9
10
```python { .api }
11
class RebuildServer:
12
def __init__(
13
self,
14
paths: list[os.PathLike[str]],
15
ignore_filter: IgnoreFilter,
16
change_callback: Callable[[Sequence[Path]], None]
17
) -> None:
18
"""
19
Initialize file watching server.
20
21
Parameters:
22
- paths: list[os.PathLike[str]] - Directories to watch for changes
23
- ignore_filter: IgnoreFilter - Filter for ignoring specific files/patterns
24
- change_callback: Callable[[Sequence[Path]], None] - Function called when changes detected
25
"""
26
27
@asynccontextmanager
28
async def lifespan(self, _app) -> AbstractAsyncContextManager[None]:
29
"""
30
ASGI lifespan context manager for server startup/shutdown.
31
32
Manages the file watching task lifecycle:
33
- Starts file watching on application startup
34
- Stops file watching on application shutdown
35
36
Parameters:
37
- _app: ASGI application instance (unused)
38
39
Yields:
40
- None - Context for application lifespan
41
"""
42
43
async def main(self) -> None:
44
"""
45
Main server event loop.
46
47
Coordinates file watching and shutdown handling:
48
- Creates file watching task
49
- Creates shutdown waiting task
50
- Cancels pending tasks on completion
51
52
Returns:
53
- None
54
"""
55
56
async def watch(self) -> None:
57
"""
58
File watching loop using watchfiles.
59
60
Monitors configured directories for changes:
61
- Filters changes through ignore_filter
62
- Calls change_callback with changed paths
63
- Runs callback in ProcessPoolExecutor for isolation
64
- Sets internal flag to notify WebSocket clients
65
66
Returns:
67
- None
68
"""
69
70
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
71
"""
72
ASGI application callable for WebSocket connections.
73
74
Handles WebSocket connections from browser clients:
75
- Accepts WebSocket connections
76
- Manages client connection lifecycle
77
- Coordinates reload notifications and disconnect handling
78
79
Parameters:
80
- scope: ASGI scope dict with connection info
81
- receive: ASGI receive callable for messages
82
- send: ASGI send callable for responses
83
84
Returns:
85
- None
86
"""
87
88
async def watch_reloads(self, ws: WebSocket) -> None:
89
"""
90
Send reload notifications to WebSocket client.
91
92
Waits for file change events and sends reload messages:
93
- Blocks until file changes detected
94
- Sends "refresh" message to browser client
95
- Resets change flag for next iteration
96
97
Parameters:
98
- ws: WebSocket - Connected client WebSocket
99
100
Returns:
101
- None
102
"""
103
104
@staticmethod
105
async def wait_client_disconnect(ws: WebSocket) -> None:
106
"""
107
Wait for WebSocket client to disconnect.
108
109
Monitors WebSocket for client disconnection:
110
- Iterates over incoming messages until connection closes
111
- Provides graceful handling of client disconnects
112
113
Parameters:
114
- ws: WebSocket - Connected client WebSocket
115
116
Returns:
117
- None
118
"""
119
```
120
121
## Usage Examples
122
123
### Basic File Watching Setup
124
125
```python
126
import asyncio
127
from pathlib import Path
128
from sphinx_autobuild.server import RebuildServer
129
from sphinx_autobuild.filter import IgnoreFilter
130
131
def build_callback(changed_paths):
132
"""Called when files change."""
133
print(f"Files changed: {[str(p) for p in changed_paths]}")
134
# Trigger rebuild logic here
135
136
# Setup file watching
137
watch_dirs = [Path('docs'), Path('source')]
138
ignore_filter = IgnoreFilter(['.git', '__pycache__'], [r'\.tmp$'])
139
server = RebuildServer(watch_dirs, ignore_filter, build_callback)
140
141
# Run the file watcher
142
async def run_watcher():
143
await server.main()
144
145
# asyncio.run(run_watcher())
146
```
147
148
### ASGI Application Integration
149
150
```python
151
from starlette.applications import Starlette
152
from starlette.middleware import Middleware
153
from starlette.routing import Mount, WebSocketRoute
154
from starlette.staticfiles import StaticFiles
155
from sphinx_autobuild.server import RebuildServer
156
from sphinx_autobuild.middleware import JavascriptInjectorMiddleware
157
158
# Create server instance
159
server = RebuildServer(watch_dirs, ignore_filter, build_callback)
160
161
# Create ASGI application
162
app = Starlette(
163
routes=[
164
WebSocketRoute("/websocket-reload", server, name="reload"),
165
Mount("/", app=StaticFiles(directory="_build/html", html=True), name="static"),
166
],
167
middleware=[Middleware(JavascriptInjectorMiddleware, ws_url="127.0.0.1:8000")],
168
lifespan=server.lifespan,
169
)
170
```
171
172
### Custom Change Handling
173
174
```python
175
from pathlib import Path
176
from sphinx_autobuild.server import RebuildServer
177
from sphinx_autobuild.filter import IgnoreFilter
178
179
class CustomChangeHandler:
180
def __init__(self):
181
self.change_count = 0
182
183
def __call__(self, changed_paths):
184
"""Custom change callback with logging and filtering."""
185
self.change_count += 1
186
187
# Filter to only .rst and .md files
188
docs_changes = [
189
p for p in changed_paths
190
if p.suffix in ['.rst', '.md', '.py']
191
]
192
193
if docs_changes:
194
print(f"Build #{self.change_count}: {len(docs_changes)} documentation files changed")
195
# Trigger actual build
196
self.rebuild_docs(docs_changes)
197
else:
198
print(f"Change #{self.change_count}: Non-documentation files changed, skipping build")
199
200
def rebuild_docs(self, paths):
201
# Custom build logic
202
pass
203
204
# Use custom handler
205
handler = CustomChangeHandler()
206
server = RebuildServer([Path('docs')], IgnoreFilter([], []), handler)
207
```
208
209
## WebSocket Protocol
210
211
### Client-Server Communication
212
213
The WebSocket protocol used for browser hot reloading is simple:
214
215
**Connection:**
216
- Browser connects to `/websocket-reload` endpoint
217
- Server accepts connection and starts monitoring for changes
218
219
**Messages:**
220
- **Server → Client**: `"refresh"` - Triggers browser reload
221
- **Client → Server**: Any message keeps connection alive
222
223
**Connection Lifecycle:**
224
1. Browser JavaScript establishes WebSocket connection
225
2. Server waits for file changes or client disconnect
226
3. On file change: server sends `"refresh"` message
227
4. Browser receives message and reloads page
228
5. New page establishes fresh WebSocket connection
229
230
### JavaScript Client Code
231
232
The browser client code (injected by middleware):
233
234
```javascript
235
const ws = new WebSocket("ws://127.0.0.1:8000/websocket-reload");
236
ws.onmessage = () => window.location.reload();
237
```
238
239
## File Watching Details
240
241
### Watchfiles Integration
242
243
Uses the `watchfiles` library for efficient file system monitoring:
244
245
- **Cross-platform**: Works on Windows, macOS, Linux
246
- **Efficient**: Uses native OS file watching APIs
247
- **Recursive**: Automatically watches subdirectories
248
- **Filter Integration**: Applies IgnoreFilter to all detected changes
249
250
### Change Processing
251
252
When files change:
253
254
1. **Detection**: watchfiles detects filesystem events
255
2. **Filtering**: IgnoreFilter determines if changes should be processed
256
3. **Callback Execution**: change_callback runs in ProcessPoolExecutor for isolation
257
4. **Notification**: WebSocket clients receive reload message
258
259
### Performance Characteristics
260
261
- **Asynchronous**: Non-blocking file watching and WebSocket handling
262
- **Process Isolation**: Build callbacks run in separate process to prevent blocking
263
- **Concurrent Clients**: Supports multiple browser connections simultaneously
264
- **Efficient Filtering**: File filtering happens before expensive operations
265
266
## Error Handling
267
268
### File Watching Errors
269
270
- **Permission Denied**: Graceful handling of inaccessible directories
271
- **Directory Not Found**: Validates watch paths during initialization
272
- **Filesystem Errors**: Continues watching other directories on isolated errors
273
274
### WebSocket Errors
275
276
- **Connection Failures**: Individual client disconnects don't affect server
277
- **Message Errors**: Malformed messages are ignored
278
- **Protocol Errors**: WebSocket protocol violations handled gracefully
279
280
### Callback Errors
281
282
- **Exception Isolation**: Errors in change_callback don't crash server
283
- **Process Boundaries**: ProcessPoolExecutor contains callback failures
284
- **Continued Operation**: Server continues watching after callback errors
285
286
## Integration Points
287
288
### With Build System
289
290
```python
291
from sphinx_autobuild.build import Builder
292
293
# Builder as change callback
294
builder = Builder(sphinx_args, url_host='127.0.0.1:8000',
295
pre_build_commands=[], post_build_commands=[])
296
server = RebuildServer([Path('docs')], ignore_filter, builder)
297
```
298
299
### With ASGI Framework
300
301
```python
302
from starlette.applications import Starlette
303
304
app = Starlette(
305
routes=[WebSocketRoute("/websocket-reload", server)],
306
lifespan=server.lifespan # Manages file watching lifecycle
307
)
308
```