OILS / core / process.py View on Github | oils.pub

2267 lines, 1070 significant
1# Copyright 2016 Andy Chu. All rights reserved.
2# Licensed under the Apache License, Version 2.0 (the "License");
3# you may not use this file except in compliance with the License.
4# You may obtain a copy of the License at
5#
6# http://www.apache.org/licenses/LICENSE-2.0
7"""process.py - Launch processes and manipulate file descriptors."""
8from __future__ import print_function
9
10from errno import EACCES, EBADF, ECHILD, EINTR, ENOENT, ENOEXEC, EEXIST
11import fcntl as fcntl_
12from fcntl import F_DUPFD, F_GETFD, F_SETFD, FD_CLOEXEC
13from signal import (SIG_DFL, SIG_IGN, SIGINT, SIGPIPE, SIGQUIT, SIGTSTP,
14 SIGTTOU, SIGTTIN, SIGWINCH)
15
16import libc
17
18from _devbuild.gen.id_kind_asdl import Id
19from _devbuild.gen.runtime_asdl import (job_state_e, job_state_t,
20 job_state_str, wait_status,
21 wait_status_t, RedirValue,
22 redirect_arg, redirect_arg_e, trace,
23 trace_t)
24from _devbuild.gen.syntax_asdl import (
25 loc_t,
26 redir_loc,
27 redir_loc_e,
28 redir_loc_t,
29)
30from _devbuild.gen.value_asdl import (value, value_e)
31from core import dev
32from core import error
33from core.error import e_die
34from core import pyutil
35from core import pyos
36from core import state
37from display import ui
38from core import util
39from data_lang import j8_lite
40from frontend import location
41from frontend import match
42from mycpp import iolib
43from mycpp import mylib
44from mycpp.mylib import log, print_stderr, probe, tagswitch, iteritems
45
46import posix_ as posix
47from posix_ import (
48 # translated by mycpp and directly called! No wrapper!
49 WIFSIGNALED,
50 WIFEXITED,
51 WIFSTOPPED,
52 WEXITSTATUS,
53 WSTOPSIG,
54 WTERMSIG,
55 WNOHANG,
56 O_APPEND,
57 O_CREAT,
58 O_EXCL,
59 O_NONBLOCK,
60 O_NOCTTY,
61 O_RDONLY,
62 O_RDWR,
63 O_WRONLY,
64 O_TRUNC,
65 F_DUPFD_CLOEXEC, # OSH patch - this didn't appear until Python 3.2
66)
67
68from typing import IO, List, Tuple, Dict, Optional, Any, cast, TYPE_CHECKING
69
70if TYPE_CHECKING:
71 from _devbuild.gen.runtime_asdl import cmd_value
72 from _devbuild.gen.syntax_asdl import command_t
73 from builtin import trap_osh
74 from core import optview
75 from core import vm
76 from core.util import _DebugFile
77 from osh.cmd_eval import CommandEvaluator
78
79NO_FD = -1
80
81# Minimum file descriptor that the shell can use. Other descriptors can be
82# directly used by user programs, e.g. exec 9>&1
83#
84# Oils uses 100 because users are allowed TWO digits in frontend/lexer_def.py.
85# This is a compromise between bash (unlimited, but requires crazy
86# bookkeeping), and dash/zsh (10) and mksh (24)
87_SHELL_MIN_FD = 100
88
89# Style for 'jobs' builtin
90STYLE_DEFAULT = 0
91STYLE_LONG = 1
92STYLE_PID_ONLY = 2
93
94# To save on allocations in JobList::JobFromSpec()
95CURRENT_JOB_SPECS = ['', '%', '%%', '%+']
96
97
98class ctx_FileCloser(object):
99
100 def __init__(self, f):
101 # type: (mylib.LineReader) -> None
102 self.f = f
103
104 def __enter__(self):
105 # type: () -> None
106 pass
107
108 def __exit__(self, type, value, traceback):
109 # type: (Any, Any, Any) -> None
110 self.f.close()
111
112
113def InitInteractiveShell(signal_safe):
114 # type: (iolib.SignalSafe) -> None
115 """Called when initializing an interactive shell."""
116
117 # The shell itself should ignore Ctrl-\.
118 iolib.sigaction(SIGQUIT, SIG_IGN)
119
120 # This prevents Ctrl-Z from suspending OSH in interactive mode.
121 iolib.sigaction(SIGTSTP, SIG_IGN)
122
123 # More signals from
124 # https://www.gnu.org/software/libc/manual/html_node/Initializing-the-Shell.html
125 # (but not SIGCHLD)
126 iolib.sigaction(SIGTTOU, SIG_IGN)
127 iolib.sigaction(SIGTTIN, SIG_IGN)
128
129 # Register a callback to receive terminal width changes.
130 # NOTE: In line_input.c, we turned off rl_catch_sigwinch.
131
132 # This is ALWAYS on, which means that it can cause EINTR, and wait() and
133 # read() have to handle it
134 iolib.RegisterSignalInterest(SIGWINCH)
135
136
137_ = F_DUPFD_CLOEXEC # shut up lint for now
138
139def SaveFd(fd):
140 # type: (int) -> int
141
142 # Note: may raise IOError_OSError
143 saved = fcntl_.fcntl(fd, F_DUPFD, _SHELL_MIN_FD) # type: int
144
145 # Bug fix: make sure we never leak the saved descriptor to child processes
146 fcntl_.fcntl(saved, F_SETFD, FD_CLOEXEC)
147
148 # TODO: use this
149 #saved = fcntl_.fcntl(fd, F_DUPFD_CLOEXEC, _SHELL_MIN_FD) # type: int
150
151 return saved
152
153
154class _RedirFrame(object):
155
156 def __init__(self, saved_fd, orig_fd, forget):
157 # type: (int, int, bool) -> None
158 self.saved_fd = saved_fd
159 self.orig_fd = orig_fd
160 self.forget = forget
161
162
163class _FdFrame(object):
164
165 def __init__(self):
166 # type: () -> None
167 self.saved = [] # type: List[_RedirFrame]
168 self.need_wait = [] # type: List[Process]
169
170 def Forget(self):
171 # type: () -> None
172 """For exec 1>&2."""
173 for rf in reversed(self.saved):
174 if rf.saved_fd != NO_FD and rf.forget:
175 posix.close(rf.saved_fd)
176
177 del self.saved[:] # like list.clear() in Python 3.3
178 del self.need_wait[:]
179
180 def __repr__(self):
181 # type: () -> str
182 return '<_FdFrame %s>' % self.saved
183
184
185class FdState(object):
186 """File descriptor state for the current process.
187
188 For example, you can do 'myfunc > out.txt' without forking. Child
189 processes inherit our state.
190 """
191
192 def __init__(
193 self,
194 errfmt, # type: ui.ErrorFormatter
195 job_control, # type: JobControl
196 job_list, # type: JobList
197 mem, # type: state.Mem
198 tracer, # type: Optional[dev.Tracer]
199 waiter, # type: Optional[Waiter]
200 exec_opts, # type: optview.Exec
201 ):
202 # type: (...) -> None
203 """
204 Args:
205 errfmt: for errors
206 job_list: For keeping track of _HereDocWriterThunk
207 """
208 self.errfmt = errfmt
209 self.job_control = job_control
210 self.job_list = job_list
211 self.cur_frame = _FdFrame() # for the top level
212 self.stack = [self.cur_frame]
213 self.mem = mem
214 self.tracer = tracer
215 self.waiter = waiter
216 self.exec_opts = exec_opts
217
218 def Open(self, path):
219 # type: (str) -> mylib.LineReader
220 """Opens a path for read, but moves it out of the reserved 3-9 fd
221 range.
222
223 Returns:
224 A Python file object. The caller is responsible for Close().
225
226 Raises:
227 IOError or OSError if the path can't be found. (This is Python-induced wart)
228 """
229 fd_mode = O_RDONLY
230 f = self._Open(path, 'r', fd_mode)
231
232 # Hacky downcast
233 return cast('mylib.LineReader', f)
234
235 # used for util.DebugFile
236 def OpenForWrite(self, path):
237 # type: (str) -> mylib.Writer
238 fd_mode = O_CREAT | O_RDWR
239 f = self._Open(path, 'w', fd_mode)
240
241 # Hacky downcast
242 return cast('mylib.Writer', f)
243
244 def _Open(self, path, c_mode, fd_mode):
245 # type: (str, str, int) -> IO[str]
246 fd = posix.open(path, fd_mode, 0o666) # may raise OSError
247
248 # Immediately move it to a new location
249 new_fd = SaveFd(fd)
250 posix.close(fd)
251
252 # Return a Python file handle
253 f = posix.fdopen(new_fd, c_mode) # may raise IOError
254 return f
255
256 def _WriteFdToMem(self, fd_name, fd):
257 # type: (str, int) -> None
258 if self.mem:
259 # setvar, not setref
260 state.OshLanguageSetValue(self.mem, location.LName(fd_name),
261 value.Str(str(fd)))
262
263 def _ReadFdFromMem(self, fd_name):
264 # type: (str) -> int
265 val = self.mem.GetValue(fd_name)
266 if val.tag() == value_e.Str:
267 try:
268 return int(cast(value.Str, val).s)
269 except ValueError:
270 return NO_FD
271 return NO_FD
272
273 def _PushSave(self, fd):
274 # type: (int) -> bool
275 """Save fd to a new location and remember to restore it later."""
276 #log('---- _PushSave %s', fd)
277 ok = True
278 try:
279 new_fd = SaveFd(fd)
280 except (IOError, OSError) as e:
281 ok = False
282 # Example program that causes this error: exec 4>&1. Descriptor 4 isn't
283 # open.
284 # This seems to be ignored in dash too in savefd()?
285 if e.errno != EBADF:
286 raise
287 if ok:
288 posix.close(fd)
289 self.cur_frame.saved.append(_RedirFrame(new_fd, fd, True))
290 else:
291 # if we got EBADF, we still need to close the original on Pop()
292 self._PushClose(fd)
293
294 return ok
295
296 def _PushDup(self, fd1, blame_loc):
297 # type: (int, redir_loc_t) -> int
298 """Save fd2 in a higher range, and dup fd1 onto fd2.
299
300 Returns whether F_DUPFD/dup2 succeeded, and the new descriptor.
301 """
302 UP_loc = blame_loc
303 if blame_loc.tag() == redir_loc_e.VarName:
304 fd2_name = cast(redir_loc.VarName, UP_loc).name
305 try:
306 # F_DUPFD: GREATER than range
307 new_fd = fcntl_.fcntl(fd1, F_DUPFD, _SHELL_MIN_FD) # type: int
308 except (IOError, OSError) as e:
309 if e.errno == EBADF:
310 print_stderr('F_DUPFD fd %d: %s' %
311 (fd1, pyutil.strerror(e)))
312 return NO_FD
313 else:
314 raise # this redirect failed
315
316 self._WriteFdToMem(fd2_name, new_fd)
317
318 elif blame_loc.tag() == redir_loc_e.Fd:
319 fd2 = cast(redir_loc.Fd, UP_loc).fd
320
321 if fd1 == fd2:
322 # The user could have asked for it to be open on descriptor 3, but open()
323 # already returned 3, e.g. echo 3>out.txt
324 return NO_FD
325
326 # Check the validity of fd1 before _PushSave(fd2)
327 try:
328 fcntl_.fcntl(fd1, F_GETFD)
329 except (IOError, OSError) as e:
330 print_stderr('F_GETFD fd %d: %s' % (fd1, pyutil.strerror(e)))
331 raise
332
333 need_restore = self._PushSave(fd2)
334
335 #log('==== dup2 %s %s\n' % (fd1, fd2))
336 try:
337 posix.dup2(fd1, fd2)
338 except (IOError, OSError) as e:
339 # bash/dash give this error too, e.g. for 'echo hi 1>&3'
340 print_stderr('dup2(%d, %d): %s' %
341 (fd1, fd2, pyutil.strerror(e)))
342
343 # Restore and return error
344 if need_restore:
345 rf = self.cur_frame.saved.pop()
346 posix.dup2(rf.saved_fd, rf.orig_fd)
347 posix.close(rf.saved_fd)
348
349 raise # this redirect failed
350
351 new_fd = fd2
352
353 else:
354 raise AssertionError()
355
356 return new_fd
357
358 def _PushCloseFd(self, blame_loc):
359 # type: (redir_loc_t) -> bool
360 """For 2>&-"""
361 # exec {fd}>&- means close the named descriptor
362
363 UP_loc = blame_loc
364 if blame_loc.tag() == redir_loc_e.VarName:
365 fd_name = cast(redir_loc.VarName, UP_loc).name
366 fd = self._ReadFdFromMem(fd_name)
367 if fd == NO_FD:
368 return False
369
370 elif blame_loc.tag() == redir_loc_e.Fd:
371 fd = cast(redir_loc.Fd, UP_loc).fd
372
373 else:
374 raise AssertionError()
375
376 self._PushSave(fd)
377
378 return True
379
380 def _PushClose(self, fd):
381 # type: (int) -> None
382 self.cur_frame.saved.append(_RedirFrame(NO_FD, fd, False))
383
384 def _PushWait(self, proc):
385 # type: (Process) -> None
386 self.cur_frame.need_wait.append(proc)
387
388 def _ApplyRedirect(self, r, err_out):
389 # type: (RedirValue, List[int]) -> None
390 arg = r.arg
391 UP_arg = arg
392 with tagswitch(arg) as case:
393
394 if case(redirect_arg_e.Path):
395 arg = cast(redirect_arg.Path, UP_arg)
396
397 if r.op_id in (Id.Redir_Great, Id.Redir_AndGreat): # > &>
398 mode = O_CREAT | O_WRONLY | O_TRUNC
399 elif r.op_id == Id.Redir_Clobber: # >|
400 mode = O_CREAT | O_WRONLY | O_TRUNC
401 elif r.op_id in (Id.Redir_DGreat,
402 Id.Redir_AndDGreat): # >> &>>
403 mode = O_CREAT | O_WRONLY | O_APPEND
404 elif r.op_id == Id.Redir_Less: # <
405 mode = O_RDONLY
406 elif r.op_id == Id.Redir_LessGreat: # <>
407 mode = O_CREAT | O_RDWR
408 else:
409 raise NotImplementedError(r.op_id)
410
411 # noclobber: don't overwrite existing files (except for special
412 # files like /dev/null)
413 noclobber = self.exec_opts.noclobber()
414
415 # Only > and &> actually follow noclobber. See
416 # spec/redirect.test.sh
417 op_respects_noclobber = r.op_id in (Id.Redir_Great, Id.Redir_AndGreat)
418
419 if noclobber and op_respects_noclobber:
420 stat = mylib.stat(arg.filename)
421 if not stat:
422 # File doesn't currently exist, open with O_EXCL (open
423 # will fail is EEXIST if arg.filename exists when we
424 # call open(2)). This guards against a race where the
425 # file may be created *after* we check it with stat.
426 mode |= O_EXCL
427
428 elif stat.isreg():
429 # This is a regular file, opening it would clobber,
430 # so raise an error.
431 err_out.append(EEXIST)
432 return
433
434 # Otherwise, the file exists and is a special file like
435 # /dev/null, we can open(2) it without O_EXCL. (Note,
436 # there is a race here. See demo/noclobber-race.sh)
437
438 # NOTE: 0666 is affected by umask, all shells use it.
439 try:
440 open_fd = posix.open(arg.filename, mode, 0o666)
441 except (IOError, OSError) as e:
442 if e.errno == EEXIST and noclobber:
443 extra = ' (noclobber)'
444 else:
445 extra = ''
446 self.errfmt.Print_(
447 "Can't open %r: %s%s" %
448 (arg.filename, pyutil.strerror(e), extra),
449 blame_loc=r.op_loc)
450 raise
451
452 new_fd = self._PushDup(open_fd, r.loc)
453 if new_fd != NO_FD:
454 posix.close(open_fd)
455
456 # Now handle &> and &>> and their variants. These pairs are the same:
457 #
458 # stdout_stderr.py &> out-err.txt
459 # stdout_stderr.py > out-err.txt 2>&1
460 #
461 # stdout_stderr.py 3&> out-err.txt
462 # stdout_stderr.py 3> out-err.txt 2>&3
463 #
464 # Ditto for {fd}> and {fd}&>
465
466 if r.op_id in (Id.Redir_AndGreat, Id.Redir_AndDGreat):
467 self._PushDup(new_fd, redir_loc.Fd(2))
468
469 elif case(redirect_arg_e.CopyFd): # e.g. echo hi 1>&2
470 arg = cast(redirect_arg.CopyFd, UP_arg)
471
472 if r.op_id == Id.Redir_GreatAnd: # 1>&2
473 self._PushDup(arg.target_fd, r.loc)
474
475 elif r.op_id == Id.Redir_LessAnd: # 0<&5
476 # The only difference between >& and <& is the default file
477 # descriptor argument.
478 self._PushDup(arg.target_fd, r.loc)
479
480 else:
481 raise NotImplementedError()
482
483 elif case(redirect_arg_e.MoveFd): # e.g. echo hi 5>&6-
484 arg = cast(redirect_arg.MoveFd, UP_arg)
485 new_fd = self._PushDup(arg.target_fd, r.loc)
486 if new_fd != NO_FD:
487 posix.close(arg.target_fd)
488
489 UP_loc = r.loc
490 if r.loc.tag() == redir_loc_e.Fd:
491 fd = cast(redir_loc.Fd, UP_loc).fd
492 else:
493 fd = NO_FD
494
495 self.cur_frame.saved.append(_RedirFrame(new_fd, fd, False))
496
497 elif case(redirect_arg_e.CloseFd): # e.g. echo hi 5>&-
498 self._PushCloseFd(r.loc)
499
500 elif case(redirect_arg_e.HereDoc):
501 arg = cast(redirect_arg.HereDoc, UP_arg)
502
503 # NOTE: Do these descriptors have to be moved out of the range 0-9?
504 read_fd, write_fd = posix.pipe()
505
506 self._PushDup(read_fd, r.loc) # stdin is now the pipe
507
508 # We can't close like we do in the filename case above? The writer can
509 # get a "broken pipe".
510 self._PushClose(read_fd)
511
512 thunk = _HereDocWriterThunk(write_fd, arg.body)
513
514 # Use PIPE_SIZE to save a process in the case of small here
515 # docs, which are the common case. (dash does this.)
516
517 # Note: could instrument this to see how often it happens.
518 # Though strace -ff can also work.
519 start_process = len(arg.body) > 4096
520 #start_process = True
521
522 if start_process:
523 here_proc = Process(thunk, self.job_control, self.job_list,
524 self.tracer)
525
526 # NOTE: we could close the read pipe here, but it doesn't really
527 # matter because we control the code.
528 here_proc.StartProcess(trace.HereDoc)
529 #log('Started %s as %d', here_proc, pid)
530 self._PushWait(here_proc)
531
532 # Now that we've started the child, close it in the parent.
533 posix.close(write_fd)
534
535 else:
536 posix.write(write_fd, arg.body)
537 posix.close(write_fd)
538
539 def Push(self, redirects, err_out):
540 # type: (List[RedirValue], List[int]) -> None
541 """Apply a group of redirects and remember to undo them."""
542
543 #log('> fd_state.Push %s', redirects)
544 new_frame = _FdFrame()
545 self.stack.append(new_frame)
546 self.cur_frame = new_frame
547
548 for r in redirects:
549 #log('apply %s', r)
550 with ui.ctx_Location(self.errfmt, r.op_loc):
551 try:
552 # _ApplyRedirect reports errors in 2 ways:
553 # 1. Raising an IOError or OSError from posix.* calls
554 # 2. Returning errors in err_out from checks like noclobber
555 self._ApplyRedirect(r, err_out)
556 except (IOError, OSError) as e:
557 err_out.append(e.errno)
558
559 if len(err_out):
560 # This can fail too
561 self.Pop(err_out)
562 return # for bad descriptor, etc.
563
564 def PushStdinFromPipe(self, r):
565 # type: (int) -> bool
566 """Save the current stdin and make it come from descriptor 'r'.
567
568 'r' is typically the read-end of a pipe. For 'lastpipe'/ZSH
569 semantics of
570
571 echo foo | read line; echo $line
572 """
573 new_frame = _FdFrame()
574 self.stack.append(new_frame)
575 self.cur_frame = new_frame
576
577 self._PushDup(r, redir_loc.Fd(0))
578 return True
579
580 def Pop(self, err_out):
581 # type: (List[int]) -> None
582 frame = self.stack.pop()
583 #log('< Pop %s', frame)
584 for rf in reversed(frame.saved):
585 if rf.saved_fd == NO_FD:
586 #log('Close %d', orig)
587 try:
588 posix.close(rf.orig_fd)
589 except (IOError, OSError) as e:
590 err_out.append(e.errno)
591 log('Error closing descriptor %d: %s', rf.orig_fd,
592 pyutil.strerror(e))
593 return
594 else:
595 try:
596 posix.dup2(rf.saved_fd, rf.orig_fd)
597 except (IOError, OSError) as e:
598 err_out.append(e.errno)
599 log('dup2(%d, %d) error: %s', rf.saved_fd, rf.orig_fd,
600 pyutil.strerror(e))
601 #log('fd state:')
602 #posix.system('ls -l /proc/%s/fd' % posix.getpid())
603 return
604 posix.close(rf.saved_fd)
605 #log('dup2 %s %s', saved, orig)
606
607 # Wait for here doc processes to finish.
608 for proc in frame.need_wait:
609 unused_status = proc.Wait(self.waiter)
610
611 def MakePermanent(self):
612 # type: () -> None
613 self.cur_frame.Forget()
614
615
616class ChildStateChange(object):
617
618 def __init__(self):
619 # type: () -> None
620 """Empty constructor for mycpp."""
621 pass
622
623 def Apply(self):
624 # type: () -> None
625 raise NotImplementedError()
626
627 def ApplyFromParent(self, proc):
628 # type: (Process) -> None
629 """Noop for all state changes other than SetPgid for mycpp."""
630 pass
631
632
633class StdinFromPipe(ChildStateChange):
634
635 def __init__(self, pipe_read_fd, w):
636 # type: (int, int) -> None
637 self.r = pipe_read_fd
638 self.w = w
639
640 def __repr__(self):
641 # type: () -> str
642 return '<StdinFromPipe %d %d>' % (self.r, self.w)
643
644 def Apply(self):
645 # type: () -> None
646 posix.dup2(self.r, 0)
647 posix.close(self.r) # close after dup
648
649 posix.close(self.w) # we're reading from the pipe, not writing
650 #log('child CLOSE w %d pid=%d', self.w, posix.getpid())
651
652
653class StdoutToPipe(ChildStateChange):
654
655 def __init__(self, r, pipe_write_fd):
656 # type: (int, int) -> None
657 self.r = r
658 self.w = pipe_write_fd
659
660 def __repr__(self):
661 # type: () -> str
662 return '<StdoutToPipe %d %d>' % (self.r, self.w)
663
664 def Apply(self):
665 # type: () -> None
666 posix.dup2(self.w, 1)
667 posix.close(self.w) # close after dup
668
669 posix.close(self.r) # we're writing to the pipe, not reading
670 #log('child CLOSE r %d pid=%d', self.r, posix.getpid())
671
672
673class StderrToPipe(ChildStateChange):
674
675 def __init__(self, r, pipe_write_fd):
676 # type: (int, int) -> None
677 self.r = r
678 self.w = pipe_write_fd
679
680 def __repr__(self):
681 # type: () -> str
682 return '<StderrToPipe %d %d>' % (self.r, self.w)
683
684 def Apply(self):
685 # type: () -> None
686 posix.dup2(self.w, 2)
687 posix.close(self.w) # close after dup
688
689 posix.close(self.r) # we're writing to the pipe, not reading
690 #log('child CLOSE r %d pid=%d', self.r, posix.getpid())
691
692
693INVALID_PGID = -1
694# argument to setpgid() that means the process is its own leader
695OWN_LEADER = 0
696
697
698class SetPgid(ChildStateChange):
699
700 def __init__(self, pgid, tracer):
701 # type: (int, dev.Tracer) -> None
702 self.pgid = pgid
703 self.tracer = tracer
704
705 def Apply(self):
706 # type: () -> None
707 try:
708 posix.setpgid(0, self.pgid)
709 except (IOError, OSError) as e:
710 self.tracer.OtherMessage(
711 'osh: child %d failed to set its process group to %d: %s' %
712 (posix.getpid(), self.pgid, pyutil.strerror(e)))
713
714 def ApplyFromParent(self, proc):
715 # type: (Process) -> None
716 try:
717 posix.setpgid(proc.pid, self.pgid)
718 except (IOError, OSError) as e:
719 self.tracer.OtherMessage(
720 'osh: parent failed to set process group for PID %d to %d: %s'
721 % (proc.pid, self.pgid, pyutil.strerror(e)))
722
723
724class ExternalProgram(object):
725 """The capability to execute an external program like 'ls'."""
726
727 def __init__(
728 self,
729 hijack_shebang, # type: str
730 fd_state, # type: FdState
731 errfmt, # type: ui.ErrorFormatter
732 debug_f, # type: _DebugFile
733 ):
734 # type: (...) -> None
735 """
736 Args:
737 hijack_shebang: The path of an interpreter to run instead of the one
738 specified in the shebang line. May be empty.
739 """
740 self.hijack_shebang = hijack_shebang
741 self.fd_state = fd_state
742 self.errfmt = errfmt
743 self.debug_f = debug_f
744
745 def Exec(self, argv0_path, cmd_val, environ):
746 # type: (str, cmd_value.Argv, Dict[str, str]) -> None
747 """Execute a program and exit this process.
748
749 Called by: ls / exec ls / ( ls / )
750 """
751 probe('process', 'ExternalProgram_Exec', argv0_path)
752 self._Exec(argv0_path, cmd_val.argv, cmd_val.arg_locs[0], environ,
753 True)
754 assert False, "This line should never execute" # NO RETURN
755
756 def _Exec(self, argv0_path, argv, argv0_loc, environ, should_retry):
757 # type: (str, List[str], loc_t, Dict[str, str], bool) -> None
758 if len(self.hijack_shebang):
759 opened = True
760 try:
761 f = self.fd_state.Open(argv0_path)
762 except (IOError, OSError) as e:
763 opened = False
764
765 if opened:
766 with ctx_FileCloser(f):
767 # Test if the shebang looks like a shell. TODO: The file might be
768 # binary with no newlines, so read 80 bytes instead of readline().
769
770 #line = f.read(80) # type: ignore # TODO: fix this
771 line = f.readline()
772
773 if match.ShouldHijack(line):
774 h_argv = [self.hijack_shebang, argv0_path]
775 h_argv.extend(argv[1:])
776 argv = h_argv
777 argv0_path = self.hijack_shebang
778 self.debug_f.writeln('Hijacked: %s' % argv0_path)
779 else:
780 #self.debug_f.log('Not hijacking %s (%r)', argv, line)
781 pass
782
783 try:
784 posix.execve(argv0_path, argv, environ)
785 except (IOError, OSError) as e:
786 # Run with /bin/sh when ENOEXEC error (no shebang). All shells do this.
787 if e.errno == ENOEXEC and should_retry:
788 new_argv = ['/bin/sh', argv0_path]
789 new_argv.extend(argv[1:])
790 self._Exec('/bin/sh', new_argv, argv0_loc, environ, False)
791 # NO RETURN
792
793 # Would be nice: when the path is relative and ENOENT: print PWD and do
794 # spelling correction?
795
796 self.errfmt.Print_(
797 "Can't execute %r: %s" % (argv0_path, pyutil.strerror(e)),
798 argv0_loc)
799
800 # POSIX mentions 126 and 127 for two specific errors. The rest are
801 # unspecified.
802 #
803 # http://pubs.opengroup.org/onlinepubs/9699919799.2016edition/utilities/V3_chap02.html#tag_18_08_02
804 if e.errno == EACCES:
805 status = 126
806 elif e.errno == ENOENT:
807 # TODO: most shells print 'command not found', rather than strerror()
808 # == "No such file or directory". That's better because it's at the
809 # end of the path search, and we're never searching for a directory.
810 status = 127
811 else:
812 # dash uses 2, but we use that for parse errors. This seems to be
813 # consistent with mksh and zsh.
814 status = 127
815
816 posix._exit(status)
817 # NO RETURN
818
819
820class Thunk(object):
821 """Abstract base class for things runnable in another process."""
822
823 def __init__(self):
824 # type: () -> None
825 """Empty constructor for mycpp."""
826 pass
827
828 def Run(self):
829 # type: () -> None
830 """Returns a status code."""
831 raise NotImplementedError()
832
833 def UserString(self):
834 # type: () -> str
835 """Display for the 'jobs' list."""
836 raise NotImplementedError()
837
838 def __repr__(self):
839 # type: () -> str
840 return self.UserString()
841
842
843class ExternalThunk(Thunk):
844 """An external executable."""
845
846 def __init__(self, ext_prog, argv0_path, cmd_val, environ):
847 # type: (ExternalProgram, str, cmd_value.Argv, Dict[str, str]) -> None
848 self.ext_prog = ext_prog
849 self.argv0_path = argv0_path
850 self.cmd_val = cmd_val
851 self.environ = environ
852
853 def UserString(self):
854 # type: () -> str
855
856 # NOTE: This is the format the Tracer uses.
857 # bash displays sleep $n & (code)
858 # but OSH displays sleep 1 & (argv array)
859 # We could switch the former but I'm not sure it's necessary.
860 tmp = [j8_lite.MaybeShellEncode(a) for a in self.cmd_val.argv]
861 return '[process] %s' % ' '.join(tmp)
862
863 def Run(self):
864 # type: () -> None
865 """An ExternalThunk is run in parent for the exec builtin."""
866 self.ext_prog.Exec(self.argv0_path, self.cmd_val, self.environ)
867
868
869class BuiltinThunk(Thunk):
870 """Builtin thunk - for running builtins in a forked subprocess"""
871
872 def __init__(self, shell_ex, builtin_id, cmd_val):
873 # type: (vm._Executor, int, cmd_value.Argv) -> None
874 self.shell_ex = shell_ex
875 self.builtin_id = builtin_id
876 self.cmd_val = cmd_val
877
878 def UserString(self):
879 # type: () -> str
880 tmp = [j8_lite.MaybeShellEncode(a) for a in self.cmd_val.argv]
881 return '[builtin] %s' % ' '.join(tmp)
882
883 def Run(self):
884 # type: () -> None
885 """No exec - we need to exit ourselves"""
886 status = self.shell_ex.RunBuiltin(self.builtin_id, self.cmd_val)
887 posix._exit(status)
888
889
890class SubProgramThunk(Thunk):
891 """A subprogram that can be executed in another process."""
892
893 def __init__(
894 self,
895 cmd_ev, # type: CommandEvaluator
896 node, # type: command_t
897 trap_state, # type: trap_osh.TrapState
898 multi_trace, # type: dev.MultiTracer
899 inherit_errexit, # type: bool
900 inherit_errtrace, # type: bool
901 ):
902 # type: (...) -> None
903 self.cmd_ev = cmd_ev
904 self.node = node
905 self.trap_state = trap_state
906 self.multi_trace = multi_trace
907 self.inherit_errexit = inherit_errexit # for bash errexit compatibility
908 self.inherit_errtrace = inherit_errtrace # for bash errtrace compatibility
909
910 def UserString(self):
911 # type: () -> str
912
913 # NOTE: These can be pieces of a pipeline, so they're arbitrary nodes.
914 # TODO: Extract SPIDS from node to display source? Note that
915 # CompoundStatus also has locations of each pipeline component; see
916 # Executor.RunPipeline()
917 thunk_str = ui.CommandType(self.node)
918 return '[subprog] %s' % thunk_str
919
920 def Run(self):
921 # type: () -> None
922 #self.errfmt.OneLineErrExit() # don't quote code in child processes
923 probe('process', 'SubProgramThunk_Run')
924
925 # TODO: break circular dep. Bit flags could go in ASDL or headers.
926 from osh import cmd_eval
927
928 # signal handlers aren't inherited
929 self.trap_state.ClearForSubProgram(self.inherit_errtrace)
930
931 # NOTE: may NOT return due to exec().
932 if not self.inherit_errexit:
933 self.cmd_ev.mutable_opts.DisableErrExit()
934 try:
935 # optimize to eliminate redundant subshells like ( echo hi ) | wc -l etc.
936 self.cmd_ev.ExecuteAndCatch(
937 self.node,
938 cmd_eval.OptimizeSubshells | cmd_eval.MarkLastCommands)
939 status = self.cmd_ev.LastStatus()
940 # NOTE: We ignore the is_fatal return value. The user should set -o
941 # errexit so failures in subprocesses cause failures in the parent.
942 except util.UserExit as e:
943 status = e.status
944
945 # Handle errors in a subshell. These two cases are repeated from main()
946 # and the core/completion.py hook.
947 except KeyboardInterrupt:
948 print('')
949 status = 130 # 128 + 2
950 except (IOError, OSError) as e:
951 print_stderr('oils I/O error (subprogram): %s' %
952 pyutil.strerror(e))
953 status = 2
954
955 # If ProcessInit() doesn't turn off buffering, this is needed before
956 # _exit()
957 pyos.FlushStdout()
958
959 self.multi_trace.WriteDumps()
960
961 # We do NOT want to raise SystemExit here. Otherwise dev.Tracer::Pop()
962 # gets called in BOTH processes.
963 # The crash dump seems to be unaffected.
964 posix._exit(status)
965
966
967class _HereDocWriterThunk(Thunk):
968 """Write a here doc to one end of a pipe.
969
970 May be be executed in either a child process or the main shell
971 process.
972 """
973
974 def __init__(self, w, body_str):
975 # type: (int, str) -> None
976 self.w = w
977 self.body_str = body_str
978
979 def UserString(self):
980 # type: () -> str
981
982 # You can hit Ctrl-Z and the here doc writer will be suspended! Other
983 # shells don't have this problem because they use temp files! That's a bit
984 # unfortunate.
985 return '[here doc writer]'
986
987 def Run(self):
988 # type: () -> None
989 """do_exit: For small pipelines."""
990 probe('process', 'HereDocWriterThunk_Run')
991 #log('Writing %r', self.body_str)
992 posix.write(self.w, self.body_str)
993 #log('Wrote %r', self.body_str)
994 posix.close(self.w)
995 #log('Closed %d', self.w)
996
997 posix._exit(0)
998
999
1000class Job(object):
1001 """Interface for both Process and Pipeline.
1002
1003 They both can be put in the background and waited on.
1004
1005 Confusing thing about pipelines in the background: They have TOO MANY NAMES.
1006
1007 sleep 1 | sleep 2 &
1008
1009 - The LAST PID is what's printed at the prompt. This is $!, a PROCESS ID and
1010 not a JOB ID.
1011 # https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html#Special-Parameters
1012 - The process group leader (setpgid) is the FIRST PID.
1013 - It's also %1 or %+. The last job started.
1014 """
1015
1016 def __init__(self):
1017 # type: () -> None
1018 # Initial state with & or Ctrl-Z is Running.
1019 self.state = job_state_e.Running
1020 self.job_id = -1
1021 self.in_background = False
1022
1023 def DisplayJob(self, job_id, f, style):
1024 # type: (int, mylib.Writer, int) -> None
1025 raise NotImplementedError()
1026
1027 def State(self):
1028 # type: () -> job_state_t
1029 return self.state
1030
1031 def ProcessGroupId(self):
1032 # type: () -> int
1033 """Return the process group ID associated with this job."""
1034 raise NotImplementedError()
1035
1036 def PidForWait(self):
1037 # type: () -> int
1038 """Return the pid we can wait on."""
1039 raise NotImplementedError()
1040
1041 def JobWait(self, waiter):
1042 # type: (Waiter) -> wait_status_t
1043 """Wait for this process/pipeline to be stopped or finished."""
1044 raise NotImplementedError()
1045
1046 def SetBackground(self):
1047 # type: () -> None
1048 """Record that this job is running in the background."""
1049 self.in_background = True
1050
1051 def SetForeground(self):
1052 # type: () -> None
1053 """Record that this job is running in the foreground."""
1054 self.in_background = False
1055
1056
1057class Process(Job):
1058 """A process to run.
1059
1060 TODO: Should we make it clear that this is a FOREGROUND process? A
1061 background process is wrapped in a "job". It is unevaluated.
1062
1063 It provides an API to manipulate file descriptor state in parent and child.
1064 """
1065
1066 def __init__(self, thunk, job_control, job_list, tracer):
1067 # type: (Thunk, JobControl, JobList, dev.Tracer) -> None
1068 """
1069 Args:
1070 thunk: Thunk instance
1071 job_list: for process bookkeeping
1072 """
1073 Job.__init__(self)
1074 assert isinstance(thunk, Thunk), thunk
1075 self.thunk = thunk
1076 self.job_control = job_control
1077 self.job_list = job_list
1078 self.tracer = tracer
1079 self.exec_opts = tracer.exec_opts
1080
1081 # For pipelines
1082 self.parent_pipeline = None # type: Pipeline
1083 self.state_changes = [] # type: List[ChildStateChange]
1084 self.close_r = -1
1085 self.close_w = -1
1086
1087 self.pid = -1
1088 self.status = -1
1089
1090 def Init_ParentPipeline(self, pi):
1091 # type: (Pipeline) -> None
1092 """For updating PIPESTATUS."""
1093 self.parent_pipeline = pi
1094
1095 def __repr__(self):
1096 # type: () -> str
1097
1098 # note: be wary of infinite mutual recursion
1099 #s = ' %s' % self.parent_pipeline if self.parent_pipeline else ''
1100 #return '<Process %s%s>' % (self.thunk, s)
1101 return '<Process pid=%d state=%s %s>' % (
1102 self.pid, _JobStateStr(self.state), self.thunk)
1103
1104 def ProcessGroupId(self):
1105 # type: () -> int
1106 """Returns the group ID of this process."""
1107 # This should only ever be called AFTER the process has started
1108 assert self.pid != -1
1109 if self.parent_pipeline:
1110 # XXX: Maybe we should die here instead? Unclear if this branch
1111 # should even be reachable with the current builtins.
1112 return self.parent_pipeline.ProcessGroupId()
1113
1114 return self.pid
1115
1116 def PidForWait(self):
1117 # type: () -> int
1118 """Return the pid we can wait on."""
1119 assert self.pid != -1
1120 return self.pid
1121
1122 def DisplayJob(self, job_id, f, style):
1123 # type: (int, mylib.Writer, int) -> None
1124 if job_id == -1:
1125 job_id_str = ' '
1126 else:
1127 job_id_str = '%%%d' % job_id
1128 if style == STYLE_PID_ONLY:
1129 f.write('%d\n' % self.pid)
1130 else:
1131 f.write('%s %d %7s ' %
1132 (job_id_str, self.pid, _JobStateStr(self.state)))
1133 f.write(self.thunk.UserString())
1134 f.write('\n')
1135
1136 def AddStateChange(self, s):
1137 # type: (ChildStateChange) -> None
1138 self.state_changes.append(s)
1139
1140 def AddPipeToClose(self, r, w):
1141 # type: (int, int) -> None
1142 self.close_r = r
1143 self.close_w = w
1144
1145 def MaybeClosePipe(self):
1146 # type: () -> None
1147 if self.close_r != -1:
1148 posix.close(self.close_r)
1149 posix.close(self.close_w)
1150
1151 def StartProcess(self, why):
1152 # type: (trace_t) -> int
1153 """Start this process with fork(), handling redirects."""
1154 pid = posix.fork()
1155 if pid < 0:
1156 # When does this happen?
1157 e_die('Fatal error in posix.fork()')
1158
1159 elif pid == 0: # child
1160 # Note: this happens in BOTH interactive and non-interactive shells.
1161 # We technically don't need to do most of it in non-interactive, since we
1162 # did not change state in InitInteractiveShell().
1163
1164 for st in self.state_changes:
1165 st.Apply()
1166
1167 # Python sets SIGPIPE handler to SIG_IGN by default. Child processes
1168 # shouldn't have this.
1169 # https://docs.python.org/2/library/signal.html
1170 # See Python/pythonrun.c.
1171 iolib.sigaction(SIGPIPE, SIG_DFL)
1172
1173 # Respond to Ctrl-\ (core dump)
1174 iolib.sigaction(SIGQUIT, SIG_DFL)
1175
1176 # Only standalone children should get Ctrl-Z. Pipelines remain in the
1177 # foreground because suspending them is difficult with our 'lastpipe'
1178 # semantics.
1179 pid = posix.getpid()
1180 if posix.getpgid(0) == pid and self.parent_pipeline is None:
1181 iolib.sigaction(SIGTSTP, SIG_DFL)
1182
1183 # More signals from
1184 # https://www.gnu.org/software/libc/manual/html_node/Launching-Jobs.html
1185 # (but not SIGCHLD)
1186 iolib.sigaction(SIGTTOU, SIG_DFL)
1187 iolib.sigaction(SIGTTIN, SIG_DFL)
1188
1189 self.tracer.OnNewProcess(pid)
1190 # clear foreground pipeline for subshells
1191 self.thunk.Run()
1192 # Never returns
1193
1194 #log('STARTED process %s, pid = %d', self, pid)
1195 self.tracer.OnProcessStart(pid, why)
1196
1197 # Class invariant: after the process is started, it stores its PID.
1198 self.pid = pid
1199
1200 # SetPgid needs to be applied from the child and the parent to avoid
1201 # racing in calls to tcsetpgrp() in the parent. See APUE sec. 9.2.
1202 for st in self.state_changes:
1203 st.ApplyFromParent(self)
1204
1205 # Program invariant: We keep track of every child process!
1206 # Waiter::WaitForOne() needs it to update state
1207 self.job_list.AddChildProcess(pid, self)
1208
1209 return pid
1210
1211 def Wait(self, waiter):
1212 # type: (Waiter) -> int
1213 """Wait for this Process to finish."""
1214 # Keep waiting if waitpid() was interrupted with a signal (unlike the
1215 # 'wait' builtin)
1216 while self.state == job_state_e.Running:
1217 result, _ = waiter.WaitForOne()
1218 if result == W1_NO_CHILDREN:
1219 break
1220
1221 # Linear search
1222 # if we get a W1_EXITED event, and the pid is OUR PID, then we can
1223 # return?
1224 # well we need the status too
1225
1226 # Cleanup - for background jobs this happens in the 'wait' builtin,
1227 # e.g. after JobWait()
1228 if self.state == job_state_e.Exited:
1229 self.job_list.PopChildProcess(self.pid)
1230
1231 assert self.status >= 0, self.status
1232 return self.status
1233
1234 def JobWait(self, waiter):
1235 # type: (Waiter) -> wait_status_t
1236 """Process::JobWait, called by wait builtin"""
1237 # wait builtin can be interrupted
1238 while self.state == job_state_e.Running:
1239 result, w1_arg = waiter.WaitForOne() # mutates self.state
1240
1241 if result == W1_CALL_INTR:
1242 return wait_status.Cancelled(w1_arg)
1243
1244 if result == W1_NO_CHILDREN:
1245 break
1246
1247 # Ignore W1_EXITED, W1_STOPPED - these are OTHER processes
1248
1249 assert self.status >= 0, self.status
1250 return wait_status.Proc(self.state, self.status)
1251
1252 def WhenContinued(self):
1253 # type: () -> None
1254 self.state = job_state_e.Running
1255
1256 if self.parent_pipeline:
1257 # TODO: do we need anything here?
1258 pass
1259
1260 # TODO: Should we remove it as a job?
1261
1262 # Now job_id is set
1263 if self.exec_opts.interactive():
1264 #if 0:
1265 print_stderr('[%%%d] PID %d Continued' % (self.job_id, self.pid))
1266
1267 #if self.in_background:
1268 if 1:
1269 self.job_control.MaybeTakeTerminal()
1270 self.SetForeground()
1271
1272 def WhenStopped(self, stop_sig):
1273 # type: (int) -> None
1274 """Called by the Waiter when this Process is stopped."""
1275 # 128 is a shell thing
1276 # https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
1277 self.status = 128 + stop_sig
1278 self.state = job_state_e.Stopped
1279
1280 if self.parent_pipeline:
1281 # TODO: do we need anything here?
1282 # We need AllStopped() just like AllExited()?
1283
1284 #self.parent_pipeline.WhenPartIsStopped(pid, status)
1285 #return
1286 pass
1287
1288 if self.job_id == -1:
1289 # This process was started in the foreground, not with &. So it
1290 # was NOT a job, but after Ctrl-Z, it's a job.
1291 self.job_list.RegisterJob(self)
1292
1293 # Now self.job_id is set
1294 if self.exec_opts.interactive():
1295 print_stderr('') # newline after ^Z (TODO: consolidate with ^C)
1296 print_stderr('[%%%d] PID %d Stopped with signal %d' %
1297 (self.job_id, self.pid, stop_sig))
1298
1299 if not self.in_background:
1300 # e.g. sleep 5; then Ctrl-Z
1301 self.job_control.MaybeTakeTerminal()
1302 self.SetBackground()
1303
1304 def WhenExited(self, pid, status):
1305 # type: (int, int) -> None
1306 """Called by the Waiter when this Process exits."""
1307
1308 #log('Process WhenExited %d %d', pid, status)
1309 assert pid == self.pid, 'Expected %d, got %d' % (self.pid, pid)
1310 self.status = status
1311 self.state = job_state_e.Exited
1312
1313 if self.parent_pipeline:
1314 # populate pipeline status array; update Pipeline state, etc.
1315 self.parent_pipeline.WhenPartExited(pid, status)
1316 return
1317
1318 if self.job_id != -1 and self.in_background:
1319 # TODO: ONE condition should determine if this was a background
1320 # job, rather than a foreground process
1321 # "Job might have been brought to the foreground after being
1322 # assigned a job ID"
1323 if self.exec_opts.interactive():
1324 print_stderr('[%%%d] PID %d Done' % (self.job_id, self.pid))
1325
1326 if not self.in_background:
1327 self.job_control.MaybeTakeTerminal()
1328
1329 def RunProcess(self, waiter, why):
1330 # type: (Waiter, trace_t) -> int
1331 """Run this process synchronously."""
1332 self.StartProcess(why)
1333 # ShellExecutor might be calling this for the last part of a pipeline.
1334 if self.parent_pipeline is None:
1335 # QUESTION: Can the PGID of a single process just be the PID? i.e. avoid
1336 # calling getpgid()?
1337 self.job_control.MaybeGiveTerminal(posix.getpgid(self.pid))
1338 return self.Wait(waiter)
1339
1340
1341class ctx_Pipe(object):
1342
1343 def __init__(self, fd_state, fd, err_out):
1344 # type: (FdState, int, List[int]) -> None
1345 fd_state.PushStdinFromPipe(fd)
1346 self.fd_state = fd_state
1347 self.err_out = err_out
1348
1349 def __enter__(self):
1350 # type: () -> None
1351 pass
1352
1353 def __exit__(self, type, value, traceback):
1354 # type: (Any, Any, Any) -> None
1355 self.fd_state.Pop(self.err_out)
1356
1357
1358class Pipeline(Job):
1359 """A pipeline of processes to run.
1360
1361 Cases we handle:
1362
1363 foo | bar
1364 $(foo | bar)
1365 foo | bar | read v
1366 """
1367
1368 def __init__(self, sigpipe_status_ok, job_control, job_list, tracer):
1369 # type: (bool, JobControl, JobList, dev.Tracer) -> None
1370 Job.__init__(self)
1371 self.job_control = job_control
1372 self.job_list = job_list
1373 self.tracer = tracer
1374 self.exec_opts = tracer.exec_opts
1375
1376 self.procs = [] # type: List[Process]
1377 self.pids = [] # type: List[int] # pids in order
1378 self.pipe_status = [] # type: List[int] # status in order
1379 self.status = -1 # for 'wait' jobs
1380
1381 self.pgid = INVALID_PGID
1382
1383 # Optional for foreground
1384 self.last_thunk = None # type: Tuple[CommandEvaluator, command_t]
1385 self.last_pipe = None # type: Tuple[int, int]
1386
1387 self.sigpipe_status_ok = sigpipe_status_ok
1388
1389 def __repr__(self):
1390 # type: () -> str
1391 return '<Pipeline pgid=%d pids=%s state=%s procs=%s>' % (
1392 self.pgid, self.pids, _JobStateStr(self.state), self.procs)
1393
1394 def ProcessGroupId(self):
1395 # type: () -> int
1396 """Returns the group ID of this pipeline.
1397
1398 In an interactive shell, it's often the FIRST.
1399 """
1400 return self.pgid
1401
1402 def PidForWait(self):
1403 # type: () -> int
1404 """Return the PID we can wait on.
1405
1406 This is the same as the PID for $!
1407
1408 Shell WART:
1409 The $! variable is the PID of the LAST pipeline part.
1410 But in an interactive shell, the PGID is the PID of the FIRST pipeline part.
1411 It would be nicer if these were consistent!
1412 """
1413 return self.pids[-1]
1414
1415 def DisplayJob(self, job_id, f, style):
1416 # type: (int, mylib.Writer, int) -> None
1417 if style == STYLE_PID_ONLY:
1418 f.write('%d\n' % self.procs[0].pid)
1419 else:
1420 # Note: this is STYLE_LONG.
1421 for i, proc in enumerate(self.procs):
1422 if i == 0: # show job ID for first element in pipeline
1423 job_id_str = '%%%d' % job_id
1424 else:
1425 job_id_str = ' ' # 2 spaces
1426
1427 f.write('%s %d %7s ' %
1428 (job_id_str, proc.pid, _JobStateStr(proc.state)))
1429 f.write(proc.thunk.UserString())
1430 f.write('\n')
1431
1432 def DebugPrint(self):
1433 # type: () -> None
1434 print('Pipeline in state %s' % _JobStateStr(self.state))
1435 if mylib.PYTHON: # %s for Process not allowed in C++
1436 for proc in self.procs:
1437 print(' proc %s' % proc)
1438 _, last_node = self.last_thunk
1439 print(' last %s' % last_node)
1440 print(' pipe_status %s' % self.pipe_status)
1441
1442 def Add(self, p):
1443 # type: (Process) -> None
1444 """Append a process to the pipeline."""
1445 if len(self.procs) == 0:
1446 self.procs.append(p)
1447 return
1448
1449 r, w = posix.pipe()
1450 #log('pipe for %s: %d %d', p, r, w)
1451 prev = self.procs[-1]
1452
1453 prev.AddStateChange(StdoutToPipe(r, w)) # applied on StartPipeline()
1454 p.AddStateChange(StdinFromPipe(r, w)) # applied on StartPipeline()
1455
1456 p.AddPipeToClose(r, w) # MaybeClosePipe() on StartPipeline()
1457
1458 self.procs.append(p)
1459
1460 def AddLast(self, thunk):
1461 # type: (Tuple[CommandEvaluator, command_t]) -> None
1462 """Append the last noden to the pipeline.
1463
1464 This is run in the CURRENT process. It is OPTIONAL, because
1465 pipelines in the background are run uniformly.
1466 """
1467 self.last_thunk = thunk
1468
1469 assert len(self.procs) != 0
1470
1471 r, w = posix.pipe()
1472 prev = self.procs[-1]
1473 prev.AddStateChange(StdoutToPipe(r, w))
1474
1475 self.last_pipe = (r, w) # So we can connect it to last_thunk
1476
1477 def StartPipeline(self, waiter):
1478 # type: (Waiter) -> None
1479
1480 # If we are creating a pipeline in a subshell or we aren't running with job
1481 # control, our children should remain in our inherited process group.
1482 # the pipelines's group ID.
1483 if self.job_control.Enabled():
1484 self.pgid = OWN_LEADER # first process in pipeline is the leader
1485
1486 for i, proc in enumerate(self.procs):
1487 if self.pgid != INVALID_PGID:
1488 proc.AddStateChange(SetPgid(self.pgid, self.tracer))
1489
1490 # Figure out the pid
1491 pid = proc.StartProcess(trace.PipelinePart)
1492 if i == 0 and self.pgid != INVALID_PGID:
1493 # Mimic bash and use the PID of the FIRST process as the group
1494 # for the whole pipeline.
1495 self.pgid = pid
1496
1497 self.pids.append(pid)
1498 self.pipe_status.append(-1) # uninitialized
1499
1500 # NOTE: This is done in the SHELL PROCESS after every fork() call.
1501 # It can't be done at the end; otherwise processes will have descriptors
1502 # from non-adjacent pipes.
1503 proc.MaybeClosePipe()
1504
1505 if self.last_thunk:
1506 self.pipe_status.append(-1) # for self.last_thunk
1507
1508 #log('Started pipeline PIDS=%s, pgid=%d', self.pids, self.pgid)
1509
1510 def Wait(self, waiter):
1511 # type: (Waiter) -> List[int]
1512 """Wait for this Pipeline to finish."""
1513
1514 assert self.procs, "no procs for Wait()"
1515 # waitpid(-1) zero or more times
1516 while self.state == job_state_e.Running:
1517 # Keep waiting until there's nothing to wait for.
1518 result, _ = waiter.WaitForOne()
1519 if result == W1_NO_CHILDREN:
1520 break
1521
1522 return self.pipe_status
1523
1524 def JobWait(self, waiter):
1525 # type: (Waiter) -> wait_status_t
1526 """Pipeline::JobWait(), called by 'wait' builtin, e.g. 'wait %1'."""
1527 # wait builtin can be interrupted
1528 assert self.procs, "no procs for Wait()"
1529 while self.state == job_state_e.Running:
1530 result, w1_arg = waiter.WaitForOne()
1531
1532 if result == W1_CALL_INTR: # signal
1533 return wait_status.Cancelled(w1_arg)
1534
1535 if result == W1_NO_CHILDREN:
1536 break
1537
1538 # Ignore W1_EXITED, W1_STOPPED - these are OTHER processes
1539
1540 assert all(st >= 0 for st in self.pipe_status), self.pipe_status
1541 return wait_status.Pipeline(self.state, self.pipe_status)
1542
1543 def RunLastPart(self, waiter, fd_state):
1544 # type: (Waiter, FdState) -> List[int]
1545 """Run this pipeline synchronously (foreground pipeline).
1546
1547 Returns:
1548 pipe_status (list of integers).
1549 """
1550 assert len(self.pids) == len(self.procs)
1551
1552 # TODO: break circular dep. Bit flags could go in ASDL or headers.
1553 from osh import cmd_eval
1554
1555 # This is tcsetpgrp()
1556 # TODO: fix race condition -- I believe the first process could have
1557 # stopped already, and thus getpgid() will fail
1558 self.job_control.MaybeGiveTerminal(self.pgid)
1559
1560 # Run the last part of the pipeline IN PARALLEL with other processes. It
1561 # may or may not fork:
1562 # echo foo | read line # no fork, the builtin runs in THIS shell process
1563 # ls | wc -l # fork for 'wc'
1564
1565 cmd_ev, last_node = self.last_thunk
1566
1567 assert self.last_pipe is not None
1568 r, w = self.last_pipe # set in AddLast()
1569 posix.close(w) # we will not write here
1570
1571 # Fix lastpipe / job control / DEBUG trap interaction
1572 cmd_flags = cmd_eval.NoDebugTrap if self.job_control.Enabled() else 0
1573
1574 # The ERR trap only runs for the WHOLE pipeline, not the COMPONENTS in
1575 # a pipeline.
1576 cmd_flags |= cmd_eval.NoErrTrap
1577
1578 io_errors = [] # type: List[int]
1579 with ctx_Pipe(fd_state, r, io_errors):
1580 cmd_ev.ExecuteAndCatch(last_node, cmd_flags)
1581
1582 if len(io_errors):
1583 e_die('Error setting up last part of pipeline: %s' %
1584 posix.strerror(io_errors[0]))
1585
1586 # We won't read anymore. If we don't do this, then 'cat' in 'cat
1587 # /dev/urandom | sleep 1' will never get SIGPIPE.
1588 posix.close(r)
1589
1590 self.pipe_status[-1] = cmd_ev.LastStatus()
1591 if self.AllExited():
1592 self.state = job_state_e.Exited
1593
1594 #log('pipestatus before all have finished = %s', self.pipe_status)
1595 return self.Wait(waiter)
1596
1597 def AllExited(self):
1598 # type: () -> bool
1599
1600 # mycpp rewrite: all(status != -1 for status in self.pipe_status)
1601 for status in self.pipe_status:
1602 if status == -1:
1603 return False
1604 return True
1605
1606 def WhenPartExited(self, pid, status):
1607 # type: (int, int) -> None
1608 """Called by Process::WhenExited()"""
1609 #log('Pipeline WhenExited %d %d', pid, status)
1610 i = self.pids.index(pid)
1611 assert i != -1, 'Unexpected PID %d' % pid
1612
1613 if status == 141 and self.sigpipe_status_ok:
1614 status = 0
1615
1616 self.pipe_status[i] = status
1617 if not self.AllExited():
1618 return
1619
1620 if self.job_id != -1 and self.in_background:
1621 # TODO: ONE condition
1622 # "Job might have been brought to the foreground after being
1623 # assigned a job ID"
1624 if self.exec_opts.interactive():
1625 print_stderr('[%%%d] PGID %d Done' %
1626 (self.job_id, self.pids[0]))
1627
1628 # Status of pipeline is status of last process
1629 self.status = self.pipe_status[-1]
1630 self.state = job_state_e.Exited
1631
1632 if not self.in_background:
1633 self.job_control.MaybeTakeTerminal()
1634
1635
1636def _JobStateStr(i):
1637 # type: (job_state_t) -> str
1638 return job_state_str(i, dot=False)
1639
1640
1641def _GetTtyFd():
1642 # type: () -> int
1643 """Returns -1 if stdio is not a TTY."""
1644 try:
1645 return posix.open("/dev/tty", O_NONBLOCK | O_NOCTTY | O_RDWR, 0o666)
1646 except (IOError, OSError) as e:
1647 return -1
1648
1649
1650class ctx_TerminalControl(object):
1651
1652 def __init__(self, job_control, errfmt):
1653 # type: (JobControl, ui.ErrorFormatter) -> None
1654 job_control.InitJobControl()
1655 self.job_control = job_control
1656 self.errfmt = errfmt
1657
1658 def __enter__(self):
1659 # type: () -> None
1660 pass
1661
1662 def __exit__(self, type, value, traceback):
1663 # type: (Any, Any, Any) -> None
1664
1665 # Return the TTY to the original owner before exiting.
1666 try:
1667 self.job_control.MaybeReturnTerminal()
1668 except error.FatalRuntime as e:
1669 # Don't abort the shell on error, just print a message.
1670 self.errfmt.PrettyPrintError(e)
1671
1672
1673class JobControl(object):
1674 """Interface to setpgid(), tcsetpgrp(), etc."""
1675
1676 def __init__(self):
1677 # type: () -> None
1678
1679 # The main shell's PID and group ID.
1680 self.shell_pid = -1
1681 self.shell_pgid = -1
1682
1683 # The fd of the controlling tty. Set to -1 when job control is disabled.
1684 self.shell_tty_fd = -1
1685
1686 # For giving the terminal back to our parent before exiting (if not a login
1687 # shell).
1688 self.original_tty_pgid = -1
1689
1690 def InitJobControl(self):
1691 # type: () -> None
1692 self.shell_pid = posix.getpid()
1693 orig_shell_pgid = posix.getpgid(0)
1694 self.shell_pgid = orig_shell_pgid
1695 self.shell_tty_fd = _GetTtyFd()
1696
1697 # If we aren't the leader of our process group, create a group and mark
1698 # ourselves as the leader.
1699 if self.shell_pgid != self.shell_pid:
1700 try:
1701 posix.setpgid(self.shell_pid, self.shell_pid)
1702 self.shell_pgid = self.shell_pid
1703 except (IOError, OSError) as e:
1704 self.shell_tty_fd = -1
1705
1706 if self.shell_tty_fd != -1:
1707 self.original_tty_pgid = posix.tcgetpgrp(self.shell_tty_fd)
1708
1709 # If stdio is a TTY, put the shell's process group in the foreground.
1710 try:
1711 posix.tcsetpgrp(self.shell_tty_fd, self.shell_pgid)
1712 except (IOError, OSError) as e:
1713 # We probably aren't in the session leader's process group. Disable job
1714 # control.
1715 self.shell_tty_fd = -1
1716 self.shell_pgid = orig_shell_pgid
1717 posix.setpgid(self.shell_pid, self.shell_pgid)
1718
1719 def Enabled(self):
1720 # type: () -> bool
1721 """
1722 Only the main shell process should bother with job control functions.
1723 """
1724 #log('ENABLED? %d', self.shell_tty_fd)
1725
1726 # TODO: get rid of getpid()? I think SubProgramThunk should set a
1727 # flag.
1728 return self.shell_tty_fd != -1 and posix.getpid() == self.shell_pid
1729
1730 # TODO: This isn't a PID. This is a process group ID?
1731 #
1732 # What should the table look like?
1733 #
1734 # Do we need the last PID? I don't know why bash prints that. Probably so
1735 # you can do wait $!
1736 # wait -n waits for any node to go from job_state_e.Running to job_state_e.Done?
1737 #
1738 # And it needs a flag for CURRENT, for the implicit arg to 'fg'.
1739 # job_id is just an integer. This is sort of lame.
1740 #
1741 # [job_id, flag, pgid, job_state, node]
1742
1743 def MaybeGiveTerminal(self, pgid):
1744 # type: (int) -> None
1745 """If stdio is a TTY, move the given process group to the
1746 foreground."""
1747 if not self.Enabled():
1748 # Only call tcsetpgrp when job control is enabled.
1749 return
1750
1751 try:
1752 posix.tcsetpgrp(self.shell_tty_fd, pgid)
1753 except (IOError, OSError) as e:
1754 e_die('osh: Failed to move process group %d to foreground: %s' %
1755 (pgid, pyutil.strerror(e)))
1756
1757 def MaybeTakeTerminal(self):
1758 # type: () -> None
1759 """If stdio is a TTY, return the main shell's process group to the
1760 foreground."""
1761 self.MaybeGiveTerminal(self.shell_pgid)
1762
1763 def MaybeReturnTerminal(self):
1764 # type: () -> None
1765 """Called before the shell exits."""
1766 self.MaybeGiveTerminal(self.original_tty_pgid)
1767
1768
1769class JobList(object):
1770 """Global list of jobs, used by a few builtins."""
1771
1772 def __init__(self):
1773 # type: () -> None
1774
1775 # self.child_procs is used by WaitForOne() to call proc.WhenExited()
1776 # and proc.WhenStopped().
1777 self.child_procs = {} # type: Dict[int, Process]
1778
1779 # self.jobs is used by 'wait %1' and 'fg %2'
1780 # job_id -> Job
1781 self.jobs = {} # type: Dict[int, Job]
1782
1783 # self.pid_to_job is used by 'wait -n' and 'wait' - to call
1784 # CleanupWhenProcessExits(). They Dict key is job.PidForWait()
1785 self.pid_to_job = {} # type: Dict[int, Job]
1786
1787 # TODO: consider linear search through JobList
1788 # - by job ID
1789 # - by PID
1790 # - then you don't have to bother as much with the dicts
1791 # - you still need the child process dict to set the status and
1792 # state?
1793
1794 self.debug_pipelines = [] # type: List[Pipeline]
1795
1796 # Counter used to assign IDs to jobs. It is incremented every time a job
1797 # is created. Once all active jobs are done it is reset to 1. I'm not
1798 # sure if this reset behavior is mandated by POSIX, but other shells do
1799 # it, so we mimic for the sake of compatibility.
1800 self.next_job_id = 1
1801
1802 def RegisterJob(self, job):
1803 # type: (Job) -> int
1804 """Create a background job, which you can wait %2, fg %2, kill %2, etc.
1805
1806 - A job is either a Process or Pipeline.
1807 - A job is registered in these 2 situations:
1808 1. async: sleep 5 &
1809 2. stopped: sleep 5; then Ctrl-Z
1810 That is, in the interactive shell, the foreground process can be
1811 receives signals, and can be stopped
1812 """
1813 job_id = self.next_job_id
1814 self.next_job_id += 1
1815
1816 # Look up the job by job ID, for wait %1, kill %1, etc.
1817 self.jobs[job_id] = job
1818
1819 # Pipelines
1820 # TODO: register all PIDs? And conversely, remove all PIDs
1821 # what do other shells do?
1822 self.pid_to_job[job.PidForWait()] = job
1823
1824 # Mutate the job itself
1825 job.job_id = job_id
1826
1827 return job_id
1828
1829 def JobFromPid(self, pid):
1830 # type: (int) -> Optional[Job]
1831 return self.pid_to_job.get(pid)
1832
1833 def _MaybeResetCounter(self):
1834 # type: () -> None
1835 if len(self.jobs) == 0:
1836 self.next_job_id = 1
1837
1838 def CleanupWhenJobExits(self, job):
1839 # type: (Job) -> None
1840 """Called when say 'fg %2' exits, and when 'wait %2' exits"""
1841 mylib.dict_erase(self.jobs, job.job_id)
1842
1843 mylib.dict_erase(self.pid_to_job, job.PidForWait())
1844
1845 self._MaybeResetCounter()
1846
1847 def CleanupWhenProcessExits(self, pid):
1848 # type: (int) -> None
1849 """Given a PID, remove the job if it has Exited."""
1850
1851 job = self.pid_to_job.get(pid)
1852 if 0:
1853 # TODO: background pipelines don't clean up properly, because only
1854 # the last PID is registered in job_list.pid_to_job
1855
1856 # Should we switch to a linear search of a background job array?
1857 # Foreground jobs are stored in self.child_procs, and we migrate
1858 # between them?
1859
1860 log('*** CleanupWhenProcessExits %d', pid)
1861 log('job %s', job)
1862 #log('STATE %s', _JobStateStr(job.state))
1863
1864 if job and job.state == job_state_e.Exited:
1865 # Note: only the LAST PID in a pipeline will ever be here, but it's
1866 # OK to try to delete it.
1867 mylib.dict_erase(self.pid_to_job, pid)
1868
1869 mylib.dict_erase(self.jobs, job.job_id)
1870
1871 self._MaybeResetCounter()
1872
1873 def AddChildProcess(self, pid, proc):
1874 # type: (int, Process) -> None
1875 """Every child process should be added here as soon as we know its PID.
1876
1877 When the Waiter gets an EXITED or STOPPED notification, we need
1878 to know about it so 'jobs' can work.
1879
1880 Note: this contains Process objects that are part of a Pipeline object.
1881 Does it need to?
1882 """
1883 self.child_procs[pid] = proc
1884
1885 def PopChildProcess(self, pid):
1886 # type: (int) -> Optional[Process]
1887 """Remove the child process with the given PID."""
1888 pr = self.child_procs.get(pid)
1889 if pr is not None:
1890 mylib.dict_erase(self.child_procs, pid)
1891 return pr
1892
1893 if mylib.PYTHON:
1894
1895 def AddPipeline(self, pi):
1896 # type: (Pipeline) -> None
1897 """For debugging only."""
1898 self.debug_pipelines.append(pi)
1899
1900 def GetCurrentAndPreviousJobs(self):
1901 # type: () -> Tuple[Optional[Job], Optional[Job]]
1902 """Return the "current" and "previous" jobs (AKA `%+` and `%-`).
1903
1904 See the POSIX specification for the `jobs` builtin for details:
1905 https://pubs.opengroup.org/onlinepubs/007904875/utilities/jobs.html
1906
1907 IMPORTANT NOTE: This method assumes that the jobs list will not change
1908 during its execution! This assumption holds for now because we only ever
1909 update the jobs list from the main loop after WaitPid() informs us of a
1910 change. If we implement `set -b` and install a signal handler for
1911 SIGCHLD we should be careful to synchronize it with this function. The
1912 unsafety of mutating GC data structures from a signal handler should
1913 make this a non-issue, but if bugs related to this appear this note may
1914 be helpful...
1915 """
1916 # Split all active jobs by state and sort each group by decreasing job
1917 # ID to approximate newness.
1918 stopped_jobs = [] # type: List[Job]
1919 running_jobs = [] # type: List[Job]
1920 for i in xrange(0, self.next_job_id):
1921 job = self.jobs.get(i, None)
1922 if not job:
1923 continue
1924
1925 if job.state == job_state_e.Stopped:
1926 stopped_jobs.append(job)
1927
1928 elif job.state == job_state_e.Running:
1929 running_jobs.append(job)
1930
1931 current = None # type: Optional[Job]
1932 previous = None # type: Optional[Job]
1933 # POSIX says: If there is any suspended job, then the current job shall
1934 # be a suspended job. If there are at least two suspended jobs, then the
1935 # previous job also shall be a suspended job.
1936 #
1937 # So, we will only return running jobs from here if there are no recent
1938 # stopped jobs.
1939 if len(stopped_jobs) > 0:
1940 current = stopped_jobs.pop()
1941
1942 if len(stopped_jobs) > 0:
1943 previous = stopped_jobs.pop()
1944
1945 if len(running_jobs) > 0 and not current:
1946 current = running_jobs.pop()
1947
1948 if len(running_jobs) > 0 and not previous:
1949 previous = running_jobs.pop()
1950
1951 if not previous:
1952 previous = current
1953
1954 return current, previous
1955
1956 def JobFromSpec(self, job_spec):
1957 # type: (str) -> Optional[Job]
1958 """Parse the given job spec and return the matching job. If there is no
1959 matching job, this function returns None.
1960
1961 See the POSIX spec for the `jobs` builtin for details about job specs:
1962 https://pubs.opengroup.org/onlinepubs/007904875/utilities/jobs.html
1963 """
1964 if job_spec in CURRENT_JOB_SPECS:
1965 current, _ = self.GetCurrentAndPreviousJobs()
1966 return current
1967
1968 if job_spec == '%-':
1969 _, previous = self.GetCurrentAndPreviousJobs()
1970 return previous
1971
1972 #log('** SEARCHING %s', self.jobs)
1973 # TODO: Add support for job specs based on prefixes of process argv.
1974 m = util.RegexSearch(r'^%([0-9]+)$', job_spec)
1975 if m is not None:
1976 assert len(m) == 2
1977 job_id = int(m[1])
1978 if job_id in self.jobs:
1979 return self.jobs[job_id]
1980
1981 return None
1982
1983 def DisplayJobs(self, style):
1984 # type: (int) -> None
1985 """Used by the 'jobs' builtin.
1986
1987 https://pubs.opengroup.org/onlinepubs/9699919799/utilities/jobs.html
1988
1989 "By default, the jobs utility shall display the status of all stopped jobs,
1990 running background jobs and all jobs whose status has changed and have not
1991 been reported by the shell."
1992 """
1993 # NOTE: A job is a background process or pipeline.
1994 #
1995 # echo hi | wc -l -- this starts two processes. Wait for TWO
1996 # echo hi | wc -l & -- this starts a process which starts two processes
1997 # Wait for ONE.
1998 #
1999 # 'jobs -l' GROUPS the PIDs by job. It has the job number, + - indicators
2000 # for %% and %-, PID, status, and "command".
2001 #
2002 # Every component of a pipeline is on the same line with 'jobs', but
2003 # they're separated into different lines with 'jobs -l'.
2004 #
2005 # See demo/jobs-builtin.sh
2006
2007 # $ jobs -l
2008 # [1]+ 24414 Stopped sleep 5
2009 # 24415 | sleep 5
2010 # [2] 24502 Running sleep 6
2011 # 24503 | sleep 6
2012 # 24504 | sleep 5 &
2013 # [3]- 24508 Running sleep 6
2014 # 24509 | sleep 6
2015 # 24510 | sleep 5 &
2016
2017 f = mylib.Stdout()
2018 for job_id, job in iteritems(self.jobs):
2019 # Use the %1 syntax
2020 job.DisplayJob(job_id, f, style)
2021
2022 def DebugPrint(self):
2023 # type: () -> None
2024
2025 f = mylib.Stdout()
2026 f.write('\n')
2027 f.write('[process debug info]\n')
2028
2029 for pid, proc in iteritems(self.child_procs):
2030 proc.DisplayJob(-1, f, STYLE_DEFAULT)
2031 #p = ' |' if proc.parent_pipeline else ''
2032 #print('%d %7s %s%s' % (pid, _JobStateStr(proc.state), proc.thunk.UserString(), p))
2033
2034 if len(self.debug_pipelines):
2035 f.write('\n')
2036 f.write('[pipeline debug info]\n')
2037 for pi in self.debug_pipelines:
2038 pi.DebugPrint()
2039
2040 def ListRecent(self):
2041 # type: () -> None
2042 """For jobs -n, which I think is also used in the interactive
2043 prompt."""
2044 pass
2045
2046 def NumRunning(self):
2047 # type: () -> int
2048 """Return the number of running jobs.
2049
2050 Used by 'wait' and 'wait -n'.
2051 """
2052 count = 0
2053 for _, job in iteritems(self.jobs): # mycpp rewrite: from itervalues()
2054 if job.State() == job_state_e.Running:
2055 count += 1
2056 return count
2057
2058
2059# Some WaitForOne() return values, which are negative. The numbers are
2060# arbitrary negative numbers.
2061#
2062# They don't overlap with iolib.UNTRAPPED_SIGWINCH == -10
2063# which LastSignal() can return
2064
2065W1_EXITED = -11 # process exited
2066W1_STOPPED = -12 # process was stopped
2067W1_CALL_INTR = -15 # the waitpid(-1) call was interrupted
2068
2069W1_NO_CHILDREN = -13 # no child processes to wait for
2070W1_NO_CHANGE = -14 # WNOHANG was passed and there were no state changes
2071
2072NO_ARG = -20
2073
2074
2075
2076
2077
2078def GetSignalMessage(sig_num):
2079 # type: (int) -> Optional[str]
2080 """Get signal message from libc."""
2081 if mylib.PYTHON:
2082 return libc.strsignal(sig_num)
2083 return None
2084
2085
2086class Waiter(object):
2087 """A capability to wait for processes.
2088
2089 This must be a singleton (and is because CommandEvaluator is a singleton).
2090
2091 Invariants:
2092 - Every child process is registered once
2093 - Every child process is waited for
2094
2095 Canonical example of why we need a GLOBAL waiter:
2096
2097 { sleep 3; echo 'done 3'; } &
2098 { sleep 4; echo 'done 4'; } &
2099
2100 # ... do arbitrary stuff ...
2101
2102 { sleep 1; exit 1; } | { sleep 2; exit 2; }
2103
2104 Now when you do wait() after starting the pipeline, you might get a pipeline
2105 process OR a background process! So you have to distinguish between them.
2106 """
2107
2108 def __init__(self, job_list, exec_opts, signal_safe, tracer):
2109 # type: (JobList, optview.Exec, iolib.SignalSafe, dev.Tracer) -> None
2110 self.job_list = job_list
2111 self.exec_opts = exec_opts
2112 self.signal_safe = signal_safe
2113 self.tracer = tracer
2114 self.last_status = 127 # wait -n error code
2115
2116 def LastStatusCode(self):
2117 # type: () -> int
2118 """Returns exit code for wait -n"""
2119 return self.last_status
2120
2121 def WaitForOne(self, waitpid_options=0):
2122 # type: (int) -> Tuple[int, int]
2123 """Wait until the next process returns (or maybe Ctrl-C).
2124
2125 Returns:
2126 One of these negative numbers:
2127 W1_NO_CHILDREN Nothing to wait for
2128 W1_NO_CHANGE no state changes when WNOHANG passed - used by
2129 main loop
2130 W1_EXITED Process exited (with or without signal)
2131 W1_STOPPED Process stopped
2132 W1_CALL_INTR
2133 UNTRAPPED_SIGWINCH
2134 Or
2135 result > 0 Signal that waitpid() was interrupted with
2136
2137 In the interactive shell, we return 0 if we get a Ctrl-C, so the caller
2138 will try again.
2139
2140 Callers:
2141 wait -n -- loop until there is one fewer process (TODO)
2142 wait -- loop until there are no processes
2143 wait $! -- loop until job state is Done (process or pipeline)
2144 Process::Wait() -- loop until Process state is done
2145 Pipeline::Wait() -- loop until Pipeline state is done
2146
2147 Comparisons:
2148 bash: jobs.c waitchld() Has a special case macro(!) CHECK_WAIT_INTR for
2149 the wait builtin
2150
2151 dash: jobs.c waitproc() uses sigfillset(), sigprocmask(), etc. Runs in a
2152 loop while (gotsigchld), but that might be a hack for System V!
2153
2154 Should we have a cleaner API like posix::wait_for_one() ?
2155
2156 wait_result =
2157 NoChildren -- ECHILD - no more
2158 | Exited(int pid) -- process done - call job_list.PopStatus() for status
2159 # do we also we want ExitedWithSignal() ?
2160 | Stopped(int pid)
2161 | Interrupted(int sig_num) -- may or may not retry
2162 | UntrappedSigwinch -- ignored
2163
2164 | NoChange -- for WNOHANG - is this a different API?
2165 """
2166 #waitpid_options |= WCONTINUED
2167 pid, status = pyos.WaitPid(waitpid_options)
2168 if pid == 0:
2169 return W1_NO_CHANGE, NO_ARG # WNOHANG passed, and no state changes
2170
2171 if pid < 0: # error case
2172 err_num = status
2173 #log('waitpid() error => %d %s', e.errno, pyutil.strerror(e))
2174 if err_num == ECHILD:
2175 return W1_NO_CHILDREN, NO_ARG
2176
2177 if err_num == EINTR: # Bug #858 fix
2178 # e.g. 1 for SIGHUP, or also be UNTRAPPED_SIGWINCH == -1
2179 last_sig = self.signal_safe.LastSignal()
2180 if last_sig == iolib.UNTRAPPED_SIGWINCH:
2181 return iolib.UNTRAPPED_SIGWINCH, NO_ARG
2182 else:
2183 return W1_CALL_INTR, last_sig
2184
2185 # No other errors? Man page says waitpid(INT_MIN) == ESRCH, "no
2186 # such process", an invalid PID
2187 raise AssertionError()
2188
2189 # All child processes are supposed to be in this dict. Even if a
2190 # grandchild outlives the child (its parent), the shell does NOT become
2191 # the parent. The init process does.
2192 proc = self.job_list.child_procs.get(pid)
2193
2194 if proc is None and self.exec_opts.verbose_warn():
2195 print_stderr("oils: PID %d exited, but oils didn't start it" % pid)
2196
2197 if 0:
2198 self.job_list.DebugPrint()
2199
2200 was_stopped = False
2201 if WIFSIGNALED(status):
2202 term_sig = WTERMSIG(status)
2203 status = 128 + term_sig
2204
2205 # Print newline after Ctrl-C.
2206 if term_sig == SIGINT:
2207 print('')
2208 else:
2209 msg = GetSignalMessage(term_sig)
2210 if msg is not None:
2211 if mylib.PYTHON:
2212 # WCOREDUMP is only available on some systems
2213 try:
2214 from os import WCOREDUMP # type: ignore
2215 if WCOREDUMP(status):
2216 msg = msg + ' (core dumped)'
2217 except (ImportError, AttributeError):
2218 pass
2219 print_stderr(msg)
2220
2221 if proc:
2222 proc.WhenExited(pid, status)
2223
2224 elif WIFEXITED(status):
2225 status = WEXITSTATUS(status)
2226 if proc:
2227 proc.WhenExited(pid, status)
2228
2229 elif WIFSTOPPED(status):
2230 was_stopped = True
2231
2232 stop_sig = WSTOPSIG(status)
2233
2234 if proc:
2235 proc.WhenStopped(stop_sig)
2236
2237 # This would be more consistent, but it's an extension to POSIX
2238 #elif WIFCONTINUED(status):
2239 # if proc:
2240 # proc.WhenContinued()
2241
2242 else:
2243 raise AssertionError(status)
2244
2245 self.last_status = status # for wait -n
2246 self.tracer.OnProcessEnd(pid, status)
2247
2248 if was_stopped:
2249 return W1_STOPPED, pid
2250 else:
2251 return W1_EXITED, pid
2252
2253 def PollForEvents(self):
2254 # type: () -> None
2255 """For the interactive shell to print when processes have exited."""
2256 while True:
2257 result, _ = self.WaitForOne(waitpid_options=WNOHANG)
2258
2259 if result == W1_NO_CHANGE:
2260 break
2261 if result == W1_NO_CHILDREN:
2262 break
2263
2264 # Keep polling here
2265 assert result in (W1_EXITED, W1_STOPPED), result
2266 # W1_CALL_INTR and iolib.UNTRAPPED_SIGWINCH should not happen,
2267 # because WNOHANG is a non-blocking call