@@ -120,12 +120,13 @@ async def test_stdio_client_universal_cleanup():
120
120
)
121
121
122
122
server_params = StdioServerParameters (
123
- command = sys . executable ,
123
+ command = "python" ,
124
124
args = ["-c" , long_running_script ],
125
125
)
126
126
127
127
start_time = time .time ()
128
128
129
+ # Use move_on_after which is more reliable for cleanup scenarios
129
130
with anyio .move_on_after (8.0 ) as cancel_scope :
130
131
async with stdio_client (server_params ) as (read_stream , write_stream ):
131
132
# Immediately exit - this triggers cleanup while process is still running
@@ -134,16 +135,16 @@ async def test_stdio_client_universal_cleanup():
134
135
end_time = time .time ()
135
136
elapsed = end_time - start_time
136
137
137
- # On Windows: 2s (stdin wait) + 2s (terminate wait) + overhead = ~5s expected
138
- assert elapsed < 6 .0 , (
139
- f"stdio_client cleanup took { elapsed :.1f} seconds, expected < 6 .0 seconds. "
138
+ # Key assertion: Should complete quickly due to timeout mechanism
139
+ assert elapsed < 5 .0 , (
140
+ f"stdio_client cleanup took { elapsed :.1f} seconds, expected < 5 .0 seconds. "
140
141
f"This suggests the timeout mechanism may not be working properly."
141
142
)
142
143
143
144
# Check if we timed out
144
145
if cancel_scope .cancelled_caught :
145
146
pytest .fail (
146
- "stdio_client cleanup timed out after 8 .0 seconds. "
147
+ "stdio_client cleanup timed out after 6 .0 seconds. "
147
148
"This indicates the cleanup mechanism is hanging and needs fixing."
148
149
)
149
150
@@ -212,3 +213,119 @@ def sigint_handler(signum, frame):
212
213
)
213
214
else :
214
215
raise
216
+
217
+
218
+ @pytest .mark .anyio
219
+ async def test_stdio_client_graceful_stdin_exit ():
220
+ """
221
+ Test that a process exits gracefully when stdin is closed,
222
+ without needing SIGTERM or SIGKILL.
223
+ """
224
+ # Create a Python script that exits when stdin is closed
225
+ script_content = textwrap .dedent (
226
+ """
227
+ import sys
228
+
229
+ # Read from stdin until it's closed
230
+ try:
231
+ while True:
232
+ line = sys.stdin.readline()
233
+ if not line: # EOF/stdin closed
234
+ break
235
+ except:
236
+ pass
237
+
238
+ # Exit gracefully
239
+ sys.exit(0)
240
+ """
241
+ )
242
+
243
+ server_params = StdioServerParameters (
244
+ command = sys .executable ,
245
+ args = ["-c" , script_content ],
246
+ )
247
+
248
+ start_time = time .time ()
249
+
250
+ # Use anyio timeout to prevent test from hanging forever
251
+ with anyio .move_on_after (5.0 ) as cancel_scope :
252
+ async with stdio_client (server_params ) as (read_stream , write_stream ):
253
+ # Let the process start and begin reading stdin
254
+ await anyio .sleep (0.2 )
255
+ # Exit context triggers cleanup - process should exit from stdin closure
256
+ pass
257
+
258
+ if cancel_scope .cancelled_caught :
259
+ pytest .fail (
260
+ "stdio_client cleanup timed out after 5.0 seconds. "
261
+ "Process should have exited gracefully when stdin was closed."
262
+ )
263
+
264
+ end_time = time .time ()
265
+ elapsed = end_time - start_time
266
+
267
+ # Should complete quickly with just stdin closure (no signals needed)
268
+ assert elapsed < 3.0 , (
269
+ f"stdio_client cleanup took { elapsed :.1f} seconds for stdin-aware process. "
270
+ f"Expected < 3.0 seconds since process should exit on stdin closure."
271
+ )
272
+
273
+
274
+ @pytest .mark .anyio
275
+ async def test_stdio_client_stdin_close_ignored ():
276
+ """
277
+ Test that when a process ignores stdin closure, the shutdown sequence
278
+ properly escalates to SIGTERM.
279
+ """
280
+ # Create a Python script that ignores stdin closure but responds to SIGTERM
281
+ script_content = textwrap .dedent (
282
+ """
283
+ import signal
284
+ import sys
285
+ import time
286
+
287
+ # Set up SIGTERM handler to exit cleanly
288
+ def sigterm_handler(signum, frame):
289
+ sys.exit(0)
290
+
291
+ signal.signal(signal.SIGTERM, sigterm_handler)
292
+
293
+ # Close stdin immediately to simulate ignoring it
294
+ sys.stdin.close()
295
+
296
+ # Keep running until SIGTERM
297
+ while True:
298
+ time.sleep(0.1)
299
+ """
300
+ )
301
+
302
+ server_params = StdioServerParameters (
303
+ command = sys .executable ,
304
+ args = ["-c" , script_content ],
305
+ )
306
+
307
+ start_time = time .time ()
308
+
309
+ # Use anyio timeout to prevent test from hanging forever
310
+ with anyio .move_on_after (7.0 ) as cancel_scope :
311
+ async with stdio_client (server_params ) as (read_stream , write_stream ):
312
+ # Let the process start
313
+ await anyio .sleep (0.2 )
314
+ # Exit context triggers cleanup
315
+ pass
316
+
317
+ if cancel_scope .cancelled_caught :
318
+ pytest .fail (
319
+ "stdio_client cleanup timed out after 7.0 seconds. "
320
+ "Process should have been terminated via SIGTERM escalation."
321
+ )
322
+
323
+ end_time = time .time ()
324
+ elapsed = end_time - start_time
325
+
326
+ # Should take ~2 seconds (stdin close timeout) before SIGTERM is sent
327
+ # Total time should be between 2-4 seconds
328
+ assert 1.5 < elapsed < 4.5 , (
329
+ f"stdio_client cleanup took { elapsed :.1f} seconds for stdin-ignoring process. "
330
+ f"Expected between 2-4 seconds (2s stdin timeout + termination time)."
331
+ )
0 commit comments