##// END OF EJS Templates
logtoprocess: do not use platform.system()...
Jun Wu -
r34641:68ed3b4f default
parent child Browse files
Show More
@@ -1,131 +1,133 b''
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 platform
40 import subprocess
39 import subprocess
41 import sys
40 import sys
42
41
43 from mercurial import encoding
42 from mercurial import (
43 encoding,
44 pycompat,
45 )
44
46
45 # 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
46 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
48 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
47 # 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
48 # leave the attribute unspecified.
50 # leave the attribute unspecified.
49 testedwith = 'ships-with-hg-core'
51 testedwith = 'ships-with-hg-core'
50
52
51 def uisetup(ui):
53 def uisetup(ui):
52 if platform.system() == 'Windows':
54 if pycompat.osname == 'nt':
53 # no fork on Windows, but we can create a detached process
55 # no fork on Windows, but we can create a detached process
54 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
56 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
55 # No stdlib constant exists for this value
57 # No stdlib constant exists for this value
56 DETACHED_PROCESS = 0x00000008
58 DETACHED_PROCESS = 0x00000008
57 _creationflags = DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
59 _creationflags = DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
58
60
59 def runshellcommand(script, env):
61 def runshellcommand(script, env):
60 # 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
61 # need to because the detached process has no console connection.
63 # need to because the detached process has no console connection.
62 subprocess.Popen(
64 subprocess.Popen(
63 script, shell=True, env=env, close_fds=True,
65 script, shell=True, env=env, close_fds=True,
64 creationflags=_creationflags)
66 creationflags=_creationflags)
65 else:
67 else:
66 def runshellcommand(script, env):
68 def runshellcommand(script, env):
67 # double-fork to completely detach from the parent process
69 # double-fork to completely detach from the parent process
68 # based on http://code.activestate.com/recipes/278731
70 # based on http://code.activestate.com/recipes/278731
69 pid = os.fork()
71 pid = os.fork()
70 if pid:
72 if pid:
71 # parent
73 # parent
72 return
74 return
73 # subprocess.Popen() forks again, all we need to add is
75 # subprocess.Popen() forks again, all we need to add is
74 # flag the new process as a new session.
76 # flag the new process as a new session.
75 if sys.version_info < (3, 2):
77 if sys.version_info < (3, 2):
76 newsession = {'preexec_fn': os.setsid}
78 newsession = {'preexec_fn': os.setsid}
77 else:
79 else:
78 newsession = {'start_new_session': True}
80 newsession = {'start_new_session': True}
79 try:
81 try:
80 # connect stdin to devnull to make sure the subprocess can't
82 # connect stdin to devnull to make sure the subprocess can't
81 # muck up that stream for mercurial.
83 # muck up that stream for mercurial.
82 subprocess.Popen(
84 subprocess.Popen(
83 script, shell=True, stdin=open(os.devnull, 'r'), env=env,
85 script, shell=True, stdin=open(os.devnull, 'r'), env=env,
84 close_fds=True, **newsession)
86 close_fds=True, **newsession)
85 finally:
87 finally:
86 # mission accomplished, this child needs to exit and not
88 # mission accomplished, this child needs to exit and not
87 # continue the hg process here.
89 # continue the hg process here.
88 os._exit(0)
90 os._exit(0)
89
91
90 class logtoprocessui(ui.__class__):
92 class logtoprocessui(ui.__class__):
91 def log(self, event, *msg, **opts):
93 def log(self, event, *msg, **opts):
92 """Map log events to external commands
94 """Map log events to external commands
93
95
94 Arguments are passed on as environment variables.
96 Arguments are passed on as environment variables.
95
97
96 """
98 """
97 script = self.config('logtoprocess', event)
99 script = self.config('logtoprocess', event)
98 if script:
100 if script:
99 if msg:
101 if msg:
100 # try to format the log message given the remaining
102 # try to format the log message given the remaining
101 # arguments
103 # arguments
102 try:
104 try:
103 # Python string formatting with % either uses a
105 # Python string formatting with % either uses a
104 # dictionary *or* tuple, but not both. If we have
106 # dictionary *or* tuple, but not both. If we have
105 # keyword options, assume we need a mapping.
107 # keyword options, assume we need a mapping.
106 formatted = msg[0] % (opts or msg[1:])
108 formatted = msg[0] % (opts or msg[1:])
107 except (TypeError, KeyError):
109 except (TypeError, KeyError):
108 # Failed to apply the arguments, ignore
110 # Failed to apply the arguments, ignore
109 formatted = msg[0]
111 formatted = msg[0]
110 messages = (formatted,) + msg[1:]
112 messages = (formatted,) + msg[1:]
111 else:
113 else:
112 messages = msg
114 messages = msg
113 # positional arguments are listed as MSG[N] keys in the
115 # positional arguments are listed as MSG[N] keys in the
114 # environment
116 # environment
115 msgpairs = (
117 msgpairs = (
116 ('MSG{0:d}'.format(i), str(m))
118 ('MSG{0:d}'.format(i), str(m))
117 for i, m in enumerate(messages, 1))
119 for i, m in enumerate(messages, 1))
118 # keyword arguments get prefixed with OPT_ and uppercased
120 # keyword arguments get prefixed with OPT_ and uppercased
119 optpairs = (
121 optpairs = (
120 ('OPT_{0}'.format(key.upper()), str(value))
122 ('OPT_{0}'.format(key.upper()), str(value))
121 for key, value in opts.iteritems())
123 for key, value in opts.iteritems())
122 env = dict(itertools.chain(encoding.environ.items(),
124 env = dict(itertools.chain(encoding.environ.items(),
123 msgpairs, optpairs),
125 msgpairs, optpairs),
124 EVENT=event, HGPID=str(os.getpid()))
126 EVENT=event, HGPID=str(os.getpid()))
125 # Connect stdin to /dev/null to prevent child processes messing
127 # Connect stdin to /dev/null to prevent child processes messing
126 # with mercurial's stdin.
128 # with mercurial's stdin.
127 runshellcommand(script, env)
129 runshellcommand(script, env)
128 return super(logtoprocessui, self).log(event, *msg, **opts)
130 return super(logtoprocessui, self).log(event, *msg, **opts)
129
131
130 # Replace the class for this instance and all clones created from it:
132 # Replace the class for this instance and all clones created from it:
131 ui.__class__ = logtoprocessui
133 ui.__class__ = logtoprocessui
General Comments 0
You need to be logged in to leave comments. Login now