# HG changeset patch # User Gregory Szorc # Date 1502741560 25200 # Node ID 2debf1e3cfa420800e960680f7713ec52ef9d44c # Parent cf0736696be05c29f70f9c7738d7eae7a4339af9 tests: test behavior of IOError during transactions (issue5658) ui._write(), ui._write_err(), and ui.flush() all trap IOError and re-raise as error.StdioError. If a caller doesn't catch StdioError when writing to stdio, it could bubble all the way to dispatch. This commit adds tests for I/O failures around various transaction operations. The most notable badness is during abort. Here, an uncaught StdioError will result in incomplete transaction rollback, requiring an `hg rollback` to recover. This can result in a client "corrupting" a remote repo via terminated HTTP and SSH socket. diff -r cf0736696be0 -r 2debf1e3cfa4 tests/test-rollback.t --- a/tests/test-rollback.t Wed Aug 16 10:24:49 2017 -0500 +++ b/tests/test-rollback.t Mon Aug 14 13:12:40 2017 -0700 @@ -210,3 +210,276 @@ abort: rollback is disabled because it is unsafe (see `hg help -v rollback` for information) [255] + + $ cd .. + +I/O errors on stdio are handled properly (issue5658) + + $ cat > badui.py << EOF + > import errno + > from mercurial.i18n import _ + > from mercurial import ( + > error, + > ui as uimod, + > ) + > + > def pretxncommit(ui, repo, **kwargs): + > ui.warn('warn during pretxncommit\n') + > + > def pretxnclose(ui, repo, **kwargs): + > ui.warn('warn during pretxnclose\n') + > + > def txnclose(ui, repo, **kwargs): + > ui.warn('warn during txnclose\n') + > + > def txnabort(ui, repo, **kwargs): + > ui.warn('warn during abort\n') + > + > class fdproxy(object): + > def __init__(self, ui, o): + > self._ui = ui + > self._o = o + > + > def __getattr__(self, attr): + > return getattr(self._o, attr) + > + > def write(self, msg): + > errors = set(self._ui.configlist('ui', 'ioerrors', [])) + > pretxncommit = msg == 'warn during pretxncommit\n' + > pretxnclose = msg == 'warn during pretxnclose\n' + > txnclose = msg == 'warn during txnclose\n' + > txnabort = msg == 'warn during abort\n' + > msgabort = msg == _('transaction abort!\n') + > msgrollback = msg == _('rollback completed\n') + > + > if pretxncommit and 'pretxncommit' in errors: + > raise IOError(errno.EPIPE, 'simulated epipe') + > if pretxnclose and 'pretxnclose' in errors: + > raise IOError(errno.EIO, 'simulated eio') + > if txnclose and 'txnclose' in errors: + > raise IOError(errno.EBADF, 'simulated badf') + > if txnabort and 'txnabort' in errors: + > raise IOError(errno.EPIPE, 'simulated epipe') + > if msgabort and 'msgabort' in errors: + > raise IOError(errno.EBADF, 'simulated ebadf') + > if msgrollback and 'msgrollback' in errors: + > raise IOError(errno.EIO, 'simulated eio') + > + > return self._o.write(msg) + > + > def uisetup(ui): + > class badui(ui.__class__): + > def write_err(self, *args, **kwargs): + > olderr = self.ferr + > try: + > self.ferr = fdproxy(self, olderr) + > return super(badui, self).write_err(*args, **kwargs) + > finally: + > self.ferr = olderr + > + > ui.__class__ = badui + > + > def reposetup(ui, repo): + > ui.setconfig('hooks', 'pretxnclose.badui', pretxnclose, 'badui') + > ui.setconfig('hooks', 'txnclose.badui', txnclose, 'badui') + > ui.setconfig('hooks', 'pretxncommit.badui', pretxncommit, 'badui') + > ui.setconfig('hooks', 'txnabort.badui', txnabort, 'badui') + > EOF + + $ cat >> $HGRCPATH << EOF + > [extensions] + > badui = $TESTTMP/badui.py + > EOF + +An I/O error during pretxncommit is handled + + $ hg init ioerror-pretxncommit + $ cd ioerror-pretxncommit + $ echo 0 > foo + $ hg -q commit -A -m initial + warn during pretxncommit + warn during pretxnclose + warn during txnclose + $ echo 1 > foo + $ hg --config ui.ioerrors=pretxncommit commit -m 'error during pretxncommit' + error: pretxncommit.badui hook raised an exception: [Errno *] simulated epipe (glob) + transaction abort! + warn during abort + rollback completed + [255] + + $ hg commit -m 'commit 1' + warn during pretxncommit + warn during pretxnclose + warn during txnclose + + $ cd .. + +An I/O error during pretxnclose is handled + + $ hg init ioerror-pretxnclose + $ cd ioerror-pretxnclose + $ echo 0 > foo + $ hg -q commit -A -m initial + warn during pretxncommit + warn during pretxnclose + warn during txnclose + + $ echo 1 > foo + $ hg --config ui.ioerrors=pretxnclose commit -m 'error during pretxnclose' + warn during pretxncommit + error: pretxnclose.badui hook raised an exception: [Errno *] simulated eio (glob) + transaction abort! + warn during abort + rollback completed + abort: simulated eio + [255] + + $ hg commit -m 'commit 1' + warn during pretxncommit + warn during pretxnclose + warn during txnclose + + $ cd .. + +An I/O error during txnclose is handled + + $ hg init ioerror-txnclose + $ cd ioerror-txnclose + $ echo 0 > foo + $ hg -q commit -A -m initial + warn during pretxncommit + warn during pretxnclose + warn during txnclose + + $ echo 1 > foo + $ hg --config ui.ioerrors=txnclose commit -m 'error during txnclose' + warn during pretxncommit + warn during pretxnclose + error: txnclose.badui hook raised an exception: [Errno *] simulated badf (glob) + (run with --traceback for stack trace) + + $ hg commit -m 'commit 1' + nothing changed + [1] + + $ cd .. + +An I/O error writing "transaction abort" is handled + + $ hg init ioerror-msgabort + $ cd ioerror-msgabort + + $ echo 0 > foo + $ hg -q commit -A -m initial + warn during pretxncommit + warn during pretxnclose + warn during txnclose + + $ echo 1 > foo + $ hg --config ui.ioerrors=msgabort --config hooks.pretxncommit=false commit -m 'error during abort message' + abort: simulated ebadf + *: DeprecationWarning: use lock.release instead of del lock (glob) + return -1 + [255] + + $ hg commit -m 'commit 1' + abort: abandoned transaction found! + (run 'hg recover' to clean up transaction) + [255] + + $ cd .. + +An I/O error during txnabort should still result in rollback + + $ hg init ioerror-txnabort + $ cd ioerror-txnabort + + $ echo 0 > foo + $ hg -q commit -A -m initial + warn during pretxncommit + warn during pretxnclose + warn during txnclose + + $ echo 1 > foo + $ hg --config ui.ioerrors=txnabort --config hooks.pretxncommit=false commit -m 'error during abort' + transaction abort! + error: txnabort.badui hook raised an exception: [Errno *] simulated epipe (glob) + (run with --traceback for stack trace) + rollback completed + abort: pretxncommit hook exited with status 1 + [255] + + $ hg commit -m 'commit 1' + warn during pretxncommit + warn during pretxnclose + warn during txnclose + + $ cd .. + +An I/O error writing "rollback completed" is handled + + $ hg init ioerror-msgrollback + $ cd ioerror-msgrollback + + $ echo 0 > foo + $ hg -q commit -A -m initial + warn during pretxncommit + warn during pretxnclose + warn during txnclose + + $ echo 1 > foo + + $ hg --config ui.ioerrors=msgrollback --config hooks.pretxncommit=false commit -m 'error during rollback message' + transaction abort! + warn during abort + rollback failed - please run hg recover + abort: pretxncommit hook exited with status 1 + [255] + + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 1 files, 1 changesets, 1 total revisions + + $ cd .. + +Multiple I/O errors after transaction open are handled. +This is effectively what happens if a peer disconnects in the middle +of a transaction. + + $ hg init ioerror-multiple + $ cd ioerror-multiple + $ echo 0 > foo + $ hg -q commit -A -m initial + warn during pretxncommit + warn during pretxnclose + warn during txnclose + + $ echo 1 > foo + + $ hg --config ui.ioerrors=pretxncommit,pretxnclose,txnclose,txnabort,msgabort,msgrollback commit -m 'multiple errors' + error: pretxncommit.badui hook raised an exception: [Errno *] simulated epipe (glob) + abort: simulated ebadf + *: DeprecationWarning: use lock.release instead of del lock (glob) + return -1 + [255] + + $ hg verify + abandoned transaction found - run hg recover + checking changesets + checking manifests + manifest@?: rev 1 points to nonexistent changeset 1 + manifest@?: 94e0ee43dbfe not in changesets + crosschecking files in changesets and manifests + checking files + foo@?: rev 1 points to nonexistent changeset 1 + (expected 0) + 1 files, 1 changesets, 2 total revisions + 1 warnings encountered! + 3 integrity errors encountered! + [1] + + $ cd ..