@@ -70,7 +70,10 @@ export function start(opts: AsyncREPLOptions): REPLServer {
70
70
}
71
71
72
72
const repl = ( opts . start ?? originalStart ) ( opts ) ;
73
- const originalEval = promisify ( wrapNoSyncDomainError ( repl . eval . bind ( repl ) ) ) ;
73
+ const originalEval = promisify (
74
+ wrapPauseInput (
75
+ repl . input ,
76
+ wrapNoSyncDomainError ( repl . eval . bind ( repl ) ) ) ) ;
74
77
75
78
( repl as Mutable < typeof repl > ) . eval = async (
76
79
input : string ,
@@ -232,3 +235,77 @@ function wrapNoSyncDomainError<Args extends any[], Ret>(fn: (...args: Args) => R
232
235
} ;
233
236
}
234
237
238
+ function wrapPauseInput < Args extends any [ ] , Ret > ( input : any , fn : ( ...args : Args ) => Ret ) {
239
+ return ( ...args : Args ) : Ret => {
240
+ // This is a hack to temporarily stop processing of input data if the
241
+ // input stream is a libuv-backed Node.js TTY stream.
242
+ // Part of the `breakEvalOnSigint` implementation in the Node.js REPL
243
+ // consists of disabling raw mode before evaluation and re-enabling it after,
244
+ // so that Ctrl+C events can actually be received by the application
245
+ // (instead of being read as raw input characters, which cannot be processed
246
+ // while the evaluation is still ongoing).
247
+ // This works fine everywhere except for Windows. On Windows, setting and
248
+ // un-setting raw mode for TTY streams in libuv actually involves first
249
+ // canceling a potential pending read operation, then changing the mode,
250
+ // and then re-scheduling that read operation, because libuv uses two different
251
+ // mechanisms to read from the Windows console depending on whether it is in
252
+ // raw mode or not.
253
+ // This is problematic, because the "canceling" part here does not involve
254
+ // actually waiting for that pending-but-now-canceled read to finish, possibly
255
+ // leaving it waiting for more input (and then discarding it when it sees that
256
+ // it was actually supposed to be canceled).
257
+ //
258
+ // In mongosh, this problem could be reproduced by running the following lines
259
+ // inside a Windows console window:
260
+ //
261
+ // > prompt = '>'
262
+ // > db.test.findOne()
263
+ // > db.test.findOne()
264
+ // > db.test.findOne() // <--- This line is discarded by libuv!
265
+ //
266
+ // (The timing here is subtle, and thus depends on the db operations here being
267
+ // async calls.)
268
+ // I did not manage to create a minimal reproduction that uses only Node.js stream
269
+ // APIs, or only using libuv APIs, although theoretically that should be possible.
270
+ // This workaround avoids the whole problem by stopping input reads during evaluation
271
+ // and re-scheduling them later, essentially doing the same thing as libuv
272
+ // already does but on a wider level. It is not *guaranteed* to be correct, but
273
+ // I consider the chances of it breaking something to be fairly low, and the chances
274
+ // of addressing the problem decent, even without a full understanding of the
275
+ // underlying problem (which might require significantly more time to address).
276
+ //
277
+ // This workaround uses internal Node.js APIs which are not guaranteed to be stable
278
+ // across major versions (i.e. _handle and its properties are all supposed to
279
+ // be internal). As of Node.js 16, it is still present, and it is unlikely to be
280
+ // removed without semver-major classification.
281
+ // If this does turn out to be a problem again in the future, I would recommend to
282
+ // investigate the issue more deeply on the libuv level, and creating a minimal
283
+ // reproduction using only the Node.js streams APIs first, and then basing a new
284
+ // workaround off of that and submitting the issue to the Node.js or libuv issue
285
+ // trackers.
286
+ // (The last state of debugging this inside libuv is captured in
287
+ // https://github.com/addaleax/node/commit/aef27e698da0dcb5c28d026324a33cb9383b222e,
288
+ // should that ever be needed again. On the mongosh side, this was tracked in
289
+ // https://jira.mongodb.org/browse/MONGOSH-998.)
290
+ const wasReadingAndNeedToWorkaroundWindowsBug =
291
+ process . platform === 'win32' &&
292
+ input . isTTY &&
293
+ input . _handle &&
294
+ input . _handle . reading &&
295
+ typeof input . _handle . readStop === 'function' &&
296
+ typeof input . _handle . readStart === 'function' ;
297
+ if ( wasReadingAndNeedToWorkaroundWindowsBug ) {
298
+ input . _handle . reading = false ;
299
+ input . _handle . readStop ( ) ;
300
+ }
301
+
302
+ try {
303
+ return fn ( ...args ) ;
304
+ } finally {
305
+ if ( wasReadingAndNeedToWorkaroundWindowsBug && ! input . _handle . reading ) {
306
+ input . _handle . reading = true ;
307
+ input . _handle . readStart ( ) ;
308
+ }
309
+ }
310
+ } ;
311
+ }
0 commit comments