| 1 | #!/usr/bin/env python3
 | 
| 2 | """
 | 
| 3 | spec/stateful/job_control.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 | from test.spec_lib import log
 | 
| 14 | 
 | 
| 15 | # Hint from Stevens book
 | 
| 16 | #
 | 
| 17 | # http://lkml.iu.edu/hypermail/linux/kernel/1006.2/02460.html
 | 
| 18 | # "TIOCSIG Generate a signal to processes in the
 | 
| 19 | # current process group of the pty."
 | 
| 20 | 
 | 
| 21 | # Generated from C header file
 | 
| 22 | TIOCSIG = 0x40045436
 | 
| 23 | 
 | 
| 24 | PYCAT = 'python2 -c "import sys; print(sys.stdin.readline().strip() + \'%s\')"'
 | 
| 25 | 
 | 
| 26 | 
 | 
| 27 | def ctrl_c(sh):
 | 
| 28 |     sh.sendcontrol('c')
 | 
| 29 |     #fcntl.ioctl(sh.child_fd, TIOCSIG, signal.SIGINT)
 | 
| 30 | 
 | 
| 31 | 
 | 
| 32 | def ctrl_z(sh):
 | 
| 33 |     sh.sendcontrol('z')
 | 
| 34 |     #fcntl.ioctl(sh.child_fd, TIOCSIG, signal.SIGTSTP)
 | 
| 35 | 
 | 
| 36 | 
 | 
| 37 | def expect_no_job(sh):
 | 
| 38 |     """Helper function."""
 | 
| 39 |     if 'osh' in sh.shell_label:
 | 
| 40 |         sh.expect('No job to put in the foreground')
 | 
| 41 |     elif sh.shell_label == 'dash':
 | 
| 42 |         sh.expect('.*fg: No current job')
 | 
| 43 |     elif sh.shell_label == 'bash':
 | 
| 44 |         sh.expect('.*fg: current: no such job.*')
 | 
| 45 |     else:
 | 
| 46 |         raise AssertionError()
 | 
| 47 | 
 | 
| 48 | 
 | 
| 49 | def expect_continued(sh):
 | 
| 50 |     if 'osh' in sh.shell_label:
 | 
| 51 |         sh.expect(r'Continue PID \d+')
 | 
| 52 |     else:
 | 
| 53 |         sh.expect('cat')
 | 
| 54 | 
 | 
| 55 | 
 | 
| 56 | @register()
 | 
| 57 | def bug_1004(sh):
 | 
| 58 |     'fg twice should not result in fatal error (issue 1004)'
 | 
| 59 | 
 | 
| 60 |     expect_prompt(sh)
 | 
| 61 |     sh.sendline('cat')
 | 
| 62 | 
 | 
| 63 |     time.sleep(0.1)
 | 
| 64 | 
 | 
| 65 |     debug = False
 | 
| 66 |     #debug = True
 | 
| 67 | 
 | 
| 68 |     if debug:
 | 
| 69 |         import os
 | 
| 70 |         #os.system('ls -l /proc/%s/fd' % os.getpid())
 | 
| 71 | 
 | 
| 72 |         # From test/group-session.sh
 | 
| 73 |         log('harness PID = %d', os.getpid())
 | 
| 74 |         import subprocess
 | 
| 75 |         #os.system('ps -o pid,ppid,pgid,sid,tpgid,comm')
 | 
| 76 | 
 | 
| 77 |         # the child shell is NOT LISTED here because it's associated WITH A
 | 
| 78 |         # DIFFERENT TERMINAL.
 | 
| 79 |         subprocess.call(['ps', '-o', 'pid,ppid,pgid,sid,tpgid,comm'])
 | 
| 80 | 
 | 
| 81 |     ctrl_z(sh)
 | 
| 82 | 
 | 
| 83 |     sh.expect('.*Stopped.*')
 | 
| 84 | 
 | 
| 85 |     #sh.expect("\r\n\\[PID \\d+\\] Stopped")
 | 
| 86 | 
 | 
| 87 |     sh.sendline('')  # needed for dash
 | 
| 88 |     expect_prompt(sh)
 | 
| 89 | 
 | 
| 90 |     sh.sendline('fg')
 | 
| 91 |     if 'osh' in sh.shell_label:
 | 
| 92 |         sh.expect(r'Continue PID \d+')
 | 
| 93 |     else:
 | 
| 94 |         sh.expect('cat')
 | 
| 95 | 
 | 
| 96 |     # Ctrl-C to terminal
 | 
| 97 |     ctrl_c(sh)
 | 
| 98 |     expect_prompt(sh)
 | 
| 99 | 
 | 
| 100 |     sh.sendline('fg')
 | 
| 101 | 
 | 
| 102 |     expect_no_job(sh)
 | 
| 103 | 
 | 
| 104 | 
 | 
| 105 | @register()
 | 
| 106 | def bug_721(sh):
 | 
| 107 |     'Call fg twice after process exits (issue 721)'
 | 
| 108 | 
 | 
| 109 |     # This test seems flaky under bash for some reason
 | 
| 110 | 
 | 
| 111 |     expect_prompt(sh)
 | 
| 112 |     sh.sendline('cat')
 | 
| 113 | 
 | 
| 114 |     time.sleep(0.1)
 | 
| 115 | 
 | 
| 116 |     ctrl_c(sh)
 | 
| 117 |     expect_prompt(sh)
 | 
| 118 | 
 | 
| 119 |     sh.sendline('fg')
 | 
| 120 |     expect_no_job(sh)
 | 
| 121 | 
 | 
| 122 |     #sh.sendline('')
 | 
| 123 |     #expect_prompt(sh)
 | 
| 124 | 
 | 
| 125 |     sh.sendline('fg')
 | 
| 126 |     expect_no_job(sh)
 | 
| 127 | 
 | 
| 128 |     sh.sendline('')
 | 
| 129 |     expect_prompt(sh)
 | 
| 130 | 
 | 
| 131 | 
 | 
| 132 | @register()
 | 
| 133 | def bug_1005(sh):
 | 
| 134 |     'sleep 10 then Ctrl-Z then wait should not hang (issue 1005)'
 | 
| 135 | 
 | 
| 136 |     expect_prompt(sh)
 | 
| 137 | 
 | 
| 138 |     sh.sendline('sleep 10')
 | 
| 139 | 
 | 
| 140 |     time.sleep(0.1)
 | 
| 141 |     ctrl_z(sh)
 | 
| 142 | 
 | 
| 143 |     sh.expect(r'.*Stopped.*')
 | 
| 144 | 
 | 
| 145 |     sh.sendline('wait')
 | 
| 146 |     sh.sendline('echo status=$?')
 | 
| 147 |     sh.expect('status=0')
 | 
| 148 | 
 | 
| 149 | 
 | 
| 150 | @register(skip_shells=['dash'])
 | 
| 151 | def bug_1005_wait_n(sh):
 | 
| 152 |     'sleep 10 then Ctrl-Z then wait -n should not hang'
 | 
| 153 | 
 | 
| 154 |     expect_prompt(sh)
 | 
| 155 | 
 | 
| 156 |     sh.sendline('sleep 10')
 | 
| 157 | 
 | 
| 158 |     time.sleep(0.1)
 | 
| 159 |     ctrl_z(sh)
 | 
| 160 | 
 | 
| 161 |     sh.expect(r'.*Stopped.*')
 | 
| 162 | 
 | 
| 163 |     sh.sendline('wait -n')
 | 
| 164 |     sh.sendline('echo status=$?')
 | 
| 165 |     sh.expect('status=127')
 | 
| 166 | 
 | 
| 167 | 
 | 
| 168 | @register()
 | 
| 169 | def bug_esrch_pipeline_with_builtin(sh):
 | 
| 170 |     'ESRCH bug - pipeline with builtin'
 | 
| 171 | 
 | 
| 172 |     # Also see test/bugs.sh, there was a history|less issue
 | 
| 173 | 
 | 
| 174 |     expect_prompt(sh)
 | 
| 175 | 
 | 
| 176 |     n = 1
 | 
| 177 |     for i in range(n):
 | 
| 178 |         #log('--- Try %d', i)
 | 
| 179 | 
 | 
| 180 |         if True:
 | 
| 181 |             #sh.sendline('echo hi | cat')
 | 
| 182 |             sh.sendline('echo hi | cat | cat | cat')
 | 
| 183 |             sh.expect(r'.*hi.*')
 | 
| 184 |         else:
 | 
| 185 |             sh.sendline('echo hi | tr a-z A-Z')
 | 
| 186 |             sh.expect(r'.*HI.*')
 | 
| 187 | 
 | 
| 188 |         time.sleep(0.1)
 | 
| 189 | 
 | 
| 190 |     sh.sendline('exit')
 | 
| 191 | 
 | 
| 192 | 
 | 
| 193 | @register()
 | 
| 194 | def stopped_process(sh):
 | 
| 195 |     'Resuming a stopped process'
 | 
| 196 |     expect_prompt(sh)
 | 
| 197 | 
 | 
| 198 |     sh.sendline('cat')
 | 
| 199 | 
 | 
| 200 |     time.sleep(0.1)  # seems necessary
 | 
| 201 | 
 | 
| 202 |     ctrl_z(sh)
 | 
| 203 | 
 | 
| 204 |     sh.expect('.*Stopped.*')
 | 
| 205 | 
 | 
| 206 |     sh.sendline('')  # needed for dash for some reason
 | 
| 207 |     expect_prompt(sh)
 | 
| 208 | 
 | 
| 209 |     sh.sendline('fg')
 | 
| 210 | 
 | 
| 211 |     if 'osh' in sh.shell_label:
 | 
| 212 |         sh.expect(r'Continue PID \d+')
 | 
| 213 |     else:
 | 
| 214 |         sh.expect('cat')
 | 
| 215 | 
 | 
| 216 |     ctrl_c(sh)
 | 
| 217 |     expect_prompt(sh)
 | 
| 218 | 
 | 
| 219 |     sh.sendline('fg')
 | 
| 220 |     expect_no_job(sh)
 | 
| 221 | 
 | 
| 222 | 
 | 
| 223 | # OSH doesn't support this because of the lastpipe issue
 | 
| 224 | # Note: it would be nice to print a message on Ctrl-Z like zsh does:
 | 
| 225 | # "job can't be suspended"
 | 
| 226 | 
 | 
| 227 | 
 | 
| 228 | @register(not_impl_shells=['osh', 'osh-cpp'])
 | 
| 229 | def stopped_pipeline(sh):
 | 
| 230 |     'Suspend and resume a pipeline (issue 1087)'
 | 
| 231 | 
 | 
| 232 |     expect_prompt(sh)
 | 
| 233 | 
 | 
| 234 |     sh.sendline('sleep 10 | cat | cat')
 | 
| 235 | 
 | 
| 236 |     time.sleep(0.1)  # seems necessary
 | 
| 237 | 
 | 
| 238 |     ctrl_z(sh)
 | 
| 239 | 
 | 
| 240 |     sh.expect('.*Stopped.*')
 | 
| 241 | 
 | 
| 242 |     sh.sendline('')  # needed for dash for some reason
 | 
| 243 |     expect_prompt(sh)
 | 
| 244 | 
 | 
| 245 |     sh.sendline('fg')
 | 
| 246 | 
 | 
| 247 |     if 'osh' in sh.shell_label:
 | 
| 248 |         sh.expect(r'Continue PID \d+')
 | 
| 249 |     else:
 | 
| 250 |         sh.expect('cat')
 | 
| 251 | 
 | 
| 252 |     ctrl_c(sh)
 | 
| 253 |     expect_prompt(sh)
 | 
| 254 | 
 | 
| 255 |     sh.sendline('fg')
 | 
| 256 |     expect_no_job(sh)
 | 
| 257 | 
 | 
| 258 | 
 | 
| 259 | @register()
 | 
| 260 | def cycle_process_bg_fg(sh):
 | 
| 261 |     'Suspend and resume a process several times'
 | 
| 262 |     expect_prompt(sh)
 | 
| 263 | 
 | 
| 264 |     sh.sendline('cat')
 | 
| 265 |     time.sleep(0.1)  # seems necessary
 | 
| 266 | 
 | 
| 267 |     for _ in range(3):
 | 
| 268 |         ctrl_z(sh)
 | 
| 269 |         sh.expect('.*Stopped.*')
 | 
| 270 |         sh.sendline('')  # needed for dash for some reason
 | 
| 271 |         expect_prompt(sh)
 | 
| 272 |         sh.sendline('fg')
 | 
| 273 |         expect_continued(sh)
 | 
| 274 | 
 | 
| 275 |     ctrl_c(sh)
 | 
| 276 |     expect_prompt(sh)
 | 
| 277 | 
 | 
| 278 |     sh.sendline('fg')
 | 
| 279 |     expect_no_job(sh)
 | 
| 280 | 
 | 
| 281 | 
 | 
| 282 | @register()
 | 
| 283 | def suspend_status(sh):
 | 
| 284 |     'Ctrl-Z and then look at $?'
 | 
| 285 | 
 | 
| 286 |     # This test seems flaky under bash for some reason
 | 
| 287 | 
 | 
| 288 |     expect_prompt(sh)
 | 
| 289 |     sh.sendline('cat')
 | 
| 290 | 
 | 
| 291 |     time.sleep(0.1)
 | 
| 292 | 
 | 
| 293 |     ctrl_z(sh)
 | 
| 294 |     expect_prompt(sh)
 | 
| 295 | 
 | 
| 296 |     sh.sendline('echo status=$?')
 | 
| 297 |     sh.expect('status=148')
 | 
| 298 |     expect_prompt(sh)
 | 
| 299 | 
 | 
| 300 | 
 | 
| 301 | @register(skip_shells=['zsh'])
 | 
| 302 | def no_spurious_tty_take(sh):
 | 
| 303 |     'A background job getting stopped (e.g. by SIGTTIN) or exiting should not disrupt foreground processes'
 | 
| 304 |     expect_prompt(sh)
 | 
| 305 | 
 | 
| 306 |     sh.sendline('cat &')  # stop
 | 
| 307 |     sh.sendline('sleep 0.1 &')  # exit
 | 
| 308 |     expect_prompt(sh)
 | 
| 309 | 
 | 
| 310 |     # background cat should have been stopped by SIGTTIN immediately, but we don't
 | 
| 311 |     # hear about it from wait() until the foreground process has been started because
 | 
| 312 |     # the shell was blocked in readline when the signal fired.
 | 
| 313 |     time.sleep(
 | 
| 314 |         0.1
 | 
| 315 |     )  # TODO: need to wait a bit for jobs to get SIGTTIN. can we be more precise?
 | 
| 316 |     sh.sendline(PYCAT % 'bar')
 | 
| 317 |     if 'osh' in sh.shell_label:
 | 
| 318 |         # Quirk of osh. TODO: suppress this print for background jobs?
 | 
| 319 |         sh.expect('.*Stopped.*')
 | 
| 320 | 
 | 
| 321 |     # foreground process should not have been stopped.
 | 
| 322 |     sh.sendline('foo')
 | 
| 323 |     sh.expect('foobar')
 | 
| 324 | 
 | 
| 325 |     ctrl_c(sh)
 | 
| 326 |     expect_prompt(sh)
 | 
| 327 | 
 | 
| 328 | 
 | 
| 329 | @register()
 | 
| 330 | def fg_current_previous(sh):
 | 
| 331 |     'Resume the special jobs: %- and %+'
 | 
| 332 |     expect_prompt(sh)
 | 
| 333 | 
 | 
| 334 |     sh.sendline(
 | 
| 335 |         'sleep 1000 &')  # will be terminated as soon as we're done with it
 | 
| 336 | 
 | 
| 337 |     # Start two jobs. Both will get stopped by SIGTTIN when they try to read() on
 | 
| 338 |     # STDIN. According to POSIX, %- and %+ should always refer to stopped jobs if
 | 
| 339 |     # there are at least two of them.
 | 
| 340 |     sh.sendline((PYCAT % 'bar') + ' &')
 | 
| 341 | 
 | 
| 342 |     time.sleep(
 | 
| 343 |         0.1
 | 
| 344 |     )  # TODO: need to wait a bit for jobs to get SIGTTIN. can we be more precise?
 | 
| 345 |     sh.sendline('cat &')
 | 
| 346 |     if 'osh' in sh.shell_label:
 | 
| 347 |         sh.expect('.*Stopped.*')
 | 
| 348 | 
 | 
| 349 |     time.sleep(
 | 
| 350 |         0.1
 | 
| 351 |     )  # TODO: need to wait a bit for jobs to get SIGTTIN. can we be more precise?
 | 
| 352 |     if 'osh' in sh.shell_label:
 | 
| 353 |         sh.sendline('')
 | 
| 354 |         sh.expect('.*Stopped.*')
 | 
| 355 | 
 | 
| 356 |     # Bring back the newest stopped job
 | 
| 357 |     sh.sendline('fg %+')
 | 
| 358 |     if 'osh' in sh.shell_label:
 | 
| 359 |         sh.expect(r'Continue PID \d+')
 | 
| 360 | 
 | 
| 361 |     sh.sendline('foo')
 | 
| 362 |     sh.expect('foo')
 | 
| 363 |     ctrl_z(sh)
 | 
| 364 | 
 | 
| 365 |     # Bring back the second-newest stopped job
 | 
| 366 |     sh.sendline('fg %-')
 | 
| 367 |     if 'osh' in sh.shell_label:
 | 
| 368 |         sh.expect(r'Continue PID \d+')
 | 
| 369 | 
 | 
| 370 |     sh.sendline('')
 | 
| 371 |     sh.expect('bar')
 | 
| 372 | 
 | 
| 373 |     # Force cat to exit
 | 
| 374 |     ctrl_c(sh)
 | 
| 375 |     expect_prompt(sh)
 | 
| 376 |     time.sleep(0.1)  # wait for cat job to go away
 | 
| 377 | 
 | 
| 378 |     # Now that cat is gone, %- should refer to the running job
 | 
| 379 |     sh.sendline('fg %-')
 | 
| 380 |     if 'osh' in sh.shell_label:
 | 
| 381 |         sh.expect(r'Continue PID \d+')
 | 
| 382 | 
 | 
| 383 |     sh.sendline('true')
 | 
| 384 |     time.sleep(0.5)
 | 
| 385 |     sh.expect('')  # sleep should swallow whatever we write to stdin
 | 
| 386 |     ctrl_c(sh)
 | 
| 387 | 
 | 
| 388 |     # %+ and %- should refer to the same thing now that there's only one job
 | 
| 389 |     sh.sendline('fg %+')
 | 
| 390 |     if 'osh' in sh.shell_label:
 | 
| 391 |         sh.expect(r'Continue PID \d+')
 | 
| 392 | 
 | 
| 393 |     sh.sendline('woof')
 | 
| 394 |     sh.expect('woof')
 | 
| 395 |     ctrl_z(sh)
 | 
| 396 |     sh.sendline('fg %-')
 | 
| 397 |     if 'osh' in sh.shell_label:
 | 
| 398 |         sh.expect(r'Continue PID \d+')
 | 
| 399 | 
 | 
| 400 |     sh.sendline('meow')
 | 
| 401 |     sh.expect('meow')
 | 
| 402 |     ctrl_c(sh)
 | 
| 403 | 
 | 
| 404 |     expect_prompt(sh)
 | 
| 405 | 
 | 
| 406 | 
 | 
| 407 | @register(skip_shells=['dash'])
 | 
| 408 | def fg_job_id(sh):
 | 
| 409 |     'Resume jobs with integral job specs using `fg` builtin'
 | 
| 410 |     expect_prompt(sh)
 | 
| 411 | 
 | 
| 412 |     sh.sendline((PYCAT % 'foo') + ' &')  # %1
 | 
| 413 | 
 | 
| 414 |     time.sleep(
 | 
| 415 |         0.1
 | 
| 416 |     )  # TODO: need to wait a bit for jobs to get SIGTTIN. can we be more precise?
 | 
| 417 |     sh.sendline((PYCAT % 'bar') + ' &')  # %2
 | 
| 418 |     if 'osh' in sh.shell_label:
 | 
| 419 |         sh.expect('.*Stopped.*')
 | 
| 420 | 
 | 
| 421 |     time.sleep(0.1)
 | 
| 422 |     sh.sendline((PYCAT % 'baz') + ' &')  # %3 and %-
 | 
| 423 |     if 'osh' in sh.shell_label:
 | 
| 424 |         sh.expect('.*Stopped.*')
 | 
| 425 | 
 | 
| 426 |     time.sleep(0.1)
 | 
| 427 |     if 'osh' in sh.shell_label:
 | 
| 428 |         sh.sendline('')
 | 
| 429 |         sh.expect('.*Stopped.*')
 | 
| 430 | 
 | 
| 431 |     sh.sendline('')
 | 
| 432 |     expect_prompt(sh)
 | 
| 433 | 
 | 
| 434 |     sh.sendline('fg %1')
 | 
| 435 |     sh.sendline('')
 | 
| 436 |     sh.expect('foo')
 | 
| 437 | 
 | 
| 438 |     sh.sendline('fg %3')
 | 
| 439 |     sh.sendline('')
 | 
| 440 |     sh.expect('baz')
 | 
| 441 | 
 | 
| 442 |     sh.sendline('fg %2')
 | 
| 443 |     sh.sendline('')
 | 
| 444 |     sh.expect('bar')
 | 
| 445 | 
 | 
| 446 | 
 | 
| 447 | @register()
 | 
| 448 | def wait_job_spec(sh):
 | 
| 449 |     'Wait using a job spec'
 | 
| 450 |     expect_prompt(sh)
 | 
| 451 | 
 | 
| 452 |     sh.sendline('(sleep 2; exit 11) &')
 | 
| 453 |     sh.sendline('(sleep 1; exit 22) &')
 | 
| 454 |     sh.sendline('(sleep 3; exit 33) &')
 | 
| 455 | 
 | 
| 456 |     time.sleep(1)
 | 
| 457 |     sh.sendline('wait %2; echo status=$?')
 | 
| 458 |     sh.expect('status=22')
 | 
| 459 | 
 | 
| 460 |     time.sleep(1)
 | 
| 461 |     sh.sendline('wait %-; echo status=$?')
 | 
| 462 |     sh.expect('status=11')
 | 
| 463 | 
 | 
| 464 |     time.sleep(1)
 | 
| 465 |     sh.sendline('wait %+; echo status=$?')
 | 
| 466 |     sh.expect('status=33')
 | 
| 467 | 
 | 
| 468 | 
 | 
| 469 | if __name__ == '__main__':
 | 
| 470 |     try:
 | 
| 471 |         sys.exit(harness.main(sys.argv))
 | 
| 472 |     except RuntimeError as e:
 | 
| 473 |         print('FATAL: %s' % e, file=sys.stderr)
 | 
| 474 |         sys.exit(1)
 |