source: tailor/vcpx/shwrap.py @ 1514

Revision 1514, 9.0 KB checked in by lele@…, 5 years ago (diff)

Allow to override what SystemCommand? assume as an ok status

Line 
1# -*- mode: python; coding: iso-8859-1 -*-
2# :Progetto: vcpx -- Tiny wrapper around external command
3# :Creato:   sab 10 apr 2004 16:43:48 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8__docformat__ = 'reStructuredText'
9
10try:
11    # Python 2.4
12    from subprocess import Popen, PIPE, STDOUT
13except ImportError:
14    # Older snakes
15    from _process import Popen, PIPE, STDOUT
16
17class ReopenableNamedTemporaryFile:
18    """
19    This uses tempfile.mkstemp() to generate a secure temp file.  It
20    then closes the file, leaving a zero-length file as a placeholder.
21    You can get the filename with ReopenableNamedTemporaryFile.name.
22    When the ReopenableNamedTemporaryFile instance is garbage
23    collected or its shutdown() method is called, it deletes the file.
24
25    Copied from Zooko's pyutil.fileutil, http://zooko.com/repos/pyutil
26    """
27    def __init__(self, suffix=None, prefix=None, dir=None, text=None):
28        from tempfile import mkstemp
29        from os import close
30
31        fd, self.name = mkstemp(suffix, prefix, dir, text)
32        close(fd)
33
34    def __del__(self):
35        self.shutdown()
36
37    def shutdown(self):
38        from os import remove
39
40        remove(self.name)
41
42
43class ExternalCommand:
44    """Wrap a single command to be executed by the shell."""
45
46    DEBUG = False
47    """Print the output of the command, when not PIPEd to the caller."""
48
49    DRY_RUN = False
50    """Don't really execute the command."""
51
52    MAX_CMDLINE_LENGTH = 8000
53    """Don't execute commands longer than this number of characters."""
54
55    def __init__(self, command=None, cwd=None, nolog=False, ok_status=None):
56        """
57        Initialize a ExternalCommand instance, specifying the command
58        to be executed and eventually the working directory.
59
60        The instance will use the logger ``tailor.shell``.
61        """
62
63        from logging import getLogger
64
65        self.command = command
66        """The command to be executed."""
67
68        self.cwd = cwd
69        """The working directory, go there before execution."""
70
71        self.exit_status = None
72        """Once the command has been executed, this is its exit status."""
73
74        self.ok_status = ok_status is None and (0,) or ok_status
75        """Used to determine which exit_status should not trigger warnings."""
76
77        self._last_command = None
78        """Last executed command."""
79
80        self.capture_stderr = False
81
82        if nolog:
83            self.log = False
84        else:
85            self.log = getLogger('tailor.shell')
86
87    def __str__(self):
88        """
89        Return a string representation of the command prefixed by working dir.
90        """
91
92        r = '$'+repr(self)
93        if self.cwd:
94            r = self.cwd + ' ' + r
95        if self.capture_stderr:
96            r = r + ' 2>&1'
97        return r
98
99    def __repr__(self):
100        """
101        Compute a reasonable shell-like representation of the external command.
102        """
103
104        result = []
105        needquote = False
106        for arg in self._last_command or self.command:
107            bs_buf = []
108
109            # Add a space to separate this argument from the others
110            result.append(' ')
111
112            needquote = (" " in arg) or ("\t" in arg)
113            if needquote:
114                result.append('"')
115
116            for c in arg:
117                if c == '\\':
118                    # Don't know if we need to double yet.
119                    bs_buf.append(c)
120                elif c == '"':
121                    # Double backspaces.
122                    result.append('\\' * len(bs_buf)*2)
123                    bs_buf = []
124                    result.append('\\"')
125                else:
126                    # Normal char
127                    if bs_buf:
128                        result.extend(bs_buf)
129                        bs_buf = []
130                    result.append(c)
131
132            # Add remaining backspaces, if any.
133            if bs_buf:
134                result.extend(bs_buf)
135
136            if needquote:
137                result.extend(bs_buf)
138                result.append('"')
139
140        return ''.join(result)
141
142    def execute(self, *args, **kwargs):
143        """Execute the command, avoiding too long command line."""
144
145        from cStringIO import StringIO
146
147        if kwargs.get('stderr'):
148            self.capture_stderr = True
149        else:
150            self.capture_stderr = False
151
152        if len(args) == 1 and type(args[0]) == type([]):
153            allargs = list(args[0])
154        else:
155            allargs = list(args)
156
157        maxlen = self.MAX_CMDLINE_LENGTH
158        if maxlen is None or len(allargs) < 2:
159            return self._execute(allargs, **kwargs)
160
161        startlen = len(' '.join(self.command))
162        allout = None
163        allerr = None
164        while allargs:
165            thisrun = []
166            clen = startlen
167            pop = allargs.pop
168            append = thisrun.append
169            while allargs and clen<maxlen:
170                thisarg = pop(0)
171                clen += len(thisarg)+1
172                append(thisarg)
173            thisout, thiserr = self._execute(*thisrun, **kwargs)
174            if thisout is not None:
175                if allout is None:
176                    allout = StringIO()
177                allout.write(thisout.read())
178            if thiserr is not None:
179                if allerr is None:
180                    allerr = StringIO()
181                allerr.write(thiserr.read())
182            if self.exit_status:
183                break
184        if allout is not None:
185            allout.seek(0)
186        if allerr is not None:
187            allerr.seek(0)
188        return allout, allerr
189
190    def _execute(self, *args, **kwargs):
191        """Execute the command."""
192
193        from sys import stderr
194        from locale import getpreferredencoding
195        from os import environ, getcwd
196        from os.path import isdir
197        from cStringIO import StringIO
198        from errno import ENOENT
199
200        self.exit_status = None
201
202        self._last_command = [chunk % kwargs for chunk in self.command]
203        if len(args) == 1 and type(args[0]) == type([]):
204            self._last_command.extend(args[0])
205        else:
206            self._last_command.extend(args)
207
208        if self.log: self.log.info(self)
209
210        if self.DRY_RUN:
211            return
212
213        cwd = kwargs.setdefault('cwd', self.cwd or getcwd())
214        if not isdir(cwd):
215            raise OSError(ENOENT, "Working directory does not exist", cwd)
216
217        if self.log: self.log.debug("Executing %r (%r)", self, cwd)
218
219        if not kwargs.has_key('env'):
220            env = kwargs['env'] = {}
221            env.update(environ)
222
223            for v in ['LANG', 'TZ', 'PATH']:
224                if kwargs.has_key(v):
225                    env[v] = kwargs[v]
226            # Override also LC_ALL that has a higher priority over LANG,
227            # and LC_MESSAGES as well.
228            if kwargs.has_key('LANG'):
229                env['LC_ALL'] = kwargs['LANG']
230                env['LC_MESSAGES'] = kwargs['LANG']
231
232        input = kwargs.get('input')
233        output = kwargs.get('stdout')
234        error = kwargs.get('stderr')
235
236        # When not in debug, redirect stderr and stdout to /dev/null
237        # when the caller didn't ask for them.
238        if not self.DEBUG:
239            try:
240                from os import devnull
241            except ImportError:
242                devnull = '/dev/null'
243            if output is None:
244                output = open(devnull, 'w')
245            if error is None:
246                error = open(devnull, 'w')
247        try:
248            process = Popen(self._last_command,
249                            stdin=input and PIPE or None,
250                            stdout=output,
251                            stderr=error,
252                            env=kwargs.get('env'),
253                            cwd=cwd,
254                            universal_newlines=True)
255        except OSError, e:
256            if e.errno == ENOENT:
257                raise OSError("%r does not exist!" % self._last_command[0])
258            else:
259                raise
260
261        if input and isinstance(input, unicode):
262            encoding = getpreferredencoding()
263            if self.log:
264                self.log.warning("Using default %s encoding, ignoring errors; "
265                                 "caller should use repository's encoding and "
266                                 "pass an already encoded input" % encoding)
267            input = input.encode(encoding, 'ignore')
268
269        out, err = process.communicate(input=input)
270
271        self.exit_status = process.returncode
272        if self.exit_status in self.ok_status:
273            if self.log: self.log.info("[Ok]")
274        else:
275            if self.log: self.log.warning("[Status %s]", self.exit_status)
276
277        # For debug purposes, copy the output to our stderr when hidden above
278        if self.DEBUG:
279            if out and output == PIPE:
280                stderr.write('Output stream:\n')
281                stderr.write(out)
282            if err and error == PIPE:
283                stderr.write('Error stream:\n')
284                stderr.write(err)
285
286        if out is not None:
287            out = StringIO(out)
288        if err is not None:
289            err = StringIO(err)
290
291        return out, err
Note: See TracBrowser for help on using the repository browser.