# HG changeset patch
# User Matt Harbison <
matt_h...@yahoo.com>
# Date 1655830250 14400
# Tue Jun 21 12:50:50 2022 -0400
# Node ID e14dede955c5c273cdc9066ee33a67f99049653c
# Parent 457aa21b45b2548a4092c25ac6eca67ff6cdc66b
# EXP-Topic stderr-redirect
windows: redirect stderr to %PROGRAMDATA% with py2exe
This fixes several problems. First, py2exe tries to write the default error log
to %PROGRAMFILES%, which UAC blocks when it is enabled, which causes the
stacktrace to be lost. %PROGRAMDATA% is where programs are supposed to write
their data.
Second, Mercurial assumes `.buffer` exists on stdio objects, but it doesn't on
the redirected stderr provided by py2exe. Third, the stderr object provided by
py2exe doesn't return the number of bytes written, which causes
`mercurial.utils.procutil.WriteAllWrapper.write()` to infinitely loop.
Therefore, stderr is replaced to write to a new log file, and stdout is
blackholed to avoid surprises with its write method not returning the number of
bytes written. (py2exe already blackholes stdout, but with a broken write
method.)
Note that the `write()` signature on the py2exe supplied stderr is a bit odd- it
has default parameters for the log filename and an `alert` parameter that is set
to `ctypes.windll.user32.MessageBoxW`. I can't see anywhere this is called with
these arguments, so I'm dropping them. Even though this code is mostly adapted
from the py2exe code[1], this doesn't seem to popup the messagebox on exit if
errors were written. It does (even on the py3 build of py2exe) if an error
occurs prior to the replacement. The new log file is definitely written to
though.
[1]
https://github.com/py2exe/py2exe/blob/af0e841bffcf9c64abce6204718fecad17a59506/py2exe/boot_common.py#L55
diff --git a/thg b/thg
--- a/thg
+++ b/thg
@@ -10,6 +10,7 @@
from __future__ import print_function
+import io
import os
import sys
@@ -24,6 +25,121 @@
# sys.stdin is invalid, should be None. Fixes svn, git subrepos
sys.stdin = None
+ # Similar to mercurial.utils.procutil.BadFile, but silently discards
+ # data on write().
+ class Blackhole(io.RawIOBase):
+ """Dummy file object to simulate /dev/null"""
+ def __init__(self):
+ self.buffer = self
+
+ def readinto(self, data):
+ raise IOError(errno.EBADF, 'Bad file descriptor')
+
+ def write(self, data):
+ return len(data)
+
+ # py2exe blackholes stdout with a custom class to prevent random
+ # exceptions when writing to it, but doesn't provide the ``.buffer``
+ # attribute that mercurial.utils.procutil wants. As of 0.11.1.0, it
+ # also doesn't return the number of bytes written, and therefore
+ # violates the interface contract. For details, see:
+ #
https://github.com/py2exe/py2exe/blob/af0e841bffcf9c64abce6204718fecad17a59506/py2exe/boot_common.py#L3
+ sys.stdout = Blackhole()
+
+ def open_log(fname):
+ import atexit, ctypes
+
+ try:
+ os.makedirs(os.path.dirname(fname), exist_ok=True)
+ return open(fname, 'ab')
+ except Exception as details:
+ atexit.register(
+ ctypes.windll.user32.MessageBoxW,
+ 0,
+ "The logfile '%s' could not be opened:\n %s" % (
+ fname, details,
+ ),
+ "Errors in %r" % os.path.basename(sys.executable),
+ 0
+ )
+ else:
+ atexit.register(
+ ctypes.windll.user32.MessageBoxW,
+ 0,
+ "See the logfile '%s' for details" % fname,
+ "Errors in %r" % os.path.basename(sys.executable),
+ 0
+ )
+
+ # Provide a .buffer backed by a log file for stderr to keep procutil
+ # stuff happy.
+ class RawStderr(object):
+ _file = None
+ _error = False
+
+ def __init__(self, log_name):
+ self.log_name = log_name
+
+ def write(self, data):
+ # The directory tree and log file creation is delayed until it
+ # is needed, as done by py2exe.
+ if self._file is None and not self._error:
+ self._file = open_log(self.log_name)
+ self._error = self._file is None
+
+ if self._file is not None:
+ written = self._file.write(data)
+ self._file.flush()
+ return written
+ else:
+ return len(data) # Data necessarily backholed
+
+ def flush(self):
+ if self._file is not None:
+ self._file.flush()
+
+ class Stderr(object):
+ def __init__(self, log_name):
+ self.buffer = RawStderr(log_name)
+
+ def write(self, text):
+ data = text.encode(sys.getdefaultencoding(),
+ errors='replace')
+ total = 0
+ while len(data) > total:
+ total += self.buffer.write(data[total:])
+
+ return len(text)
+
+ def flush(self):
+ self.buffer.flush()
+
+ # py2exe writes its log file next to the executable in %PROGRAMFILES%,
+ # but UAC blocks that if it is enabled. Since we have to fiddle with
+ # stderr.buffer to keep procutil happy, redirect everything to an
+ # always writeable area.
+ program_data = os.environ.get("PROGRAMDATA")
+
+ if program_data is not None:
+ log_name = os.path.join(
+ program_data,
+ "TortoiseHg",
+ os.path.splitext(
+ os.path.basename(sys.executable)
+ )[0] + '.log')
+
+ sys.stderr = Stderr(log_name)
+ else:
+ # py2exe messages come through sys.stderr; mercurial.ui.error()
+ # messages come through sys.stderr.buffer. Drop everything for the
+ # *w.exe executable if the log file cannot be created, in order to
+ # prevent various issues mentioned above.
+ sys.stderr = Blackhole()
+
+ del Stderr
+ del RawStderr
+ del Blackhole
+
# Make `pip install --user` packages visible, because py2exe doesn't
# process sitecustomize.py.
vi = sys.version_info