@@ -59,6 +59,7 @@ def reload_begin():
59
59
# Called from child process when new application instance starts up
60
60
def reload_end ():
61
61
import websockets
62
+ import websockets .asyncio .client
62
63
63
64
# os.kill(os.getppid(), signal.SIGUSR1)
64
65
@@ -70,12 +71,12 @@ def reload_end():
70
71
71
72
async def _ () -> None :
72
73
options = {
73
- "extra_headers " : {
74
+ "additional_headers " : {
74
75
"Shiny-Autoreload-Secret" : os .getenv ("SHINY_AUTORELOAD_SECRET" , "" ),
75
76
}
76
77
}
77
78
try :
78
- async with websockets .connect (
79
+ async with websockets .asyncio . client . connect (
79
80
url , ** options # pyright: ignore[reportArgumentType]
80
81
) as websocket :
81
82
await websocket .send ("reload_end" )
@@ -169,6 +170,17 @@ def start_server(port: int, app_port: int, launch_browser: bool):
169
170
os .environ ["SHINY_AUTORELOAD_PORT" ] = str (port )
170
171
os .environ ["SHINY_AUTORELOAD_SECRET" ] = secret
171
172
173
+ # websockets 14.0 (and presumably later) log an error if a connection is opened and
174
+ # closed before any data is sent. Our VS Code extension does exactly this--opens a
175
+ # connection to check if the server is running, then closes it. It's better that it
176
+ # does this and doesn't actually perform an HTTP request because we can't guarantee
177
+ # that the HTTP request will be cheap (we do the same ping on both the autoreload
178
+ # socket and the main uvicorn socket). So better to just suppress all errors until
179
+ # we think we have a problem. You can unsuppress by setting the environment variable
180
+ # to DEBUG.
181
+ loglevel = os .getenv ("SHINY_AUTORELOAD_LOG_LEVEL" , "CRITICAL" )
182
+ logging .getLogger ("websockets" ).setLevel (loglevel )
183
+
172
184
app_url = get_proxy_url (f"http://127.0.0.1:{ app_port } /" )
173
185
174
186
# Run on a background thread so our event loop doesn't interfere with uvicorn.
@@ -186,6 +198,8 @@ async def _coro_main(
186
198
port : int , app_url : str , secret : str , launch_browser : bool
187
199
) -> None :
188
200
import websockets
201
+ import websockets .asyncio .server
202
+ from websockets .http11 import Request , Response
189
203
190
204
reload_now : asyncio .Event = asyncio .Event ()
191
205
@@ -198,18 +212,22 @@ def nudge():
198
212
reload_now .set ()
199
213
reload_now .clear ()
200
214
201
- async def reload_server (conn : websockets .server .WebSocketServerProtocol ):
215
+ async def reload_server (conn : websockets .asyncio . server .ServerConnection ):
202
216
try :
203
- if conn .path == "/autoreload" :
217
+ if conn .request is None :
218
+ raise RuntimeError (
219
+ "Autoreload server received a connection with no request"
220
+ )
221
+ elif conn .request .path == "/autoreload" :
204
222
# The client wants to be notified when the app has reloaded. The client
205
223
# in this case is the web browser, specifically shiny-autoreload.js.
206
224
while True :
207
225
await reload_now .wait ()
208
226
await conn .send ("autoreload" )
209
- elif conn .path == "/notify" :
227
+ elif conn .request . path == "/notify" :
210
228
# The client is notifying us that the app has reloaded. The client in
211
229
# this case is the uvicorn worker process (see reload_end(), above).
212
- req_secret = conn .request_headers .get ("Shiny-Autoreload-Secret" , "" )
230
+ req_secret = conn .request . headers .get ("Shiny-Autoreload-Secret" , "" )
213
231
if req_secret != secret :
214
232
# The client couldn't prove that they were from a child process
215
233
return
@@ -225,18 +243,20 @@ async def reload_server(conn: websockets.server.WebSocketServerProtocol):
225
243
# VSCode extension used in RSW sniffs out ports that are being listened on, which
226
244
# leads to confusion if all you get is an error.
227
245
async def process_request (
228
- path : str , request_headers : websockets .datastructures .Headers
229
- ) -> Optional [tuple [http .HTTPStatus , websockets .datastructures .HeadersLike , bytes ]]:
230
- # If there's no Upgrade header, it's not a WebSocket request.
231
- if request_headers .get ("Upgrade" ) is None :
232
- # For some unknown reason, this fixes a tendency on GitHub Codespaces to
233
- # correctly proxy through this request, but give a 404 when the redirect is
234
- # followed and app_url is requested. With the sleep, both requests tend to
235
- # succeed reliably.
236
- await asyncio .sleep (1 )
237
- return (http .HTTPStatus .MOVED_PERMANENTLY , [("Location" , app_url )], b"" )
238
-
239
- async with websockets .serve (
246
+ connection : websockets .asyncio .server .ServerConnection ,
247
+ request : Request ,
248
+ ) -> Response | None :
249
+ if request .headers .get ("Upgrade" ) is None :
250
+ return Response (
251
+ status_code = http .HTTPStatus .MOVED_PERMANENTLY ,
252
+ reason_phrase = "Moved Permanently" ,
253
+ headers = websockets .Headers (Location = app_url ),
254
+ body = None ,
255
+ )
256
+ else :
257
+ return None
258
+
259
+ async with websockets .asyncio .server .serve (
240
260
reload_server , "127.0.0.1" , port , process_request = process_request
241
261
):
242
262
await asyncio .Future () # wait forever
0 commit comments