Skip to content

Commit d48113d

Browse files
authored
fix(cli-repl): work around Windows libuv TTY bug MONGOSH-998 (#1147)
1 parent ff02731 commit d48113d

File tree

1 file changed

+78
-1
lines changed

1 file changed

+78
-1
lines changed

packages/cli-repl/src/async-repl.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ export function start(opts: AsyncREPLOptions): REPLServer {
7070
}
7171

7272
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))));
7477

7578
(repl as Mutable<typeof repl>).eval = async(
7679
input: string,
@@ -232,3 +235,77 @@ function wrapNoSyncDomainError<Args extends any[], Ret>(fn: (...args: Args) => R
232235
};
233236
}
234237

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

Comments
 (0)