OILS / core / process.py View on Github | oilshell.org

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