huangcm
2025-02-28 b45e871a67cd1272e3da9ba5bd383f832b0f1824
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
# -*- coding:utf-8 -*-
# Copyright 2016 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
 
"""Various utility functions."""
 
from __future__ import print_function
 
import errno
import functools
import os
import signal
import subprocess
import sys
import tempfile
import time
 
_path = os.path.realpath(__file__ + '/../..')
if sys.path[0] != _path:
    sys.path.insert(0, _path)
del _path
 
# pylint: disable=wrong-import-position
import rh.shell
import rh.signals
 
 
class CommandResult(object):
    """An object to store various attributes of a child process."""
 
    def __init__(self, cmd=None, error=None, output=None, returncode=None):
        self.cmd = cmd
        self.error = error
        self.output = output
        self.returncode = returncode
 
    @property
    def cmdstr(self):
        """Return self.cmd as a nicely formatted string (useful for logs)."""
        return rh.shell.cmd_to_str(self.cmd)
 
 
class RunCommandError(Exception):
    """Error caught in RunCommand() method."""
 
    def __init__(self, msg, result, exception=None):
        self.msg, self.result, self.exception = msg, result, exception
        if exception is not None and not isinstance(exception, Exception):
            raise ValueError('exception must be an exception instance; got %r'
                             % (exception,))
        Exception.__init__(self, msg)
        self.args = (msg, result, exception)
 
    def stringify(self, error=True, output=True):
        """Custom method for controlling what is included in stringifying this.
 
        Each individual argument is the literal name of an attribute
        on the result object; if False, that value is ignored for adding
        to this string content.  If true, it'll be incorporated.
 
        Args:
          error: See comment about individual arguments above.
          output: See comment about individual arguments above.
        """
        items = [
            'return code: %s; command: %s' % (
                self.result.returncode, self.result.cmdstr),
        ]
        if error and self.result.error:
            items.append(self.result.error)
        if output and self.result.output:
            items.append(self.result.output)
        if self.msg:
            items.append(self.msg)
        return '\n'.join(items)
 
    def __str__(self):
        # __str__ needs to return ascii, thus force a conversion to be safe.
        return self.stringify().decode('utf-8', 'replace').encode(
            'ascii', 'xmlcharrefreplace')
 
    def __eq__(self, other):
        return (type(self) == type(other) and
                self.args == other.args)
 
    def __ne__(self, other):
        return not self.__eq__(other)
 
 
class TerminateRunCommandError(RunCommandError):
    """We were signaled to shutdown while running a command.
 
    Client code shouldn't generally know, nor care about this class.  It's
    used internally to suppress retry attempts when we're signaled to die.
    """
 
 
def sudo_run_command(cmd, user='root', **kwargs):
    """Run a command via sudo.
 
    Client code must use this rather than coming up with their own RunCommand
    invocation that jams sudo in- this function is used to enforce certain
    rules in our code about sudo usage, and as a potential auditing point.
 
    Args:
      cmd: The command to run.  See RunCommand for rules of this argument-
          SudoRunCommand purely prefixes it with sudo.
      user: The user to run the command as.
      kwargs: See RunCommand options, it's a direct pass thru to it.
          Note that this supports a 'strict' keyword that defaults to True.
          If set to False, it'll suppress strict sudo behavior.
 
    Returns:
      See RunCommand documentation.
 
    Raises:
      This function may immediately raise RunCommandError if we're operating
      in a strict sudo context and the API is being misused.
      Barring that, see RunCommand's documentation- it can raise the same things
      RunCommand does.
    """
    sudo_cmd = ['sudo']
 
    if user == 'root' and os.geteuid() == 0:
        return run_command(cmd, **kwargs)
 
    if user != 'root':
        sudo_cmd += ['-u', user]
 
    # Pass these values down into the sudo environment, since sudo will
    # just strip them normally.
    extra_env = kwargs.pop('extra_env', None)
    extra_env = {} if extra_env is None else extra_env.copy()
 
    sudo_cmd.extend('%s=%s' % (k, v) for k, v in extra_env.iteritems())
 
    # Finally, block people from passing options to sudo.
    sudo_cmd.append('--')
 
    if isinstance(cmd, basestring):
        # We need to handle shell ourselves so the order is correct:
        #  $ sudo [sudo args] -- bash -c '[shell command]'
        # If we let RunCommand take care of it, we'd end up with:
        #  $ bash -c 'sudo [sudo args] -- [shell command]'
        shell = kwargs.pop('shell', False)
        if not shell:
            raise Exception('Cannot run a string command without a shell')
        sudo_cmd.extend(['/bin/bash', '-c', cmd])
    else:
        sudo_cmd.extend(cmd)
 
    return run_command(sudo_cmd, **kwargs)
 
 
def _kill_child_process(proc, int_timeout, kill_timeout, cmd, original_handler,
                        signum, frame):
    """Used as a signal handler by RunCommand.
 
    This is internal to Runcommand.  No other code should use this.
    """
    if signum:
        # If we've been invoked because of a signal, ignore delivery of that
        # signal from this point forward.  The invoking context of this func
        # restores signal delivery to what it was prior; we suppress future
        # delivery till then since this code handles SIGINT/SIGTERM fully
        # including delivering the signal to the original handler on the way
        # out.
        signal.signal(signum, signal.SIG_IGN)
 
    # Do not trust Popen's returncode alone; we can be invoked from contexts
    # where the Popen instance was created, but no process was generated.
    if proc.returncode is None and proc.pid is not None:
        try:
            while proc.poll() is None and int_timeout >= 0:
                time.sleep(0.1)
                int_timeout -= 0.1
 
            proc.terminate()
            while proc.poll() is None and kill_timeout >= 0:
                time.sleep(0.1)
                kill_timeout -= 0.1
 
            if proc.poll() is None:
                # Still doesn't want to die.  Too bad, so sad, time to die.
                proc.kill()
        except EnvironmentError as e:
            print('Ignoring unhandled exception in _kill_child_process: %s' % e,
                  file=sys.stderr)
 
        # Ensure our child process has been reaped.
        proc.wait()
 
    if not rh.signals.relay_signal(original_handler, signum, frame):
        # Mock up our own, matching exit code for signaling.
        cmd_result = CommandResult(cmd=cmd, returncode=signum << 8)
        raise TerminateRunCommandError('Received signal %i' % signum,
                                       cmd_result)
 
 
class _Popen(subprocess.Popen):
    """subprocess.Popen derivative customized for our usage.
 
    Specifically, we fix terminate/send_signal/kill to work if the child process
    was a setuid binary; on vanilla kernels, the parent can wax the child
    regardless, on goobuntu this apparently isn't allowed, thus we fall back
    to the sudo machinery we have.
 
    While we're overriding send_signal, we also suppress ESRCH being raised
    if the process has exited, and suppress signaling all together if the
    process has knowingly been waitpid'd already.
    """
 
    def send_signal(self, signum):
        if self.returncode is not None:
            # The original implementation in Popen allows signaling whatever
            # process now occupies this pid, even if the Popen object had
            # waitpid'd.  Since we can escalate to sudo kill, we do not want
            # to allow that.  Fixing this addresses that angle, and makes the
            # API less sucky in the process.
            return
 
        try:
            os.kill(self.pid, signum)
        except EnvironmentError as e:
            if e.errno == errno.EPERM:
                # Kill returns either 0 (signal delivered), or 1 (signal wasn't
                # delivered).  This isn't particularly informative, but we still
                # need that info to decide what to do, thus error_code_ok=True.
                ret = sudo_run_command(['kill', '-%i' % signum, str(self.pid)],
                                       redirect_stdout=True,
                                       redirect_stderr=True, error_code_ok=True)
                if ret.returncode == 1:
                    # The kill binary doesn't distinguish between permission
                    # denied and the pid is missing.  Denied can only occur
                    # under weird grsec/selinux policies.  We ignore that
                    # potential and just assume the pid was already dead and
                    # try to reap it.
                    self.poll()
            elif e.errno == errno.ESRCH:
                # Since we know the process is dead, reap it now.
                # Normally Popen would throw this error- we suppress it since
                # frankly that's a misfeature and we're already overriding
                # this method.
                self.poll()
            else:
                raise
 
 
# pylint: disable=redefined-builtin
def run_command(cmd, error_message=None, redirect_stdout=False,
                redirect_stderr=False, cwd=None, input=None,
                shell=False, env=None, extra_env=None, ignore_sigint=False,
                combine_stdout_stderr=False, log_stdout_to_file=None,
                error_code_ok=False, int_timeout=1, kill_timeout=1,
                stdout_to_pipe=False, capture_output=False,
                quiet=False, close_fds=True):
    """Runs a command.
 
    Args:
      cmd: cmd to run.  Should be input to subprocess.Popen.  If a string, shell
          must be true.  Otherwise the command must be an array of arguments,
          and shell must be false.
      error_message: Prints out this message when an error occurs.
      redirect_stdout: Returns the stdout.
      redirect_stderr: Holds stderr output until input is communicated.
      cwd: The working directory to run this cmd.
      input: The data to pipe into this command through stdin.  If a file object
          or file descriptor, stdin will be connected directly to that.
      shell: Controls whether we add a shell as a command interpreter.  See cmd
          since it has to agree as to the type.
      env: If non-None, this is the environment for the new process.
      extra_env: If set, this is added to the environment for the new process.
          This dictionary is not used to clear any entries though.
      ignore_sigint: If True, we'll ignore signal.SIGINT before calling the
          child.  This is the desired behavior if we know our child will handle
          Ctrl-C.  If we don't do this, I think we and the child will both get
          Ctrl-C at the same time, which means we'll forcefully kill the child.
      combine_stdout_stderr: Combines stdout and stderr streams into stdout.
      log_stdout_to_file: If set, redirects stdout to file specified by this
          path.  If |combine_stdout_stderr| is set to True, then stderr will
          also be logged to the specified file.
      error_code_ok: Does not raise an exception when command returns a non-zero
          exit code.  Instead, returns the CommandResult object containing the
          exit code.
      int_timeout: If we're interrupted, how long (in seconds) should we give
          the invoked process to clean up before we send a SIGTERM.
      kill_timeout: If we're interrupted, how long (in seconds) should we give
          the invoked process to shutdown from a SIGTERM before we SIGKILL it.
      stdout_to_pipe: Redirect stdout to pipe.
      capture_output: Set |redirect_stdout| and |redirect_stderr| to True.
      quiet: Set |stdout_to_pipe| and |combine_stdout_stderr| to True.
      close_fds: Whether to close all fds before running |cmd|.
 
    Returns:
      A CommandResult object.
 
    Raises:
      RunCommandError: Raises exception on error with optional error_message.
    """
    if capture_output:
        redirect_stdout, redirect_stderr = True, True
 
    if quiet:
        stdout_to_pipe, combine_stdout_stderr = True, True
 
    # Set default for variables.
    stdout = None
    stderr = None
    stdin = None
    cmd_result = CommandResult()
 
    # Force the timeout to float; in the process, if it's not convertible,
    # a self-explanatory exception will be thrown.
    kill_timeout = float(kill_timeout)
 
    def _get_tempfile():
        try:
            return tempfile.TemporaryFile(bufsize=0)
        except EnvironmentError as e:
            if e.errno != errno.ENOENT:
                raise
            # This can occur if we were pointed at a specific location for our
            # TMP, but that location has since been deleted.  Suppress that
            # issue in this particular case since our usage gurantees deletion,
            # and since this is primarily triggered during hard cgroups
            # shutdown.
            return tempfile.TemporaryFile(bufsize=0, dir='/tmp')
 
    # Modify defaults based on parameters.
    # Note that tempfiles must be unbuffered else attempts to read
    # what a separate process did to that file can result in a bad
    # view of the file.
    # The Popen API accepts either an int or a file handle for stdout/stderr.
    # pylint: disable=redefined-variable-type
    if log_stdout_to_file:
        stdout = open(log_stdout_to_file, 'w+')
    elif stdout_to_pipe:
        stdout = subprocess.PIPE
    elif redirect_stdout:
        stdout = _get_tempfile()
 
    if combine_stdout_stderr:
        stderr = subprocess.STDOUT
    elif redirect_stderr:
        stderr = _get_tempfile()
    # pylint: enable=redefined-variable-type
 
    # If subprocesses have direct access to stdout or stderr, they can bypass
    # our buffers, so we need to flush to ensure that output is not interleaved.
    if stdout is None or stderr is None:
        sys.stdout.flush()
        sys.stderr.flush()
 
    # If input is a string, we'll create a pipe and send it through that.
    # Otherwise we assume it's a file object that can be read from directly.
    if isinstance(input, basestring):
        stdin = subprocess.PIPE
    elif input is not None:
        stdin = input
        input = None
 
    if isinstance(cmd, basestring):
        if not shell:
            raise Exception('Cannot run a string command without a shell')
        cmd = ['/bin/bash', '-c', cmd]
        shell = False
    elif shell:
        raise Exception('Cannot run an array command with a shell')
 
    # If we are using enter_chroot we need to use enterchroot pass env through
    # to the final command.
    env = env.copy() if env is not None else os.environ.copy()
    env.update(extra_env if extra_env else {})
 
    cmd_result.cmd = cmd
 
    proc = None
    # Verify that the signals modules is actually usable, and won't segfault
    # upon invocation of getsignal.  See signals.SignalModuleUsable for the
    # details and upstream python bug.
    use_signals = rh.signals.signal_module_usable()
    try:
        proc = _Popen(cmd, cwd=cwd, stdin=stdin, stdout=stdout,
                      stderr=stderr, shell=False, env=env,
                      close_fds=close_fds)
 
        if use_signals:
            old_sigint = signal.getsignal(signal.SIGINT)
            if ignore_sigint:
                handler = signal.SIG_IGN
            else:
                handler = functools.partial(
                    _kill_child_process, proc, int_timeout, kill_timeout, cmd,
                    old_sigint)
            signal.signal(signal.SIGINT, handler)
 
            old_sigterm = signal.getsignal(signal.SIGTERM)
            handler = functools.partial(_kill_child_process, proc, int_timeout,
                                        kill_timeout, cmd, old_sigterm)
            signal.signal(signal.SIGTERM, handler)
 
        try:
            (cmd_result.output, cmd_result.error) = proc.communicate(input)
        finally:
            if use_signals:
                signal.signal(signal.SIGINT, old_sigint)
                signal.signal(signal.SIGTERM, old_sigterm)
 
            if stdout and not log_stdout_to_file and not stdout_to_pipe:
                # The linter is confused by how stdout is a file & an int.
                # pylint: disable=maybe-no-member,no-member
                stdout.seek(0)
                cmd_result.output = stdout.read()
                stdout.close()
 
            if stderr and stderr != subprocess.STDOUT:
                # The linter is confused by how stderr is a file & an int.
                # pylint: disable=maybe-no-member,no-member
                stderr.seek(0)
                cmd_result.error = stderr.read()
                stderr.close()
 
        cmd_result.returncode = proc.returncode
 
        if not error_code_ok and proc.returncode:
            msg = 'cwd=%s' % cwd
            if extra_env:
                msg += ', extra env=%s' % extra_env
            if error_message:
                msg += '\n%s' % error_message
            raise RunCommandError(msg, cmd_result)
    except OSError as e:
        estr = str(e)
        if e.errno == errno.EACCES:
            estr += '; does the program need `chmod a+x`?'
        if error_code_ok:
            cmd_result = CommandResult(cmd=cmd, error=estr, returncode=255)
        else:
            raise RunCommandError(estr, CommandResult(cmd=cmd), exception=e)
    finally:
        if proc is not None:
            # Ensure the process is dead.
            _kill_child_process(proc, int_timeout, kill_timeout, cmd, None,
                                None, None)
 
    return cmd_result
# pylint: enable=redefined-builtin
 
 
def collection(classname, **kwargs):
    """Create a new class with mutable named members.
 
    This is like collections.namedtuple, but mutable.  Also similar to the
    python 3.3 types.SimpleNamespace.
 
    Example:
      # Declare default values for this new class.
      Foo = collection('Foo', a=0, b=10)
      # Create a new class but set b to 4.
      foo = Foo(b=4)
      # Print out a (will be the default 0) and b (will be 4).
      print('a = %i, b = %i' % (foo.a, foo.b))
    """
 
    def sn_init(self, **kwargs):
        """The new class's __init__ function."""
        # First verify the kwargs don't have excess settings.
        valid_keys = set(self.__slots__[1:])
        these_keys = set(kwargs.keys())
        invalid_keys = these_keys - valid_keys
        if invalid_keys:
            raise TypeError('invalid keyword arguments for this object: %r' %
                            invalid_keys)
 
        # Now initialize this object.
        for k in valid_keys:
            setattr(self, k, kwargs.get(k, self.__defaults__[k]))
 
    def sn_repr(self):
        """The new class's __repr__ function."""
        return '%s(%s)' % (classname, ', '.join(
            '%s=%r' % (k, getattr(self, k)) for k in self.__slots__[1:]))
 
    # Give the new class a unique name and then generate the code for it.
    classname = 'Collection_%s' % classname
    expr = '\n'.join((
        'class %(classname)s(object):',
        '  __slots__ = ["__defaults__", "%(slots)s"]',
        '  __defaults__ = {}',
    )) % {
        'classname': classname,
        'slots': '", "'.join(sorted(str(k) for k in kwargs)),
    }
 
    # Create the class in a local namespace as exec requires.
    namespace = {}
    exec expr in namespace  # pylint: disable=exec-used
    new_class = namespace[classname]
 
    # Bind the helpers.
    new_class.__defaults__ = kwargs.copy()
    new_class.__init__ = sn_init
    new_class.__repr__ = sn_repr
 
    return new_class