Bug 6207 - output after quitting pager on python3.7 with chg
Summary: output after quitting pager on python3.7 with chg
Status: RESOLVED FIXED
Alias: None
Product: Mercurial
Classification: Unclassified
Component: python3 (show other bugs)
Version: 5.2rc0
Hardware: PC Linux
: wish bug
Assignee: Bugzilla
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2019-10-23 18:21 UTC by Kim "Zash" Alvefur
Modified: 2020-07-29 00:01 UTC (History)
6 users (show)

See Also:
Python Version: 3.7


Attachments
trigger bug even in the non-chg case (496 bytes, patch)
2020-07-18 22:34 UTC, Manuel Jacob
Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Kim "Zash" Alvefur 2019-10-23 18:21 UTC
To reproduce:

$ hg log # ( or anything that invokes the pager )
observe output and pager prompt
quit pager
observe output

Python2 behavior:
after quitting pager, no additional output is printed, shell prompt replaces pager prompt

Python3 behavior:
after quitting pager, some amount of additional output is shown, causing output shown prior to quitting pager to move up.
The extra output does not appear to be the lines that would have directly followed the previously shown lines, but seems to have jumped a bit.

Example:

(in a clone of mercurial itself, terminal window about 23 lines high)

$ hg log <enter>
Terminal shows:
===============================================================================
changeset:   43316:4565a0afc289
branch:      stable
bookmark:    @
tag:         tip
user:        Gregory Szorc <gregory.szorc@gmail.com>
date:        Mon Oct 21 19:53:30 2019 -0700
summary:     packaging: upgrade packages distributed with Windows installers

changeset:   43315:388ada1c07c6
branch:      stable
user:        Gregory Szorc <gregory.szorc@gmail.com>
date:        Mon Oct 21 19:28:23 2019 -0700
summary:     automation: install Python 2.7.17, 3.7.5, and PyPy 7.2.0

changeset:   43314:8676a8b2d20a
branch:      stable
user:        Gregory Szorc <gregory.szorc@gmail.com>
date:        Mon Oct 21 19:25:06 2019 -0700
summary:     contrib: install Python 2.7.17 and 3.7.5 in Windows environment

changeset:   43313:a882c088dc2e
branch:      stable
user:        Denis Laxalde <denis@laxalde.org>
:
===============================================================================

After pressing 'q':

===============================================================================
user:        Denis Laxalde <denis@laxalde.org>
changeset:   43039:7902001aaf41
user:        Pierre-Yves David <pierre-yves.david@octobus.net>
date:        Fri Sep 27 16:40:07 2019 +0200
summary:     sidedata: make sure we don't use the flag if there are not sidedata

changeset:   43038:7bb5a2465501
user:        Pierre-Yves David <pierre-yves.david@octobus.net>
date:        Tue Sep 03 23:45:38 2019 +0200
summary:     revlog: add the appropriate flag is sidedata are passed to `addrevision`

changeset:   43037:142deb539ccf
user:        Pierre-Yves David <pierre-yves.david@octobus.net>
date:        Wed Sep 04 03:20:55 2019 +0200
summary:     sidedata: register the flag processors if the repository allows for it

changeset:   43036:e8bc4c3d9a0b
user:        Pierre-Yves David <pierre-yves.david@octobus.net>
date:        Wed Sep 04 03:19:58 2019 +0200
summary:     sidedata: add a function for _processflagsraw usage

hg$ 
===============================================================================

$ hg debuginstall
checking encoding (UTF-8)...
checking Python executable (/usr/bin/python3)
checking Python version (3.7.3)
checking Python lib (/usr/lib/python3.7)...
checking Python security support (sni,tls1.0,tls1.1,tls1.2)
checking Mercurial version (5.2rc0)
checking Mercurial custom build (10-4565a0afc289)
checking module policy (c)
checking installed modules (/usr/lib/python3/dist-packages/mercurial)...
checking registered compression engines (bz2, bz2truncated, none, zlib, zstd)
checking available compression engines (bz2, bz2truncated, none, zlib, zstd)
checking available compression engines for wire protocol (zstd, zlib, bz2, none)
checking "re2" regexp engine (missing)
checking templates (/usr/lib/python3/dist-packages/mercurial/templates)...
checking default template (/usr/lib/python3/dist-packages/mercurial/templates/map-cmdline.default)
checking commit editor... (sensible-editor)
checking username...
 no username supplied
 (specify a username in your configuration file) (actually ignore, semi-clean environment to ensure my config didn't interfere)
1 problems detected, please check your install!
Comment 1 Denis Laxalde 2019-10-24 04:42 UTC
I cannot reproduce. My "hg debuginstall" is the same. Do you have any special pager configuration?
Comment 2 Kim "Zash" Alvefur 2019-10-24 10:57 UTC
(In reply to Denis Laxalde from comment #1)

My normal pager config is:
~$ hg config pager
pager.pager=less -FRX

I did this in /tmp with HOME=$PWD, any pager configuration would be the defaults.

hg$ hg config pager
pager.pager=sensible-pager

~$ env|grep LESS
LESS=FXR
LESSCLOSE=/usr/bin/lesspipe %s %s
LESSOPEN=| /usr/bin/lesspipe %s

Still happens if I unset those variables.

Terminal is xfce4-terminal on Debian 10.
Happens with st (stterm) too.
Not sure what else might affect this. The main change was in Python version.
Comment 3 Ian Moody [:Kwan] 2019-10-24 12:30 UTC
Can reproduce on Ubuntu 18.04 with Python 3.6.

$ hg debuginstall
checking encoding (UTF-8)...
checking Python executable (/home/ian/hgpy3/bin/python3)
checking Python version (3.6.8)
checking Python lib (/home/ian/hgpy3/lib/python3.6)...
checking Python security support (sni,tls1.0,tls1.1,tls1.2)
checking Mercurial version (5.2rc0)
checking Mercurial custom build ()
checking module policy (c)
checking installed modules (/home/ian/hgpy3/lib/python3.6/site-packages/mercurial)...
checking registered compression engines (bz2, bz2truncated, none, zlib, zstd)
checking available compression engines (bz2, bz2truncated, none, zlib, zstd)
checking available compression engines for wire protocol (zstd, zlib, bz2, none)
checking "re2" regexp engine (missing)
checking templates (/home/ian/hgpy3/lib/python3.6/site-packages/mercurial/templates)...
checking default template (/home/ian/hgpy3/lib/python3.6/site-packages/mercurial/templates/map-cmdline.default)
checking commit editor... (subl)
checking username (Ian Moody <[redacted]>)
no problems detected

$ hg config pager
pager.pager=less -EFRSXQ
Comment 4 Kim "Zash" Alvefur 2019-10-24 19:10 UTC
This is with chg (I had forgotten my hg=chg alias)

It behaves as expected when running the real hg.
Comment 5 Denis Laxalde 2019-10-25 03:08 UTC
Ok, confirmed. I can reproduce with chg.
Comment 6 Bugzilla 2020-04-17 00:02 UTC
Bug was inactive for 150 days, archiving
Comment 7 Manuel Jacob 2020-07-18 22:33 UTC
The bug still exists. I’m currently writing a detailed bug analysis. A little spoiler: The bug happens even in the non-chg case, but it’s masked by another bug. With the attached diff bug-non-chg.diff, you can observe a similar error without chg.
Comment 8 Manuel Jacob 2020-07-18 22:34 UTC
Created attachment 2081 [details]
trigger bug even in the non-chg case
Comment 9 Manuel Jacob 2020-07-19 20:35 UTC
Mercurial writes it output to stream objects (Python 2’s `file` objects, wrapping libc’s FILE objects, and Python 3’s own implementation in the `io` module). When a pager is started, the FDs underlying these stream objects are redirected (using dup2()) to the writing end of a pipe whose reading side is the pager’s stdin. There are three levels on which buffering happens. First, the (user-space) stream objects are buffered (usually line-buffered), the pipes are buffered in the kernel, and the pager might do some buffering. If the pipe gets full (because the pager doesn’t read enough data), the write() system call blocks.

When the pager exits, the pipe breaks, causing a SIGPIPE signal and that the write() system call exits. The (kernel-space) pipe gets abandoned. What happens with the buffered bytes in the stream objects is not well-defined. Glibc’s stream objects seem to discard them when encountering a broken pipe. Python 3’s stream objects don’t discard them. Everytime the flush() method gets called, it will try to write the buffered bytes to the underlying FD.

By accident, the user-visible bug doesn’t happen in the non-chg case. The finally clause in `mercurial.dispatch.dispatch()` calls `req.ui.flush()`. On Python 3, this will try to write to the broken pipe again. An IOError with EPIPE is explicitly silenced in `mercurial.ui.flush()`. However, before the pager was started, the SIGPIPE signal was changed to raise `mercurial.error.SignalInterrupt`, which is not silenced. Therefore the exception bubbles up, causing `dispatch()` to not run the exit handlers. How we should handle this sitation is maybe dependent on how we decide to solve the user-visible bug, so I suggest to postpone this question.

The `killpager()` function in `mercurial.ui` is such an exit handler and will redirect the stdout and stderr FDs back to the original files. Chg handles this otherwise (therefore it’s not affected by the bug described in the previous paragraph). When redirecting the underlying FD to a different file than the broken pipe, both Glibc and Python 3 continue to work.

An indeterminate number of bytes was written by Mercurial at this point (depending on how much the pager read, how much the kernel buffered, and how much the stream objects buffered). The bytes written to the pipe get lost in any case. On Python 2, more bytes (submitted to the libc, but not written to the pipe) could get lost. Therefore, there’s a high chance that some bytes are lost. When continuing to write, there would be a hole of indeterminate size in between. In the case of ui.fout, I think it’s unacceptable if bytes in the middle of the stream get lost, as it harms the integrity of the output. We should therefore close ui.fout when encountering a broken pipe. For ui.ferr, it’s not as clear. Incomplete error messages could be misleading, but overall it’s probably better to not drop error messages.

Yuya, I’d appreciate your opinion on this.
Comment 10 Yuya Nishihara 2020-07-20 07:29 UTC
As far as I can see, the data is flushed to the stdout of the server process,
not to the client. So the simplest fix would be to discard the buffered data
before restoring the original fd.

> close ui.fout when encountering a broken pipe.

Closing stdout wouldn't be nice for chg, which heavly depends on os.dup2().

> overall it’s probably better to not drop error messages.

Maybe, but in order to make both plain hg and chg behave exactly the same,
we'll need some more dup2() work. chg doesn't backup the client's stdout/err fd.
I think it's fine to discard both stdout/err contents on EPIPE.

diff --git a/mercurial/chgserver.py b/mercurial/chgserver.py
--- a/mercurial/chgserver.py
+++ b/mercurial/chgserver.py
@@ -434,8 +434,9 @@ class chgcmdserver(commandserver.server)
             self._oldios.append((ch, fp, fd))
 
     def _restoreio(self):
+        nullfd = os.open(os.devnull, os.O_WRONLY)
         ui = self.ui
-        for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
+        for (ch, fp, fd), (cn, fn, mode) in zip(self._oldios, _iochannels):
             newfp = getattr(ui, fn)
             # close newfp while it's associated with client; otherwise it
             # would be closed when newfp is deleted
@@ -443,6 +444,12 @@ class chgcmdserver(commandserver.server)
                 newfp.close()
             # restore original fd: fp is open again
             try:
+                if newfp is fp and b'w' in mode:
+                    # Discard buffered data which couldn't be flushed because
+                    # of EPIPE. The data should belong to the current session
+                    # and should never persist.
+                    os.dup2(nullfd, fp.fileno())
+                    fp.flush()
                 os.dup2(fd, fp.fileno())
             except OSError as err:
                 # According to issue6330, running chg on heavy loaded systems
@@ -459,6 +466,7 @@ class chgcmdserver(commandserver.server)
             os.close(fd)
             setattr(self, cn, ch)
             setattr(ui, fn, fp)
+        os.close(nullfd)
         del self._oldios[:]
 
     def validate(self):
Comment 11 Yuya Nishihara 2020-07-21 08:36 UTC
> the data is flushed to the stdout of the server process,

This appears wrong. It is the stdout when "runcommand" request is issued,
which I think is the client stdout.
Comment 12 HG Bot 2020-07-21 12:50 UTC
Fixed by https://mercurial-scm.org/repo/hg/rev/a17454a189d1
Yuya Nishihara <yuya@tcha.org>
chgserver: discard buffered output before restoring fds (issue6207)

On Python 3, flush() appears not discarding buffered data on EPIPE, and
the buffered data will be carried over to the restored stdout.

(please test the fix)
Comment 13 Bugzilla 2020-07-29 00:01 UTC
Bug was set to TESTING for 7 days, resolving