@@ -89,45 +89,28 @@ export async function startRunResultNamedPipe(
8989 traceVerbose ( 'Starting Test Result named pipe' ) ;
9090 const pipeName : string = generateRandomPipeName ( 'python-test-results' ) ;
9191
92+ // NOTE: `cancellationToken` is intentionally only forwarded to `createReaderPipe`
93+ // (which uses it to cancel pipe *creation*). Once the reader is connected we do
94+ // not wire a cancellation handler here. Disposing the reader on cancellation
95+ // would close the socket while data was still buffered in the kernel pipe, and
96+ // any results not yet delivered to the `reader.listen` callback would be lost.
97+ // This is exactly the regression seen in the debug path, where cancellation
98+ // fires the moment the debug session terminates.
99+ //
100+ // Instead, disposal is fully event-driven: when the subprocess closes its end
101+ // of the pipe (either by exiting normally or by being killed via the caller's
102+ // own cancellation handling), the OS delivers all remaining buffered bytes and
103+ // then EOF, which fires `reader.onClose` below and triggers dispose.
92104 const reader = await createReaderPipe ( pipeName , cancellationToken ) ;
93105 traceVerbose ( `Test Results named pipe ${ pipeName } connected` ) ;
94106 let disposables : Disposable [ ] = [ ] ;
95- let disposed = false ;
96107 const disposable = new Disposable ( ( ) => {
97108 traceVerbose ( `Test Results named pipe ${ pipeName } disposed` ) ;
98- disposed = true ;
99109 disposables . forEach ( ( d ) => d . dispose ( ) ) ;
100110 disposables = [ ] ;
101111 deferredTillServerClose . resolve ( ) ;
102112 } ) ;
103113
104- if ( cancellationToken ) {
105- disposables . push (
106- cancellationToken ?. onCancellationRequested ( ( ) => {
107- traceLog (
108- `Test Result named pipe ${ pipeName } cancelled; draining buffered data before dispose` ,
109- ) ;
110- // Do NOT dispose the reader immediately. In the debug path, cancellation
111- // fires as soon as the debug session terminates, but the result pipe may
112- // still have buffered messages that have not been delivered to the
113- // `reader.listen` callback yet. Disposing now would close the reader and
114- // drop those pending results.
115- //
116- // The reader's `onClose` event (registered below) will fire once the
117- // subprocess closes its end of the pipe and all buffered data has been
118- // drained, and that handler will dispose. Use a safety timeout to force
119- // disposal in case the pipe never closes naturally (e.g. subprocess hang).
120- setTimeout ( ( ) => {
121- if ( ! disposed ) {
122- traceVerbose (
123- `Test Result named pipe ${ pipeName } drain timeout, forcing dispose` ,
124- ) ;
125- disposable . dispose ( ) ;
126- }
127- } , 5000 ) ;
128- } ) ,
129- ) ;
130- }
131114 disposables . push (
132115 reader ,
133116 reader . listen ( ( data : Message ) => {
@@ -136,9 +119,10 @@ export async function startRunResultNamedPipe(
136119 dataReceivedCallback ( ( data as ExecutionResultMessage ) . params as ExecutionTestPayload ) ;
137120 } ) ,
138121 reader . onClose ( ( ) => {
139- // this is called once the server close, once per run instance
122+ // Fires once the subprocess has closed its end of the pipe and the OS
123+ // has delivered all buffered data. This is the only path that disposes
124+ // the reader, so no results can be dropped due to a premature close.
140125 traceVerbose ( `Test Result named pipe ${ pipeName } closed. Disposing of listener/s.` ) ;
141- // dispose of all data listeners and cancelation listeners
142126 disposable . dispose ( ) ;
143127 } ) ,
144128 reader . onError ( ( error ) => {
0 commit comments