Show More
@@ -0,0 +1,129 b'' | |||||
|
1 | # logtoprocess.py - send ui.log() data to a subprocess | |||
|
2 | # | |||
|
3 | # Copyright 2016 Facebook, Inc. | |||
|
4 | # | |||
|
5 | # This software may be used and distributed according to the terms of the | |||
|
6 | # GNU General Public License version 2 or any later version. | |||
|
7 | """Send ui.log() data to a subprocess (EXPERIMENTAL) | |||
|
8 | ||||
|
9 | This extension lets you specify a shell command per ui.log() event, | |||
|
10 | sending all remaining arguments to as environment variables to that command. | |||
|
11 | ||||
|
12 | Each positional argument to the method results in a `MSG[N]` key in the | |||
|
13 | environment, starting at 1 (so `MSG1`, `MSG2`, etc.). Each keyword argument | |||
|
14 | is set as a `OPT_UPPERCASE_KEY` variable (so the key is uppercased, and | |||
|
15 | prefixed with `OPT_`). The original event name is passed in the `EVENT` | |||
|
16 | environment variable, and the process ID of mercurial is given in `HGPID`. | |||
|
17 | ||||
|
18 | So given a call `ui.log('foo', 'bar', 'baz', spam='eggs'), a script configured | |||
|
19 | for the `foo` event can expect an environment with `MSG1=bar`, `MSG2=baz`, and | |||
|
20 | `OPT_SPAM=eggs`. | |||
|
21 | ||||
|
22 | Scripts are configured in the `[logtoprocess]` section, each key an event name. | |||
|
23 | For example:: | |||
|
24 | ||||
|
25 | [logtoprocess] | |||
|
26 | commandexception = echo "$MSG2$MSG3" > /var/log/mercurial_exceptions.log | |||
|
27 | ||||
|
28 | would log the warning message and traceback of any failed command dispatch. | |||
|
29 | ||||
|
30 | Scripts are run asychronously as detached daemon processes; mercurial will | |||
|
31 | not ensure that they exit cleanly. | |||
|
32 | ||||
|
33 | """ | |||
|
34 | ||||
|
35 | from __future__ import absolute_import | |||
|
36 | ||||
|
37 | import itertools | |||
|
38 | import os | |||
|
39 | import platform | |||
|
40 | import subprocess | |||
|
41 | import sys | |||
|
42 | ||||
|
43 | # Note for extension authors: ONLY specify testedwith = 'internal' for | |||
|
44 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | |||
|
45 | # be specifying the version(s) of Mercurial they are tested with, or | |||
|
46 | # leave the attribute unspecified. | |||
|
47 | testedwith = 'internal' | |||
|
48 | ||||
|
49 | def uisetup(ui): | |||
|
50 | if platform.system() == 'Windows': | |||
|
51 | # no fork on Windows, but we can create a detached process | |||
|
52 | # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx | |||
|
53 | # No stdlib constant exists for this value | |||
|
54 | DETACHED_PROCESS = 0x00000008 | |||
|
55 | _creationflags = DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP | |||
|
56 | ||||
|
57 | def runshellcommand(script, env): | |||
|
58 | # we can't use close_fds *and* redirect stdin. I'm not sure that we | |||
|
59 | # need to because the detached process has no console connection. | |||
|
60 | subprocess.Popen( | |||
|
61 | script, shell=True, env=env, close_fds=True, | |||
|
62 | creationflags=_creationflags) | |||
|
63 | else: | |||
|
64 | def runshellcommand(script, env): | |||
|
65 | # double-fork to completely detach from the parent process | |||
|
66 | # based on http://code.activestate.com/recipes/278731 | |||
|
67 | pid = os.fork() | |||
|
68 | if pid: | |||
|
69 | # parent | |||
|
70 | return | |||
|
71 | # subprocess.Popen() forks again, all we need to add is | |||
|
72 | # flag the new process as a new session. | |||
|
73 | if sys.version_info < (3, 2): | |||
|
74 | newsession = {'preexec_fn': os.setsid} | |||
|
75 | else: | |||
|
76 | newsession = {'start_new_session': True} | |||
|
77 | try: | |||
|
78 | # connect stdin to devnull to make sure the subprocess can't | |||
|
79 | # muck up that stream for mercurial. | |||
|
80 | subprocess.Popen( | |||
|
81 | script, shell=True, stdin=open(os.devnull, 'r'), env=env, | |||
|
82 | close_fds=True, **newsession) | |||
|
83 | finally: | |||
|
84 | # mission accomplished, this child needs to exit and not | |||
|
85 | # continue the hg process here. | |||
|
86 | os._exit(0) | |||
|
87 | ||||
|
88 | class logtoprocessui(ui.__class__): | |||
|
89 | def log(self, event, *msg, **opts): | |||
|
90 | """Map log events to external commands | |||
|
91 | ||||
|
92 | Arguments are passed on as environment variables. | |||
|
93 | ||||
|
94 | """ | |||
|
95 | script = ui.config('logtoprocess', event) | |||
|
96 | if script: | |||
|
97 | if msg: | |||
|
98 | # try to format the log message given the remaining | |||
|
99 | # arguments | |||
|
100 | try: | |||
|
101 | # Python string formatting with % either uses a | |||
|
102 | # dictionary *or* tuple, but not both. If we have | |||
|
103 | # keyword options, assume we need a mapping. | |||
|
104 | formatted = msg[0] % (opts or msg[1:]) | |||
|
105 | except (TypeError, KeyError): | |||
|
106 | # Failed to apply the arguments, ignore | |||
|
107 | formatted = msg[0] | |||
|
108 | messages = (formatted,) + msg[1:] | |||
|
109 | else: | |||
|
110 | messages = msg | |||
|
111 | # positional arguments are listed as MSG[N] keys in the | |||
|
112 | # environment | |||
|
113 | msgpairs = ( | |||
|
114 | ('MSG{0:d}'.format(i), str(m)) | |||
|
115 | for i, m in enumerate(messages, 1)) | |||
|
116 | # keyword arguments get prefixed with OPT_ and uppercased | |||
|
117 | optpairs = ( | |||
|
118 | ('OPT_{0}'.format(key.upper()), str(value)) | |||
|
119 | for key, value in opts.iteritems()) | |||
|
120 | env = dict(itertools.chain(os.environ.items(), | |||
|
121 | msgpairs, optpairs), | |||
|
122 | EVENT=event, HGPID=str(os.getpid())) | |||
|
123 | # Connect stdin to /dev/null to prevent child processes messing | |||
|
124 | # with mercurial's stdin. | |||
|
125 | runshellcommand(script, env) | |||
|
126 | return super(logtoprocessui, self).log(event, *msg, **opts) | |||
|
127 | ||||
|
128 | # Replace the class for this instance and all clones created from it: | |||
|
129 | ui.__class__ = logtoprocessui |
@@ -0,0 +1,54 b'' | |||||
|
1 | Test if logtoprocess correctly captures command-related log calls. | |||
|
2 | ||||
|
3 | $ hg init | |||
|
4 | $ cat > $TESTTMP/foocommand.py << EOF | |||
|
5 | > from mercurial import cmdutil | |||
|
6 | > from time import sleep | |||
|
7 | > cmdtable = {} | |||
|
8 | > command = cmdutil.command(cmdtable) | |||
|
9 | > @command('foo', []) | |||
|
10 | > def foo(ui, repo): | |||
|
11 | > ui.log('foo', 'a message: %(bar)s\n', bar='spam') | |||
|
12 | > EOF | |||
|
13 | $ cat >> $HGRCPATH << EOF | |||
|
14 | > [extensions] | |||
|
15 | > logtoprocess= | |||
|
16 | > foocommand=$TESTTMP/foocommand.py | |||
|
17 | > [logtoprocess] | |||
|
18 | > command=echo 'logtoprocess command output:'; | |||
|
19 | > echo "\$EVENT"; | |||
|
20 | > echo "\$MSG1"; | |||
|
21 | > echo "\$MSG2" | |||
|
22 | > commandfinish=echo 'logtoprocess commandfinish output:'; | |||
|
23 | > echo "\$EVENT"; | |||
|
24 | > echo "\$MSG1"; | |||
|
25 | > echo "\$MSG2"; | |||
|
26 | > echo "\$MSG3" | |||
|
27 | > foo=echo 'logtoprocess foo output:'; | |||
|
28 | > echo "\$EVENT"; | |||
|
29 | > echo "\$MSG1"; | |||
|
30 | > echo "\$OPT_BAR" | |||
|
31 | > EOF | |||
|
32 | ||||
|
33 | Running a command triggers both a ui.log('command') and a | |||
|
34 | ui.log('commandfinish') call. The foo command also uses ui.log. | |||
|
35 | ||||
|
36 | Use head to ensure we wait for all lines to be produced, and sort to avoid | |||
|
37 | ordering issues between the various processes we spawn: | |||
|
38 | $ hg foo | head -n 17 | sort | |||
|
39 | ||||
|
40 | ||||
|
41 | ||||
|
42 | 0 | |||
|
43 | a message: spam | |||
|
44 | command | |||
|
45 | commandfinish | |||
|
46 | foo | |||
|
47 | foo | |||
|
48 | foo | |||
|
49 | foo | |||
|
50 | foo exited 0 after * seconds (glob) | |||
|
51 | logtoprocess command output: | |||
|
52 | logtoprocess commandfinish output: | |||
|
53 | logtoprocess foo output: | |||
|
54 | spam |
General Comments 0
You need to be logged in to leave comments.
Login now