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