OILS / spec / stateful / job_control.py View on Github | oils.pub

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