console: don't mix stdout/stderr with readline prompt
The idea is borrowed from [1]: hide and save prompt, user's input and cursor position before writing to stdout/stderr and return everything back afterwards. Not every stdout/stderr write is handled this way: only tarantool's logger (when it writes to stderr) and tarantool's print() Lua function performs the prompt hide/show actions. For example, `io.stdout:write(<...>)` Lua call or `write(STDOUT_FILENO, <...>)` C call may mix readline's prompt with actual output. However the logger and print() is likely enough for the vast majority of usages. The readline's interactive search state (usually invoked by Ctrl+R) is not covered by this patch. Sadly, I didn't find a way to properly save and restore readline's output in this case. Implementation details ---------------------- Several words about the allocation strategy. On the first glance it may look worthful to pre-allocate a buffer to store prompt and user's input data and reallocate it on demand. However rl_set_prompt() already performs free() plus malloc() at each call[^1], so avoid doing malloc() on our side would not change the picture much. Moreover, this code interacts with a human, which is on many orders of magnitude slower that a machine and will not notice a difference. So I decided to keep the code simpler. [^1]: Verified on readline 8.1 sources. However it worth to note that rl_replace_line() keeps the buffer and performs realloc() on demand. The code is organized to make say and print modules calling some callbacks without knowledge about its origin and dependency on the console module (or whatever else module would implement this interaction with readline). The downside here is that console needs to know all places to set the callbacks. OTOH, it offers explicit list of such callbacks in one place and, at whole, keep the relevant code together. We can redefine the print() function from every place in the code, but I prefer to make it as explicit as possible, so added the new internal print.lua module. We could redefine _G.print on demand instead of setting callbacks for a function assigned to _G.print once. The downside here is that if a user save/capture the old _G.print value, it'll use the raw print() directly instead of our replacement. Current implementation seems to be more safe. Alternatives considered ----------------------- I guess we can clear readline's prompt and user input manually and don't let readline know that something was changed (and restore the prompt/user input afterwards). It would save allocations and string copying, but likely would lean on readline internals too much and repeat some of its functionality. I considered this option as unstable and declined. We can redefine behavior for all writes to stdout and stderr. There are different ways to do so: 1. Redefine libc's write() with our own implementation, which will call the original libc's write()[^2]. It is defined as a weak symbol in libc (at least in glibc), so there is no problem to do so. 2. Use pipe(), dup() and dup2() to execute our own code at STDOUT_FILENO, STDERR_FILENO writes. [^2]: There is a good article about pitfalls on this road: [2]. It is about LD_PRELOAD, but I guess everything is very similar with wrapping libc's function from an executable. In my opinion, those options are dangerous, because they implicitly change behavior of a lot of code, which unlikely expects something of this kind. The second option (use pipe()) adds more user space/kernel space context switches, more copying and also would add possible implicit fiber yield at any `write(STD*_FILENO, <...>)` call -- unlikely all user's code is ready for that. Fixes #7169 [1]: https://metacpan.org/dist/AnyEvent-ReadLine-Gnu/source/Gnu.pm [2]: https://tbrindus.ca/correct-ld-preload-hooking-libc/ NO_DOC=this patch prevents mixing of output streams on a terminal and it is what a user actually expects; no reason to describe how bad would be his/her life without it
Loading
Please register or sign in to comment