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

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