diff -r b65966f50058 -r a368da441b32 hgext/logtoprocess.py --- /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