github.com/pwn-term/docker@v0.0.0-20210616085119-6e977cce2565/tini/test/run_inner_tests.py (about)

     1  #!/usr/bin/env python3
     2  # coding:utf-8
     3  import os
     4  import sys
     5  import signal
     6  import subprocess
     7  import time
     8  import psutil
     9  import bitmap
    10  import re
    11  import itertools
    12  import tempfile
    13  
    14  DEVNULL = open(os.devnull, "wb")
    15  
    16  SIGNUM_TO_SIGNAME = dict(
    17      (v, k) for k, v in signal.__dict__.items() if re.match("^SIG[A-Z]+$", k)
    18  )
    19  
    20  
    21  def busy_wait(condition_callable, timeout):
    22      checks = 100
    23      increment = float(timeout) / checks
    24  
    25      for _ in range(checks):
    26          if condition_callable():
    27              return
    28          time.sleep(increment)
    29  
    30      assert False, "Condition was never met"
    31  
    32  
    33  def main():
    34      src = os.environ["SOURCE_DIR"]
    35      build = os.environ["BUILD_DIR"]
    36  
    37      args_disabled = os.environ.get("MINIMAL")
    38  
    39      proxy = os.path.join(src, "test", "subreaper-proxy.py")
    40      tini = os.path.join(build, "tini")
    41  
    42      subreaper_support = bool(int(os.environ["FORCE_SUBREAPER"]))
    43  
    44      # Run the exit code test. We use POSIXLY_CORRECT here to not need --
    45      # until that's the default in Tini anyways.
    46      if not args_disabled:
    47          print("Running exit code test for {0}".format(tini))
    48          for code in range(0, 256):
    49              p = subprocess.Popen(
    50                  [tini, "-e", str(code), "--", "sh", "-c", "exit {0}".format(code)],
    51                  stdout=DEVNULL,
    52                  stderr=DEVNULL,
    53                  universal_newlines=True,
    54              )
    55              ret = p.wait()
    56              assert ret == 0, "Inclusive exit code test failed for %s, exit: %s" % (
    57                  code,
    58                  ret,
    59              )
    60  
    61              other_codes = [x for x in range(0, 256) if x != code]
    62              args = list(itertools.chain(*[["-e", str(x)] for x in other_codes]))
    63  
    64              p = subprocess.Popen(
    65                  [tini] + args + ["sh", "-c", "exit {0}".format(code)],
    66                  env=dict(os.environ, POSIXLY_CORRECT="1"),
    67                  stdout=DEVNULL,
    68                  stderr=DEVNULL,
    69                  universal_newlines=True,
    70              )
    71              ret = p.wait()
    72              assert ret == code, "Exclusive exit code test failed for %s, exit: %s" % (
    73                  code,
    74                  ret,
    75              )
    76  
    77      tests = [([proxy, tini], {})]
    78  
    79      if subreaper_support:
    80          if not args_disabled:
    81              tests.append(([tini, "-s"], {}))
    82          tests.append(([tini], {"TINI_SUBREAPER": ""}))
    83  
    84      for target, env in tests:
    85          # Run the reaping test
    86          print("Running reaping test ({0} with env {1})".format(" ".join(target), env))
    87          p = subprocess.Popen(
    88              target + [os.path.join(src, "test", "reaping", "stage_1.py")],
    89              env=dict(os.environ, **env),
    90              stdout=subprocess.PIPE,
    91              stderr=subprocess.PIPE,
    92              universal_newlines=True,
    93          )
    94  
    95          out, err = p.communicate()
    96  
    97          if subreaper_support:
    98              # If subreaper support sin't available, Tini won't looku p its subreaper bit
    99              # and will output the error message here.
   100              assert "zombie reaping won't work" not in err, "Warning message was output!"
   101          ret = p.wait()
   102          assert (
   103              "Reaped zombie process with pid=" not in err
   104          ), "Warning message was output!"
   105          assert ret == 0, "Reaping test failed!\nOUT: %s\nERR: %s" % (out, err)
   106  
   107          if not args_disabled:
   108              print(
   109                  "Running reaping display test ({0} with env {1})".format(
   110                      " ".join(target), env
   111                  )
   112              )
   113              p = subprocess.Popen(
   114                  target + ["-w", os.path.join(src, "test", "reaping", "stage_1.py")],
   115                  env=dict(os.environ, **env),
   116                  stdout=subprocess.PIPE,
   117                  stderr=subprocess.PIPE,
   118                  universal_newlines=True,
   119              )
   120  
   121              out, err = p.communicate()
   122              ret = p.wait()
   123              assert (
   124                  "Reaped zombie process with pid=" in err
   125              ), "Warning message was output!"
   126              assert ret == 0, "Reaping display test failed!\nOUT: %s\nERR: %s" % (
   127                  out,
   128                  err,
   129              )
   130  
   131          # Run the signals test
   132          for signum in [signal.SIGTERM, signal.SIGUSR1, signal.SIGUSR2]:
   133              print(
   134                  "running signal test for: {0} ({1} with env {2})".format(
   135                      signum, " ".join(target), env
   136                  )
   137              )
   138              p = subprocess.Popen(
   139                  target + [os.path.join(src, "test", "signals", "test.py")],
   140                  env=dict(os.environ, **env),
   141                  universal_newlines=True,
   142              )
   143              busy_wait(
   144                  lambda: len(psutil.Process(p.pid).children(recursive=True)) > 1, 10
   145              )
   146              p.send_signal(signum)
   147              ret = p.wait()
   148              assert (
   149                  ret == 128 + signum
   150              ), "Signals test failed (ret was {0}, expected {1})".format(
   151                  ret, 128 + signum
   152              )
   153  
   154      # Run the process group test
   155      # This test has Tini spawn a process that ignores SIGUSR1 and spawns a child that doesn't (and waits on the child)
   156      # We send SIGUSR1 to Tini, and expect the grand-child to terminate, then the child, and then Tini.
   157      if not args_disabled:
   158          print("Running process group test (arguments)")
   159          p = subprocess.Popen(
   160              [tini, "-g", os.path.join(src, "test", "pgroup", "stage_1.py")],
   161              stdout=subprocess.PIPE,
   162              stderr=subprocess.PIPE,
   163              universal_newlines=True,
   164          )
   165  
   166          busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) == 2, 10)
   167          p.send_signal(signal.SIGUSR1)
   168          busy_wait(lambda: p.poll() is not None, 10)
   169  
   170      print("Running process group test (environment variable)")
   171      p = subprocess.Popen(
   172          [tini, os.path.join(src, "test", "pgroup", "stage_1.py")],
   173          stdout=subprocess.PIPE,
   174          stderr=subprocess.PIPE,
   175          env=dict(os.environ, TINI_KILL_PROCESS_GROUP="1"),
   176          universal_newlines=True,
   177      )
   178  
   179      busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) == 2, 10)
   180      p.send_signal(signal.SIGUSR1)
   181      busy_wait(lambda: p.poll() is not None, 10)
   182  
   183      # Run failing test. Force verbosity to 1 so we see the subreaper warning
   184      # regardless of whether MINIMAL is set.
   185      print("Running zombie reaping failure test (Tini should warn)")
   186      p = subprocess.Popen(
   187          [tini, os.path.join(src, "test", "reaping", "stage_1.py")],
   188          stdout=subprocess.PIPE,
   189          stderr=subprocess.PIPE,
   190          env=dict(os.environ, TINI_VERBOSITY="1"),
   191          universal_newlines=True,
   192      )
   193      out, err = p.communicate()
   194      assert "zombie reaping won't work" in err, "No warning message was output!"
   195      ret = p.wait()
   196      assert ret == 1, "Reaping test succeeded (it should have failed)!"
   197  
   198      # Test that the signals are properly in place here.
   199      print("Running signal configuration test")
   200  
   201      p = subprocess.Popen(
   202          [os.path.join(build, "sigconf-test"), tini, "cat", "/proc/self/status"],
   203          stdout=subprocess.PIPE,
   204          stderr=subprocess.PIPE,
   205          universal_newlines=True,
   206      )
   207      out, err = p.communicate()
   208  
   209      # Extract the signal properties, and add a zero at the end.
   210      props = [line.split(":") for line in out.splitlines()]
   211      props = [(k.strip(), v.strip()) for (k, v) in props]
   212      props = [
   213          (k, bitmap.BitMap.fromstring(bin(int(v, 16))[2:].zfill(32)))
   214          for (k, v) in props
   215          if k in ["SigBlk", "SigIgn", "SigCgt"]
   216      ]
   217      props = dict(props)
   218  
   219      # Print actual handling configuration
   220      for k, bmp in props.items():
   221          print(
   222              "{0}: {1}".format(
   223                  k,
   224                  ", ".join(
   225                      [
   226                          "{0} ({1})".format(SIGNUM_TO_SIGNAME[n + 1], n + 1)
   227                          for n in bmp.nonzero()
   228                      ]
   229                  ),
   230              )
   231          )
   232  
   233      for signal_set_name, signals_to_test_for in [
   234          ("SigIgn", [signal.SIGTTOU, signal.SIGSEGV, signal.SIGINT]),
   235          ("SigBlk", [signal.SIGTTIN, signal.SIGILL, signal.SIGTERM]),
   236      ]:
   237          for signum in signals_to_test_for:
   238              # Use signum - 1 because the bitmap is 0-indexed but represents signals strting at 1
   239              assert (signum - 1) in props[
   240                  signal_set_name
   241              ].nonzero(), "{0} ({1}) is missing in {2}!".format(
   242                  SIGNUM_TO_SIGNAME[signum], signum, signal_set_name
   243              )
   244  
   245      # Test parent death signal handling.
   246      if not args_disabled:
   247          print("Running parent death signal test")
   248          f = tempfile.NamedTemporaryFile()
   249          try:
   250              p = subprocess.Popen(
   251                  [os.path.join(src, "test", "pdeathsignal", "stage_1.py"), tini, f.name],
   252                  stdout=DEVNULL,
   253                  stderr=DEVNULL,
   254                  universal_newlines=True,
   255              )
   256              p.wait()
   257  
   258              busy_wait(lambda: open(f.name).read() == "ok", 10)
   259          finally:
   260              f.close()
   261  
   262      print("---------------------------")
   263      print("All done, tests as expected")
   264      print("---------------------------")
   265  
   266  
   267  if __name__ == "__main__":
   268      main()