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

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