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

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