--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/logtoprocess.py Tue Apr 05 17:39:59 2016 +0100
@@ -0,0 +1,129 @@
+# logtoprocess.py - send ui.log() data to a subprocess
+#
+# Copyright 2016 Facebook, Inc.
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+"""Send ui.log() data to a subprocess (EXPERIMENTAL)
+
+This extension lets you specify a shell command per ui.log() event,
+sending all remaining arguments to as environment variables to that command.
+
+Each positional argument to the method results in a `MSG[N]` key in the
+environment, starting at 1 (so `MSG1`, `MSG2`, etc.). Each keyword argument
+is set as a `OPT_UPPERCASE_KEY` variable (so the key is uppercased, and
+prefixed with `OPT_`). The original event name is passed in the `EVENT`
+environment variable, and the process ID of mercurial is given in `HGPID`.
+
+So given a call `ui.log('foo', 'bar', 'baz', spam='eggs'), a script configured
+for the `foo` event can expect an environment with `MSG1=bar`, `MSG2=baz`, and
+`OPT_SPAM=eggs`.
+
+Scripts are configured in the `[logtoprocess]` section, each key an event name.
+For example::
+
+ [logtoprocess]
+ commandexception = echo "$MSG2$MSG3" > /var/log/mercurial_exceptions.log
+
+would log the warning message and traceback of any failed command dispatch.
+
+Scripts are run asychronously as detached daemon processes; mercurial will
+not ensure that they exit cleanly.
+
+"""
+
+from __future__ import absolute_import
+
+import itertools
+import os
+import platform
+import subprocess
+import sys
+
+# Note for extension authors: ONLY specify testedwith = 'internal' for
+# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
+# be specifying the version(s) of Mercurial they are tested with, or
+# leave the attribute unspecified.
+testedwith = 'internal'
+
+def uisetup(ui):
+ if platform.system() == 'Windows':
+ # no fork on Windows, but we can create a detached process
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
+ # No stdlib constant exists for this value
+ DETACHED_PROCESS = 0x00000008
+ _creationflags = DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
+
+ def runshellcommand(script, env):
+ # we can't use close_fds *and* redirect stdin. I'm not sure that we
+ # need to because the detached process has no console connection.
+ subprocess.Popen(
+ script, shell=True, env=env, close_fds=True,
+ creationflags=_creationflags)
+ else:
+ def runshellcommand(script, env):
+ # double-fork to completely detach from the parent process
+ # based on http://code.activestate.com/recipes/278731
+ pid = os.fork()
+ if pid:
+ # parent
+ return
+ # subprocess.Popen() forks again, all we need to add is
+ # flag the new process as a new session.
+ if sys.version_info < (3, 2):
+ newsession = {'preexec_fn': os.setsid}
+ else:
+ newsession = {'start_new_session': True}
+ try:
+ # connect stdin to devnull to make sure the subprocess can't
+ # muck up that stream for mercurial.
+ subprocess.Popen(
+ script, shell=True, stdin=open(os.devnull, 'r'), env=env,
+ close_fds=True, **newsession)
+ finally:
+ # mission accomplished, this child needs to exit and not
+ # continue the hg process here.
+ os._exit(0)
+
+ class logtoprocessui(ui.__class__):
+ def log(self, event, *msg, **opts):
+ """Map log events to external commands
+
+ Arguments are passed on as environment variables.
+
+ """
+ script = ui.config('logtoprocess', event)
+ if script:
+ if msg:
+ # try to format the log message given the remaining
+ # arguments
+ try:
+ # Python string formatting with % either uses a
+ # dictionary *or* tuple, but not both. If we have
+ # keyword options, assume we need a mapping.
+ formatted = msg[0] % (opts or msg[1:])
+ except (TypeError, KeyError):
+ # Failed to apply the arguments, ignore
+ formatted = msg[0]
+ messages = (formatted,) + msg[1:]
+ else:
+ messages = msg
+ # positional arguments are listed as MSG[N] keys in the
+ # environment
+ msgpairs = (
+ ('MSG{0:d}'.format(i), str(m))
+ for i, m in enumerate(messages, 1))
+ # keyword arguments get prefixed with OPT_ and uppercased
+ optpairs = (
+ ('OPT_{0}'.format(key.upper()), str(value))
+ for key, value in opts.iteritems())
+ env = dict(itertools.chain(os.environ.items(),
+ msgpairs, optpairs),
+ EVENT=event, HGPID=str(os.getpid()))
+ # Connect stdin to /dev/null to prevent child processes messing
+ # with mercurial's stdin.
+ runshellcommand(script, env)
+ return super(logtoprocessui, self).log(event, *msg, **opts)
+
+ # Replace the class for this instance and all clones created from it:
+ ui.__class__ = logtoprocessui