##// END OF EJS Templates
logtoprocess: fix message formatting...
Boris Feld -
r40437:6bd477ee default
parent child Browse files
Show More
@@ -1,142 +1,140
1 1 # logtoprocess.py - send ui.log() data to a subprocess
2 2 #
3 3 # Copyright 2016 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """send ui.log() data to a subprocess (EXPERIMENTAL)
8 8
9 9 This extension lets you specify a shell command per ui.log() event,
10 10 sending all remaining arguments to as environment variables to that command.
11 11
12 12 Each positional argument to the method results in a `MSG[N]` key in the
13 13 environment, starting at 1 (so `MSG1`, `MSG2`, etc.). Each keyword argument
14 14 is set as a `OPT_UPPERCASE_KEY` variable (so the key is uppercased, and
15 15 prefixed with `OPT_`). The original event name is passed in the `EVENT`
16 16 environment variable, and the process ID of mercurial is given in `HGPID`.
17 17
18 18 So given a call `ui.log('foo', 'bar', 'baz', spam='eggs'), a script configured
19 19 for the `foo` event can expect an environment with `MSG1=bar`, `MSG2=baz`, and
20 20 `OPT_SPAM=eggs`.
21 21
22 22 Scripts are configured in the `[logtoprocess]` section, each key an event name.
23 23 For example::
24 24
25 25 [logtoprocess]
26 26 commandexception = echo "$MSG2$MSG3" > /var/log/mercurial_exceptions.log
27 27
28 28 would log the warning message and traceback of any failed command dispatch.
29 29
30 30 Scripts are run asynchronously as detached daemon processes; mercurial will
31 31 not ensure that they exit cleanly.
32 32
33 33 """
34 34
35 35 from __future__ import absolute_import
36 36
37 37 import itertools
38 38 import os
39 39 import subprocess
40 40 import sys
41 41
42 42 from mercurial import (
43 43 pycompat,
44 44 )
45 45
46 46 from mercurial.utils import (
47 47 procutil,
48 48 )
49 49
50 50 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
51 51 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
52 52 # be specifying the version(s) of Mercurial they are tested with, or
53 53 # leave the attribute unspecified.
54 54 testedwith = 'ships-with-hg-core'
55 55
56 56 def uisetup(ui):
57 57 if pycompat.iswindows:
58 58 # no fork on Windows, but we can create a detached process
59 59 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
60 60 # No stdlib constant exists for this value
61 61 DETACHED_PROCESS = 0x00000008
62 62 _creationflags = DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
63 63
64 64 def runshellcommand(script, env):
65 65 # we can't use close_fds *and* redirect stdin. I'm not sure that we
66 66 # need to because the detached process has no console connection.
67 67 subprocess.Popen(
68 68 procutil.tonativestr(script),
69 69 shell=True, env=procutil.tonativeenv(env), close_fds=True,
70 70 creationflags=_creationflags)
71 71 else:
72 72 def runshellcommand(script, env):
73 73 # double-fork to completely detach from the parent process
74 74 # based on http://code.activestate.com/recipes/278731
75 75 pid = os.fork()
76 76 if pid:
77 77 # parent
78 78 return
79 79 # subprocess.Popen() forks again, all we need to add is
80 80 # flag the new process as a new session.
81 81 if sys.version_info < (3, 2):
82 82 newsession = {'preexec_fn': os.setsid}
83 83 else:
84 84 newsession = {'start_new_session': True}
85 85 try:
86 86 # connect std* to devnull to make sure the subprocess can't
87 87 # muck up these stream for mercurial.
88 88 # Connect all the streams to be more close to Windows behavior
89 89 # and pager will wait for scripts to end if we don't do that
90 90 nullrfd = open(os.devnull, 'r')
91 91 nullwfd = open(os.devnull, 'w')
92 92 subprocess.Popen(
93 93 procutil.tonativestr(script),
94 94 shell=True, stdin=nullrfd,
95 95 stdout=nullwfd, stderr=nullwfd,
96 96 env=procutil.tonativeenv(env),
97 97 close_fds=True, **newsession)
98 98 finally:
99 99 # mission accomplished, this child needs to exit and not
100 100 # continue the hg process here.
101 101 os._exit(0)
102 102
103 103 class logtoprocessui(ui.__class__):
104 104 def log(self, event, *msg, **opts):
105 105 """Map log events to external commands
106 106
107 107 Arguments are passed on as environment variables.
108 108
109 109 """
110 110 script = self.config('logtoprocess', event)
111 111 if script:
112 112 if msg:
113 113 # try to format the log message given the remaining
114 114 # arguments
115 115 try:
116 # Python string formatting with % either uses a
117 # dictionary *or* tuple, but not both. If we have
118 # keyword options, assume we need a mapping.
119 formatted = msg[0] % (opts or msg[1:])
116 # Format the message as blackbox does
117 formatted = msg[0] % msg[1:]
120 118 except (TypeError, KeyError):
121 119 # Failed to apply the arguments, ignore
122 120 formatted = msg[0]
123 121 messages = (formatted,) + msg[1:]
124 122 else:
125 123 messages = msg
126 124 # positional arguments are listed as MSG[N] keys in the
127 125 # environment
128 126 msgpairs = (
129 127 ('MSG{0:d}'.format(i), str(m))
130 128 for i, m in enumerate(messages, 1))
131 129 # keyword arguments get prefixed with OPT_ and uppercased
132 130 optpairs = (
133 131 ('OPT_{0}'.format(key.upper()), str(value))
134 132 for key, value in opts.iteritems())
135 133 env = dict(itertools.chain(procutil.shellenviron().items(),
136 134 msgpairs, optpairs),
137 135 EVENT=event, HGPID=str(os.getpid()))
138 136 runshellcommand(script, env)
139 137 return super(logtoprocessui, self).log(event, *msg, **opts)
140 138
141 139 # Replace the class for this instance and all clones created from it:
142 140 ui.__class__ = logtoprocessui
@@ -1,124 +1,124
1 1 #require no-windows
2 2
3 3 ATTENTION: logtoprocess runs commands asynchronously. Be sure to append "| cat"
4 4 to hg commands, to wait for the output, if you want to test its output.
5 5 Otherwise the test will be flaky.
6 6
7 7 Test if logtoprocess correctly captures command-related log calls.
8 8
9 9 $ hg init
10 10 $ cat > $TESTTMP/foocommand.py << EOF
11 11 > from __future__ import absolute_import
12 12 > from mercurial import registrar
13 13 > cmdtable = {}
14 14 > command = registrar.command(cmdtable)
15 15 > configtable = {}
16 16 > configitem = registrar.configitem(configtable)
17 17 > configitem('logtoprocess', 'foo',
18 18 > default=None,
19 19 > )
20 20 > @command(b'foo', [])
21 21 > def foo(ui, repo):
22 > ui.log('foo', 'a message: %(bar)s\n', bar='spam')
22 > ui.log('foo', 'a message: %s\n', 'spam')
23 23 > EOF
24 24 $ cp $HGRCPATH $HGRCPATH.bak
25 25 $ cat >> $HGRCPATH << EOF
26 26 > [extensions]
27 27 > logtoprocess=
28 28 > foocommand=$TESTTMP/foocommand.py
29 29 > [logtoprocess]
30 30 > command=(echo 'logtoprocess command output:';
31 31 > echo "\$EVENT";
32 32 > echo "\$MSG1";
33 33 > echo "\$MSG2") > $TESTTMP/command.log
34 34 > commandfinish=(echo 'logtoprocess commandfinish output:';
35 35 > echo "\$EVENT";
36 36 > echo "\$MSG1";
37 37 > echo "\$MSG2";
38 38 > echo "\$MSG3") > $TESTTMP/commandfinish.log
39 39 > foo=(echo 'logtoprocess foo output:';
40 40 > echo "\$EVENT";
41 41 > echo "\$MSG1";
42 > echo "\$OPT_BAR") > $TESTTMP/foo.log
42 > echo "\$MSG2") > $TESTTMP/foo.log
43 43 > EOF
44 44
45 45 Running a command triggers both a ui.log('command') and a
46 46 ui.log('commandfinish') call. The foo command also uses ui.log.
47 47
48 48 Use sort to avoid ordering issues between the various processes we spawn:
49 49 $ hg foo
50 50 $ sleep 1
51 51 $ cat $TESTTMP/command.log | sort
52 52
53 53 command
54 54 foo
55 55 foo
56 56 logtoprocess command output:
57 57
58 58 #if no-chg
59 59 $ cat $TESTTMP/commandfinish.log | sort
60 60
61 61 0
62 62 commandfinish
63 63 foo
64 64 foo exited 0 after * seconds (glob)
65 65 logtoprocess commandfinish output:
66 66 $ cat $TESTTMP/foo.log | sort
67 67
68 68 a message: spam
69 69 foo
70 70 logtoprocess foo output:
71 71 spam
72 72 #endif
73 73
74 74 Confirm that logging blocked time catches stdio properly:
75 75 $ cp $HGRCPATH.bak $HGRCPATH
76 76 $ cat >> $HGRCPATH << EOF
77 77 > [extensions]
78 78 > logtoprocess=
79 79 > pager=
80 80 > [logtoprocess]
81 81 > uiblocked=echo "\$EVENT stdio \$OPT_STDIO_BLOCKED ms command \$OPT_COMMAND_DURATION ms" > $TESTTMP/uiblocked.log
82 82 > [ui]
83 83 > logblockedtimes=True
84 84 > EOF
85 85
86 86 $ hg log
87 87 $ sleep 1
88 88 $ cat $TESTTMP/uiblocked.log
89 89 uiblocked stdio [0-9]+.[0-9]* ms command [0-9]+.[0-9]* ms (re)
90 90
91 91 Try to confirm that pager wait on logtoprocess:
92 92
93 93 Add a script that wait on a file to appears for 5 seconds, if it sees it touch
94 94 another file or die after 5 seconds. If the scripts is awaited by hg, the
95 95 script will die after the timeout before we could touch the file and the
96 96 resulting file will not exists. If not, we will touch the file and see it.
97 97
98 98 $ cat > $TESTTMP/wait-output.sh << EOF
99 99 > #!/bin/sh
100 100 > for i in \`$TESTDIR/seq.py 50\`; do
101 101 > if [ -f "$TESTTMP/wait-for-touched" ];
102 102 > then
103 103 > touch "$TESTTMP/touched";
104 104 > break;
105 105 > else
106 106 > sleep 0.1;
107 107 > fi
108 108 > done
109 109 > EOF
110 110 $ chmod +x $TESTTMP/wait-output.sh
111 111
112 112 $ cat >> $HGRCPATH << EOF
113 113 > [extensions]
114 114 > logtoprocess=
115 115 > pager=
116 116 > [logtoprocess]
117 117 > commandfinish=$TESTTMP/wait-output.sh
118 118 > EOF
119 119 $ hg version -q --pager=always
120 120 Mercurial Distributed SCM (version *) (glob)
121 121 $ touch $TESTTMP/wait-for-touched
122 122 $ sleep 0.2
123 123 $ test -f $TESTTMP/touched && echo "SUCCESS Pager is not waiting on ltp" || echo "FAIL Pager is waiting on ltp"
124 124 SUCCESS Pager is not waiting on ltp
General Comments 0
You need to be logged in to leave comments. Login now