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()