| 1 | #!/usr/bin/env python3
 | 
| 2 | """
 | 
| 3 | signals.py
 | 
| 4 | """
 | 
| 5 | from __future__ import print_function
 | 
| 6 | 
 | 
| 7 | import signal
 | 
| 8 | import sys
 | 
| 9 | import time
 | 
| 10 | 
 | 
| 11 | import harness
 | 
| 12 | from harness import register, expect_prompt
 | 
| 13 | 
 | 
| 14 | #
 | 
| 15 | # Test Cases
 | 
| 16 | #
 | 
| 17 | # TODO:
 | 
| 18 | # - Fold code from demo/
 | 
| 19 | #   - sigwinch-bug.sh -- invokes $OSH with different snippets, then manual window resize
 | 
| 20 | #   - signal-during-read.sh -- bash_read and osh_read with manual kill -HUP $PID
 | 
| 21 | #     trap handler HUP
 | 
| 22 | #   - bug-858-trap.sh -- wait and kill -USR1 $PID
 | 
| 23 | #     trap handler USR1
 | 
| 24 | #     trap handler USR2
 | 
| 25 | # - Fill out this TEST MATRIX.
 | 
| 26 | #
 | 
| 27 | # A. Which shell?  osh, bash, dash, etc.
 | 
| 28 | #
 | 
| 29 | # B. What mode is it in?
 | 
| 30 | #
 | 
| 31 | #    1. Interactive (stdin is a terminal)
 | 
| 32 | #    2. Non-interactive
 | 
| 33 | #
 | 
| 34 | # C. What is the main thread of the shell doing?
 | 
| 35 | #
 | 
| 36 | #    1. waiting for external process: sleep 1
 | 
| 37 | #    2. wait builtin:                 sleep 5 & wait
 | 
| 38 | #       variants: wait -n: this matters when testing exit code
 | 
| 39 | #    3. read builtin                  read
 | 
| 40 | #       variants: FIVE kinds, read -d, read -n, etc.
 | 
| 41 | #    4. computation, e.g. fibonacci with $(( a + b ))
 | 
| 42 | #
 | 
| 43 | # if interactive:
 | 
| 44 | #    5. keyboard input from terminal with select()
 | 
| 45 | #
 | 
| 46 | #    Another way to categorize the main loop:
 | 
| 47 | #    1. running script code
 | 
| 48 | #    2. running trap code
 | 
| 49 | #    3. running TAB completion plugin code
 | 
| 50 | #
 | 
| 51 | # D. What is it interrupted by?
 | 
| 52 | #
 | 
| 53 | #    1. SIGINT
 | 
| 54 | #    2. SIGTSTP
 | 
| 55 | #    3. SIGWINCH
 | 
| 56 | #    4. SIGUSR1 -- doesn't this quit?
 | 
| 57 | #
 | 
| 58 | # if interactive:
 | 
| 59 | #    1. SIGINT  Ctrl-C from terminal (relies on signal distribution to child?)
 | 
| 60 | #    2. SIGTSTP Ctrl-Z from terminal
 | 
| 61 | #
 | 
| 62 | # E. What is the signal state?
 | 
| 63 | #
 | 
| 64 | #    1. no trap handlers installed
 | 
| 65 | #    2. trap 'echo X' SIGWINCH
 | 
| 66 | #    3. trap 'echo X' SIGINT ?
 | 
| 67 | 
 | 
| 68 | 
 | 
| 69 | @register()
 | 
| 70 | def sigusr1_trapped_prompt(sh):
 | 
| 71 |     'Trapped SIGUSR1 while waiting at prompt (bug 1080)'
 | 
| 72 | 
 | 
| 73 |     # Notes on shell behavior:
 | 
| 74 |     # bash and dash:
 | 
| 75 |     # - If there are multiple USR1 signals, only one handler is run.
 | 
| 76 |     # - The handlers aren't run in between PS1 and PS2.  There has to be another
 | 
| 77 |     #   command.  So it's run in the InteractiveLoop and not the LineReader.
 | 
| 78 |     # zsh and mksh:
 | 
| 79 |     # - It's printed immediately!  You don't even have to hit ENTER.  That's
 | 
| 80 |     #   probably because the line editor is integrated in both shells.
 | 
| 81 |     # I think it's OK t match bash and dash.
 | 
| 82 | 
 | 
| 83 |     expect_prompt(sh)
 | 
| 84 | 
 | 
| 85 |     sh.sendline("trap 'echo zzz-USR1' USR1")
 | 
| 86 |     expect_prompt(sh)
 | 
| 87 | 
 | 
| 88 |     sh.kill(signal.SIGUSR1)
 | 
| 89 | 
 | 
| 90 |     # send an empty line
 | 
| 91 |     sh.sendline('')
 | 
| 92 |     sh.expect('zzz-USR1')
 | 
| 93 | 
 | 
| 94 | 
 | 
| 95 | @register()
 | 
| 96 | def sigmultiple_trapped_prompt(sh):
 | 
| 97 |     'Trapped USR1 and USR2 while waiting at prompt'
 | 
| 98 | 
 | 
| 99 |     expect_prompt(sh)
 | 
| 100 | 
 | 
| 101 |     sh.sendline("trap 'echo zzz-USR1' USR1")
 | 
| 102 |     expect_prompt(sh)
 | 
| 103 |     sh.sendline("trap 'echo zzz-USR2' USR2")
 | 
| 104 |     expect_prompt(sh)
 | 
| 105 | 
 | 
| 106 |     sh.kill(signal.SIGUSR1)
 | 
| 107 |     time.sleep(
 | 
| 108 |         0.1)  # pause for a bit to avoid racing on signal order in osh-cpp
 | 
| 109 |     sh.kill(signal.SIGUSR2)
 | 
| 110 | 
 | 
| 111 |     sh.sendline('echo hi')
 | 
| 112 | 
 | 
| 113 |     sh.expect('zzz-USR1')
 | 
| 114 |     sh.expect('zzz-USR2')
 | 
| 115 |     sh.sendline('hi')
 | 
| 116 | 
 | 
| 117 | 
 | 
| 118 | @register()
 | 
| 119 | def sighup_trapped_wait(sh):
 | 
| 120 |     'trapped SIGHUP during wait builtin'
 | 
| 121 | 
 | 
| 122 |     sh.sendline("trap 'echo HUP' HUP")
 | 
| 123 |     expect_prompt(sh)
 | 
| 124 |     sh.sendline('sleep 1 &')
 | 
| 125 |     # TODO: expect something like [1] 24370
 | 
| 126 |     sh.sendline('wait')
 | 
| 127 | 
 | 
| 128 |     time.sleep(0.1)
 | 
| 129 | 
 | 
| 130 |     sh.kill(signal.SIGHUP)
 | 
| 131 | 
 | 
| 132 |     expect_prompt(sh)
 | 
| 133 | 
 | 
| 134 |     sh.sendline('echo status=$?')
 | 
| 135 |     sh.expect('status=129')
 | 
| 136 | 
 | 
| 137 | 
 | 
| 138 | @register()
 | 
| 139 | def sigint_trapped_wait(sh):
 | 
| 140 |     'trapped SIGINT during wait builtin'
 | 
| 141 | 
 | 
| 142 |     # This is different than Ctrl-C during wait builtin, because it's trapped!
 | 
| 143 | 
 | 
| 144 |     sh.sendline("trap 'echo INT' INT")
 | 
| 145 |     sh.sendline('sleep 1 &')
 | 
| 146 |     sh.sendline('wait')
 | 
| 147 | 
 | 
| 148 |     time.sleep(0.1)
 | 
| 149 | 
 | 
| 150 |     # simulate window size change
 | 
| 151 |     sh.kill(signal.SIGINT)
 | 
| 152 | 
 | 
| 153 |     expect_prompt(sh)
 | 
| 154 | 
 | 
| 155 |     sh.sendline('echo status=$?')
 | 
| 156 |     sh.expect('status=130')
 | 
| 157 | 
 | 
| 158 | 
 | 
| 159 | @register()
 | 
| 160 | def ctrl_c_after_removing_sigint_trap(sh):
 | 
| 161 |     'Ctrl-C after removing SIGINT trap (issue 1607)'
 | 
| 162 | 
 | 
| 163 |     sh.sendline('echo status=$?')
 | 
| 164 |     sh.expect('status=0')
 | 
| 165 | 
 | 
| 166 |     sh.sendintr()  # SIGINT
 | 
| 167 | 
 | 
| 168 |     expect_prompt(sh)
 | 
| 169 | 
 | 
| 170 |     sh.sendline('trap - SIGINT')
 | 
| 171 |     expect_prompt(sh)
 | 
| 172 | 
 | 
| 173 |     # Why do we need this?  Weird race condition
 | 
| 174 |     # I would have thought expect_prompt() should be enough synchronization
 | 
| 175 |     time.sleep(0.1)
 | 
| 176 | 
 | 
| 177 |     sh.sendintr()  # SIGINT
 | 
| 178 |     expect_prompt(sh)
 | 
| 179 | 
 | 
| 180 |     # bash, zsh, mksh give status 130, dash and OSH gives 0
 | 
| 181 |     #sh.sendline('echo status=$?')
 | 
| 182 |     #sh.expect('status=130')
 | 
| 183 | 
 | 
| 184 |     sh.sendline('echo hi')
 | 
| 185 |     sh.expect('hi')
 | 
| 186 |     expect_prompt(sh)
 | 
| 187 | 
 | 
| 188 | 
 | 
| 189 | @register()
 | 
| 190 | def sigwinch_trapped_wait(sh):
 | 
| 191 |     'trapped SIGWINCH during wait builtin'
 | 
| 192 | 
 | 
| 193 |     sh.sendline("trap 'echo WINCH' WINCH")
 | 
| 194 |     sh.sendline('sleep 1 &')
 | 
| 195 |     sh.sendline('wait')
 | 
| 196 | 
 | 
| 197 |     time.sleep(0.1)
 | 
| 198 | 
 | 
| 199 |     # simulate window size change
 | 
| 200 |     sh.kill(signal.SIGWINCH)
 | 
| 201 | 
 | 
| 202 |     expect_prompt(sh)
 | 
| 203 | 
 | 
| 204 |     sh.sendline('echo status=$?')
 | 
| 205 |     sh.expect('status=156')
 | 
| 206 | 
 | 
| 207 | 
 | 
| 208 | @register()
 | 
| 209 | def sigwinch_untrapped_wait(sh):
 | 
| 210 |     'untrapped SIGWINCH during wait builtin (issue 1067)'
 | 
| 211 | 
 | 
| 212 |     sh.sendline('sleep 1 &')
 | 
| 213 |     sh.sendline('wait')
 | 
| 214 | 
 | 
| 215 |     time.sleep(0.1)
 | 
| 216 | 
 | 
| 217 |     # simulate window size change
 | 
| 218 |     sh.kill(signal.SIGWINCH)
 | 
| 219 | 
 | 
| 220 |     expect_prompt(sh)
 | 
| 221 | 
 | 
| 222 |     sh.sendline('echo status=$?')
 | 
| 223 |     sh.expect('status=0')
 | 
| 224 | 
 | 
| 225 | 
 | 
| 226 | # dash and mksh don't have pipestatus
 | 
| 227 | @register(skip_shells=['dash', 'mksh'])
 | 
| 228 | def sigwinch_untrapped_wait_n(sh):
 | 
| 229 |     'untrapped SIGWINCH during wait -n'
 | 
| 230 | 
 | 
| 231 |     sh.sendline('sleep 1 &')
 | 
| 232 |     sh.sendline('wait -n')
 | 
| 233 | 
 | 
| 234 |     time.sleep(0.1)
 | 
| 235 | 
 | 
| 236 |     # simulate window size change
 | 
| 237 |     sh.kill(signal.SIGWINCH)
 | 
| 238 | 
 | 
| 239 |     expect_prompt(sh)
 | 
| 240 | 
 | 
| 241 |     sh.sendline('echo status=$?')
 | 
| 242 |     sh.expect('status=0')
 | 
| 243 | 
 | 
| 244 | 
 | 
| 245 | # dash and mksh don't have pipestatus
 | 
| 246 | @register()
 | 
| 247 | def sigwinch_untrapped_wait_pid(sh):
 | 
| 248 |     'untrapped SIGWINCH during wait $!'
 | 
| 249 | 
 | 
| 250 |     sh.sendline('sleep 1 &')
 | 
| 251 |     sh.sendline('wait $!')
 | 
| 252 | 
 | 
| 253 |     time.sleep(0.1)
 | 
| 254 | 
 | 
| 255 |     # simulate window size change
 | 
| 256 |     sh.kill(signal.SIGWINCH)
 | 
| 257 | 
 | 
| 258 |     expect_prompt(sh)
 | 
| 259 | 
 | 
| 260 |     sh.sendline('echo status=$?')
 | 
| 261 |     sh.expect('status=0')
 | 
| 262 | 
 | 
| 263 | 
 | 
| 264 | @register()
 | 
| 265 | def sigwinch_untrapped_external(sh):
 | 
| 266 |     'untrapped SIGWINCH during external command'
 | 
| 267 | 
 | 
| 268 |     sh.sendline('sleep 0.5')  # slower than timeout
 | 
| 269 | 
 | 
| 270 |     time.sleep(0.1)
 | 
| 271 | 
 | 
| 272 |     # simulate window size change
 | 
| 273 |     sh.kill(signal.SIGWINCH)
 | 
| 274 | 
 | 
| 275 |     expect_prompt(sh)
 | 
| 276 | 
 | 
| 277 |     sh.sendline('echo status=$?')
 | 
| 278 |     sh.expect('status=0')
 | 
| 279 | 
 | 
| 280 | 
 | 
| 281 | # dash doesn't have pipestatus
 | 
| 282 | @register(skip_shells=['dash'])
 | 
| 283 | def sigwinch_untrapped_pipeline(sh):
 | 
| 284 |     'untrapped SIGWINCH during pipeline'
 | 
| 285 | 
 | 
| 286 |     sh.sendline('sleep 0.5 | echo x')  # slower than timeout
 | 
| 287 | 
 | 
| 288 |     time.sleep(0.1)
 | 
| 289 | 
 | 
| 290 |     # simulate window size change
 | 
| 291 |     sh.kill(signal.SIGWINCH)
 | 
| 292 | 
 | 
| 293 |     expect_prompt(sh)
 | 
| 294 | 
 | 
| 295 |     sh.sendline('echo pipestatus=${PIPESTATUS[@]}')
 | 
| 296 |     sh.expect('pipestatus=0 0')
 | 
| 297 | 
 | 
| 298 | 
 | 
| 299 | @register()
 | 
| 300 | def t1(sh):
 | 
| 301 |     'Ctrl-C during external command'
 | 
| 302 | 
 | 
| 303 |     sh.sendline('sleep 5')
 | 
| 304 | 
 | 
| 305 |     time.sleep(0.1)
 | 
| 306 |     sh.sendintr()  # SIGINT
 | 
| 307 | 
 | 
| 308 |     expect_prompt(sh)
 | 
| 309 | 
 | 
| 310 |     sh.sendline('echo status=$?')
 | 
| 311 |     sh.expect('status=130')
 | 
| 312 | 
 | 
| 313 | 
 | 
| 314 | @register(skip_shells=['mksh'])
 | 
| 315 | def t4(sh):
 | 
| 316 |     'Ctrl-C during pipeline'
 | 
| 317 |     sh.sendline('sleep 5 | cat')
 | 
| 318 | 
 | 
| 319 |     time.sleep(0.1)
 | 
| 320 |     sh.sendintr()  # SIGINT
 | 
| 321 | 
 | 
| 322 |     expect_prompt(sh)
 | 
| 323 | 
 | 
| 324 |     sh.sendline('echo status=$?')
 | 
| 325 |     sh.expect('status=130')
 | 
| 326 | 
 | 
| 327 | 
 | 
| 328 | # TODO:
 | 
| 329 | # - Expand this to call kinds of reads
 | 
| 330 | #   - note that mksh only has some of them
 | 
| 331 | # - Expand to trapped and untrapped
 | 
| 332 | # - Expand to Ctrl-C vs. SIGWINCH
 | 
| 333 | 
 | 
| 334 | 
 | 
| 335 | @register()
 | 
| 336 | def t2(sh):
 | 
| 337 |     'Ctrl-C during read builtin'
 | 
| 338 | 
 | 
| 339 |     sh.sendline('read myvar')
 | 
| 340 | 
 | 
| 341 |     time.sleep(0.1)
 | 
| 342 |     sh.sendintr()  # SIGINT
 | 
| 343 | 
 | 
| 344 |     expect_prompt(sh)
 | 
| 345 | 
 | 
| 346 |     sh.sendline('echo status=$?')
 | 
| 347 |     sh.expect('status=130')
 | 
| 348 | 
 | 
| 349 | 
 | 
| 350 | @register()
 | 
| 351 | def read_d(sh):
 | 
| 352 |     'Ctrl-C during read -d'
 | 
| 353 | 
 | 
| 354 |     sh.sendline('read -d :')
 | 
| 355 | 
 | 
| 356 |     time.sleep(0.1)
 | 
| 357 |     sh.sendintr()  # SIGINT
 | 
| 358 | 
 | 
| 359 |     expect_prompt(sh)
 | 
| 360 | 
 | 
| 361 |     sh.sendline('echo status=$?')
 | 
| 362 |     sh.expect('status=130')
 | 
| 363 | 
 | 
| 364 | 
 | 
| 365 | @register()
 | 
| 366 | def c_wait(sh):
 | 
| 367 |     'Ctrl-C (untrapped) during wait builtin'
 | 
| 368 | 
 | 
| 369 |     sh.sendline('sleep 5 &')
 | 
| 370 |     sh.sendline('wait')
 | 
| 371 | 
 | 
| 372 |     time.sleep(0.1)
 | 
| 373 | 
 | 
| 374 |     # TODO: actually send Ctrl-C through the terminal, not SIGINT?
 | 
| 375 |     sh.sendintr()  # SIGINT
 | 
| 376 | 
 | 
| 377 |     expect_prompt(sh)
 | 
| 378 | 
 | 
| 379 |     sh.sendline('echo status=$?')
 | 
| 380 |     sh.expect('status=130')
 | 
| 381 | 
 | 
| 382 | 
 | 
| 383 | @register()
 | 
| 384 | def c_wait_n(sh):
 | 
| 385 |     'Ctrl-C (untrapped) during wait -n builtin'
 | 
| 386 | 
 | 
| 387 |     sh.sendline('sleep 5 &')
 | 
| 388 |     sh.sendline('wait -n')
 | 
| 389 | 
 | 
| 390 |     time.sleep(0.1)
 | 
| 391 | 
 | 
| 392 |     # TODO: actually send Ctrl-C through the terminal, not SIGINT?
 | 
| 393 |     sh.sendintr()  # SIGINT
 | 
| 394 | 
 | 
| 395 |     expect_prompt(sh)
 | 
| 396 | 
 | 
| 397 |     sh.sendline('echo status=$?')
 | 
| 398 |     sh.expect('status=130')
 | 
| 399 | 
 | 
| 400 | 
 | 
| 401 | @register()
 | 
| 402 | def c_wait_line(sh):
 | 
| 403 |     'Ctrl-C (untrapped) cancels entire interactive line'
 | 
| 404 | 
 | 
| 405 |     # the echo should be cancelled
 | 
| 406 |     sh.sendline('sleep 10 & wait; echo status=$?')
 | 
| 407 | 
 | 
| 408 |     time.sleep(0.1)
 | 
| 409 | 
 | 
| 410 |     # TODO: actually send Ctrl-C through the terminal, not SIGINT?
 | 
| 411 |     sh.sendintr()  # SIGINT
 | 
| 412 | 
 | 
| 413 |     expect_prompt(sh)
 | 
| 414 | 
 | 
| 415 |     sh.sendline('echo done=$?')
 | 
| 416 |     sh.expect('done=130')
 | 
| 417 | 
 | 
| 418 | 
 | 
| 419 | @register()
 | 
| 420 | def t5(sh):
 | 
| 421 |     'Ctrl-C during Command Sub (issue 467)'
 | 
| 422 |     sh.sendline('`sleep 5`')
 | 
| 423 | 
 | 
| 424 |     time.sleep(0.1)
 | 
| 425 |     sh.sendintr()  # SIGINT
 | 
| 426 | 
 | 
| 427 |     expect_prompt(sh)
 | 
| 428 | 
 | 
| 429 |     sh.sendline('echo status=$?')
 | 
| 430 |     # TODO: This should be status 130 like bash
 | 
| 431 |     sh.expect('status=130')
 | 
| 432 | 
 | 
| 433 | 
 | 
| 434 | @register()
 | 
| 435 | def loop_break(sh):
 | 
| 436 |     'Ctrl-C (untrapped) exits loop'
 | 
| 437 | 
 | 
| 438 |     sh.sendline('while true; do continue; done')
 | 
| 439 | 
 | 
| 440 |     time.sleep(0.1)
 | 
| 441 | 
 | 
| 442 |     # TODO: actually send Ctrl-C through the terminal, not SIGINT?
 | 
| 443 |     sh.sendintr()  # SIGINT
 | 
| 444 | 
 | 
| 445 |     expect_prompt(sh)
 | 
| 446 | 
 | 
| 447 |     sh.sendline('echo done=$?')
 | 
| 448 |     sh.expect('done=130')
 | 
| 449 | 
 | 
| 450 | 
 | 
| 451 | if __name__ == '__main__':
 | 
| 452 |     try:
 | 
| 453 |         sys.exit(harness.main(sys.argv))
 | 
| 454 |     except RuntimeError as e:
 | 
| 455 |         print('FATAL: %s' % e, file=sys.stderr)
 | 
| 456 |         sys.exit(1)
 |